// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.

/**
 * ADR-140 Phase 2b — generator for `skills.generated.ts`.
 *
 * Reads `src/skills/<name>/SKILL.md` files, parses YAML frontmatter, and emits
 * a TypeScript constant consumed by SkillsRegistry. Generation at build time
 * (not runtime FS reads) so npm-published `dist/` ships bundled bytes — `.md`
 * files are excluded by `.npmignore`.
 *
 * Frontmatter shape:
 *   ---
 *   name: kebab-case-name
 *   description: one-line
 *   triggers:
 *     - phrase one
 *     - phrase two
 *   audience: manager / artist / etc
 *   tools_required:
 *     - tool_a
 *     - tool_b
 *   output: one-line description
 *   ---
 *
 * Hand-rolled YAML parser (frontmatter only — no nested objects, no anchors).
 * Adding a `js-yaml` dep would be overkill for the constrained shape.
 *
 * Re-runs are deterministic: same .md files produce a byte-identical output.
 */

import { readFileSync, readdirSync, writeFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const MCP_SERVER_ROOT = join(__dirname, "..");
const SKILLS_ROOT = join(MCP_SERVER_ROOT, "src/skills");
const OUTPUT_PATH = join(SKILLS_ROOT, "skills.generated.ts");

interface SkillFrontmatter {
  name: string;
  description: string;
  triggers: string[];
  audience: string;
  tools_required: string[];
  output: string;
  definition_of_done?: string;
  when_not_to_use?: string;
}

interface ParsedSkill extends SkillFrontmatter {
  body: string;
}

function parseFrontmatter(src: string, sourceFile: string): ParsedSkill {
  // Strip leading HTML comment (copyright header) if present, then expect ---
  let i = 0;
  if (src.startsWith("<!--")) {
    const end = src.indexOf("-->");
    if (end === -1) {
      throw new Error(`${sourceFile}: unterminated HTML comment header`);
    }
    i = end + 3;
    // Skip whitespace/newlines
    while (i < src.length && /\s/.test(src[i])) i++;
  }

  if (src.slice(i, i + 3) !== "---") {
    throw new Error(`${sourceFile}: missing --- frontmatter opener`);
  }
  i += 3;
  // Skip newline after opener
  while (i < src.length && src[i] !== "\n") i++;
  i++;

  const closeIdx = src.indexOf("\n---", i);
  if (closeIdx === -1) {
    throw new Error(`${sourceFile}: missing --- frontmatter closer`);
  }

  const frontmatterText = src.slice(i, closeIdx);
  const bodyStart = closeIdx + 4; // skip \n---
  const body = src.slice(bodyStart).replace(/^\n+/, "").trimEnd() + "\n";

  // Parse: key: value OR key: (list follows)
  const fm: Record<string, string | string[]> = {};
  const lines = frontmatterText.split("\n");
  let currentListKey: string | null = null;

  for (const line of lines) {
    if (line.trim() === "") {
      currentListKey = null;
      continue;
    }
    const listItemMatch = line.match(/^\s+-\s+(.+)$/);
    if (listItemMatch && currentListKey) {
      (fm[currentListKey] as string[]).push(listItemMatch[1].trim());
      continue;
    }
    const kvMatch = line.match(/^([a-z_][a-z0-9_]*)\s*:\s*(.*)$/i);
    if (!kvMatch) continue;
    const key = kvMatch[1];
    const value = kvMatch[2].trim();
    if (value === "") {
      // List follows
      fm[key] = [];
      currentListKey = key;
    } else {
      fm[key] = value;
      currentListKey = null;
    }
  }

  // Validate required fields
  const required = [
    "name",
    "description",
    "triggers",
    "audience",
    "tools_required",
    "output",
  ];
  for (const field of required) {
    if (!(field in fm)) {
      throw new Error(
        `${sourceFile}: missing required frontmatter field "${field}"`,
      );
    }
  }
  if (!Array.isArray(fm.triggers) || fm.triggers.length === 0) {
    throw new Error(`${sourceFile}: triggers must be a non-empty list`);
  }
  if (!Array.isArray(fm.tools_required) || fm.tools_required.length === 0) {
    throw new Error(`${sourceFile}: tools_required must be a non-empty list`);
  }

  return {
    name: fm.name as string,
    description: fm.description as string,
    triggers: fm.triggers as string[],
    audience: fm.audience as string,
    tools_required: fm.tools_required as string[],
    output: fm.output as string,
    ...(typeof fm.definition_of_done === "string"
      ? { definition_of_done: fm.definition_of_done }
      : {}),
    ...(typeof fm.when_not_to_use === "string"
      ? { when_not_to_use: fm.when_not_to_use }
      : {}),
    body,
  };
}

/** Exported for unit testing — do not call directly in production paths. */
export { parseFrontmatter };

function discoverSkills(): ParsedSkill[] {
  if (!existsSync(SKILLS_ROOT)) {
    return [];
  }
  const entries = readdirSync(SKILLS_ROOT, { withFileTypes: true });
  const skills: ParsedSkill[] = [];
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const skillFile = join(SKILLS_ROOT, entry.name, "SKILL.md");
    if (!existsSync(skillFile)) continue;
    const src = readFileSync(skillFile, "utf-8");
    const parsed = parseFrontmatter(src, `src/skills/${entry.name}/SKILL.md`);
    if (parsed.name !== entry.name) {
      throw new Error(
        `src/skills/${entry.name}/SKILL.md: frontmatter name "${parsed.name}" must match directory name "${entry.name}"`,
      );
    }
    skills.push(parsed);
  }
  skills.sort((a, b) => a.name.localeCompare(b.name));
  return skills;
}

