import type { Modeldocument, Modelonderdeel } from "./types";
import { extraheerParameters } from "./samenstellen";
import { vulModelVoorbeeldIn } from "./voorbeeldwaarden";
import { bouwParameterCatalogus } from "./parameter-catalogus";
import { PARAMETER_ALIASSEN } from "./parameter-register";
import { aktetypeRollen } from "./rollen";
import { TE_VERTALEN } from "./pariteit";
import { koppelModelParen, onvertaaldeClausules } from "./rechtshandeling";
import { normaliseerVoorVingerafdruk } from "./parallelle-clausules";

// ── Bibliotheek-audit ────────────────────────────────────────────────────────
// Eén pure, herbruikbare auditmotor voor de kwaliteit van modellen en
// clausules. Dezelfde controles draaien op drie plaatsen, zodat de bewaking
// meegroeit met de bibliotheek in plaats van te steunen op losse scripts:
//
//   1. de kwaliteitstest (bibliotheek-audit.test.ts): 0 fouten, plafonds op
//      waarschuwingen — elke `npm run poort`/CI-run controleert de hele
//      bibliotheek;
//   2. het tabblad Overzicht & hergebruik (weergave "Kwaliteit"): de notaris
//      ziet de actuele stand, inclusief eigen (IndexedDB-)items;
//   3. het tabblad Te valideren: elk binnenkomend voorstel (UI of MCP) wordt
//      geauditeerd tegen de toekomstige bibliotheekstand, vóór goedkeuring.
//
// Nieuwe controle toevoegen = één functie hieronder + één plafond in de test.

export type AuditErnst = "fout" | "waarschuwing";

/** Vaste, machineleesbare codes; de test legt per code een plafond op. */
export type AuditCode =
  | "rol-mapping-ontbreekt"
  | "ontbrekend-onderdeel"
  | "generatie-restfout"
  | "dode-declaratie"
  | "te-vertalen-markering"
  | "onvertaalde-clausule"
  | "tegenhanger-ontbreekt"
  | "parameter-nergens-gedeclareerd"
  | "alias-parameternaam"
  | "parameter-taalpariteit"
  | "duplicaat-tekst";

export interface AuditBevinding {
  ernst: AuditErnst;
  code: AuditCode;
  /** Id van het model of onderdeel waarop de bevinding slaat. */
  onderwerp: string;
  omschrijving: string;
  /** Aggregatiesleutel (bv. de parameternaam) voor tellingen over de bibliotheek heen. */
  sleutel?: string;
}

/** Placeholder-namen die door de rollen-mapping worden ingevuld, niet door parameters. */
const ROL_PLACEHOLDER = /^[Rr]ol_/;

