import path from 'node:path';
import fs from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { findFiles, ensureDirectory, copyFile, removeFile } from '../../../lib/fs.js';
import { detectDbContexts, findStartupProject, hasDotnetProject } from '../lib/detect-dbcontexts.js';
import { runDotnet, dotnetAvailable, dotnetEfAvailable } from '../lib/ef-runner.js';
import { buildMigrationName } from '../lib/migration-name.js';
import type { SquashSpec, SquashResult, SquashAssemblyResult } from './types.js';

const execFileAsync = promisify(execFile);

async function git(args: string[], cwd: string): Promise<{ stdout: string; exitCode: number; stderr: string }> {
  try {
    const { stdout, stderr } = await execFileAsync('git', args, { cwd, encoding: 'utf-8', maxBuffer: 20 * 1024 * 1024 });
    return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
  } catch (err) {
    const e = err as { stdout?: string; stderr?: string; code?: number };
    return {
      stdout: (e.stdout || '').trim(),
      stderr: (e.stderr || '').trim(),
      exitCode: typeof e.code === 'number' ? e.code : 1,
    };
  }
}

async function resolveBaseRef(cwd: string, preferred?: string): Promise<string | null> {
  if (preferred) {
    const r = await git(['rev-parse', '--verify', preferred], cwd);
    if (r.exitCode === 0) return preferred;
  }
  for (const ref of ['origin/develop', 'develop', 'origin/main', 'main']) {
    const r = await git(['rev-parse', '--verify', ref], cwd);
    if (r.exitCode === 0) return ref;
  }
  return null;
}

function capitalize(s: string): string {
  if (!s) return s;
  if (s.toLowerCase() === 'sqlserver') return 'SqlServer';
  return s.charAt(0).toUpperCase() + s.slice(1);
}

async function listMigrationFilesInDir(dir: string): Promise<string[]> {
  const files = await findFiles('*.cs', { cwd: dir });
  return files.filter(
    (f) =>
      path.dirname(f) === dir &&
      !f.endsWith('ModelSnapshot.cs'),
  );
}

async function getBranchOnlyMigrationFiles(
  migrationsDir: string,
  cwd: string,
  baseRef: string,
  aggressive: boolean,
): Promise<string[]> {
  const all = await listMigrationFilesInDir(migrationsDir);
  if (all.length === 0) return [];

  if (aggressive) return all;

  const rel = path.relative(cwd, migrationsDir).replace(/\\/g, '/');
  const baseListing = await git(['ls-tree', '-r', '--name-only', baseRef, rel], cwd);
  if (baseListing.exitCode !== 0) return [];

  const baseSet = new Set(baseListing.stdout.split('\n').filter(Boolean));

  return all.filter((abs) => {
    const relFile = path.relative(cwd, abs).replace(/\\/g, '/');
    return !baseSet.has(relFile);
  });
}

async function backupFiles(files: string[], backupDir: string, cwd: string): Promise<void> {
  await ensureDirectory(backupDir);
  for (const f of files) {
    const rel = path.relative(cwd, f);
    const dest = path.join(backupDir, rel);
    await ensureDirectory(path.dirname(dest));
    await copyFile(f, dest);
  }
}

async function resetSnapshotToBase(snapshotPath: string, cwd: string, baseRef: string): Promise<boolean> {
  const rel = path.relative(cwd, snapshotPath).replace(/\\/g, '/');
  const r = await git(['checkout', baseRef, '--', rel], cwd);
  return r.exitCode === 0;
}

