import path from 'node:path';
import { existsSync, readFileSync } from 'node:fs';
import { detectDbContexts, findStartupProject, hasDotnetProject } from '../lib/detect-dbcontexts.js';
import { runDotnet, listMigrationsJson, dotnetAvailable, dotnetEfAvailable } from '../lib/ef-runner.js';
import { getCurrentBranch } from '../../../lib/git.js';
import { classifyDbTarget, decideMigrationAction } from '../lib/migration-policy.js';
import type { ApplySpec, ApplyResult, ApplyContextResult } from './types.js';

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

/**
 * Read `ConnectionStrings:DefaultConnection` from the startup project's appsettings,
 * preferring the dev override chain (Local > Development > base) — the same precedence
 * `ss dev` uses. Returns null when nothing resolves (→ classifier reports `unknown` →
 * the policy gates the apply as non-local, fail-closed).
 */
function readDefaultConnection(startupProjectPath: string | null): string | null {
  if (!startupProjectPath) return null;
  const dir = path.dirname(startupProjectPath);
  for (const file of ['appsettings.Local.json', 'appsettings.Development.json', 'appsettings.json']) {
    const p = path.join(dir, file);
    if (!existsSync(p)) continue;
    try {
      const json = JSON.parse(readFileSync(p, 'utf-8')) as { ConnectionStrings?: Record<string, unknown> };
      const cs = json?.ConnectionStrings?.DefaultConnection;
      if (typeof cs === 'string' && cs.trim()) return cs;
    } catch {
      /* malformed appsettings — try the next file */
    }
  }
  return null;
}

export async function execute(spec: ApplySpec): Promise<ApplyResult> {
  const warnings: string[] = [];
  const op: 'apply' | 'revert' = spec.targetMigration ? 'revert' : 'apply';
  const branch = (await getCurrentBranch(spec.cwd)) ?? 'HEAD';

  // Resolve the target DB + policy verdict up front (cheap, no dotnet build).
  const startupProject = spec.startupProject ?? (await findStartupProject(spec.cwd));
  const connString = spec.connectionString ?? readDefaultConnection(startupProject);
  const dbClass = classifyDbTarget(connString);
  const policy = decideMigrationAction({ op, branch, dbTarget: dbClass.target });

  const head = { cwd: spec.cwd, branch, op, dbTarget: dbClass.target, dbHost: dbClass.host, policy };

  // 🔴 — autonomous refusal. A human decides (e.g. a deploy to a shared DB).
  if (policy.blocked) {
    return { success: false, ...head, blocked: true, contexts: [], warnings, error: policy.reason };
  }
  // 🟡 — needs an explicit confirm token before we touch the DB.
  if (policy.requiresConfirmation && !spec.confirm) {
    return {
      success: false,
      ...head,
      needsConfirmation: true,
      contexts: [],
      warnings: [...warnings, `Re-run with "confirm": true to proceed — ${policy.reason}`],
      error: policy.reason,
    };
  }

  // 🟢 (or confirmed 🟡) — proceed. Now we may pay for dotnet.
  if (!(await hasDotnetProject(spec.cwd))) {
    return { success: false, ...head, contexts: [], warnings, error: 'No .csproj in worktree — nothing to apply.' };
  }
  if (!spec.dryRun) {
    if (!(await dotnetAvailable())) {
      return { success: false, ...head, contexts: [], warnings, error: 'dotnet CLI is not available on PATH' };
    }
    if (!(await dotnetEfAvailable())) {
      return { success: false, ...head, contexts: [], warnings, error: 'dotnet-ef is not installed — run: dotnet tool install --global dotnet-ef' };
    }
  }

  const detection = await detectDbContexts(spec.cwd);
  const contexts = spec.contextName
    ? detection.contexts.filter((c) => c.name === spec.contextName)
    : detection.contexts;
  if (contexts.length === 0) {
    return {
      success: false,
      ...head,
      contexts: [],
      warnings,
      error: spec.contextName
        ? `Context "${spec.contextName}" not found. Detected: ${detection.contexts.map((c) => c.name).join(', ') || 'none'}`
        : 'No DbContext detected in worktree.',
    };
  }

  const results: ApplyContextResult[] = [];
  for (const ctx of contexts) {
    const asm = ctx.assemblies[0]; // generated app: one assembly per context (runtime provider)
    if (!asm) continue;
    const extraEnv = asm.provider ? { STUDIO_DESIGN_PROVIDER: capitalize(asm.provider) } : undefined;

    const list = await listMigrationsJson(spec.cwd, {
      context: ctx.name,
      projectPath: asm.csprojPath,
      startupProjectPath: startupProject ?? undefined,
      extraEnv,
    });
    const pendingList = list.migrations.filter((m) => !m.applied).map((m) => m.name);
    const pending = pendingList.length;

    // Apply-to-latest with nothing pending → no-op (skip the build cost of update).
    if (op === 'apply' && pending === 0 && list.success) {
      results.push({ contextName: ctx.name, assemblyName: asm.assemblyName, pending: 0, applied: false, appliedMigrations: [], output: 'database is up to date', success: true });
      continue;
    }

    if (spec.dryRun) {
      results.push({
        contextName: ctx.name,
        assemblyName: asm.assemblyName,
        pending,
        applied: false,
        appliedMigrations: pendingList,
        output: `[dry-run] dotnet ef database update ${spec.targetMigration ?? '(latest)'} --context ${ctx.name}`,
        success: true,
      });
      continue;
    }

    const args = ['ef', 'database', 'update'];
    if (spec.targetMigration) args.push(spec.targetMigration);
    args.push('--context', ctx.name, '--project', asm.csprojPath, '--no-color');
    if (startupProject) args.push('--startup-project', startupProject);

    const r = await runDotnet(args, spec.cwd, { timeoutMs: 5 * 60_000, extraEnv });
    const success = r.exitCode === 0;
    results.push({
      contextName: ctx.name,
      assemblyName: asm.assemblyName,
      pending,
      applied: success,
      appliedMigrations: success ? pendingList : [],
      output: `${r.stdout}${r.stderr ? `\n${r.stderr}` : ''}`,
      success,
      error: success ? undefined : r.stderr || r.stdout || 'dotnet ef database update failed',
    });
  }

  const allOk = results.every((r) => r.success);
  return {
    success: allOk,
    ...head,
    cwd: detection.cwd,
    contexts: results,
    warnings,
    error: allOk ? undefined : 'One or more contexts failed to apply — see per-context error',
  };
}
