import path from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { findFiles } from '../../../lib/fs.js';
import { detectDbContexts, findStartupProject, hasDotnetProject } from '../lib/detect-dbcontexts.js';
import { listMigrationsJson, dotnetAvailable, dotnetEfAvailable } from '../lib/ef-runner.js';
import { determineBaseBranch } from '../lib/squash-base.js';
import { findMissingReferenceMigrations, referenceMigrationNames } from '../lib/squash-integrity.js';
import type { AssemblyStatus, StatusResult, StatusSpec } from './types.js';

const execFileAsync = promisify(execFile);

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

/**
 * Resolve the reference branch BY TYPE — same GOLDEN RULE as the squash
 * (feature→develop, release/hotfix/develop→main). The legacy "develop first"
 * order mis-reported the branch-only count on release/hotfix branches.
 *
 * Returns `{ name, resolved }`. `resolved` is false when neither `origin/<name>`
 * nor `<name>` exists — the caller must then skip the branch-only computation
 * instead of treating every local migration as branch-only (finding #2).
 */
async function resolveBaseBranch(
  cwd: string,
  currentBranch: string,
  preferred?: string,
): Promise<{ name: string; resolved: boolean }> {
  const decision = determineBaseBranch(currentBranch);
  // On a protected branch nothing is "branch-only" → compare against itself.
  const wanted = preferred ?? (decision.blocked ? currentBranch : (decision.baseBranch as string));
  for (const candidate of [`origin/${wanted}`, wanted]) {
    const r = await git(['rev-parse', '--verify', '--quiet', candidate], cwd);
    if (r.exitCode === 0 && r.stdout) return { name: candidate, resolved: true };
  }
  return { name: wanted, resolved: false };
}

/** Pure migration `.cs` files on disk in `dir` — excludes ModelSnapshot and .Designer.cs sidecars. */
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') && !f.endsWith('.Designer.cs'),
  );
}

interface MigrationDiff {
  /** ✂️ Migrations unique to the current branch (tracked, not in the reference). */
  branchOnly: string[];
  /** 🔒 Reference-branch migration names (the preserved/untouchable set). */
  parentMigrations: string[];
  /** ☠️ Reference migrations absent from the working tree — a merge would drop them from prod. */
  missingFromWorkingTree: string[];
}

async function diffMigrations(migrationsDir: string, cwd: string, base: string): Promise<MigrationDiff> {
  const empty: MigrationDiff = { branchOnly: [], parentMigrations: [], missingFromWorkingTree: [] };
  const rel = path.relative(cwd, migrationsDir).replace(/\\/g, '/');
  if (!rel || rel.startsWith('..')) return empty;

  const baseResult = await git(['ls-tree', '-r', '--name-only', base, rel], cwd);
  // If the reference tree can't be read, we can't tell what's branch-only —
  // report nothing rather than flag every migration (finding #2).
  if (baseResult.exitCode !== 0) return empty;
  const basePaths = baseResult.stdout
    .split('\n')
    .filter((l) => l.endsWith('.cs') && !l.endsWith('ModelSnapshot.cs') && !l.endsWith('.Designer.cs'));
  const baseSet = new Set(basePaths);

  // branch-only is computed against TRACKED files (git ls-files), preserving the
  // prior semantics; missing-from-working-tree is computed against DISK truth.
  const current = await git(['ls-files', rel], cwd);
  const currentSet = new Set(
    (current.exitCode === 0 ? current.stdout.split('\n') : [])
      .filter((l) => l.endsWith('.cs') && !l.endsWith('ModelSnapshot.cs') && !l.endsWith('.Designer.cs')),
  );
  const onDisk = await listMigrationFilesInDir(migrationsDir);

  return {
    branchOnly: Array.from(currentSet)
      .filter((f) => !baseSet.has(f))
      .map((f) => path.basename(f, '.cs')),
    parentMigrations: referenceMigrationNames(basePaths),
    missingFromWorkingTree: findMissingReferenceMigrations(basePaths, onDisk),
  };
}