function emit(skills: ParsedSkill[]): string {
  const header = `// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.

/**
 * GENERATED FILE — DO NOT EDIT BY HAND.
 *
 * Regenerated by \`scripts/build-skills.ts\` from \`src/skills/<name>/SKILL.md\`.
 * Runs in \`prebuild\` and \`prepublishOnly\`. See ADR-140 Phase 2b.
 */

export interface Skill {
  /** Stable kebab-case identifier matching the directory name. */
  name: string;
  /** One-line description shown in pica_skill_list. */
  description: string;
  /** Trigger phrases an agent should recognise as invoking this skill. */
  triggers: string[];
  /** Audience tag (manager / artist / sync supervisor / etc). */
  audience: string;
  /** Tools the skill body chains. Lint-checkable. */
  tools_required: string[];
  /** One-line output shape description. */
  output: string;
  /** Optional: the evidence required before this skill is "done" (ADR-271). */
  definition_of_done?: string;
  /** Optional: when the agent should NOT run this skill (ADR-271). */
  when_not_to_use?: string;
  /** Full skill methodology body — markdown, exposed by pica_skill_get. */
  body: string;
}

`;
  const lines: string[] = [
    header,
    "export const SKILLS: Record<string, Skill> = {",
  ];
  for (const skill of skills) {
    lines.push(`  ${JSON.stringify(skill.name)}: {`);
    lines.push(`    name: ${JSON.stringify(skill.name)},`);
    lines.push(`    description: ${JSON.stringify(skill.description)},`);
    lines.push(`    triggers: ${JSON.stringify(skill.triggers)},`);
    lines.push(`    audience: ${JSON.stringify(skill.audience)},`);
    lines.push(`    tools_required: ${JSON.stringify(skill.tools_required)},`);
    lines.push(`    output: ${JSON.stringify(skill.output)},`);
    if (skill.definition_of_done !== undefined) {
      lines.push(
        `    definition_of_done: ${JSON.stringify(skill.definition_of_done)},`,
      );
    }
    if (skill.when_not_to_use !== undefined) {
      lines.push(
        `    when_not_to_use: ${JSON.stringify(skill.when_not_to_use)},`,
      );
    }
    lines.push(`    body: ${JSON.stringify(skill.body)},`);
    lines.push(`  },`);
  }
  lines.push("};");
  lines.push("");
  lines.push(
    `export const SKILL_NAMES: ReadonlyArray<string> = ${JSON.stringify(skills.map((s) => s.name))};`,
  );
  lines.push("");
  return lines.join("\n");
}

function main(): void {
  const skills = discoverSkills();
  const output = emit(skills);
  writeFileSync(OUTPUT_PATH, output, "utf-8");
  console.log(
    `[build-skills] Wrote ${skills.length} skill(s) to src/skills/skills.generated.ts`,
  );
  for (const skill of skills) {
    console.log(`  - ${skill.name} (${skill.body.length} bytes body)`);
  }
}

// Guard: only run when executed directly (not when imported for testing).
// ESM equivalent of `if (require.main === module)`.
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  main();
}