const gebruiktRolPlaceholders = (o: Modelonderdeel): boolean =>
  [o.tekst, ...o.varianten.map((v) => v.tekst)].some((t) => /\{\{\s*[Rr]ol_/.test(t));

const alleTeksten = (o: Modelonderdeel): string[] => [o.tekst, ...o.varianten.map((v) => v.tekst)];

/** Vooraf berekende context zodat een bibliotheekbrede audit niet kwadratisch wordt. */
interface AuditContext {
  catalogus: ReadonlyMap<string, { naam: string }>;
  /** vingerafdruk → onderdeel-ids met exact die tekst. */
  vingerafdrukken: ReadonlyMap<string, string[]>;
  /** id → onderdeel, voor de NL/FR-parameterpariteit. */
  perId: ReadonlyMap<string, Modelonderdeel>;
}

function bouwContext(onderdelen: readonly Modelonderdeel[]): AuditContext {
  const vingerafdrukken = new Map<string, string[]>();
  for (const o of onderdelen) {
    const afdruk = normaliseerVoorVingerafdruk(alleTeksten(o).join("\n"));
    if (!afdruk.trim()) continue;
    vingerafdrukken.set(afdruk, [...(vingerafdrukken.get(afdruk) ?? []), o.id]);
  }
  return {
    catalogus: bouwParameterCatalogus(onderdelen),
    vingerafdrukken,
    perId: new Map(onderdelen.map((o) => [o.id, o])),
  };
}

function auditOnderdeelMetContext(onderdeel: Modelonderdeel, context: AuditContext): AuditBevinding[] {
  const bevindingen: AuditBevinding[] = [];
  const teksten = alleTeksten(onderdeel);
  const inTekst = new Set(extraheerParameters(...teksten));

  // Dode declaratie: gedeclareerde parameter die nergens in de tekst voorkomt —
  // typisch een verzonnen naam naast de werkelijke placeholder (fout).
  for (const p of onderdeel.parameters) {
    if (!inTekst.has(p.naam)) {
      bevindingen.push({
        ernst: "fout",
        code: "dode-declaratie",
        onderwerp: onderdeel.id,
        sleutel: p.naam,
        omschrijving: `parameter "${p.naam}" is gedeclareerd maar komt nergens in de tekst voor — hernoem naar de werkelijke {{placeholder}} of verwijder de declaratie.`,
      });
    }
  }

  // Parameter zonder énige declaratie in de bibliotheek: de UI kan geen
  // omschrijving/voorbeeld tonen (waarschuwing, geplafonneerd in de test).
  for (const naam of inTekst) {
    if (ROL_PLACEHOLDER.test(naam) || context.catalogus.has(naam)) continue;
    bevindingen.push({
      ernst: "waarschuwing",
      code: "parameter-nergens-gedeclareerd",
      onderwerp: onderdeel.id,
      sleutel: naam,
      omschrijving: `parameter "${naam}" is nergens in de bibliotheek gedeclareerd (geen omschrijving/voorbeeld beschikbaar).`,
    });
  }

  // Heringevoerde aliasnaam: de seeds zijn bij de parameterharmonisatie
  // hernoemd naar de canonieke namen; een nieuwe clausule die een oude alias
  // (parameter-register.ts) gebruikt, breekt de conventie "één naamset over
  // de hele bibliotheek en beide talen" (fout — gebruik de canonieke naam).
  for (const naam of inTekst) {
    if (naam in PARAMETER_ALIASSEN) {
      bevindingen.push({
        ernst: "fout",
        code: "alias-parameternaam",
        onderwerp: onderdeel.id,
        sleutel: naam,
        omschrijving: `parameter "${naam}" is een alias — gebruik de canonieke naam "${PARAMETER_ALIASSEN[naam]}" (zie parameter-register.ts).`,
      });
    }
  }

  // NL/FR-parameterpariteit: een FR-clausule hoort exact dezelfde (canonieke)
  // parameternamen te dragen als haar NL-spiegel, zodat één dossierwaardenset
  // beide taalversies vult (richtlijnen.ts → gedeeldeParameternamen). De
  // rol_-placeholders vallen erbuiten (rollen-mapping, geen parameters).
  if ((onderdeel.taal ?? "nl") === "fr" && onderdeel.vertalingVanId) {
    const nlTwin = context.perId.get(onderdeel.vertalingVanId);
    if (nlTwin) {
      const nlNamen = new Set(extraheerParameters(...alleTeksten(nlTwin)).filter((n) => !ROL_PLACEHOLDER.test(n)));
      const afwijkend = [...inTekst].filter((n) => !ROL_PLACEHOLDER.test(n) && !nlNamen.has(n));
      if (afwijkend.length > 0) {
        bevindingen.push({
          ernst: "waarschuwing",
          code: "parameter-taalpariteit",
          onderwerp: onderdeel.id,
          omschrijving:
            `${afwijkend.length} parameternamen ontbreken in de NL-spiegel "${nlTwin.id}" ` +
            `(o.m. ${afwijkend.slice(0, 4).join(", ")}) — hernoem naar de gedeelde namen van de NL-spiegel.`,
        });
      }
    }
  }

  // Voorlopige vertaling die nog in de tekst staat.
  if (teksten.some((t) => t.includes(TE_VERTALEN))) {
    bevindingen.push({
      ernst: "waarschuwing",
      code: "te-vertalen-markering",
      onderwerp: onderdeel.id,
      omschrijving: `bevat nog een ${TE_VERTALEN}-markering — de authentieke vertaling ontbreekt.`,
    });
  }

  // Exact dezelfde tekst als een ander onderdeel: vermoedelijk een duplicaat
  // (parallelle versies groeien uit elkaar; zie de éne-clausule-regel in AGENTS.md).
  const afdruk = normaliseerVoorVingerafdruk(teksten.join("\n"));
  const zelfdeTekst = (context.vingerafdrukken.get(afdruk) ?? []).filter((id) => id !== onderdeel.id);
  if (afdruk.trim() && zelfdeTekst.length > 0) {
    bevindingen.push({
      ernst: "waarschuwing",
      code: "duplicaat-tekst",
      onderwerp: onderdeel.id,
      omschrijving: `tekst is identiek aan ${zelfdeTekst.join(", ")} — voeg samen of maak het onderscheid expliciet.`,
    });
  }

  return bevindingen;
}

/**
 * Audit van één onderdeel (bv. een binnenkomend voorstel) tegen de bibliotheek
 * waarin het zou landen. Geef in `alleOnderdelen` de toekomstige stand mee:
 * de bestaande bibliotheek mét het voorstel (en zonder de oude versie ervan).
 */
export function auditOnderdeel(
  onderdeel: Modelonderdeel,
  alleOnderdelen: readonly Modelonderdeel[]
): AuditBevinding[] {
  return auditOnderdeelMetContext(onderdeel, bouwContext(alleOnderdelen));
}

function auditModelMetContext(
  model: Modeldocument,
  onderdelen: readonly Modelonderdeel[],
  alleModellen: readonly Modeldocument[]
): AuditBevinding[] {
  const bevindingen: AuditBevinding[] = [];
  const byId = new Map(onderdelen.map((o) => [o.id, o]));

  // Structuurverwijzing die nergens naartoe leidt: de clausule ontbreekt
  // stilzwijgend in elk gegenereerd document (fout).
  const gevonden = model.structuur
    .map((item) => ({ id: item.onderdeelId, onderdeel: byId.get(item.onderdeelId) }))
    .filter((x): x is { id: string; onderdeel: Modelonderdeel } => {
      if (!x.onderdeel) {
        bevindingen.push({
          ernst: "fout",
          code: "ontbrekend-onderdeel",
          onderwerp: model.id,
          sleutel: x.id,
          omschrijving: `structuur verwijst naar onbestaand onderdeel "${x.id}".`,
        });
        return false;
      }
      return true;
    });

  // Rollen-mapping: {{rol_*}} zonder vermelding in aktetypeRollen rendert de
  // generieke standaardrollen in élk gegenereerd document (fout).
  if (gevonden.some(({ onderdeel }) => gebruiktRolPlaceholders(onderdeel)) && !(model.akteType in aktetypeRollen)) {
    bevindingen.push({
      ernst: "fout",
      code: "rol-mapping-ontbreekt",
      onderwerp: model.id,
      sleutel: model.akteType,
      omschrijving: `clausules gebruiken {{rol_*}} maar akteType "${model.akteType}" staat niet in aktetypeRollen (rollen.ts) — het document toont de generieke rollen "overdrager"/"verkrijger".`,
    });
  }

  // Generatie-smoke: het ingevulde voorbeeld mag geen kale placeholders of
  // invulmarkeringen overhouden (fout).
  const voorbeeld = vulModelVoorbeeldIn(model, [...onderdelen]);
  const restanten = [...new Set(voorbeeld.match(/\{\{[^}]+\}\}/g) ?? [])];
  if (restanten.length > 0 || voorbeeld.includes("[AAN TE VULLEN")) {
    bevindingen.push({
      ernst: "fout",
      code: "generatie-restfout",
      onderwerp: model.id,
      omschrijving: `het gegenereerde voorbeeld houdt onopgeloste invulpunten over: ${restanten.slice(0, 5).join(", ") || "[AAN TE VULLEN]"}.`,
    });
  }

  // Clausules in de andere taal of met [TE VERTALEN]: het model is niet
  // volledig in zijn eigen taal (waarschuwing).
  for (const id of onvertaaldeClausules(model, [...onderdelen])) {
    bevindingen.push({
      ernst: "waarschuwing",
      code: "onvertaalde-clausule",
      onderwerp: model.id,
      sleutel: id,
      omschrijving: `clausule "${id}" is niet (volledig) beschikbaar in de taal van het model (${model.taal ?? "nl"}).`,
    });
  }

  // NL/FR-tegenhanger: elk model hoort in beide talen te bestaan (waarschuwing;
  // bij een nieuw voorstel is dit het signaal dat de spiegel nog moet volgen).
  const paar = koppelModelParen([...alleModellen, ...(alleModellen.some((m) => m.id === model.id) ? [] : [model])]).find(
    (p) => p.nl?.id === model.id || p.fr?.id === model.id
  );
  if (paar && !(paar.nl && paar.fr)) {
    bevindingen.push({
      ernst: "waarschuwing",
      code: "tegenhanger-ontbreekt",
      onderwerp: model.id,
      omschrijving: `geen ${(model.taal ?? "nl") === "fr" ? "NL" : "FR"}-tegenhanger gekoppeld (vertalingVanId) — de spiegelversie ontbreekt nog.`,
    });
  }

  return bevindingen;
}

/**
 * Audit van één model (bv. een binnenkomend modelvoorstel). Geef in
 * `onderdelen` de toekomstige stand mee (bestaande bibliotheek + de nieuwe
 * onderdelen van het voorstel) en in `alleModellen` de bestaande modellen.
 */
export function auditModel(
  model: Modeldocument,
  onderdelen: readonly Modelonderdeel[],
  alleModellen: readonly Modeldocument[]
): AuditBevinding[] {
  return auditModelMetContext(model, onderdelen, alleModellen);
}

/** Volledige audit van de bibliotheek: alle onderdelen en alle modellen, fouten eerst. */
export function auditBibliotheek(
  modellen: readonly Modeldocument[],
  onderdelen: readonly Modelonderdeel[]
): AuditBevinding[] {
  const context = bouwContext(onderdelen);
  const bevindingen = [
    ...onderdelen.flatMap((o) => auditOnderdeelMetContext(o, context)),
    ...modellen.flatMap((m) => auditModelMetContext(m, onderdelen, modellen)),
  ];
  return bevindingen.sort((a, b) =>
    a.ernst === b.ernst ? a.code.localeCompare(b.code) : a.ernst === "fout" ? -1 : 1
  );
}

/** Telling per code, met voor parameter-bevindingen het aantal unieke namen (sleutels). */
export function telPerCode(bevindingen: readonly AuditBevinding[]): Map<AuditCode, number> {
  const sleutelsPerCode = new Map<AuditCode, Set<string>>();
  const telling = new Map<AuditCode, number>();
  for (const b of bevindingen) {
    if (b.code === "parameter-nergens-gedeclareerd" && b.sleutel) {
      const set = sleutelsPerCode.get(b.code) ?? new Set<string>();
      set.add(b.sleutel);
      sleutelsPerCode.set(b.code, set);
      telling.set(b.code, set.size);
    } else {
      telling.set(b.code, (telling.get(b.code) ?? 0) + 1);
    }
  }
  return telling;
}