export async function execute(spec: SquashSpec): Promise<SquashResult> {
  const warnings: string[] = [];

  if (!(await hasDotnetProject(spec.cwd))) {
    return {
      success: false,
      cwd: spec.cwd,
      baseBranch: '',
      currentBranch: '',
      assemblies: [],
      warnings,
      error: 'No .csproj in worktree.',
    };
  }

  const currentBranch = (await git(['branch', '--show-current'], spec.cwd)).stdout || 'HEAD';
  if (currentBranch === 'main' || currentBranch === 'master') {
    return {
      success: false,
      cwd: spec.cwd,
      baseBranch: '',
      currentBranch,
      assemblies: [],
      warnings,
      error: `Refusing to squash on protected branch "${currentBranch}"`,
    };
  }

  const baseRef = await resolveBaseRef(spec.cwd, spec.baseBranch);
  if (!baseRef) {
    return {
      success: false,
      cwd: spec.cwd,
      baseBranch: spec.baseBranch ?? '',
      currentBranch,
      assemblies: [],
      warnings,
      error: 'Could not resolve a base branch (tried origin/develop, develop, origin/main, main)',
    };
  }

  if (!spec.dryRun) {
    if (!(await dotnetAvailable())) {
      return { success: false, cwd: spec.cwd, baseBranch: baseRef, currentBranch, assemblies: [], warnings, error: 'dotnet CLI not on PATH' };
    }
    if (!(await dotnetEfAvailable())) {
      return { success: false, cwd: spec.cwd, baseBranch: baseRef, currentBranch, assemblies: [], warnings, error: 'dotnet-ef not installed globally' };
    }
  }

  const detection = await detectDbContexts(spec.cwd);
  const ctx = detection.contexts.find((c) => c.name === spec.contextName);
  if (!ctx) {
    return {
      success: false,
      cwd: spec.cwd,
      baseBranch: baseRef,
      currentBranch,
      assemblies: [],
      warnings,
      error: `Context "${spec.contextName}" not detected in worktree`,
    };
  }

  const targets = spec.assembly
    ? ctx.assemblies.filter((a) => a.assemblyName === spec.assembly)
    : ctx.assemblies;

  if (targets.length === 0) {
    return {
      success: false,
      cwd: spec.cwd,
      baseBranch: baseRef,
      currentBranch,
      assemblies: [],
      warnings,
      error: `No migration assemblies matched filter`,
    };
  }

  const startupProject = (await findStartupProject(spec.cwd)) ?? undefined;
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const backupRoot = path.join(spec.cwd, '.efcore-squash-backup', timestamp);

  const results: SquashAssemblyResult[] = [];

  for (const asm of targets) {
    const branchOnlyFiles = await getBranchOnlyMigrationFiles(asm.migrationsDir, spec.cwd, baseRef, spec.aggressive);

    if (branchOnlyFiles.length < 2) {
      results.push({
        assemblyName: asm.assemblyName,
        contextName: spec.contextName,
        backupDir: null,
        deletedFiles: [],
        resetSnapshot: false,
        newMigrationName: null,
        dotnetOutput: '',
        success: true,
        error: branchOnlyFiles.length === 0
          ? 'No branch-only migrations — nothing to squash'
          : 'Only 1 branch-only migration — nothing to consolidate',
      });
      continue;
    }

    const assemblyBackup = path.join(backupRoot, asm.assemblyName);

    if (spec.dryRun) {
      results.push({
        assemblyName: asm.assemblyName,
        contextName: spec.contextName,
        backupDir: assemblyBackup,
        deletedFiles: branchOnlyFiles.map((f) => path.basename(f)),
        resetSnapshot: true,
        newMigrationName: buildMigrationName({
          contextName: spec.contextName,
          version: asm.version,
          sequence: 1,
          description: spec.description,
        }),
        dotnetOutput: '[dry-run] dotnet ef migrations add would run here',
        success: true,
      });
      continue;
    }

    // 1. Backup
    await backupFiles([asm.snapshotPath, ...branchOnlyFiles, ...branchOnlyFiles.map((f) => f.replace(/\.cs$/, '.Designer.cs'))], assemblyBackup, spec.cwd);

    // 2. Delete branch-only files (+ designers)
    const deleted: string[] = [];
    for (const f of branchOnlyFiles) {
      await removeFile(f);
      deleted.push(path.basename(f));
      const designer = f.replace(/\.cs$/, '.Designer.cs');
      try {
        await fs.access(designer);
        await removeFile(designer);
        deleted.push(path.basename(designer));
      } catch {
        /* no designer */
      }
    }

    // 3. Reset snapshot to base
    const resetOk = await resetSnapshotToBase(asm.snapshotPath, spec.cwd, baseRef);
    if (!resetOk) {
      results.push({
        assemblyName: asm.assemblyName,
        contextName: spec.contextName,
        backupDir: assemblyBackup,
        deletedFiles: deleted,
        resetSnapshot: false,
        newMigrationName: null,
        dotnetOutput: '',
        success: false,
        error: `git checkout ${baseRef} -- ${asm.snapshotPath} failed`,
      });
      continue;
    }

    // 4. Compute sequence (count migrations on base) + build consolidated name
    const existingCount = (await listMigrationFilesInDir(asm.migrationsDir)).length;
    const newMigrationName = buildMigrationName({
      contextName: spec.contextName,
      version: asm.version,
      sequence: existingCount + 1,
      description: spec.description,
    });

    // 5. Regenerate via dotnet ef migrations add
    const extraEnv = asm.provider ? { STUDIO_DESIGN_PROVIDER: capitalize(asm.provider) } : undefined;
    const args = [
      'ef', 'migrations', 'add', newMigrationName,
      '--project', asm.csprojPath,
      '--context', spec.contextName,
      '--no-color',
    ];
    if (startupProject) args.push('--startup-project', startupProject);
    const addRes = await runDotnet(args, spec.cwd, { timeoutMs: 5 * 60_000, extraEnv });

    results.push({
      assemblyName: asm.assemblyName,
      contextName: spec.contextName,
      backupDir: assemblyBackup,
      deletedFiles: deleted,
      resetSnapshot: true,
      newMigrationName: addRes.exitCode === 0 ? newMigrationName : null,
      dotnetOutput: `${addRes.stdout}${addRes.stderr ? `\n${addRes.stderr}` : ''}`,
      success: addRes.exitCode === 0,
      error: addRes.exitCode === 0 ? undefined : addRes.stderr || addRes.stdout || 'dotnet ef migrations add failed',
    });
  }

  const allOk = results.every((r) => r.success);
  return {
    success: allOk,
    cwd: detection.cwd,
    baseBranch: baseRef,
    currentBranch,
    assemblies: results,
    warnings,
    error: allOk ? undefined : 'One or more assemblies failed — inspect per-assembly error',
  };
}