async function isSnapshotDirty(snapshotPath: string, cwd: string): Promise<boolean> {
  const rel = path.relative(cwd, snapshotPath).replace(/\\/g, '/');
  const r = await git(['status', '--porcelain', '--', rel], cwd);
  return r.stdout.length > 0;
}

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

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

  if (!(await hasDotnetProject(spec.cwd))) {
    return {
      success: true,
      cwd: spec.cwd,
      kind: 'unknown',
      currentBranch: '',
      baseBranch: '',
      baseResolved: false,
      behindBase: 0,
      assemblies: [],
      warnings: ['No .csproj found in worktree.'],
    };
  }

  if (!(await dotnetAvailable())) {
    return {
      success: false,
      cwd: spec.cwd,
      kind: 'unknown',
      currentBranch: '',
      baseBranch: '',
      baseResolved: false,
      behindBase: 0,
      assemblies: [],
      warnings,
      error: 'dotnet CLI is not available on PATH',
    };
  }

  if (!(await dotnetEfAvailable())) {
    warnings.push('dotnet-ef is not installed globally — install via: dotnet tool install --global dotnet-ef');
  }

  const currentBranch = (await git(['branch', '--show-current'], spec.cwd)).stdout || 'HEAD';
  const branchType = determineBaseBranch(currentBranch).branchType;
  const { name: baseBranch, resolved: baseResolved } = await resolveBaseBranch(spec.cwd, currentBranch, spec.baseBranch);

  // Behind-base reporting (finding #1) — squashing while behind the reference
  // can DROP reference-branch migrations.
  let behindBase = 0;
  if (baseResolved) {
    const r = await git(['rev-list', '--count', `HEAD..${baseBranch}`], spec.cwd);
    if (r.exitCode === 0) {
      const n = Number(r.stdout.trim());
      behindBase = Number.isFinite(n) ? n : 0;
    }
    if (behindBase > 0) {
      warnings.push(
        `⚠️ "${currentBranch}" is ${behindBase} commit(s) behind "${baseBranch}". ` +
          `Sync before squashing (e.g. /gitflow sync) — squashing while behind can DROP reference-branch migrations.`,
      );
    }
  } else {
    warnings.push(
      `Reference branch "${baseBranch}" not found (fetch it: git fetch origin ${baseBranch.replace(/^origin\//, '')}). ` +
        `branch-only migrations were not computed.`,
    );
  }

  const detection = await detectDbContexts(spec.cwd);
  const startupProject = await findStartupProject(spec.cwd);

  const assemblies: AssemblyStatus[] = [];

  for (const ctx of detection.contexts) {
    for (const asm of ctx.assemblies) {
      const extraEnv = asm.provider
        ? { STUDIO_DESIGN_PROVIDER: capitalize(asm.provider) }
        : undefined;

      const listResult = await listMigrationsJson(spec.cwd, {
        context: ctx.name,
        projectPath: asm.csprojPath,
        startupProjectPath: startupProject ?? undefined,
        extraEnv,
      });

      const diff = baseResolved
        ? await diffMigrations(asm.migrationsDir, spec.cwd, baseBranch)
        : { branchOnly: [], parentMigrations: [], missingFromWorkingTree: [] };
      const snapshotDirty = await isSnapshotDirty(asm.snapshotPath, spec.cwd);

      assemblies.push({
        assemblyName: asm.assemblyName,
        contextName: ctx.name,
        migrationsDir: asm.migrationsDir,
        provider: asm.provider,
        version: asm.version,
        total: listResult.migrations.length,
        applied: listResult.migrations.filter((m) => m.applied).length,
        pending: listResult.migrations.filter((m) => !m.applied).length,
        branchOnly: diff.branchOnly,
        parentMigrations: diff.parentMigrations,
        missingFromWorkingTree: diff.missingFromWorkingTree,
        snapshotDirty,
        error: listResult.success ? undefined : listResult.error,
      });
    }
  }

  // The prod-break signal: any reference migration missing from the working
  // tree means a manual re-baseline already removed an already-applied
  // migration. Surface it FIRST and loudly — a merge would drop it from prod.
  const missing = Array.from(new Set(assemblies.flatMap((a) => a.missingFromWorkingTree)));
  if (missing.length > 0) {
    warnings.unshift(
      `☠️ DANGER: ${missing.length} migration(s) exist on "${baseBranch}" but are MISSING from your working tree (${missing.join(', ')}). ` +
        `A merge would DROP them from production. Restore them (git checkout ${baseBranch} -- <Migrations dir>), ` +
        `or — only for a deliberate release re-baseline — use /efcore -BruteForce (logged to .claude/Efcore-history). DO NOT merge as-is.`,
    );
  }

  return {
    success: true,
    cwd: detection.cwd,
    kind: detection.kind,
    currentBranch,
    baseBranch,
    baseResolved,
    behindBase,
    branchType,
    assemblies,
    warnings,
  };
}
