/**
 * scripts/generate-docs/index.ts
 *
 * Entry point of the SmartStack CLI documentation generator.
 * Reads `templates/skills/<...>/SKILL.md` + `docs-src/*.md`, renders the
 * Handlebars page templates, and writes everything to `.documentation/`.
 *
 * Modes:
 *   npx tsx scripts/generate-docs/index.ts            # build
 *   npx tsx scripts/generate-docs/index.ts --check    # exit 1 on drift
 *   npx tsx scripts/generate-docs/index.ts --watch    # rebuild on change
 */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { performance } from 'node:perf_hooks';

import { parseAllSkills } from './lib/skill-parser';
import { parseAllPages, type PageMeta } from './lib/markdown-parser';
import { validateSidebar, buildSidebarForPage } from './lib/sidebar-builder';
import { collectStats } from './lib/stats';
import { buildContext } from './lib/context-builder';
import { loadLayoutTemplate, loadPageTemplate } from './lib/handlebars-setup';

const REPO_ROOT = join(__dirname, '..', '..');
const DOCS_OUT = join(REPO_ROOT, '.documentation');

interface CliFlags {
  check: boolean;
  watch: boolean;
  verbose: boolean;
}

function parseFlags(argv: string[]): CliFlags {
  return {
    check: argv.includes('--check'),
    watch: argv.includes('--watch'),
    verbose: argv.includes('--verbose') || argv.includes('-v'),
  };
}

/**
 * Render one page to its final HTML string. The page template renders the
 * "body" — i.e. main content of the page — and the layout wraps it in the
 * shared HTML chrome (head, header, sidebar, footer).
 */
function renderPage(page: PageMeta, allSkills: ReturnType<typeof parseAllSkills>, stats: ReturnType<typeof collectStats>): string {
  const sidebar = buildSidebarForPage(page.slug);
  const ctx = buildContext(page, sidebar, allSkills, stats);

  // 1. Render the page template (often just `{{{bodyHtml}}}` plus auto-injected
  //    skill grids); the result is the inner content placed in the layout's
  //    {{{bodyHtml}}} slot.
  const pageTpl = loadPageTemplate(page.pageTemplate);
  const innerHtml = pageTpl(ctx);

  // 2. Render the outer layout with the inner content as its body.
  const layoutTpl = loadLayoutTemplate();
  const html = layoutTpl({ ...ctx, bodyHtml: innerHtml });

  // 3. Normalise line endings — committed files use LF (see .gitattributes).
  return html.replace(/\r\n/g, '\n');
}

interface RenderResult {
  generated: Map<string, string>;
  duration: number;
}

function build(): RenderResult {
  const t0 = performance.now();
  const skills = parseAllSkills();
  const pages = parseAllPages();
  validateSidebar(pages.map((p) => p.slug));
  const stats = collectStats();

  const generated = new Map<string, string>();
  for (const page of pages) {
    generated.set(page.slug, renderPage(page, skills, stats));
  }
  return { generated, duration: performance.now() - t0 };
}

function writeAll(generated: Map<string, string>): void {
  if (!existsSync(DOCS_OUT)) mkdirSync(DOCS_OUT, { recursive: true });
  for (const [slug, html] of generated) {
    writeFileSync(join(DOCS_OUT, `${slug}.html`), html, 'utf-8');
  }
}

function checkDrift(generated: Map<string, string>): number {
  let drift = 0;
  for (const [slug, expected] of generated) {
    const file = join(DOCS_OUT, `${slug}.html`);
    const actual = existsSync(file) ? readFileSync(file, 'utf-8').replace(/\r\n/g, '\n') : '';
    if (actual !== expected) {
      drift++;
      // eslint-disable-next-line no-console
      console.error(`DRIFT: .documentation/${slug}.html differs from generator output`);
      if (process.env.DOCS_CHECK_DIFF === '1') {
        // Minimal first-divergence diff — full diff via the `diff` package on demand
        const minLen = Math.min(actual.length, expected.length);
        let i = 0;
        while (i < minLen && actual[i] === expected[i]) i++;
        const ctx = (s: string) => s.slice(Math.max(0, i - 40), i + 80);
        // eslint-disable-next-line no-console
        console.error(`  first diff at offset ${i}:`);
        // eslint-disable-next-line no-console
        console.error(`    actual:   ...${JSON.stringify(ctx(actual))}...`);
        // eslint-disable-next-line no-console
        console.error(`    expected: ...${JSON.stringify(ctx(expected))}...`);
      }
    }
  }
  return drift;
}

async function main(): Promise<number> {
  const flags = parseFlags(process.argv.slice(2));

  const { generated, duration } = build();
  const count = generated.size;

  if (flags.check) {
    const drift = checkDrift(generated);
    if (drift > 0) {
      // eslint-disable-next-line no-console
      console.error(`\n${drift} file(s) out of sync. Run \`npm run build:docs\` to regenerate.`);
      return 1;
    }
    // eslint-disable-next-line no-console
    console.log(`docs in sync (${count} pages, ${duration.toFixed(0)}ms)`);
    return 0;
  }

  writeAll(generated);
  // eslint-disable-next-line no-console
  console.log(`Generated ${count} pages in ${duration.toFixed(0)}ms → .documentation/`);

  if (flags.watch) {
    const { watch } = await import('chokidar');
    const watcher = watch(
      [
        join(REPO_ROOT, 'docs-src', '**', '*'),
        join(REPO_ROOT, 'templates', 'skills', '**', 'SKILL.md'),
        join(__dirname, 'templates', '**', '*'),
      ],
      { ignoreInitial: true }
    );
    watcher.on('all', (event, path) => {
      try {
        // eslint-disable-next-line no-console
        console.log(`[${event}] ${path} → rebuild`);
        const { generated: g } = build();
        writeAll(g);
      } catch (err) {
        // eslint-disable-next-line no-console
        console.error('build failed:', err);
      }
    });
    // eslint-disable-next-line no-console
    console.log('watching for changes (Ctrl+C to stop)…');
    return new Promise<number>(() => undefined); // never resolves
  }

  return 0;
}

main().then(
  (code) => process.exit(code),
  (err) => {
    // eslint-disable-next-line no-console
    console.error(err);
    process.exit(2);
  }
);
