// scripts/check-annotations.mjs
import fs from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";

const CWD = process.cwd();
const ANNO_DIR = path.join(CWD, "src", "annotations");
const DIST_DIR = path.join(CWD, "dist");
const ALIASES_PATH = path.join(CWD, "src", "anchors", "anchor-aliases.json");

async function exists(p) {
  try { await fs.access(p); return true; } catch { return false; }
}

async function walk(dir) {
  const out = [];
  const ents = await fs.readdir(dir, { withFileTypes: true });
  for (const e of ents) {
    const p = path.join(dir, e.name);
    if (e.isDirectory()) out.push(...(await walk(p)));
    else out.push(p);
  }
  return out;
}

function escRe(s) {
  return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function normalizePageKey(s) {
  return String(s || "").replace(/^\/+/, "").replace(/\/+$/, "");
}

function isPlainObject(x) {
  return !!x && typeof x === "object" && !Array.isArray(x);
}

function isParaId(s) {
  return /^p-\d+-/i.test(String(s || ""));
}

/**
 * Supporte:
 * - monolith: src/annotations/<pageKey>.yml -> pageKey = rel sans ext
 * - shard   : src/annotations/<pageKey>/<paraId>.yml -> pageKey = dirname(rel), paraId = basename
 *
 * shard seulement si le fichier est dans un sous-dossier (anti cas pathologique).
 */
function inferFromFile(fileAbs) {
  const rel = path.relative(ANNO_DIR, fileAbs).replace(/\\/g, "/");
  const relNoExt = rel.replace(/\.(ya?ml|json)$/i, "");
  const parts = relNoExt.split("/").filter(Boolean);
  const base = parts[parts.length - 1] || "";
  const dirParts = parts.slice(0, -1);

  const isShard = dirParts.length > 0 && isParaId(base);
  const pageKey = isShard ? dirParts.join("/") : relNoExt;
  const paraId = isShard ? base : "";

  return { pageKey: normalizePageKey(pageKey), paraId };
}

async function loadAliases() {
  if (!(await exists(ALIASES_PATH))) return {};
  try {
    const raw = await fs.readFile(ALIASES_PATH, "utf8");
    const json = JSON.parse(raw);
    return isPlainObject(json) ? json : {};
  } catch {
    return {};
  }
}

function parseDoc(raw, fileAbs) {
  if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
  return YAML.parse(raw);
}

function getAlias(aliases, pageKey, oldId) {
  // supporte:
  // 1) { "<pageKey>": { "<old>": "<new>" } }
  // 2) { "<old>": "<new>" }
  const k1 = String(pageKey || "");
  const k2 = k1 ? ("/" + k1.replace(/^\/+|\/+$/g, "") + "/") : "";
  const a1 = (aliases?.[k1]?.[oldId]) || (k2 ? aliases?.[k2]?.[oldId] : "");
  if (a1) return String(a1);
  const a2 = aliases?.[oldId];
  if (a2) return String(a2);
  return "";
}

async function main() {
  if (!(await exists(ANNO_DIR))) {
    console.log("✅ annotations: aucun dossier src/annotations — rien à vérifier.");
    process.exit(0);
  }

  if (!(await exists(DIST_DIR))) {
    console.error("FAIL: dist/ absent. Lance d’abord `npm run build` (ou `npm test`).");
    process.exit(1);
  }

  const aliases = await loadAliases();
  const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));

  // perf: cache HTML par page (shards = beaucoup de fichiers pour 1 page)
  const htmlCache = new Map();        // pageKey -> html
  const missingDistPage = new Set();  // pageKey

  let pagesSeen = new Set();
  let checked = 0;
  let failures = 0;
  const notes = [];

  for (const f of files) {
    const rel = path.relative(CWD, f).replace(/\\/g, "/");
    const raw = await fs.readFile(f, "utf8");

    let doc;
    try {
      doc = parseDoc(raw, f);
    } catch (e) {
      failures++;
      notes.push(`- PARSE FAIL: ${rel} (${String(e?.message ?? e)})`);
      continue;
    }

    if (!isPlainObject(doc) || doc.schema !== 1) {
      failures++;
      notes.push(`- INVALID: ${rel} (schema must be 1)`);
      continue;
    }

    const { pageKey, paraId: shardParaId } = inferFromFile(f);

    if (doc.page != null && normalizePageKey(doc.page) !== pageKey) {
      failures++;
      notes.push(`- PAGE MISMATCH: ${rel} (page="${doc.page}" != path="${pageKey}")`);
      continue;
    }

    if (!isPlainObject(doc.paras)) {
      failures++;
      notes.push(`- INVALID: ${rel} (missing object key "paras")`);
      continue;
    }

    // shard invariant (fort) : doit contenir paras[paraId]
    if (shardParaId) {
      if (!Object.prototype.hasOwnProperty.call(doc.paras, shardParaId)) {
        failures++;
        notes.push(`- SHARD MISMATCH: ${rel} (expected paras["${shardParaId}"] present)`);
        continue;
      }
      // si extras -> warning (non destructif)
      const keys = Object.keys(doc.paras);
      if (!(keys.length === 1 && keys[0] === shardParaId)) {
        notes.push(`- WARN shard has extra paras: ${rel} (expected only "${shardParaId}", got ${keys.join(", ")})`);
      }
    }

    pagesSeen.add(pageKey);

    const distFile = path.join(DIST_DIR, pageKey, "index.html");
    if (!(await exists(distFile))) {
      if (!missingDistPage.has(pageKey)) {
        missingDistPage.add(pageKey);
        failures++;
        notes.push(`- MISSING PAGE: dist/${pageKey}/index.html (from ${rel})`);
      } else {
        notes.push(`- WARN missing page already reported: dist/${pageKey}/index.html (from ${rel})`);
      }
      continue;
    }

    let html = htmlCache.get(pageKey);
    if (!html) {
      html = await fs.readFile(distFile, "utf8");
      htmlCache.set(pageKey, html);
    }

    for (const paraId of Object.keys(doc.paras)) {
      checked++;

      if (!isParaId(paraId)) {
        failures++;
        notes.push(`- INVALID ID: ${rel} (${paraId})`);
        continue;
      }

      const re = new RegExp(`\\bid=["']${escRe(paraId)}["']`, "g");
      if (re.test(html)) continue;

      const alias = getAlias(aliases, pageKey, paraId);
      if (alias) {
        const re2 = new RegExp(`\\bid=["']${escRe(alias)}["']`, "g");
        if (re2.test(html)) {
          notes.push(`- WARN alias used: ${pageKey} ${paraId} -> ${alias}`);
          continue;
        }
      }

      failures++;
      notes.push(`- MISSING ID: ${pageKey} (#${paraId})`);
    }
  }

  const warns = notes.filter((x) => x.startsWith("- WARN"));
  const pages = pagesSeen.size;

  if (failures > 0) {
    console.error(`FAIL: annotations invalid (pages=${pages} checked=${checked} failures=${failures})`);
    for (const n of notes) console.error(n);
    process.exit(1);
  }

  for (const w of warns) console.log(w);
  console.log(`✅ annotations OK: pages=${pages} checked=${checked} warnings=${warns.length}`);
}

main().catch((e) => {
  console.error("FAIL: annotations check crashed:", e);
  process.exit(1);
});