// src/lib/spec/checklist-sync.ts — logique pure du pipeline checklist :
// parse/sérialisation du format scripts/spec-source/<category>.md et rendu de
// checklist-data.ts. Aucune I/O (pas de node:fs) — les scripts CLI
// (generate-checklist.ts, sync-spec-content.ts) ne gardent que la lecture/écriture.
import type {
  SpecCategory,
  SpecCategoryId,
  SpecItem,
  SpecStatus,
} from "./types";
import { CATEGORY_META } from "./types";

const ITEM_RE =
  /^- \[ \] \*\*(.+?)\*\* _\((required|recommended|optional|avoid)\)_ — (.+)$/;

export function slugFromUrl(url: string): string {
  return url.replace(/\/+$/, "").split("/").pop() ?? "";
}

/**
 * Parse un fichier spec-source (`- [ ] **Title** _(status)_ — desc` + URL
 * indentée sur la ligne suivante) en items. Jette sur URL manquante, slug vide
 * ou slug dupliqué dans la catégorie.
 */
export function parseSpecSourceCategory(
  md: string,
  id: SpecCategoryId,
): SpecItem[] {
  const lines = md.split("\n");
  const items: SpecItem[] = [];
  const seen = new Set<string>();
  for (let i = 0; i < lines.length; i++) {
    const m = lines[i].match(ITEM_RE);
    if (!m) continue;
    const [, title, status, description] = m;
    const url = (lines[i + 1] ?? "").trim();
    if (!url.startsWith("http"))
      throw new Error(`Missing URL after "${title}" in ${id}.md`);
    const slug = slugFromUrl(url);
    if (!slug) throw new Error(`Empty slug from URL "${url}" in ${id}.md`);
    const itemId = `${id}/${slug}`;
    if (seen.has(itemId))
      throw new Error(`Duplicate slug "${slug}" in ${id}.md`);
    seen.add(itemId);
    items.push({
      id: itemId,
      category: id,
      title,
      status: status as SpecItem["status"],
      description,
      url,
    });
  }
  return items;
}

/**
 * Sérialise une catégorie au format spec-source existant (round-trip exact avec
 * parseSpecSourceCategory). L'en-tête porte le compte réel d'items.
 */
export function serializeSpecSource(
  meta: { id: SpecCategoryId; label: string },
  items: SpecItem[],
): string {
  const lines: string[] = [
    `# The Website Specification — checklist (${items.length} items)`,
    "",
    `## ${meta.label}`,
  ];
  for (const item of items) {
    lines.push(
      `- [ ] **${item.title}** _(${item.status})_ — ${item.description}`,
    );
    lines.push(`      ${item.url}`);
  }
  return `${lines.join("\n")}\n`;
}

// --- Parse du checklist.md upstream (https://specification.website/checklist.md) ---
//
// Format : préambule, puis par catégorie un heading `## Label` (labels identiques
// à CATEGORY_META), une ligne de description de catégorie (non indentée, ignorée),
// et des items `- [ ] [Titre](URL) — Status` suivis de la description indentée
// de 2 espaces (jointe si wrappée sur plusieurs lignes). Les chevrons y sont
// échappés (`\<html\>`).

const UPSTREAM_ITEM_RE =
  /^- \[ \] \[(.+?)\]\((https?:\/\/[^\s)]+)\)\s+—\s+(Required|Recommended|Optional|Avoid)\s*$/;

const LABEL_TO_ID = new Map<string, SpecCategoryId>(
  CATEGORY_META.map((c) => [c.label, c.id]),
);

function unescapeMd(s: string): string {
  return s.replace(/\\([<>])/g, "$1");
}

/**
 * Parse le checklist.md upstream complet en catégories ordonnées comme upstream
 * (labels/summaries pris dans CATEGORY_META — la copie UI reste la nôtre).
 * Jette sur heading de catégorie inconnu (nouvelle catégorie upstream = décision
 * humaine : types, CATEGORY_META, UI), item hors catégorie, description
 * manquante, statut inattendu ou slug dupliqué intra-catégorie.
 */
