#!/usr/bin/env node
/**
 * cli:scaffold-frontend-auth — Scaffolds useAuth + PermissionGuard.
 *
 * Idempotent : skips writes if the existing content matches; honours
 * `// @customised` (or `/* @customised *\/`) marker at the top.
 */
import { parseArgs } from 'node:util';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { validate } from './validate.js';
import { generate } from './generate.js';
import {
  generateEnvelope,
  failGenerate,
  printEnvelope,
} from '../../../../../lib/output.js';

const COMMAND = 'scaffold-frontend-auth';

function isCustomised(source: string): boolean {
  const head = source.trimStart().slice(0, 100);
  return head.startsWith('/* @customised') || head.startsWith('// @customised');
}

function main(): void {
  const { values } = parseArgs({
    options: {
      spec: { type: 'string' },
      'project-path': { type: 'string' },
      'app-code': { type: 'string' },
      dry_run: { type: 'boolean', default: false },
    },
    strict: true,
  });

  // Three invocation modes (priority order) :
  //  - --spec '<json>' (Phase 3a orchestrated): the orchestrator passes a
  //    full JSON spec including projectPath + appCode.
  //  - --project-path <abs> --app-code <code> (sub-step runner direct
  //    invocation, post-cli-args-resolver fix): the runner hands over both
  //    the project path AND the canonical appCode it already knows from
  //    DevSubStepRunArgs. This is the production path — preferred over the
  //    legacy --project-path-only mode because deriveAppCode()'s fallback
  //    walks the project's web/ folder and can pick a stale '*-web' subdir
  //    created by a previous misrouted run, perpetuating the bug (e.g. the
  //    classic 'web/app-web/' regression where the auth scaffold lands in
  //    a placeholder dir while the real app is at 'web/{nameLower}-web/').
  //  - --project-path <abs> only (legacy + standalone CLI): falls back to
  //    deriveAppCode() heuristics. Kept for hand-running the CLI from a
  //    terminal without orchestration context, but flagged as fragile.
  let raw: unknown;
  if (values.spec) {
    try {
      raw = JSON.parse(values.spec);
    } catch {
      printEnvelope(failGenerate(COMMAND, ['Invalid JSON in --spec']));
      process.exit(1);
    }
  } else if (values['project-path']) {
    const projectPath = resolve(values['project-path']);
    const appCode = values['app-code'] ?? deriveAppCode(projectPath);
    raw = {
      projectPath,
      appCode,
      force: false,
    };
  } else {
    printEnvelope(failGenerate(COMMAND, ['--spec or --project-path is required']));
    process.exit(1);
  }

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

  const spec = validation.spec;
  const files = generate(spec);

  if (values.dry_run) {
    printEnvelope(generateEnvelope(COMMAND, {
      data: { dryRun: true, files: files.map((f) => f.path) },
      warnings: validation.warnings,
    }));
    process.exit(0);
  }

  const filesCreated: string[] = [];
  const filesModified: string[] = [];
  const skipped: string[] = [];

  for (const file of files) {
    const abs = resolve(join(spec.projectPath, file.path));
    if (existsSync(abs)) {
      const existing = readFileSync(abs, 'utf-8');
      if (isCustomised(existing)) {
        skipped.push(file.path);
        continue;
      }
      if (!spec.force && existing === file.content) continue;
      mkdirSync(dirname(abs), { recursive: true });
      writeFileSync(abs, file.content, 'utf-8');
      filesModified.push(abs);
    } else {
      mkdirSync(dirname(abs), { recursive: true });
      writeFileSync(abs, file.content, 'utf-8');
      filesCreated.push(abs);
    }
  }

  const nextSteps: string[] = [];
  if (skipped.length > 0) {
    nextSteps.push(`Skipped ${skipped.length} customised file(s): ${skipped.join(', ')}. Remove the @customised marker to allow overwrites.`);
  }
  if (filesCreated.length === 0 && filesModified.length === 0 && skipped.length === 0) {
    nextSteps.push('Auth primitives already up-to-date — no changes needed.');
  }
  if (filesCreated.length > 0) {
    nextSteps.push('Verify the backend exposes `GET /api/auth/me` returning `{ user, permissions: string[] }`.');
    nextSteps.push('Components can now import `<PermissionGuard permission="..." />` from `@/components/auth/PermissionGuard` and `useAuth()` from `@/business/auth/useAuth`.');
  }

  printEnvelope(generateEnvelope(COMMAND, {
    data: { fileCount: filesCreated.length + filesModified.length, skipped: skipped.length },
    filesCreated,
    filesModified,
    warnings: validation.warnings,
    nextSteps,
  }));
}

/**
 * Best-effort appCode derivation when the orchestrator only hands us a
 * projectPath (i.e. neither `--spec` nor `--app-code` was provided —
 * typically a hand-run from a terminal). Strategy, ordered by reliability :
 *
 *   1. Read `.smartstack/init-state.json > projectConfig.nameLower`. This
 *      is the canonical appCode emitted by `smartstack-studio init` and
 *      stays stable for the project's lifetime.
 *   2. Fallback : take the basename of projectPath (e.g. "TestV2") and
 *      lowercase it. Works for any project that follows the convention
 *      `<projectName>/.smartstack/...` even when init-state was deleted.
 *   3. Last-resort fallback : `'app'`. Returned only when both reads above
 *      throw — at that point we can't tell anything from the disk.
 *
 * The previous heuristic walked `web/{*-web}/` and returned the first match.
 * That bit us hard in production : once a stray `web/app-web/` had been
 * created by a misrouted earlier run (because `appCode` defaulted to
 * `'app'` somewhere), every subsequent run picked it again — the bug was
 * self-reinforcing. Reading `.smartstack/init-state.json` cuts the loop
 * because the file is written ONCE at project init by the canonical name.
 */
function deriveAppCode(projectPath: string): string {
  // 1. init-state.json — authoritative source written by `smartstack-studio init`.
  try {
    const initStatePath = join(projectPath, '.smartstack', 'init-state.json');
    if (existsSync(initStatePath)) {
      const parsed = JSON.parse(readFileSync(initStatePath, 'utf-8')) as {
        projectConfig?: { nameLower?: unknown };
      };
      const nameLower = parsed?.projectConfig?.nameLower;
      if (typeof nameLower === 'string' && nameLower.length > 0) {
        return nameLower;
      }
    }
  } catch {
    /* fall through to basename heuristic */
  }
  // 2. Basename of the projectPath, lowercased.
  try {
    return basename(projectPath).toLowerCase();
  } catch {
    return 'app';
  }
}

main();
