import path from 'node:path';
import { existsSync, readFileSync } from 'node:fs';
import { findFiles } from '../../../lib/fs.js';
import { detectDbContexts, findStartupProject, hasDotnetProject } from '../lib/detect-dbcontexts.js';
import { runDotnet, dotnetAvailable, dotnetEfAvailable, buildMigrationsAddArgs, migrationsOutputDir } from '../lib/ef-runner.js';
import { buildMigrationName } from '../lib/migration-name.js';
import { getCurrentBranch } from '../../../lib/git.js';
import { decideMigrationAction, hasDestructiveUp } from '../lib/migration-policy.js';
import { ensureSqlObjectsInMigration, previewSqlObjectsForMigration } from '../../../lib/sql-objects.js';
import type { CreateSpec, CreateResult, CreateAssemblyResult } from './types.js';

/** Extract the Up() body from a generated migration .cs so the destructive scan never
 *  sees the Down() inverse (whose ops are legitimately the opposite). Heuristic but
 *  robust: Up() always precedes Down() in an EF-generated migration. */
function extractUpBody(csContent: string): string {
  const up = csContent.indexOf('void Up(');
  if (up === -1) return '';
  const down = csContent.indexOf('void Down(', up);
  return csContent.slice(up, down > up ? down : undefined);
}

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 countExistingMigrations(migrationsDir: string): Promise<number> {
  const files = await findFiles('*.cs', { cwd: migrationsDir });
  return files.filter(
    (f) =>
      !f.endsWith('ModelSnapshot.cs') &&
      !f.endsWith('.Designer.cs') &&
      path.dirname(f) === migrationsDir,
  ).length;
}

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

  if (!(await hasDotnetProject(spec.cwd))) {
    return {
      success: false,
      cwd: spec.cwd,
      contextName: spec.contextName,
      assemblies: [],
      warnings,
      error: 'No .csproj in worktree — cannot create a migration.',
    };
  }

  if (!spec.dryRun) {
    if (!(await dotnetAvailable())) {
      return {
        success: false,
        cwd: spec.cwd,
        contextName: spec.contextName,
        assemblies: [],
        warnings,
        error: 'dotnet CLI is not available on PATH',
      };
    }
    if (!(await dotnetEfAvailable())) {
      return {
        success: false,
        cwd: spec.cwd,
        contextName: spec.contextName,
        assemblies: [],
        warnings,
        error: 'dotnet-ef is not installed — run: dotnet tool install --global dotnet-ef',
      };
    }
  }

  // 3-tier policy gate (migration-policy): never mint a migration on a protected
  // production branch (🔴); require confirmation on release/hotfix (🟡). feature /
  // develop are 🟢 — the snapshot is reconciled by squash-before-PR at merge time.
  const branch = (await getCurrentBranch(spec.cwd)) ?? 'HEAD';
  const policy = decideMigrationAction({ op: 'create', branch });
  if (policy.blocked) {
    return { success: false, cwd: spec.cwd, contextName: spec.contextName, assemblies: [], warnings, policy, blocked: true, error: policy.reason };
  }
  if (policy.requiresConfirmation && !spec.confirm) {
    return {
      success: false,
      cwd: spec.cwd,
      contextName: spec.contextName,
      assemblies: [],
      warnings: [...warnings, `Re-run with "confirm": true to create on ${branch} — ${policy.reason}`],
      policy,
      needsConfirmation: true,
      error: policy.reason,
    };
  }

  const detection = await detectDbContexts(spec.cwd);
  const ctx = detection.contexts.find((c) => c.name === spec.contextName);
  if (!ctx) {
    return {
      success: false,
      cwd: spec.cwd,
      contextName: spec.contextName,
      assemblies: [],
      warnings,
      error: `Context "${spec.contextName}" not found in worktree. Detected: ${detection.contexts.map((c) => c.name).join(', ') || 'none'}`,
    };
  }

  const targetAssemblies = spec.assembly
    ? ctx.assemblies.filter((a) => a.assemblyName === spec.assembly)
    : spec.allProviders
      ? ctx.assemblies
      : [ctx.assemblies[0]].filter(Boolean);

  if (targetAssemblies.length === 0) {
    return {
      success: false,
      cwd: spec.cwd,
      contextName: spec.contextName,
      assemblies: [],
      warnings,
      error: spec.assembly
        ? `Assembly "${spec.assembly}" not found for context ${spec.contextName}`
        : `No migration assembly detected for context ${spec.contextName}`,
    };
  }

  const startupProject = spec.startupProject ?? (await findStartupProject(spec.cwd)) ?? undefined;
  if (!startupProject && !spec.dryRun) {
    warnings.push('No startup project detected — dotnet ef may fail. Pass --startupProject if necessary.');
  }

  const results: CreateAssemblyResult[] = [];

  for (const asm of targetAssemblies) {
    const sequence = (await countExistingMigrations(asm.migrationsDir)) + 1;
    const migrationName = buildMigrationName({
      contextName: spec.contextName,
      version: asm.version,
      sequence,
      description: spec.description,
    });

    if (spec.dryRun) {
      const wouldInline = await previewSqlObjectsForMigration({
        projectDir: path.dirname(asm.csprojPath),
        migrationsDir: asm.migrationsDir,
      });
      results.push({
        assemblyName: asm.assemblyName,
        migrationName,
        csprojPath: asm.csprojPath,
        output: `[dry-run] dotnet ef migrations add ${migrationName} --project ${asm.csprojPath} --context ${spec.contextName} --output-dir ${migrationsOutputDir(asm.csprojPath, asm.migrationsDir)}${startupProject ? ` --startup-project ${startupProject}` : ''}${wouldInline.length ? ` + inline ${wouldInline.length} SQL object(s): ${wouldInline.join(', ')}` : ''}`,
        success: true,
        filesCreated: [],
        sqlObjectsInlined: wouldInline,
      });
      continue;
    }

    const extraEnv = asm.provider ? { STUDIO_DESIGN_PROVIDER: capitalize(asm.provider) } : undefined;

    const args = buildMigrationsAddArgs({
      migrationName,
      csprojPath: asm.csprojPath,
      contextName: spec.contextName,
      migrationsDir: asm.migrationsDir,
      startupProject,
    });

    const r = await runDotnet(args, spec.cwd, { timeoutMs: 5 * 60_000, extraEnv });
    const success = r.exitCode === 0;

    const filesCreated = success
      ? [
          path.join(asm.migrationsDir, `${migrationName}.cs`),
          path.join(asm.migrationsDir, `${migrationName}.Designer.cs`),
          asm.snapshotPath,
        ]
      : [];

    // Freeze any new/changed SQL objects (functions, views, procs) from the
    // project's SqlObjects folder INTO the generated migration — they are not
    // tracked by the EF model, so `migrations add` never emits them.
    let sqlObjectsInlined: string[] = [];
    if (success) {
      try {
        const ensure = await ensureSqlObjectsInMigration({
          projectDir: path.dirname(asm.csprojPath),
          migrationsDir: asm.migrationsDir,
          migrationName,
        });
        sqlObjectsInlined = ensure.inlined;
        if (ensure.unparsable.length > 0) {
          warnings.push(
            `${asm.assemblyName}: ${ensure.unparsable.length} .sql file(s) under SqlObjects have no CREATE [OR ALTER] header — not inlined: ${ensure.unparsable.join(', ')}`,
          );
        }
        if (ensure.injected) {
          warnings.push(
            `${asm.assemblyName}: inlined ${ensure.inlined.length} SQL object(s) into ${migrationName} so \`database update\` deploys them: ${ensure.inlined.join(', ')}`,
          );
        }
      } catch (e) {
        warnings.push(`${asm.assemblyName}: SQL-object inlining skipped (${(e as Error).message}).`);
      }
    }

    // Surface a data-losing Up() (Drop/narrow) so the caller reviews before applying
    // or committing — a 🟡 signal even on a 🟢 branch (often an unintended rename).
    let destructive = false;
    if (success) {
      const migFile = path.join(asm.migrationsDir, `${migrationName}.cs`);
      if (existsSync(migFile)) {
        try {
          destructive = hasDestructiveUp(extractUpBody(readFileSync(migFile, 'utf-8')));
        } catch {
          /* unreadable — skip the scan */
        }
      }
      if (destructive) {
        warnings.push(
          `${asm.assemblyName}: ${migrationName} has a DESTRUCTIVE Up() (Drop/narrow) — confirm it is an intended schema change, not an unintended rename, before applying or committing.`,
        );
      }
    }

    results.push({
      assemblyName: asm.assemblyName,
      migrationName,
      csprojPath: asm.csprojPath,
      output: `${r.stdout}${r.stderr ? `\n${r.stderr}` : ''}`,
      success,
      error: success ? undefined : r.stderr || r.stdout || 'dotnet ef migrations add failed',
      filesCreated,
      sqlObjectsInlined,
      destructive,
    });
  }

  const allOk = results.every((r) => r.success);
  return {
    success: allOk,
    cwd: detection.cwd,
    contextName: spec.contextName,
    assemblies: results,
    warnings,
    policy,
    error: allOk ? undefined : 'One or more assemblies failed to add migration — see per-assembly error',
  };
}
