/**
 * generator.ts — idempotent SKILL.md generator for all 42 agent skills.
 *
 * Usage (library):
 *   import { generateAgentSkills, buildSkillMarkdown } from "@/lib/agentSkills/generator";
 *
 * Usage (CLI):
 *   node scripts/skills/generate-agent-skills.mjs [--apply] [--prune] [--only=id1,id2] [--json]
 *
 * Safety contract:
 *  - Default: dryRun=true, prune=false (no filesystem writes).
 *  - dryRun=false is required for any actual writes.
 *  - prune=true AND dryRun=false is required to delete orphan directories.
 *  - F9 is responsible for running apply; F3 only implements + tests the generator.
 */

import fs from "node:fs";
import path from "node:path";
import { getCatalog, refreshCatalog } from "./catalog";
import { parseOpenapi } from "./openapiParser";
import { parseCliRegistry } from "./cliRegistryParser";
import type { AgentSkill, GeneratorOptions, GeneratorReport } from "./types";
import type { ParsedOpenapi } from "./openapiParser";
import type { ParsedCliRegistry } from "./cliRegistryParser";

// ── Constants ─────────────────────────────────────────────────────────────────

const GENERATED_COMMENT =
  "<!-- generated by src/lib/agentSkills/generator.ts; manual edits will be overwritten -->";

const CUSTOM_START_MARKER = "<!-- skill:custom-start -->";
const CUSTOM_END_MARKER = "<!-- skill:custom-end -->";

// ── Types ─────────────────────────────────────────────────────────────────────

interface BuildSources {
  openapi: ParsedOpenapi;
  cliRegistry: ParsedCliRegistry;
}

// ── Frontmatter helpers ───────────────────────────────────────────────────────

/**
 * Serializes frontmatter to YAML front-matter block.
 * Handles descriptions with colons or special characters safely.
 */