export function parseUpstreamChecklist(md: string): SpecCategory[] {
  const lines = md.split("\n");
  const categories: SpecCategory[] = [];
  let current: SpecCategory | null = null;
  let pendingItem: SpecItem | null = null;
  const seen = new Set<string>();
  const seenCategories = new Set<SpecCategoryId>();

  const flushPending = () => {
    if (!pendingItem) return;
    if (!pendingItem.description)
      throw new Error(`Missing description for "${pendingItem.id}"`);
    current?.items.push(pendingItem);
    pendingItem = null;
  };

  for (const line of lines) {
    const heading = line.match(/^## (.+)$/);
    if (heading) {
      flushPending();
      const label = heading[1].trim();
      const id = LABEL_TO_ID.get(label);
      if (!id) throw new Error(`Unknown upstream category heading "${label}"`);
      if (seenCategories.has(id))
        throw new Error(`Duplicate upstream category heading "${label}"`);
      seenCategories.add(id);
      const meta = CATEGORY_META.find((c) => c.id === id);
      if (!meta) throw new Error(`No CATEGORY_META for "${id}"`);
      current = { ...meta, items: [] };
      categories.push(current);
      continue;
    }

    const item = line.match(UPSTREAM_ITEM_RE);
    if (item) {
      flushPending();
      if (!current)
        throw new Error(`Item before any category heading: "${line}"`);
      const [, rawTitle, url, rawStatus] = item;
      const slug = slugFromUrl(url);
      if (!slug) throw new Error(`Empty slug from URL "${url}"`);
      const itemId = `${current.id}/${slug}`;
      if (seen.has(itemId)) throw new Error(`Duplicate slug "${itemId}"`);
      seen.add(itemId);
      pendingItem = {
        id: itemId,
        category: current.id,
        title: unescapeMd(rawTitle),
        status: rawStatus.toLowerCase() as SpecStatus,
        description: "",
        url,
      };
      continue;
    }

    // Ligne de description d'item : indentée de 2 espaces, rattachée à l'item
    // en cours (jointe par un espace si wrappée). Tout le reste (préambule,
    // description de catégorie non indentée, lignes vides) est ignoré.
    if (pendingItem && /^ {2}\S/.test(line)) {
      const text = unescapeMd(line.trim());
      pendingItem.description = pendingItem.description
        ? `${pendingItem.description} ${text}`
        : text;
      continue;
    }
    flushPending();
  }
  flushPending();

  if (categories.length !== CATEGORY_META.length) {
    const got = new Set(categories.map((c) => c.id));
    const missing = CATEGORY_META.filter((c) => !got.has(c.id)).map(
      (c) => c.id,
    );
    throw new Error(
      `Upstream checklist missing categories: ${missing.join(", ")}`,
    );
  }
  return categories;
}

// --- Diff entre deux états de la checklist ---

export type ChecklistDiff = {
  added: SpecItem[];
  removed: SpecItem[];
  statusChanged: { id: string; from: SpecStatus; to: SpecStatus }[];
  /** ids dont title/description/url a changé (indépendant du statut). */
  textChanged: string[];
};

export function diffChecklists(
  before: SpecCategory[],
  after: SpecCategory[],
): ChecklistDiff {
  const flat = (cats: SpecCategory[]) =>
    new Map(cats.flatMap((c) => c.items).map((it) => [it.id, it]));
  const beforeById = flat(before);
  const afterById = flat(after);

  const diff: ChecklistDiff = {
    added: [],
    removed: [],
    statusChanged: [],
    textChanged: [],
  };
  for (const [id, item] of afterById) {
    const prev = beforeById.get(id);
    if (!prev) {
      diff.added.push(item);
      continue;
    }
    if (prev.status !== item.status)
      diff.statusChanged.push({ id, from: prev.status, to: item.status });
    if (
      prev.title !== item.title ||
      prev.description !== item.description ||
      prev.url !== item.url
    )
      diff.textChanged.push(id);
  }
  for (const [id, item] of beforeById) {
    if (!afterById.has(id)) diff.removed.push(item);
  }
  return diff;
}

/** Rend le contenu complet de src/lib/spec/checklist-data.ts (fichier généré). */
export function renderChecklistDataTs(categories: SpecCategory[]): string {
  const banner = `// AUTO-GENERATED by scripts/generate-checklist.ts — do not edit by hand.\n// Source: scripts/spec-source/*.md (MCP specification-website get_checklist).\n`;
  return `${banner}import type { SpecCategory } from "./types";\n\nexport const CHECKLIST: SpecCategory[] = ${JSON.stringify(
    categories,
    null,
    2,
  )};\n`;
}
