// src/pages/annotations-index.json.ts
import type { APIRoute } from "astro";
import fs from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";

const CWD = process.cwd();
const ANNO_ROOT = path.join(CWD, "src", "annotations");

const isObj = (x: any) => !!x && typeof x === "object" && !Array.isArray(x);
const isArr = (x: any) => Array.isArray(x);

function normPath(s: string) {
  return String(s || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
}
function paraNum(pid: string) {
  const m = String(pid).match(/^p-(\d+)-/i);
  return m ? Number(m[1]) : Number.POSITIVE_INFINITY;
}
function toIso(v: any) {
  if (v instanceof Date) return v.toISOString();
  return typeof v === "string" ? v : "";
}
function stableSortByTs(arr: any[]) {
  if (!Array.isArray(arr)) return;
  arr.sort((a, b) => {
    const ta = Date.parse(toIso(a?.ts)) || 0;
    const tb = Date.parse(toIso(b?.ts)) || 0;
    if (ta !== tb) return ta - tb;
    return JSON.stringify(a).localeCompare(JSON.stringify(b));
  });
}

function keyMedia(x: any) { return String(x?.src || ""); }
function keyRef(x: any) {
  return `${x?.url || ""}||${x?.label || ""}||${x?.kind || ""}||${x?.citation || ""}`;
}
function keyComment(x: any) { return String(x?.text || "").trim(); }

function uniqUnion(dst: any[], src: any[], keyFn: (x:any)=>string) {
  const out = isArr(dst) ? [...dst] : [];
  const seen = new Set(out.map((x) => keyFn(x)));
  for (const it of (isArr(src) ? src : [])) {
    const k = keyFn(it);
    if (!k) continue;
    if (!seen.has(k)) { seen.add(k); out.push(it); }
  }
  return out;
}

function deepMergeEntry(dst: any, src: any) {
  if (!isObj(dst) || !isObj(src)) return;

  for (const [k, v] of Object.entries(src)) {
    if (k === "media" && isArr(v)) { dst.media = uniqUnion(dst.media, v, keyMedia); continue; }
    if (k === "refs" && isArr(v)) { dst.refs = uniqUnion(dst.refs, v, keyRef); continue; }
    if (k === "comments_editorial" && isArr(v)) { dst.comments_editorial = uniqUnion(dst.comments_editorial, v, keyComment); continue; }

    if (isObj(v)) {
      if (!isObj((dst as any)[k])) (dst as any)[k] = {};
      deepMergeEntry((dst as any)[k], v);
      continue;
    }

    if (isArr(v)) {
      const cur = isArr((dst as any)[k]) ? (dst as any)[k] : [];
      const seen = new Set(cur.map((x:any) => JSON.stringify(x)));
      const out = [...cur];
      for (const it of v) {
        const s = JSON.stringify(it);
        if (!seen.has(s)) { seen.add(s); out.push(it); }
      }
      (dst as any)[k] = out;
      continue;
    }

    if (!(k in (dst as any)) || (dst as any)[k] == null || (dst as any)[k] === "") (dst as any)[k] = v;
  }
}

async function walk(dir: string): Promise<string[]> {
  const out: string[] = [];
  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 if (e.isFile() && /\.ya?ml$/i.test(e.name)) out.push(p);
  }
  return out;
}

function inferExpected(relNoExt: string) {
  const parts = relNoExt.split("/").filter(Boolean);
  const last = parts.at(-1) || "";
  const isShard = parts.length > 1 && /^p-\d+-/i.test(last); // ✅ durcissement
  const pageKey = isShard ? parts.slice(0, -1).join("/") : relNoExt;
  const paraId = isShard ? last : null;
  return { isShard, pageKey, paraId };
}