function serializeFrontmatter(fm: { name: string; description: string }): string {
  // Use block scalar for description if it contains colons, quotes, or newlines
  const needsQuote = (v: string): boolean => /[:\n"']/.test(v) || v.startsWith(" ");

  // Escape order matters for double-quoted YAML scalars: backslash FIRST (so the
  // escapes we add below are not themselves re-escaped), then the double-quote,
  // then collapse real newlines into the \n escape sequence.
  const escDq = (v: string): string => v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
  const nameStr = needsQuote(fm.name) ? `"${escDq(fm.name)}"` : fm.name;
  const descStr = needsQuote(fm.description)
    ? `"${escDq(fm.description).replace(/\n/g, "\\n")}"`
    : fm.description;

  return `---\nname: ${nameStr}\ndescription: ${descStr}\n---\n`;
}

/**
 * Extracts the custom block content from an existing SKILL.md string.
 * Returns null if no custom block is found.
 */
function extractCustomBlock(content: string): string | null {
  const startIdx = content.indexOf(CUSTOM_START_MARKER);
  const endIdx = content.indexOf(CUSTOM_END_MARKER);
  if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return null;
  return content.slice(startIdx, endIdx + CUSTOM_END_MARKER.length);
}

// ── Body builders ─────────────────────────────────────────────────────────────

function buildApiBody(skill: AgentSkill, sources: BuildSources): string {
  const areaMap = sources.openapi.areas;
  const ops = areaMap.get(skill.area as Parameters<typeof areaMap.get>[0]) ?? [];

  const lines: string[] = [];

  lines.push("## Overview\n");
  lines.push(skill.description);
  lines.push("");

  lines.push("## Authentication\n");
  lines.push(
    "All requests require a valid Bearer token or session cookie. " +
      "Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development.",
  );
  lines.push("");

  lines.push("## Endpoints\n");

  if (ops.length === 0) {
    lines.push("_No endpoints mapped for this area yet._");
  } else {
    for (const op of ops) {
      lines.push(`### ${op.method} ${op.path}\n`);
      if (op.summary) {
        lines.push(`${op.summary}`);
        lines.push("");
      }
      if (op.description) {
        lines.push(op.description);
        lines.push("");
      }
      // Minimal curl example
      const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `;
      lines.push("```bash");
      lines.push(
        `curl ${curlMethod}https://localhost:20128${op.path} \\`,
      );
      lines.push('  -H "Authorization: Bearer $OMNIROUTE_TOKEN"');
      if (["POST", "PUT", "PATCH"].includes(op.method)) {
        lines.push('  -H "Content-Type: application/json" \\');
        lines.push("  -d '{}'");
      }
      lines.push("```");
      lines.push("");
    }
  }

  lines.push("## Payloads\n");
  lines.push(
    "See the full OpenAPI specification at `GET /api/openapi/spec` or " +
      "`docs/reference/openapi.yaml` for detailed request/response schemas.",
  );
  lines.push("");

  return lines.join("\n");
}

function buildCliBody(skill: AgentSkill, sources: BuildSources): string {
  const familyMap = sources.cliRegistry.families;
  const cmds =
    familyMap.get(skill.area as Parameters<typeof familyMap.get>[0]) ?? [];

  const lines: string[] = [];

  lines.push("## Overview\n");
  lines.push(skill.description);
  lines.push("");

  lines.push("## Quick install\n");
  lines.push("```bash");
  lines.push("npm install -g omniroute   # or: npx omniroute");
  lines.push("omniroute --version");
  lines.push("```");
  lines.push("");

  lines.push("## Subcommands\n");

  if (cmds.length === 0) {
    lines.push("_No CLI subcommands mapped for this family yet._");
    lines.push("");
  } else {
    for (const cmd of cmds) {
      lines.push(`### \`${cmd.name}\`\n`);
      if (cmd.description) {
        lines.push(cmd.description);
        lines.push("");
      }

      if (cmd.flags.length > 0) {
        lines.push("**Flags:**\n");
        for (const flag of cmd.flags) {
          lines.push(`- \`${flag}\``);
        }
        lines.push("");
      }

      lines.push("**Example:**\n");
      lines.push("```bash");
      lines.push(`omniroute ${cmd.name}`);
      lines.push("```");
      lines.push("");
    }
  }

  return lines.join("\n");
}

// ── Core builder ─────────────────────────────────────────────────────────────

/**
 * Generates the full SKILL.md string (frontmatter + comment + body) for a given skill.
 *
 * If `existingContent` is provided, any `<!-- skill:custom-start --> ... <!-- skill:custom-end -->`
 * block found in it is re-injected after the generated body (marker preservation / D24).
 */
export function buildSkillMarkdown(
  skillId: string,
  sources: BuildSources,
  existingContent?: string,
): { frontmatter: { name: string; description: string }; body: string } {
  const skill = getCatalog().find((s) => s.id === skillId);
  if (!skill) {
    throw new Error(`buildSkillMarkdown: skill "${skillId}" not found in catalog`);
  }

  const fm = {
    name: skill.id,
    description: skill.description.slice(0, 2000),
  };

  const bodyLines =
    skill.category === "api"
      ? buildApiBody(skill, sources)
      : buildCliBody(skill, sources);

  // Re-inject custom block if present in existing content
  let customBlock = "";
  if (existingContent) {
    const extracted = extractCustomBlock(existingContent);
    if (extracted) {
      customBlock = `\n${extracted}\n`;
    }
  }

  const body =
    GENERATED_COMMENT +
    "\n\n" +
    bodyLines +
    customBlock;

  return { frontmatter: fm, body };
}

/**
 * Assembles the full file content string from frontmatter + body.
 */
function assembleFileContent(fm: { name: string; description: string }, body: string): string {
  return serializeFrontmatter(fm) + body;
}

// ── Main generator ─────────────────────────────────────────────────────────────

/**
 * Runs the full generator. Idempotent.
 *
 * - dryRun=true (default): returns report but writes nothing to disk.
 * - dryRun=false: writes/updates SKILL.md files as needed.
 * - prune=true: detects orphan directories in `skills/` not in the catalog.
 *   - dryRun=true + prune=true: lists orphans, no deletions.
 *   - dryRun=false + prune=true: deletes orphan directories.
 */
export async function generateAgentSkills(opts: GeneratorOptions): Promise<GeneratorReport> {
  const { dryRun = true, prune = false, outputDir = "skills", onlyIds } = opts;

  const outputBase = path.resolve(process.cwd(), outputDir);

  const catalog = getCatalog();
  const catalogIds = new Set(catalog.map((s) => s.id));

  // Filter to onlyIds if provided
  const skillsToProcess = onlyIds
    ? catalog.filter((s) => onlyIds.includes(s.id))
    : catalog;

  // Lazily parse sources (only once per generator run)
  let _openapi: ParsedOpenapi | null = null;
  let _cliRegistry: ParsedCliRegistry | null = null;

  function getSources(): BuildSources {
    if (!_openapi) {
      try {
        _openapi = parseOpenapi();
      } catch {
        _openapi = { paths: new Map(), areas: new Map() };
      }
    }
    if (!_cliRegistry) {
      try {
        _cliRegistry = parseCliRegistry();
      } catch {
        _cliRegistry = { commands: new Map(), families: new Map() };
      }
    }
    return { openapi: _openapi, cliRegistry: _cliRegistry };
  }

  const report: GeneratorReport = {
    generated: [],
    unchanged: [],
    pruned: [],
    orphansDetected: [],
    errors: [],
  };

  // ── Detect orphans ──────────────────────────────────────────────────────────
  if (prune) {
    let dirs: string[] = [];
    try {
      dirs = fs
        .readdirSync(outputBase, { withFileTypes: true })
        .filter((e) => e.isDirectory())
        .map((e) => e.name);
    } catch {
      // outputDir doesn't exist yet — no orphans
    }

    for (const dir of dirs) {
      if (!catalogIds.has(dir)) {
        report.orphansDetected.push(dir);
        if (!dryRun) {
          try {
            fs.rmSync(path.join(outputBase, dir), { recursive: true, force: true });
            report.pruned.push(dir);
          } catch (err) {
            report.errors.push({
              id: dir,
              error: `Failed to prune: ${err instanceof Error ? err.message : String(err)}`,
            });
          }
        }
      }
    }
  }

  // ── Generate / compare each skill ──────────────────────────────────────────
  const sources = getSources();

  for (const skill of skillsToProcess) {
    try {
      const skillDir = path.join(outputBase, skill.id);
      const skillFile = path.join(skillDir, "SKILL.md");

      // Read existing content for marker preservation
      let existingContent: string | undefined;
      try {
        existingContent = fs.readFileSync(skillFile, "utf-8");
      } catch {
        existingContent = undefined;
      }

      const { frontmatter, body } = buildSkillMarkdown(skill.id, sources, existingContent);
      const newContent = assembleFileContent(frontmatter, body);

      // Compare with existing to detect actual changes
      if (existingContent !== undefined && existingContent === newContent) {
        report.unchanged.push(skill.id);
        continue;
      }

      if (dryRun) {
        // In dry-run: classify as generated (would write) but don't touch disk
        report.generated.push(skill.id);
      } else {
        // Apply: ensure directory + write file
        fs.mkdirSync(skillDir, { recursive: true });
        fs.writeFileSync(skillFile, newContent, "utf-8");
        report.generated.push(skill.id);
      }
    } catch (err) {
      report.errors.push({
        id: skill.id,
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }

  // After writing, refresh catalog cache so subsequent calls reflect new files
  if (!dryRun && report.generated.length > 0) {
    refreshCatalog();
  }

  return report;
}

// Exported for unit testing only — keep the internal helpers reachable without
// widening the public surface.
export const __testing = { serializeFrontmatter };
