/**
 * `everystack runbook` — compile docs/RUNBOOK.md, the per-app operations manual.
 *
 * db:generate for documentation (docs/plans/everystack-runbook.md): pure
 * detection → templates keyed by the app's reality → es:gen/es:slot merge that
 * preserves human words. The pure core lives in ../runbook/; this shell is the
 * IO: read the old file, load the app's Models for the data-model chapter,
 * write/check/diff, map to exit codes.
 *
 *   everystack runbook            generate or update (slot content preserved)
 *   everystack runbook --check    exit 1 if regeneration would change the file
 *   everystack runbook --diff     name the sections that would change; write nothing
 *   everystack runbook --force    overwrite hand-edits inside generated blocks
 */

import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { detectProjectReality } from '../runbook/detect.js';
import { generateRunbook, type RunbookResult } from '../runbook/generate.js';
import { renderDataModelSection } from '../runbook/model-section.js';
import { parseRunbook, sha8 } from '../runbook/markers.js';
import { fail, info, step, success, warn } from '../output.js';

const OUT_REL = path.join('docs', 'RUNBOOK.md');

/** This package's version, for the provenance stamp. tsx polyfills __dirname in ESM; ts-jest is CJS. */
function cliVersion(): string {
  try {
    const dir = eval('__dirname') as string;
    const pkg = JSON.parse(fs.readFileSync(path.join(dir, '..', '..', '..', 'package.json'), 'utf8'));
    return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
  } catch {
    return '0.0.0';
  }
}

/** Import the app's Model barrel for the data-model chapter. Absent or unloadable → honest warning, no chapter. */
async function loadDataModel(root: string, hasIndex: boolean): Promise<{ dataModel: string | null; warning: string | null }> {
  if (!hasIndex) return { dataModel: null, warning: null };
  const barrel = path.join(root, 'models', 'index.ts');
  try {
    const mod = await import(pathToFileURL(barrel).href);
    const models = mod.models ?? mod.default;
    if (!Array.isArray(models)) {
      return { dataModel: null, warning: `models/index.ts exports no \`models\` array — data-model chapter skipped.` };
    }
    return { dataModel: renderDataModelSection(models), warning: null };
  } catch (err: any) {
    return {
      dataModel: null,
      warning: `Could not load models from ${barrel} (${err.message}) — data-model chapter skipped.`,
    };
  }
}

export interface RunbookRun {
  outPath: string;
  oldText: string | null;
  result: RunbookResult;
  modelWarning: string | null;
}

/** Result-producing core, shared by the command, `--check`, and the `audit` capstone. */
export async function runRunbook(root: string): Promise<RunbookRun> {
  const reality = detectProjectReality(root);
  const { dataModel, warning } = await loadDataModel(reality.root, reality.models.hasIndex);
  const outPath = path.join(reality.root, OUT_REL);
  const oldText = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : null;
  const result = generateRunbook(reality, { cliVersion: cliVersion(), dataModel }, oldText ?? undefined);
  return { outPath, oldText, result, modelWarning: warning };
}

export interface RunbookCheck {
  status: 'missing' | 'stale' | 'current';
  conflicts: string[];
}

/** Is docs/RUNBOOK.md current? Pure read — writes nothing. */
export async function checkRunbook(root: string): Promise<RunbookCheck> {
  const { oldText, result } = await runRunbook(root);
  if (oldText === null) return { status: 'missing', conflicts: [] };
  return { status: result.changed ? 'stale' : 'current', conflicts: result.conflicts };
}

export interface RunbookDiffSummary {
  added: string[];
  removed: string[];
  changed: string[];
}

/** Section-level diff: which gen blocks a regeneration would add, remove, or rewrite. */
export function diffSummary(oldText: string, newText: string): RunbookDiffSummary {
  const genShas = (text: string) =>
    new Map(
      parseRunbook(text).blocks.flatMap((b) => (b.kind === 'gen' ? [[b.id, sha8(b.content)] as const] : [])),
    );
  const before = genShas(oldText);
  const after = genShas(newText);
  return {
    added: [...after.keys()].filter((id) => !before.has(id)),
    removed: [...before.keys()].filter((id) => !after.has(id)),
    changed: [...after.keys()].filter((id) => before.has(id) && before.get(id) !== after.get(id)),
  };
}

export async function runbookCommand(flags: Record<string, string>): Promise<void> {
  const root = flags.dir || '.';
  let run: RunbookRun;
  try {
    run = await runRunbook(root);
  } catch (err: any) {
    fail(err.message);
    process.exit(1);
  }
  if (run.modelWarning) warn(run.modelWarning);
  const rel = path.relative(process.cwd(), run.outPath) || OUT_REL;

  if ('check' in flags) {
    if (run.oldText === null) {
      fail(`${rel} is missing — run \`everystack runbook\` to generate it.`);
      process.exit(1);
    }
    if (run.result.conflicts.length) {
      warn(`Hand-edited generated section(s): ${run.result.conflicts.join(', ')} — move those words to a slot or Notes.`);
    }
    if (run.result.changed) {
      fail(`${rel} is stale — run \`everystack runbook\` to regenerate.`);
      process.exit(1);
    }
    success(`${rel} is current.`);
    process.exit(0);
  }

  if ('diff' in flags) {
    if (run.oldText === null) {
      info(`${rel} does not exist yet — \`everystack runbook\` would create it.`);
      process.exit(0);
    }
    if (!run.result.changed) {
      success(`${rel} is current — nothing would change.`);
      process.exit(0);
    }
    const summary = diffSummary(run.oldText, run.result.text);
    if (summary.added.length) info(`sections added:   ${summary.added.join(', ')}`);
    if (summary.removed.length) info(`sections removed: ${summary.removed.join(', ')}`);
    if (summary.changed.length) info(`sections updated: ${summary.changed.join(', ')}`);
    if (run.result.conflicts.length) {
      warn(`hand-edits that would be overwritten: ${run.result.conflicts.join(', ')} (regenerate with --force)`);
    }
    process.exit(0);
  }

  // default: write
  if (run.result.conflicts.length && !('force' in flags)) {
    fail(
      `Generated section(s) were hand-edited: ${run.result.conflicts.join(', ')}. ` +
        'Move those words into a slot or the Notes section, or rerun with --force to overwrite them.',
    );
    process.exit(1);
  }
  if (run.oldText !== null && !run.result.changed) {
    success(`${rel} is already current.`);
    process.exit(0);
  }
  step(`Writing ${rel} ...`);
  fs.mkdirSync(path.dirname(run.outPath), { recursive: true });
  fs.writeFileSync(run.outPath, run.result.text);
  success(
    run.oldText === null
      ? `${rel} generated. Fill the marked slots (overview, stage-notes, notes) — they survive every regeneration.`
      : `${rel} regenerated. Slot content preserved.`,
  );
  process.exit(0);
}
