#!/usr/bin/env node
/**
 * cli:scaffold-core-seed
 *
 * Generates 5 Core Foundation Seed providers per application (under
 * Persistence/Seeding/Applications/{AppPascal}/Core/) + patches
 * DependencyInjection.cs once per application from a multi-app
 * CoreSeedSpec JSON.
 *
 * Usage:
 *   npx tsx skills/development/backend/core-seed/cli/scaffold-core-seed/index.ts \
 *     --spec-file /path/to/core-seed-spec.json [--outdir /path/to/target-app] [--dry-run]
 *
 * Or with inline spec (small specs only — JSON gets quoted on the shell):
 *   npx tsx ... --spec '{"appCode":"TestV2","applications":[...],...}'
 *
 * Called by Phase 0 of /ba-develop (skill backend-core-seed).
 * The spec is built by the Studio backend (prepareCoreSeedContext) from the
 * BA tables and dropped to a temp file before the agent invokes this CLI.
 */
import { parseArgs } from 'node:util';
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
import { dirname, resolve, join } from 'node:path';
import { validate } from './validate.js';
import { generate, patchDependencyInjection } from './generate.js';
import { generateEnvelope, failGenerate, printEnvelope } from '../../../../../lib/output.js';
import type { CoreSeedSpec } from './types.js';

const COMMAND = 'scaffold-core-seed';

function main(): void {
  const { values } = parseArgs({
    options: {
      spec: { type: 'string' },
      'spec-file': { type: 'string' },
      outdir: { type: 'string' },
      'dry-run': { type: 'boolean', default: false },
    },
    strict: true,
  });

  const rawJson = loadSpecJson(values);
  if (rawJson === null) {
    printEnvelope(failGenerate(COMMAND, ['Either --spec or --spec-file is required']));
    process.exit(1);
  }

  let raw: unknown;
  try {
    raw = JSON.parse(rawJson);
  } catch {
    printEnvelope(failGenerate(COMMAND, ['Invalid JSON in spec']));
    process.exit(1);
  }

  const validation = validate(raw);
  if (!validation.valid) {
    printEnvelope(failGenerate(COMMAND, validation.errors));
    process.exit(1);
  }

  const spec = raw as CoreSeedSpec;
  const result = generate(spec);

  if (values['dry-run']) {
    printEnvelope(
      generateEnvelope(COMMAND, {
        data: {
          dryRun: true,
          files: result.files.map((f) => f.path),
          diBlocks: result.diRegistration.perApp.map((p) => ({
            appCode: p.appCode,
            appPascal: p.appPascal,
            block: p.markerBlock,
          })),
          diCandidates: result.diRegistration.candidatePaths,
          testUsersManifest: result.testUsersManifest?.path ?? null,
        },
        warnings: validation.warnings,
      }),
    );
    process.exit(0);
  }

  const outdir = values.outdir ?? spec.projectPath;
  const filesCreated: string[] = [];
  const filesModified: string[] = [];
  const warnings: string[] = [...validation.warnings];

  // 1. Write the per-application Core providers + optional shared test users manifest.
  for (const file of result.files) {
    const fullPath = resolve(join(outdir, file.path));
    mkdirSync(dirname(fullPath), { recursive: true });
    writeFileSync(fullPath, file.content, 'utf-8');
    filesCreated.push(fullPath);
  }
  if (result.testUsersManifest) {
    const fullPath = resolve(join(outdir, result.testUsersManifest.path));
    mkdirSync(dirname(fullPath), { recursive: true });
    writeFileSync(fullPath, result.testUsersManifest.content, 'utf-8');
    filesCreated.push(fullPath);
  }

  // 2. Patch DependencyInjection.cs (or sibling) once per application. Walk
  // candidates in priority order, patch the FIRST file that exists. Each
  // application's block has its own `<<< CORE-SEED-DI-{AppPascal} BEGIN/END >>>`
  // marker pair, so re-runs replace blocks idempotently per app.
  let diHostPath: string | null = null;
  for (const candidate of result.diRegistration.candidatePaths) {
    const fullPath = resolve(join(outdir, candidate));
    if (existsSync(fullPath)) {
      diHostPath = fullPath;
      break;
    }
  }

  const appsPatched: string[] = [];
  if (diHostPath !== null) {
    let source = readFileSync(diHostPath, 'utf-8');
    let dirty = false;
    for (const reg of result.diRegistration.perApp) {
      const next = patchDependencyInjection(source, reg);
      if (next !== null) {
        source = next;
        dirty = true;
      }
      appsPatched.push(reg.appCode);
    }
    if (dirty) {
      writeFileSync(diHostPath, source, 'utf-8');
      filesModified.push(diHostPath);
    }
  } else {
    const allBlocks = result.diRegistration.perApp.map((p) => p.markerBlock).join('\n\n');
    warnings.push(
      `No DependencyInjection.cs / ServiceCollectionExtensions.cs / *Module.cs found under src/${spec.appCode}.Infrastructure/. The Core providers will not be resolved at startup until they are registered. Register manually:\n${allBlocks}`,
    );
  }

  printEnvelope(
    generateEnvelope(COMMAND, {
      data: {
        appCode: spec.appCode,
        applications: spec.applications.map((a) => ({
          code: a.code,
          modules: a.modules,
          navCount: a.navigation.length,
          roleCount: a.roles.length,
          permissionCount: a.permissions.length,
          rolePermissionCount: a.rolePermissions.length,
          testUserCount: a.testUsers.length,
        })),
        applicationCount: spec.applications.length,
        diPatched: diHostPath !== null,
        diHostPath,
        appsPatched,
      },
      filesCreated,
      filesModified,
      warnings,
      nextSteps: [
        'Run `dotnet build` to verify the per-app Core providers compile.',
        'Run the Core Foundation Seed gate (verifyCoreFoundationSeed in ba-prd-develop-gates.ts) to confirm every PRD module has nav + roles + permissions.',
        diHostPath !== null
          ? 'DI registration is in place — the providers will be resolved at startup.'
          : 'DI registration MISSING — register the Core providers in DependencyInjection.cs (or equivalent). See the warning above.',
      ],
    }),
  );
}

function loadSpecJson(values: { spec?: string; 'spec-file'?: string }): string | null {
  if (values['spec-file']) {
    try {
      return readFileSync(values['spec-file'], 'utf-8');
    } catch (err) {
      printEnvelope(
        failGenerate(COMMAND, [
          `Failed to read --spec-file ${values['spec-file']}: ${err instanceof Error ? err.message : String(err)}`,
        ]),
      );
      process.exit(1);
    }
  }
  return values.spec ?? null;
}

main();
