#!/usr/bin/env node
/*
 * render-report.mjs — convert llm-security markdown reports into self-contained
 * HTML files. Zero npm deps. Reads markdown from stdin or a file, parses via
 * PARSERS, renders via RENDERERS, wraps the output in a standalone HTML doc
 * with inlined Playground Design System CSS.
 *
 * Usage:
 *   node render-report.mjs <commandId> --in <md-file>             (file → reports/<command>-<ts>.html)
 *   node render-report.mjs <commandId> --in <md-file> --out <html>
 *   node render-report.mjs <commandId> --in -                     (stdin → reports/<command>-<ts>.html)
 *   node render-report.mjs <commandId> --in <md> --out -          (file → stdout)
 *   node render-report.mjs <commandId> --in - --out -             (stdin → stdout)
 *
 * Exit codes:
 *   0 success · 1 parser error or empty input · 2 renderer threw · 3 file I/O failed
 *
 * Standalone HTML wraps in <main class="page"> so DS Tier 3 page-shell styling
 * matches the playground render exactly. Output uses absolute file:// URLs to
 * stdout (Ghostty cmd-click requires absolute paths).
 */

import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { resolve, dirname, isAbsolute, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { PARSERS, RENDERERS } from './lib/report-renderers.mjs';

const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = resolve(SCRIPT_DIR, '..');
const VENDOR_CSS_DIR = join(PLUGIN_ROOT, 'playground', 'vendor', 'playground-design-system');

// DS stylesheets to inline. Skip fonts.css — tokens.css has system-ui
// fallbacks (-apple-system, BlinkMacSystemFont, system-ui, sans-serif),
// so reports stay self-contained without bundling ~940 KB of woff2.
const CSS_FILES = [
  'tokens.css',
  'base.css',
  'components.css',
  'components-tier2.css',
  'components-tier3.css',
  'components-tier3-supplement.css'
];

// .report-table is a playground-local DS supplement (DS doesn't ship it). Same
// styling lives in playground inline <style> block. Keep in sync with playground
// llm-security-playground.html (search for "v7.6.1 fix: .report-table").
const REPORT_TABLE_CSS = `
.report-table { width: 100%; border-collapse: collapse; margin: var(--space-3) 0; font-size: var(--font-size-sm); }
.report-table th { text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--color-border-moderate); background: var(--color-bg-soft); font-weight: var(--font-weight-semibold); color: var(--color-text-secondary); text-transform: uppercase; font-size: 11px; letter-spacing: 0.04em; }
.report-table td { padding: 8px 12px; border-bottom: 1px solid var(--color-border-subtle); vertical-align: top; color: var(--color-text-primary); }
.report-table tr:last-child td { border-bottom: none; }
.report-table tbody tr:hover { background: var(--color-bg-soft); }
.report-table code { font-family: var(--font-family-mono); font-size: 12px; background: var(--color-surface-sunken); padding: 1px 6px; border-radius: var(--radius-sm); }
.recommendation-card__body { overflow-wrap: anywhere; word-break: break-word; }
main.page { max-width: 1100px; margin: 0 auto; padding: var(--space-6) var(--space-5); }
@media (max-width: 720px) { main.page { padding: var(--space-4) var(--space-3); } }
`;

function parseArgs(argv) {
  const args = { commandId: null, in: null, out: null };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--in') args.in = argv[++i];
    else if (a === '--out') args.out = argv[++i];
    else if (!a.startsWith('--') && !args.commandId) args.commandId = a;
  }
  return args;
}

function commandToRendererName(commandId) {
  return 'render' + commandId.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('');
}

function readStdinSync() {
  try {
    return readFileSync(0, 'utf8');
  } catch {
    return '';
  }
}

function timestamp() {
  const d = new Date();
  const pad = n => String(n).padStart(2, '0');
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}

function loadDsCss() {
  return CSS_FILES.map(name => {
    const path = join(VENDOR_CSS_DIR, name);
    if (!existsSync(path)) return `/* missing: ${name} */`;
    return `/* === ${name} === */\n` + readFileSync(path, 'utf8');
  }).join('\n');
}

function wrapHtml(commandId, bodyHtml, dsCss) {
  return `<!DOCTYPE html>
<html lang="nb" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>llm-security · ${commandId}</title>
<script>
(function(){var t=null;try{var s=localStorage.getItem('llm-security-theme');if(s==='light'||s==='dark')t=s;}catch(e){}if(!t&&window.matchMedia)t=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';if(!t)t=document.documentElement.getAttribute('data-theme')||'dark';document.documentElement.setAttribute('data-theme',t);document.documentElement.style.colorScheme=t;})();
</script>
<style>
${dsCss}
${REPORT_TABLE_CSS}
</style>
</head>
<body>
<main class="page">
${bodyHtml}
</main>
</body>
</html>
`;
}

function fail(code, msg) {
  process.stderr.write(`render-report: ${msg}\n`);
  process.exit(code);
}

function main() {
  const args = parseArgs(process.argv.slice(2));
  if (!args.commandId) fail(1, 'missing <commandId> (e.g. scan, audit, posture, deep-scan)');
  if (!args.in) fail(1, 'missing --in <path|->');

  const parser = PARSERS[args.commandId];
  if (!parser) fail(1, `unknown commandId "${args.commandId}". Valid: ${Object.keys(PARSERS).join(', ')}`);

  const rendererName = commandToRendererName(args.commandId);
  const renderer = RENDERERS[rendererName];
  if (typeof renderer !== 'function') fail(1, `no renderer found for "${args.commandId}" (looked up ${rendererName})`);

  let md;
  try {
    md = args.in === '-' ? readStdinSync() : readFileSync(resolve(args.in), 'utf8');
  } catch (e) {
    fail(1, `cannot read input: ${e.message}`);
  }
  if (!md || !md.trim()) fail(1, 'input markdown is empty');

  let data;
  try {
    data = parser(md);
  } catch (e) {
    fail(1, `parser threw: ${e.message}`);
  }
  if (!data || typeof data !== 'object') fail(1, 'parser returned non-object');

  const slot = { innerHTML: '' };
  try {
    renderer(data, slot);
  } catch (e) {
    fail(2, `renderer threw: ${e.message}`);
  }
  if (!slot.innerHTML) fail(2, 'renderer produced empty HTML');

  const dsCss = loadDsCss();
  const html = wrapHtml(args.commandId, slot.innerHTML, dsCss);

  if (args.out === '-') {
    process.stdout.write(html);
    return;
  }

  let outPath;
  if (args.out) {
    outPath = isAbsolute(args.out) ? args.out : resolve(args.out);
  } else {
    const reportsDir = resolve('reports');
    try {
      mkdirSync(reportsDir, { recursive: true });
    } catch (e) {
      fail(3, `cannot create reports dir: ${e.message}`);
    }
    outPath = join(reportsDir, `${args.commandId}-${timestamp()}.html`);
  }

  try {
    mkdirSync(dirname(outPath), { recursive: true });
    writeFileSync(outPath, html, 'utf8');
  } catch (e) {
    fail(3, `cannot write ${outPath}: ${e.message}`);
  }

  process.stdout.write(`file://${outPath}\n`);
}

main();
