/**
 * ef-runner.ts — Spawn wrapper for `dotnet ef` commands.
 *
 * `dotnet ef` must be on the PATH (`dotnet tool install --global dotnet-ef`).
 * On Windows the global tools cache is at `%USERPROFILE%\.dotnet\tools`; under
 * Git Bash `$HOME/.dotnet/tools`. Both are prepended to PATH so the wrapper
 * works from either shell.
 */
import { execFile } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';

export interface EfRunResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

function buildPathEnv(): NodeJS.ProcessEnv {
  const env = { ...process.env };
  const home = os.homedir();
  const candidates = [
    path.join(home, '.dotnet', 'tools'),
    process.env.USERPROFILE ? path.join(process.env.USERPROFILE, '.dotnet', 'tools') : null,
  ].filter((p): p is string => Boolean(p));
  const sep = process.platform === 'win32' ? ';' : ':';
  const existing = env.PATH ?? env.Path ?? '';
  const merged = [...candidates, existing].filter(Boolean).join(sep);
  env.PATH = merged;
  env.Path = merged;
  return env;
}

export function runDotnet(
  args: string[],
  cwd: string,
  opts: { timeoutMs?: number; extraEnv?: NodeJS.ProcessEnv } = {},
): Promise<EfRunResult> {
  const timeout = opts.timeoutMs ?? 180_000;
  return new Promise((resolve) => {
    execFile(
      'dotnet',
      args,
      {
        cwd,
        encoding: 'utf-8',
        timeout,
        maxBuffer: 20 * 1024 * 1024,
        env: { ...buildPathEnv(), ...(opts.extraEnv ?? {}) },
      },
      (error, stdout, stderr) => {
        resolve({
          stdout: (stdout || '').trim(),
          stderr: (stderr || '').trim(),
          exitCode: error ? ((error as NodeJS.ErrnoException).code as number | undefined ?? 1) : 0,
        });
      },
    );
  });
}

/**
 * Compute the EF Core `--output-dir` value for `dotnet ef migrations add`.
 *
 * EF writes a new migration (and the model snapshot) to "Migrations/" by
 * default. SmartStack repos keep them under "Persistence/Migrations", so the
 * generated files must be told where the EXISTING migrations live — otherwise
 * they land in the wrong folder with the wrong namespace and the build breaks.
 * The path EF expects is relative to the PROJECT directory (the `--project`
 * .csproj folder), NOT the cwd, which is usually the solution root.
 *
 * Returns a forward-slash path ("Persistence/Migrations", or "Migrations" for
 * the default layout) so the value is identical on Windows and POSIX.
 */
export function migrationsOutputDir(csprojPath: string, migrationsDir: string): string {
  const rel = path.relative(path.dirname(csprojPath), migrationsDir);
  return rel.split(/[\\/]/).filter(Boolean).join('/') || 'Migrations';
}

/**
 * Build the full `dotnet ef migrations add` argument vector. Shared by the
 * create and squash CLIs so the command can never drift again: the missing
 * `--output-dir` regression came from three hand-maintained copies of this
 * array, one of which silently lacked the flag.
 */
export function buildMigrationsAddArgs(opts: {
  migrationName: string;
  csprojPath: string;
  contextName: string;
  migrationsDir: string;
  startupProject?: string;
}): string[] {
  const args = [
    'ef', 'migrations', 'add', opts.migrationName,
    '--project', opts.csprojPath,
    '--context', opts.contextName,
    '--output-dir', migrationsOutputDir(opts.csprojPath, opts.migrationsDir),
    '--no-color',
  ];
  if (opts.startupProject) args.push('--startup-project', opts.startupProject);
  return args;
}

/**
 * Fetch the list of migrations as parsed JSON. Uses `--json` which EF Core
 * 10 supports on `migrations list`. Fallback to text parsing if `--json` is
 * unavailable on the target SDK.
 */
export async function listMigrationsJson(
  cwd: string,
  opts: { context?: string; projectPath?: string; startupProjectPath?: string; extraEnv?: NodeJS.ProcessEnv } = {},
): Promise<{
  success: boolean;
  migrations: Array<{ id: string; name: string; applied: boolean }>;
  raw: string;
  error?: string;
}> {
  const args = ['ef', 'migrations', 'list', '--json', '--no-color'];
  if (opts.context) args.push('--context', opts.context);
  if (opts.projectPath) args.push('--project', opts.projectPath);
  if (opts.startupProjectPath) args.push('--startup-project', opts.startupProjectPath);

  const result = await runDotnet(args, cwd, { extraEnv: opts.extraEnv });
  if (result.exitCode !== 0) {
    return {
      success: false,
      migrations: [],
      raw: result.stdout + (result.stderr ? `\n${result.stderr}` : ''),
      error: result.stderr || result.stdout || 'dotnet ef migrations list failed',
    };
  }

  const parsed = extractJsonBlock(result.stdout);
  if (!parsed) {
    // Fallback: plain text parse (one migration per line, applied ones have no marker)
    const lines = result.stdout
      .split('\n')
      .map((l) => l.trim())
      .filter((l) => l && !l.startsWith('Build') && !l.startsWith('info:') && !l.includes('Done.'));
    return {
      success: true,
      raw: result.stdout,
      migrations: lines.map((line) => ({
        id: line.replace(/\s+\(Pending\)$/, ''),
        name: line.replace(/\s+\(Pending\)$/, ''),
        applied: !/\(Pending\)$/.test(line),
      })),
    };
  }

  const migrations = (Array.isArray(parsed) ? parsed : []).map((m) => ({
    id: String(m.id ?? m.name ?? ''),
    name: String(m.name ?? m.id ?? ''),
    applied: Boolean(m.applied),
  }));

  return { success: true, migrations, raw: result.stdout };
}

function extractJsonBlock(text: string): unknown | null {
  // `dotnet ef --json` emits a marker line around the JSON payload on newer SDKs.
  const lines = text.split('\n');
  const startIdx = lines.findIndex((l) => l.trim().startsWith('['));
  if (startIdx === -1) return null;
  // Find matching end bracket (greedy — the JSON is always the last array).
  for (let end = lines.length - 1; end >= startIdx; end--) {
    const block = lines.slice(startIdx, end + 1).join('\n').trim();
    if (!block.endsWith(']')) continue;
    try {
      return JSON.parse(block);
    } catch {
      continue;
    }
  }
  return null;
}

export async function dotnetAvailable(): Promise<boolean> {
  const res = await runDotnet(['--version'], process.cwd(), { timeoutMs: 5_000 });
  return res.exitCode === 0;
}

export async function dotnetEfAvailable(): Promise<boolean> {
  const res = await runDotnet(['ef', '--version'], process.cwd(), { timeoutMs: 10_000 });
  return res.exitCode === 0;
}