export const GET: APIRoute = async () => {
  const pages: Record<string, { paras: Record<string, any> }> = {};
  const errors: Array<{ file: string; error: string }> = [];

  let files: string[] = [];
  let missingRoot = false;

  try {
    files = await walk(ANNO_ROOT);
  } catch (e: any) {
    // ✅ FAIL-OPEN : pas d’annotations => index vide (ne casse pas la build)
    missingRoot = true;
    console.warn(`[annotations-index] Missing annotations root: ${ANNO_ROOT} (${e?.message || e})`);
    files = [];
    // ✅ surtout PAS d'errors.push ici
  }

  for (const fp of files) {
    const rel = normPath(path.relative(ANNO_ROOT, fp));
    const relNoExt = rel.replace(/\.ya?ml$/i, "");
    const { isShard, pageKey, paraId } = inferExpected(relNoExt);

    try {
      const raw = await fs.readFile(fp, "utf8");
      const doc = YAML.parse(raw) || {};

      if (!isObj(doc) || doc.schema !== 1) continue;

      const docPage = normPath(doc.page || "");
      if (docPage && docPage !== pageKey) {
        throw new Error(`page mismatch (page="${doc.page}" vs path="${pageKey}")`);
      }
      if (!doc.page) doc.page = pageKey;

      if (!isObj(doc.paras)) throw new Error(`missing object key "paras"`);

      const pg = pages[pageKey] ??= { paras: {} };

      if (isShard) {
        if (!paraId) throw new Error("internal: missing paraId");
        if (!(paraId in doc.paras)) {
          throw new Error(`shard mismatch: file must contain paras["${paraId}"]`);
        }
        // ✅ invariant aligné avec build-annotations-index
        const keys = Object.keys(doc.paras).map(String);
        if (!(keys.length === 1 && keys[0] === paraId)) {
          throw new Error(`shard invariant violated: shard must contain ONLY paras["${paraId}"] (got: ${keys.join(", ")})`);
        }

        const entry = doc.paras[paraId];
        if (!isObj(pg.paras[paraId])) pg.paras[paraId] = {};
        if (isObj(entry)) deepMergeEntry(pg.paras[paraId], entry);

        stableSortByTs(pg.paras[paraId].media);
        stableSortByTs(pg.paras[paraId].refs);
        stableSortByTs(pg.paras[paraId].comments_editorial);
      } else {
        for (const [pid, entry] of Object.entries(doc.paras)) {
          const p = String(pid);
          if (!isObj(pg.paras[p])) pg.paras[p] = {};
          if (isObj(entry)) deepMergeEntry(pg.paras[p], entry);

          stableSortByTs(pg.paras[p].media);
          stableSortByTs(pg.paras[p].refs);
          stableSortByTs(pg.paras[p].comments_editorial);
        }
      }
    } catch (e: any) {
      errors.push({ file: `src/annotations/${rel}`, error: String(e?.message || e) });
    }
  }

  for (const [pk, pg] of Object.entries(pages)) {
    const keys = Object.keys(pg.paras || {});
    keys.sort((a, b) => {
      const ia = paraNum(a);
      const ib = paraNum(b);
      if (Number.isFinite(ia) && Number.isFinite(ib) && ia !== ib) return ia - ib;
      return String(a).localeCompare(String(b));
    });
    const next: Record<string, any> = {};
    for (const k of keys) next[k] = pg.paras[k];
    pg.paras = next;
  }

  const warnings: Array<{ where: string; warning: string }> = [];
  if (missingRoot) {
    warnings.push({
      where: "src/pages/annotations-index.json.ts",
      warning: `Missing annotations root "${ANNO_ROOT}" (treated as empty).`,
    });
  }

  const out = {
    schema: 1,
    generatedAt: new Date().toISOString(),
    pages,
    stats: {
      pages: Object.keys(pages).length,
      paras: Object.values(pages).reduce((n, p) => n + Object.keys(p.paras || {}).length, 0),
      errors: errors.length,
    },
    warnings,
    errors,
  };

  // ✅ FAIL-OPEN uniquement si le dossier manque.
  // Si le dossier existe mais qu’un YAML est cassé -> fail-closed.
  if (errors.length && !missingRoot) {
    throw new Error(`${errors[0].file}: ${errors[0].error}`);
  }

  return new Response(JSON.stringify(out), {
    headers: { "Content-Type": "application/json; charset=utf-8" },
  });
};