#!/usr/bin/env node
"use strict";
/**
 * Dev-only generator. Single-sources the "inlined fallback" persona copies that
 * live at the TAIL of the Claude Code command files (commands/ask-*.md) from the
 * canonical personas at prompts/*.md.
 *
 * The fallbacks are what /ask-* uses when the plugin cache is missing (the prompt
 * Glob fails), so they must stay byte-identical to prompts/*.md. They used to be
 * hand-maintained and drifted; this generator owns the marker-to-EOF region of
 * each command file and leaves the hand-written command prose above it untouched.
 *
 * Why a separate generator (not sync-hosts.js): sync-hosts emits whole host-NEUTRAL
 * files and its CLAUDE_ONLY_TOKENS guard bans /deliberation:, mcp__deliberation,
 * .claude/, ${CLAUDE_PLUGIN_ROOT} - tokens the Claude Code command prose is full of.
 * Commands also need a PARTIAL (region) rewrite, not a whole-file write.
 *
 * This script is NEVER imported by the runtime or the esbuild bundle.
 *
 * Usage:
 *   node scripts/sync-fallbacks.js          # rewrite the fallback region in each command file
 *   node scripts/sync-fallbacks.js --check  # exit 1 (listing files) on any drift, 0 if clean
 */

const fs = require("fs");
const path = require("path");
const S = require("./hosts/_shared");

const REPO_ROOT = path.resolve(__dirname, "..");

/**
 * Experts that have an inlined fallback, in the order they appear in the command
 * files. NOTE: this order intentionally differs from _shared.EXPERTS (it matches
 * the existing on-disk order) to keep the regeneration diff minimal. `arbiter`
 * has no command fallback, so it is absent.
 */
const FALLBACK_ORDER = [
  "architect",
  "code-reviewer",
  "security-analyst",
  "plan-reviewer",
  "scope-analyst",
  "researcher",
  "debugger",
];

/** Personas adapted from oh-my-opencode carry an attribution blockquote. */
const ATTRIBUTION = new Set(["architect", "plan-reviewer", "scope-analyst"]);
const ATTRIBUTION_LINE =
  "> Adapted from [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) by [@code-yeongyu](https://github.com/code-yeongyu)";

/** Owns the region. Any line matching /fallback/ inside an HTML comment is the anchor. */
const MARKER =
  "<!-- GENERATED by scripts/sync-fallbacks.js from prompts/*.md - do not edit below. Required fallback if the plugin cache is missing. -->";

/** Matches BOTH the new marker and the old `DO NOT DELETE ... fallback` marker. */
const MARKER_RE = /^<!--[^\n]*fallback[^\n]*-->$/m;

/** The command files that carry the fallback block. */
const TARGET_FILES = ["commands/ask-all.md", "commands/ask-gpt.md", "commands/ask-gemini.md", "commands/ask-grok.md"];

/**
 * Build one `## Inlined fallback - <Name>` section for an expert: heading, optional
 * attribution, then the persona body with its leading `# <Name>` H1 stripped. No
 * trailing newline (the caller joins/terminates).
 * @param {string} repoRoot @param {string} key @returns {string}
 */
function sectionFor(repoRoot, key) {
  const raw = S.readText(repoRoot, `prompts/${key}.md`); // LF, single trailing \n
  const h1 = raw.match(/^#\s+(.+)$/m);
  if (!h1) throw new Error(`prompts/${key}.md: missing leading "# <Name>" H1`);
  const display = h1[1].trim();
  const body = raw.replace(/^#[^\n]*\n+/, "").replace(/\n+$/, "");
  const attrib = ATTRIBUTION.has(key) ? `${ATTRIBUTION_LINE}\n\n` : "";
  return `## Inlined fallback - ${display}\n\n${attrib}${body}`;
}

/**
 * Build the full marker-to-EOF region (marker + every fallback section), ending in
 * exactly one newline.
 * @param {string} [repoRoot] @returns {string}
 */
function buildRegion(repoRoot) {
  const root = repoRoot || REPO_ROOT;
  const sections = FALLBACK_ORDER.map((key) => sectionFor(root, key));
  return `${MARKER}\n\n${sections.join("\n\n")}\n`;
}

/**
 * Apply the generated region to a command file's text: keep everything before the
 * marker (trimmed), then a blank line, then the region. Idempotent.
 * @param {string} fileText @param {string} region @param {string} rel @returns {string}
 */
function applyRegion(fileText, region, rel) {
  const lf = fileText.replace(/\r\n/g, "\n");
  const m = MARKER_RE.exec(lf);
  if (!m) throw new Error(`${rel}: no fallback marker found (expected an HTML comment containing "fallback")`);
  const pre = lf.slice(0, m.index).replace(/\s+$/, "");
  return `${pre}\n\n${region}`;
}

/** Read a file as LF text (or null if absent) so the drift compare ignores CRLF checkout. */
function readDiskLF(file) {
  return fs.existsSync(file) ? fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n") : null;
}

function main() {
  const check = process.argv.includes("--check");
  const region = buildRegion(REPO_ROOT);
  /** @type {string[]} */
  const drifted = [];

  for (const rel of TARGET_FILES) {
    const file = path.join(REPO_ROOT, rel);
    const current = readDiskLF(file);
    if (current === null) throw new Error(`${rel}: missing (expected a command file with a fallback marker)`);
    const next = applyRegion(current, region, rel);
    if (check) {
      if (current !== next) drifted.push(rel);
      continue;
    }
    fs.writeFileSync(file, next);
  }

  if (check) {
    if (drifted.length) {
      process.stderr.write(
        "command fallbacks out of date - run `node scripts/sync-fallbacks.js`:\n" +
          drifted.map((r) => `  - ${r}`).join("\n") +
          "\n"
      );
      process.exit(1);
    }
    process.stdout.write(`command fallbacks up to date (${TARGET_FILES.length} files)\n`);
    return;
  }
  process.stdout.write(`wrote ${TARGET_FILES.length} command fallback blocks\n`);
}

if (require.main === module) main();

module.exports = {
  FALLBACK_ORDER,
  ATTRIBUTION,
  ATTRIBUTION_LINE,
  MARKER,
  MARKER_RE,
  TARGET_FILES,
  sectionFor,
  buildRegion,
  applyRegion,
  REPO_ROOT,
};
