import type {
  Modeldocument,
  Modelonderdeel,
  ModelVariant,
  StructuurItem,
  VoorstelPrioriteit,
  Wijzigingsvoorstel,
} from "./types";
import { vulRollenIn } from "./rollen";
import { berekenParallelVingerafdruk } from "./parallelle-clausules";
import { canoniekeParameterNaam, normaliseerParameterWaarden } from "./parameter-register";

// ── Samenstellen en genereren ────────────────────────────────────────────────
// Pure functies (geen React, geen browser-API's) die modelonderdelen
// componeren tot modelteksten en ingevulde werkdocumenten.

/** Haalt alle {{parameter}}-namen uit één of meer teksten (gededupliceerd). */
export function extraheerParameters(...teksten: string[]): string[] {
  const set = new Set<string>();
  for (const tekst of teksten) {
    for (const m of tekst.matchAll(/\{\{([^}]+)\}\}/g)) set.add(m[1].trim());
  }
  return [...set];
}

/**
 * Combineert de seed-bibliotheek met gebruikersitems uit IndexedDB:
 * een gebruikersitem met hetzelfde id vervangt het seed-item (override),
 * nieuwe gebruikersitems komen achteraan.
 */
export function combineerMetSeed<T extends { id: string }>(seed: T[], gebruikers: T[]): T[] {
  const overrides = new Map(gebruikers.map((item) => [item.id, item]));
  const basis = seed.map((item) => overrides.get(item.id) ?? item);
  const nieuw = gebruikers.filter((item) => !seed.some((s) => s.id === item.id));
  return [...basis, ...nieuw];
}

/**
 * De hypothese-varianten die op één structuurpositie zichtbaar zijn: het
 * volledige variantenblok van het onderdeel, beperkt door de eventuele
 * positie-eigen prefixfilters (`enkelVariantenMetPrefix`/
 * `zonderVariantenMetPrefix` — zie StructuurItem). Zo kan hetzelfde onderdeel
 * bewust tweemaal in een structuur staan met elk een eigen deelrol
 * (identiteit-partijen: partij-hypotheses vs. notaris-vaststelling) zonder
 * dat beide posities de volledige catalogus tonen.
 */
function structuurVarianten(item: StructuurItem, varianten: ModelVariant[]): ModelVariant[] {
  let zichtbaar = varianten;
  if (item.enkelVariantenMetPrefix) {
    zichtbaar = zichtbaar.filter((v) => v.id.startsWith(item.enkelVariantenMetPrefix!));
  }
  if (item.zonderVariantenMetPrefix) {
    zichtbaar = zichtbaar.filter((v) => !v.id.startsWith(item.zonderVariantenMetPrefix!));
  }
  return zichtbaar;
}

/**
 * Stelt de volledige modeltekst samen uit de structuur en de beschikbare
 * onderdelen. Facultatieve onderdelen en hypothese-varianten krijgen
 * markeringen volgens de conventie van de AI-prompts module
 * ([NAKIJKEN OF SCHRAPPEN], hypothese-keuze), zodat de output rechtstreeks
 * bruikbaar is als in te vullen werkdocument.
 */
export function stelModelSamen(
  model: Modeldocument,
  onderdelen: Modelonderdeel[]
): string {
  const delen: string[] = [model.titel.toUpperCase()];

  if (model.inleiding?.trim()) delen.push(model.inleiding.trim());

  let huidigeGroep: string | undefined;
  model.structuur.forEach((item, i) => {
    const onderdeel = onderdelen.find((o) => o.id === item.onderdeelId);
    if (!onderdeel) {
      delen.push(`${i + 1}. [ONTBREKEND ONDERDEEL: ${item.onderdeelId}]`);
      return;
    }
    if (item.groep && item.groep !== huidigeGroep) {
      huidigeGroep = item.groep;
      delen.push(`=== ${item.groep} ===`);
    }
    const regels: string[] = [`${i + 1}. ${onderdeel.titel.toUpperCase()}`];
    if (!item.verplicht) {
      regels.push(
        `[NAKIJKEN OF SCHRAPPEN: facultatief onderdeel${item.conditie ? ` — ${item.conditie}` : ""}]`
      );
    } else if (item.conditie) {
      regels.push(`[NB: ${item.conditie}]`);
    }
    if (onderdeel.tekst.trim()) regels.push(onderdeel.tekst.trim());
    const zichtbareVarianten = structuurVarianten(item, onderdeel.varianten);
    if (zichtbareVarianten.length > 0) {
      if (zichtbareVarianten.length > 1) {
        regels.push("[KIES DE TOEPASSELIJKE HYPOTHESE EN SCHRAP DE OVERIGE:]");
      }
      for (const variant of zichtbareVarianten) {
        regels.push(`▸ HYPOTHESE — ${variant.hypothese}:`);
        regels.push(variant.tekst.trim());
      }
    }
    delen.push(regels.join("\n\n"));
  });

  if (model.slot?.trim()) delen.push(model.slot.trim());

  return vulRollenIn(delen.join("\n\n──────────\n\n"), model.akteType, model.taal === "fr" ? "fr" : "nl");
}

/** Volledige tekst van een onderdeel (basistekst + hypotheses), voor vergelijking in het validatie-overzicht. */
export function onderdeelVoorbeeldtekst(o: Modelonderdeel): string {
  const delen: string[] = [];
  if (o.tekst.trim()) delen.push(o.tekst.trim());
  for (const v of o.varianten) {
    delen.push(`▸ HYPOTHESE — ${v.hypothese}:\n${v.tekst.trim()}`);
  }
  return normaliseerWitruimte(delen.join("\n\n").replace(/\u2014/g, "-"));
}

/**
 * Interne titel-prefix van de modelonderdelen ("Heldere taal — …" / "Langage
 * clair — …"); die administratieve prefix mag NIET in het document verschijnen.
 */
const INTERNE_KOP_PREFIX = /^(?:Heldere taal|Langage clair)\s*[—–-]\s*/i;

/**
 * Bepaalt de kop die in het GEGENEREERDE document boven een clausule hoort, los
 * van de interne (administratieve) onderdeel-titel. De officiële modelteksten
 * beginnen met hun eigen, nette clausulekop (bv. "GEBRUIK – GENOT"); die wordt
 * als kop gebruikt en uit de body gehaald zodat ze niet twee keer verschijnt.
 * Begint de tekst niet met zo'n kop, dan valt de kop terug op de interne titel
 * zonder de "Heldere taal —"-prefix. De interne titel zelf verschijnt nooit.
 */
export function clausuleKopEnBody(onderdeel: Modelonderdeel): { kop: string; body: string } {
  const tekst = onderdeel.tekst ?? "";
  const lijnen = tekst.split("\n");
  let i = 0;
  while (i < lijnen.length && lijnen[i].trim() === "") i++;
  const eerste = (lijnen[i] ?? "").trim();
  const naEerste = lijnen.slice(i + 1);
  const heeftBlankNa = naEerste.length > 0 && naEerste[0].trim() === "";
  // Een kop-regel: korte, eigen alinea (gevolgd door een lege regel), geen
  // commentaarblok ("[…]") en geen lopende zin.
  const kopAchtig = eerste !== "" && !eerste.startsWith("[") && eerste.length <= 80 && heeftBlankNa;
  const body = kopAchtig ? naEerste.join("\n").replace(/^\n+/, "") : tekst;
  // Kop uit de tekst behoudt zijn eigen opmaak; de titel-terugval wordt in
  // hoofdletters gezet, in lijn met de clausulekoppen in de modelteksten.
  const kop = kopAchtig ? eerste : onderdeel.titel.replace(INTERNE_KOP_PREFIX, "").toUpperCase();
  return { kop, body };
}

/**
 * Markeert de tussentitels (subkoppen) in de lopende clausuletekst met "### ",
 * zodat ze in het Word-document een eigen kopstijl krijgen i.p.v. gewone tekst.
 * Een subkop is een korte, op zichzelf staande regel volledig in HOOFDLETTERS
 * (bv. "DE KOSTEN VERBONDEN AAN DE VERKOOP", "HET KADASTRAAL INKOMEN"). Regels
 * met kleine letters, commentaarblokken ("[…]") of bestaande markeringen blijven
 * ongemoeid.
 */
export function markeerSubkoppen(tekst: string): string {
  const lijnen = tekst.split("\n");
  return lijnen
    .map((regel, i) => {
      const t = regel.trim();
      if (!t || t.length > 80) return regel;
      if (t.startsWith("[") || t.startsWith("##") || t.startsWith("===") || t.startsWith("▸")) return regel;
      const vorige = lijnen[i - 1];
      const volgende = lijnen[i + 1];
      const opZichzelf = volgende === undefined || volgende.trim() === "";
      // (a) HOOFDLETTER-subkop op clausuletitelniveau (Kop 3 → "### "): een korte,
      // op zichzelf staande regel volledig in hoofdletters. Een toelichting
      // tussen haakjes aan het einde mag kleine letters bevatten — "ASBEST
      // (toegankelijke constructies met bouwjaar vóór 2001)" en "CONTROLE VAN DE
      // ELEKTRICITEITSINSTALLATIE (enkel voor woongelegenheden)" zijn óók
      // subkoppen (kwaliteitsnazicht notaris 2026-07-10).
      const kern = t.replace(/\s*\([^)]*\)$/, "").trim();
      if (kern && /[A-ZÀ-Ÿ]/.test(kern) && kern === kern.toUpperCase() && opZichzelf) return `### ${t}`;
      // (b) Zelfde subkopniveau (Kop 3 → "### "), enkel een andere schrijfwijze:
      // een korte titel in hoofdletter+kleine-letters (geen volzin), voorafgegaan
      // door een lege regel en gevolgd door tekst (bv. "Exécution forcée ou
      // résolution", "Intérêts de retard", "Condition suspensive", "Engagements
      // du vendeur"). Beide vormen krijgen dezelfde "N.M."-nummering en dus ook
      // hetzelfde kopniveau — anders staan gelijkwaardige subonderdelen van
      // eenzelfde clausule op een ongelijke diepte in de navigatie/inhoudsopgave.
      const naVorigeLeeg = vorige === undefined || vorige.trim() === "";
      const eindigtOpLeesteken = /[.!?:;,]$/.test(t);
      const woorden = t.split(/\s+/).length;
      const begintHoofdletter = /^[A-ZÀ-Ÿ]/.test(t);
      const heeftKleineLetter = /[a-zà-ÿ]/.test(t);
      if (
        naVorigeLeeg &&
        volgende !== undefined &&
        volgende.trim() !== "" &&
        t.length <= 60 &&
        woorden <= 8 &&
        begintHoofdletter &&
        heeftKleineLetter &&
        !eindigtOpLeesteken
      ) {
        return `### ${t}`;
      }
      return regel;
    })
    .join("\n");
}

/**
 * Nummert alle koppen hiërarchisch en doorlopend: een clausulekop ("## ") krijgt
 * "N." (al gezet door genereerDocument) en elke onderliggende tussentitel
 * ("### " HOOFDLETTERS of "#### " fijnere titel) krijgt "N.M." onder de lopende
 * clausule. Zo is ELKE kop genummerd en blijft het visuele niveauverschil
 * (Kop 2 vs Kop 3) behouden. De sectiegroepen ("=== … ===") blijven banners
 * zonder nummer. Tussentitels die vóór de eerste clausule staan, blijven ongemoeid.
 */
export function nummerKoppen(tekst: string): string {
  let hoofdNr = 0;
  let subNr = 0;
  return tekst
    .split("\n")
    .map((regel) => {
      const hoofd = regel.match(/^## (\d+)\. /);
      if (hoofd) {
        hoofdNr = Number(hoofd[1]);
        subNr = 0;
        return regel;
      }
      const sub = regel.match(/^(### |#### )(.+)$/);
      if (sub && hoofdNr > 0) {
        subNr += 1;
        return `${sub[1]}${hoofdNr}.${subNr}. ${sub[2]}`;
      }
      return regel;
    })
    .join("\n");
}

// ── Invul-/generatiemodus ────────────────────────────────────────────────────

/** Keuzes per structuuritem bij het genereren van een document. */
export interface GeneratieKeuze {
  /** Onderdeel opnemen in het gegenereerde document. */
  opnemen: boolean;
  /** Gekozen hypothese-variant; "alle" behoudt alle hypotheses met markering. */
  variantId: string;
  /**
   * True wanneer een facultatief onderdeel se (niet-)opname deterministisch
   * werd beslist op basis van een reeds gekend dossierfeit (bv. het gewest van
   * het goed) — de generieke "facultatief onderdeel, enkel van toepassing
   * indien …"-markering is dan overbodig (het feit is al bevestigd) en wordt
   * onderdrukt. Standaard false/undefined: de markering blijft dan staan.
   */
  conditieBevestigd?: boolean;
}

/**
 * Eén herhaling van een per-partij hernomen onderdeel (bv. de identificatie
 * van één partij). `label` verschijnt als kop boven de herhaling; `waarden`
 * overschrijven de basiswaarden voor die ene herhaling.
 */
export interface Herhaling {
  label?: string;
  waarden: Record<string, string>;
  /** Hypothese-variant voor deze ene herhaling (bv. burgerlijke staat); overschrijft de onderdeel-keuze. */
  variantId?: string;
  /**
   * Geordende reeks bouwsteen-instanties voor deze herhaling, elk met een
   * eigen waardenset: de oorsprong van eigendom bestaat uit een basistekst
   * (oudste titel) gevolgd door nul of meer tussenliggende overgangen
   * (verkoop, overlijden, schenking) — elk een hypothese-variant van dezelfde
   * clausule die één of meerdere keren, telkens met eigen waarden, wordt
   * geïnstantieerd. Voor de clausule waarop een schakel mikt (`onderdeelId`,
   * ook herkend via de vertaling ervan) worden de varianten dan NIET als te
   * kiezen hypotheses gemarkeerd maar in deze volgorde effectief opgenomen;
   * andere herhaal-clausules in dezelfde herhaling blijven ongemoeid.
   */
  schakels?: { onderdeelId?: string; variantId: string; waarden: Record<string, string> }[];
}

/**
 * De namen van de {{parameters}} die voorkomen in de per-partij hernomen
 * onderdelen van het model. Die parameters worden bij het genereren per
 * herhaling ingevuld en hoeven dus niet als globale (eenmalige) parameter te
 * worden uitgevraagd.
 */
export function herhaaldePartijParameters(
  model: Modeldocument,
  onderdelen: Modelonderdeel[]
): Set<string> {
  return herhaaldeParameters(model, onderdelen, (item) => !!item.herhaalPerPartij);
}

/**
 * De namen van de {{parameters}} die voorkomen in de per-goed hernomen
 * onderdelen (beschrijving en oorsprong): die worden per goed ingevuld en
 * hoeven niet als globale parameter te worden uitgevraagd zodra er
 * goed-herhalingen zijn.
 */
export function herhaaldeGoedParameters(
  model: Modeldocument,
  onderdelen: Modelonderdeel[]
): Set<string> {
  return herhaaldeParameters(model, onderdelen, (item) => !!item.herhaalPerGoed);
}

function herhaaldeParameters(
  model: Modeldocument,
  onderdelen: Modelonderdeel[],
  filter: (item: Modeldocument["structuur"][number]) => boolean
): Set<string> {
  const namen = new Set<string>();
  for (const item of model.structuur) {
    if (!filter(item)) continue;
    const onderdeel = onderdelen.find((o) => o.id === item.onderdeelId);
    if (!onderdeel) continue;
    const teksten = [onderdeel.tekst, ...onderdeel.varianten.map((v) => v.tekst)];
    for (const naam of extraheerParameters(...teksten)) namen.add(naam);
  }
  return namen;
}

/**
 * Standaardkeuzes: ALLE onderdelen aan (verplicht én facultatief), alle
 * hypotheses behouden. Veiligheidsprincipe: bij twijfel niets schrappen. Een
 * facultatief onderdeel blijft dus standaard staan (gemarkeerd "[NAKIJKEN OF
 * SCHRAPPEN]") en verdwijnt enkel wanneer een uitdrukkelijk feit het op
 * `opnemen: false` zet. Zo kan een ontbrekend of fout feit nooit stilzwijgend
 * een clausule verwijderen — wat zware gevolgen kan hebben.
 */
export function standaardKeuzes(model: Modeldocument): GeneratieKeuze[] {
  return model.structuur.map(() => ({ opnemen: true, variantId: "alle" }));
}

/** Een uit het dossier afgeleide keuze voor één onderdeel. */
export interface KeuzeOverride {
  onderdeelId: string;
  variantId?: string;
  opnemen?: boolean;
  conditieBevestigd?: boolean;
}

/**
 * Past keuze-overrides toe op de standaardkeuzes: per structuuritem dat met een
 * override overeenkomt, wordt de hypothese-variant gekozen en/of het onderdeel
 * opgenomen. Overrides voor onbekende onderdelen worden genegeerd.
 */
export function pasKeuzesToe(model: Modeldocument, overrides: KeuzeOverride[]): GeneratieKeuze[] {
  const keuzes = standaardKeuzes(model);
  for (const override of overrides) {
    model.structuur.forEach((item, i) => {
      if (item.onderdeelId !== override.onderdeelId) return;
      if (override.variantId !== undefined) keuzes[i] = { ...keuzes[i], variantId: override.variantId };
      if (override.opnemen !== undefined) keuzes[i] = { ...keuzes[i], opnemen: override.opnemen };
      if (override.conditieBevestigd !== undefined) keuzes[i] = { ...keuzes[i], conditieBevestigd: override.conditieBevestigd };
    });
  }
  return keuzes;
}

function geselecteerdeTeksten(
  model: Modeldocument,
  onderdelen: Modelonderdeel[],
  keuzes: GeneratieKeuze[]
): string[] {
  const teksten: string[] = [];
  if (model.inleiding) teksten.push(model.inleiding);
  model.structuur.forEach((item, i) => {
    const keuze = keuzes[i];
    if (!keuze?.opnemen) return;
    const onderdeel = onderdelen.find((o) => o.id === item.onderdeelId);
    if (!onderdeel) return;
    if (onderdeel.tekst) teksten.push(onderdeel.tekst);
    const zichtbaar = structuurVarianten(item, onderdeel.varianten);
    const varianten =
      keuze.variantId === "alle" ? zichtbaar : zichtbaar.filter((v) => v.id === keuze.variantId);
    teksten.push(...varianten.map((v) => v.tekst));
  });
  if (model.slot) teksten.push(model.slot);
  return teksten;
}

/** Parameters die voorkomen in de geselecteerde onderdelen/varianten. */
export function verzamelParameters(
  model: Modeldocument,
  onderdelen: Modelonderdeel[],
  keuzes: GeneratieKeuze[]
): string[] {
  return extraheerParameters(...geselecteerdeTeksten(model, onderdelen, keuzes));
}

// ── Opmaak-markers (vet) ───────────────────────────────────
// Bedragen (prijs, waarborg) en de namen in het identiteitsblok worden vet
// weergegeven, gemarkeerd met twee private-use-tekens (U+E000/U+E001) rond het
// vette deel. De Word-render zet die om naar vette runs; voor platte tekst
// (JSON-API, UI-preview) worden ze met verwijderOpmaakMarkers() weggehaald.
const VET_OPEN = "\uE000";
const VET_SLUIT = "\uE001";
// Geel-markers (U+E002/U+E003): markeren een tekstdeel dat in de Word-render geel
// gearceerd wordt (standaardverklaringen die de notaris moet nakijken, bv.
// rookmelders, overstroming, stookolietank, en de aktedatum-termijn).
export const GEEL_OPEN = "\uE002";
export const GEEL_SLUIT = "\uE003";
/** Markeert een tekstdeel als geel te arceren in de Word-uitvoer. */
export function geelMarkeer(tekst: string): string {
  return `${GEEL_OPEN}${tekst}${GEEL_SLUIT}`;
}
// Onderlijn-markers (U+E006/U+E007): markeren een tekstdeel dat onderlijnd wordt.
// Worden voor gemeente/kadastrale afdeling gecombineerd met de vet-markers.
const ONDER_OPEN = "";
const ONDER_SLUIT = "";
/** Markeert een tekstdeel als vet in de Word-uitvoer (bv. een vast woord als "huis"). */
export function vetMarkeer(tekst: string): string {
  return `${VET_OPEN}${tekst}${VET_SLUIT}`;
}
/**
 * Normaliseert de witruimte van een samengesteld document: spaties/tabs aan het
 * regeleinde weg, en hoogstens één lege regel tussen blokken (de compromis-
 * standaard: één lege regel tussen een titel/kop en de erop volgende alinea).
 * Zo verdwijnen de opeenstapelingen van lege regels die ontstaan wanneer een
 * onderdeel zelf al op een lege regel eindigt en de join er nog een toevoegt.
 */
function normaliseerWitruimte(tekst: string, geenLegeRegels = false): string {
  const zonderTrailing = tekst.replace(/[ \t]+$/gm, "");
  // Authentieke akte: geen lege regels tussen de alinea's (notariële conventie);
  // onderhands document (compromis): hoogstens één lege regel.
  return (geenLegeRegels ? zonderTrailing.replace(/\n{2,}/g, "\n") : zonderTrailing.replace(/\n{3,}/g, "\n\n")).trim();
}
/**
 * Vertaalt de interne werk-markeringen (de standaard NL-conventie die in \u00e1lle
 * modellen wordt gebruikt) naar het Frans. Bedoeld voor de FR-Word-uitvoer: zo
 * ziet de notaris Franstalige markeringen, terwijl de interne tekst (voor
 * detectie/JSON) de uniforme conventie behoudt. Vertaalt enkel de labels, niet
 * de Nederlandstalige omschrijvingen die soms in een marker staan.
 */
const FR_MARKER_VERVANGINGEN: [RegExp, string][] = [
  // Volledige engine-keuzemarkering eerst (v\u00f3\u00f3r de generieke "[KIES").
  [/\[KIES DE TOEPASSELIJKE HYPOTHESE EN SCHRAP DE OVERIGE:\]/g, "[CHOISIR L'HYPOTH\u00c8SE APPLICABLE ET SUPPRIMER LES AUTRES :]"],
  [/\u25b8 HYPOTHESE /g, "\u25b8 HYPOTH\u00c8SE "],
  [/ONTBREKEND ONDERDEEL/g, "SECTION MANQUANTE"],
  [/⚠ PARALLELLE CLAUSULE GEWIJZIGD:/g, "⚠ CLAUSE PARALLÈLE MODIFIÉE :"],
  [/facultatief onderdeel/g, "clause facultative"],
  // Verbose markeringen: ook tussen haakjes "(" of zonder bracket geschreven.
  [/AAN TE VULLEN/g, "\u00c0 COMPL\u00c9TER"],
  [/NAKIJKEN OF SCHRAPPEN/g, "\u00c0 V\u00c9RIFIER OU SUPPRIMER"],
  [/\bNAKIJKEN\b/g, "\u00c0 V\u00c9RIFIER"],
  [/TE VERIFI\u00cbREN/g, "\u00c0 V\u00c9RIFIER"],
  [/\[KIES/g, "[CHOISIR"],
];
export function vertaalMarkeringenFR(tekst: string): string {
  return FR_MARKER_VERVANGINGEN.reduce((t, [re, ver]) => t.replace(re, ver), tekst);
}
/** Verwijdert de opmaak-markers (vet/geel/onderlijn, U+E000\u2013U+E007, en link U+E004/E005) uit een tekst (voor platte weergave). */
export function verwijderOpmaakMarkers(tekst: string): string {
  return tekst.replace(/[\uE000-\uE007]/g, "");
}
/** Parameters waarvan de waarde vet wordt weergegeven (bedragen, type goed, ligging). */
const VETTE_PARAMETERS = new Set([
  "prijs_cijfers", "prijs_voluit", "prix_chiffres", "prix_lettres",
  "type_goed", "nature_du_lot", "adres_onroerend_goed", "adresse_bien",
  "solde_prix_lettres", "saldo_prijs_voluit",
  "saldo_prijs", "solde_prix",
]);
/** Parameters waarvan de waarde vet \u00E9n onderlijnd wordt (gemeente en kadastrale afdeling). */
const VET_ONDERLIJNDE_PARAMETERS = new Set([
  "gemeente", "commune", "afdeling_omschrijving", "description_division",
]);
/**
 * Parameters waarvan de waarde geel gearceerd wordt (na te kijken). De
 * erfdienstbaarheden/bijzondere voorwaarden uit de eigendomstitel zijn
 * cruciaal (de koper moet ze respecteren als ze nog gelden) en moeten dus
 * altijd — ook bij een correct ingevulde waarde — uitdrukkelijk door de
 * notaris nagekeken worden vóór oplevering.
 */
const GELE_PARAMETERS = new Set<string>([
  "datum_eigendomsakte", "date_titre_propriete",
  "erfdienstbaarheden_titel", "servitudes_titre",
]);
/**
 * Parameters waarvan de waarde zowel vet als geel gearceerd wordt: het bedrag
 * is inhoudelijk een bedrag (vet, zoals de overige bedragen) maar blijft, ook
 * bij een plausibele default (10% van de prijs), ter bevestiging aan de
 * notaris voorgelegd.
 */
const VET_GELE_PARAMETERS = new Set([
  "bedrag_waarborg", "montant_garantie", "bedrag_waarborg_voluit", "montant_garantie_lettres",
  // Forfaitaire kredietvergoeding: gangbare default (0,50%, Fednot-suggestie)
  // die de notaris uitdrukkelijk bevestigt — daarom geel gemarkeerd.
  "percentage_schadevergoeding", "pourcentage_indemnite",
]);

/**
 * Vult {{parameter}}-placeholders in een willekeurige modeltekst in met de
 * gegeven waarden (ontbrekende waarde → "[AAN TE VULLEN: naam]"); past ook de
 * vaste opmaak-markeringen toe (vet/onderlijn/geel) voor de bekende
 * parameternamen. Geëxporteerd zodat andere modules (bv. de identiteitsclausule
 * in lib/dossier/parameters.ts) dezelfde, ene substitutielogica hergebruiken
 * in plaats van ze te dupliceren.
 */
export function vulParametersIn(
  tekst: string,
  waarden: Record<string, string>,
  akteType: string,
  taal: "nl" | "fr" = "nl"
): string {
  // Aliasresolutie (parameter-register.ts): een waarde die onder een oude of
  // afwijkende naam binnenkomt vult ook de canonieke placeholder, en omgekeerd
  // — externe agenten en oudere gebruikersclausules blijven zo gewoon werken.
  const canoniek = normaliseerParameterWaarden(waarden);
  return vulRollenIn(tekst, akteType, taal).replace(/\{\{([^}]+)\}\}/g, (_, naam: string) => {
    const sleutel = naam.trim();
    const waarde = (waarden[sleutel] ?? canoniek[canoniekeParameterNaam(sleutel)])?.trim();
    if (!waarde) return `[AAN TE VULLEN: ${sleutel}]`;
    if (VET_ONDERLIJNDE_PARAMETERS.has(sleutel)) return `${VET_OPEN}${ONDER_OPEN}${waarde}${ONDER_SLUIT}${VET_SLUIT}`;
    if (VET_GELE_PARAMETERS.has(sleutel)) return `${VET_OPEN}${GEEL_OPEN}${waarde}${GEEL_SLUIT}${VET_SLUIT}`;
    if (VETTE_PARAMETERS.has(sleutel)) return `${VET_OPEN}${waarde}${VET_SLUIT}`;
    if (GELE_PARAMETERS.has(sleutel)) return `${GEEL_OPEN}${waarde}${GEEL_SLUIT}`;
    return waarde;
  });
}

/**
 * Knipperlicht van het parallelclausule-mechanisme (integratie/
 * PARALLELLE-CLAUSULES.md): meldingen voor de parallellen van één onderdeel
 * die inhoudelijk gewijzigd zijn sinds dit onderdeel er laatst tegen werd
 * nagekeken (opgeslagen vingerafdruk ≠ actuele vingerafdruk van de partner),
 * of waarvoor nog geen vingerafdruk is opgeslagen. Een lege lijst betekent:
 * alle parallellen lopen gelijk. Onbestaande partner-ids worden hier
 * genegeerd — die bewaakt de ratchet-test (parallelle-clausules.test.ts) al.
 * De seed-bibliotheek is per die test altijd in balans; dit knipperlicht
 * vuurt dus enkel wanneer een gebruikersbewerking (IndexedDB-override via
 * combineerMetSeed) één kant van een parallel wijzigde.
 */
export function parallelWaarschuwingen(
  onderdeel: Modelonderdeel,
  bibliotheek: Modelonderdeel[],
  vingerafdrukCache: Map<string, string> = new Map()
): string[] {
  const meldingen: string[] = [];
  for (const partnerId of onderdeel.parallelMetIds ?? []) {
    const partner = bibliotheek.find((o) => o.id === partnerId);
    if (!partner) continue;
    let actueel = vingerafdrukCache.get(partnerId);
    if (actueel === undefined) {
      actueel = berekenParallelVingerafdruk(partner);
      vingerafdrukCache.set(partnerId, actueel);
    }
    if (onderdeel.parallelVingerafdrukken?.[partnerId] !== actueel) {
      meldingen.push(
        `⚠ PARALLELLE CLAUSULE GEWIJZIGD: "${partner.titel}" (${partnerId}) werd recenter gewijzigd dan dit onderdeel — laat nakijken of deze clausule mee moet worden aangepast.`
      );
    }
  }
  return meldingen;
}

/**
 * Genereert het ingevulde document: enkel de gekozen onderdelen en
 * hypotheses, parameters vervangen door waarden of [AAN TE VULLEN: …].
 * Niet-gemaakte hypothese-keuzes blijven gemarkeerd staan, zodat het
 * resultaat een werkdocument blijft dat de notaris naleest.
 */
export function genereerDocument(
  model: Modeldocument,
  onderdelen: Modelonderdeel[],
  keuzes: GeneratieKeuze[],
  waarden: Record<string, string>,
  partijHerhalingen: Herhaling[] = [],
  goedHerhalingen: Herhaling[] = []
): string {
  const delen: string[] = [model.titel.toUpperCase()];
  const taal: "nl" | "fr" = model.taal === "fr" ? "fr" : "nl";

  if (model.inleiding?.trim()) delen.push(vulParametersIn(model.inleiding.trim(), waarden, model.akteType, taal));

  // Eén vingerafdruk-cache per generatie: een partner die bij meerdere
  // onderdelen als parallel voorkomt, wordt maar één keer gehasht.
  const parallelCache = new Map<string, string>();
  let huidigeGroep: string | undefined;
  let clausuleNr = 0;
  model.structuur.forEach((item, i) => {
    const keuze = keuzes[i];
    if (!keuze?.opnemen) return;
    const onderdeel = onderdelen.find((o) => o.id === item.onderdeelId);
    if (!onderdeel) {
      delen.push(`[ONTBREKEND ONDERDEEL: ${item.onderdeelId}]`);
      return;
    }
    // Sectiegroep-kop (Word-stijl "Kop 1") zodra de groep wijzigt.
    if (item.groep && item.groep !== huidigeGroep) {
      huidigeGroep = item.groep;
      delen.push(`=== ${item.groep} ===`);
    }
    // Clausulekop (Word-stijl "Kop 2"), gemarkeerd met "## ". NIET de interne
    // onderdeel-titel: de nette clausulekop komt uit de modeltekst zelf.
    const { kop, body: ruweBody } = clausuleKopEnBody(onderdeel);
    // Verwijder de manuele keuze-instructie ("[KIES de gewestvariant hierna.]",
    // "[KIES de toepasselijke hypothese hierna en schrap …]") uit de basistekst:
    // bij een deterministisch gekozen variant is ze overbodig, en blijven er
    // meerdere hypotheses staan, dan voegt de engine zelf "[KIES DE TOEPASSELIJKE
    // HYPOTHESE EN SCHRAP DE OVERIGE:]" toe.
    const body = ruweBody.replace(/\[KIES de (?:gewestvariant|toepasselijke hypothese)[^\]]*\]/g, "").trim();
    // Genummerde clausulekop (Word-stijl "Kop 2"): doorlopend 1., 2., 3., … Een
    // onderdeel met "geenEigenNummer" krijgt geen eigen kop/nummer (de tekst
    // sluit aan bij het voorgaande onderdeel, waarvan het nummer voor de
    // eventuele subkoppen (### / ####) blijft gelden); zonder eigen "## N."
    // schuift de nummering van de volgende onderdelen niet op.
    const regels: string[] = [];
    if (!onderdeel.geenEigenNummer) {
      clausuleNr += 1;
      regels.push(`## ${clausuleNr}. ${kop}`);
    }
    // Facultatief onderdeel: standaard behouden (veiligheid), maar uitdrukkelijk
    // markeren zodat de notaris het nakijkt of schrapt indien niet van toepassing.
    // Is de (niet-)opname al deterministisch beslist op basis van een gekend
    // dossierfeit (keuze.conditieBevestigd), dan is die markering overbodig —
    // het feit is al bevestigd, opnieuw vragen zou de notaris nodeloos een
    // reeds beantwoorde vraag laten herhalen.
    if (!item.verplicht && !keuze.conditieBevestigd) {
      regels.push(`[NAKIJKEN OF SCHRAPPEN: facultatief onderdeel${item.conditie ? ` — ${item.conditie}` : ""}]`);
    }
    // Knipperlicht: is een parallelle clausule (ander aktetype, zelfde
    // onderwerp) recenter gewijzigd dan dit onderdeel, dan komt er een geel
    // gearceerde melding in het werkdocument tot de divergentie is nagekeken.
    for (const melding of parallelWaarschuwingen(onderdeel, onderdelen, parallelCache)) {
      regels.push(geelMarkeer(melding));
    }

    const zichtbareVarianten = structuurVarianten(item, onderdeel.varianten);
    const varianten =
      keuze.variantId === "alle"
        ? zichtbareVarianten
        : zichtbareVarianten.filter((v) => v.id === keuze.variantId);

    // Eén weergave van het onderdeel (basistekst + hypotheses), ingevuld met
    // de meegegeven waarden. Een herhaling kan haar eigen hypothese-variant
    // opleggen (bv. burgerlijke staat), die dan de onderdeel-keuze overschrijft.
    const renderOnderdeel = (
      eigenWaarden: Record<string, string>,
      eigenVarianten: typeof varianten
    ): string[] => {
      const r: string[] = [];
      if (body.trim()) r.push(vulParametersIn(markeerSubkoppen(body.trim()), eigenWaarden, model.akteType, taal));
      if (eigenVarianten.length > 1) {
        r.push("[KIES DE TOEPASSELIJKE HYPOTHESE EN SCHRAP DE OVERIGE:]");
        for (const variant of eigenVarianten) {
          r.push(`▸ HYPOTHESE — ${variant.hypothese}:`);
          r.push(vulParametersIn(markeerSubkoppen(variant.tekst.trim()), eigenWaarden, model.akteType, taal));
        }
      } else if (eigenVarianten.length === 1) {
        r.push(vulParametersIn(markeerSubkoppen(eigenVarianten[0].tekst.trim()), eigenWaarden, model.akteType, taal));
      }
      return r;
    };

    // Eén weergave met een vaste, geordende reeks bouwsteen-instanties
    // (Herhaling.schakels): basistekst gevolgd door elke schakel-variant met
    // haar eigen waarden — géén te-kiezen-hypothesemarkering, de keten IS de
    // keuze (bv. de oorsprong van eigendom over meerdere overgangen heen).
    const renderMetSchakels = (
      eigenWaarden: Record<string, string>,
      schakels: NonNullable<Herhaling["schakels"]>
    ): string[] => {
      const r: string[] = [];
      if (body.trim()) r.push(vulParametersIn(markeerSubkoppen(body.trim()), eigenWaarden, model.akteType, taal));
      for (const schakel of schakels) {
        const variant = onderdeel.varianten.find((v) => v.id === schakel.variantId);
        if (!variant) {
          r.push(`[ONBEKENDE BOUWSTEEN: ${schakel.variantId}]`);
          continue;
        }
        r.push(
          vulParametersIn(
            markeerSubkoppen(variant.tekst.trim()),
            { ...eigenWaarden, ...schakel.waarden },
            model.akteType,
            taal
          )
        );
      }
      return r;
    };

    // Per partij of per goed hernemen: één blok per herhaling, met de per
    // herhaling gekende waarden over de basiswaarden gelegd.
    const herhalingen: { lijst: Herhaling[]; labelPrefix: string } | null =
      item.herhaalPerPartij && partijHerhalingen.length > 0
        ? { lijst: partijHerhalingen, labelPrefix: "Partij" }
        : item.herhaalPerGoed && goedHerhalingen.length > 0
        ? { lijst: goedHerhalingen, labelPrefix: "Goed" }
        : null;

    if (herhalingen) {
      herhalingen.lijst.forEach((herhaling, idx) => {
        // Bij één enkel goed zonder eigen label is een tussenkop overbodig.
        if (herhalingen.lijst.length > 1 || herhaling.label) {
          regels.push(`— ${herhaling.label ?? `${herhalingen.labelPrefix} ${idx + 1}`} —`);
        }
        const samengevoegd = { ...waarden, ...herhaling.waarden };
        // Schakels gelden enkel voor de clausule waarop ze mikken (id of de
        // vertaling ervan); zonder onderdeelId gelden ze voor elke clausule.
        const schakels = (herhaling.schakels ?? []).filter(
          (sch) => !sch.onderdeelId || sch.onderdeelId === onderdeel.id || sch.onderdeelId === onderdeel.vertalingVanId
        );
        if (schakels.length > 0) {
          regels.push(...renderMetSchakels(samengevoegd, schakels));
        } else {
          // De per-herhaling variantkeuze geldt enkel wanneer die variant in
          // déze clausule bestaat (bv. het type goed in de beschrijving);
          // anders behoudt de clausule haar gewone keuze/hypothesemarkering.
          const eigenVariant = herhaling.variantId
            ? zichtbareVarianten.filter((v) => v.id === herhaling.variantId)
            : [];
          regels.push(...renderOnderdeel(samengevoegd, eigenVariant.length > 0 ? eigenVariant : varianten));
        }
      });
    } else {
      regels.push(...renderOnderdeel(waarden, varianten));
    }
    delen.push(regels.join("\n\n"));
  });

  if (model.slot?.trim()) delen.push(vulParametersIn(markeerSubkoppen(model.slot.trim()), waarden, model.akteType, taal));

  const resultaat = normaliseerWitruimte(nummerKoppen(delen.join("\n\n")).replace(/\u2014/g, "-"), model.authentiekeAkte ?? false);
  // Franstalig model \u2192 Franstalige werk-markeringen, overal (UI-voorbeeld,
  // finale, sessie-paneel, Word): een FR-akte met "[NAKIJKEN OF SCHRAPPEN\u2026]"
  // en "[KIES DE TOEPASSELIJKE HYPOTHESE\u2026]" is onleesbaar voor een Franstalige
  // partij (kwaliteitsnazicht notaris 2026-07-10). De Word-render past de
  // vertaling ook zelf toe \u2014 die is idempotent, dus dubbel toepassen is veilig.
  return (model.taal ?? "nl") === "fr" ? vertaalMarkeringenFR(resultaat) : resultaat;
}

// ── Inline keuzes ([KIES: A / B / C]) ────────────────────────────────────────
// In tegenstelling tot hypothese-varianten (ModelVariant, hierboven via
// GeneratieKeuze/variantId opgelost) zitten veel keuzes — vooral in de
// vennootschap- en erfrechtonderdelen — als [KIES: optie1 / optie2]-blokken in
// de lopende tekst zelf, zonder apart variant-record. Hiervoor bestond tot nu
// toe geen programmatisch resolutiemechanisme: ze bleven altijd als
// keuzeblok staan tot de notaris ze handmatig wegschrapte. De functies
// hieronder maken die keuzes wél programmatisch leesbaar en oplosbaar, als
// basis voor (toekomstige) automatische of UI-ondersteunde afhandeling.
//
// Bewuste beperking: deze laag weet niets over de *betekenis* van een optie
// (welke van "echtgenoot / echtgenote" past, is geen taalkundig probleem maar
// een feitelijke dossierkeuze). Ze biedt enkel de mechanica om, gegeven een
// expliciete keuze per voorkomen, de tekst correct te herschrijven. Een
// voorkomen waarvoor geen keuze is opgegeven (of waarvan de index buiten
// bereik valt) blijft ongewijzigd staan — net als vandaag.

/** Herkent zowel de Nederlandse als de Franse keuzeblok-conventie. */
const INLINE_KEUZE_PATROON = /\[(?:KIES|CHOISIR):\s*([^\]]+?)\]/g;

/** Eén [KIES: ...]-blok zoals het in de tekst voorkomt. */
export interface InlineKeuzeBlok {
  /** 1-gebaseerde positie binnen de tekst (1 = eerste voorkomen, in leesvolgorde). */
  positie: number;
  /** De volledige bracket-tekst, bv. "[KIES: A / B]". */
  blok: string;
  /** De keuzemogelijkheden, getrimd. */
  opties: string[];
}

/** Vindt alle inline [KIES: ...]/[CHOISIR: ...]-blokken in een tekst, in leesvolgorde. */
export function vindInlineKeuzes(tekst: string): InlineKeuzeBlok[] {
  const blokken: InlineKeuzeBlok[] = [];
  let positie = 0;
  for (const match of tekst.matchAll(INLINE_KEUZE_PATROON)) {
    positie += 1;
    blokken.push({
      positie,
      blok: match[0],
      opties: match[1].split("/").map((o) => o.trim()).filter(Boolean),
    });
  }
  return blokken;
}

/** Een gekozen optie voor één inline keuzeblok. */
export interface InlineKeuzeOverride {
  /** Positie van het blok (zie InlineKeuzeBlok.positie). */
  positie: number;
  /** 0-gebaseerde index van de gekozen optie binnen dat blok. */
  optieIndex: number;
}

/**
 * Vervangt elk [KIES: ...]/[CHOISIR: ...]-blok waarvoor een geldige override
 * is opgegeven door de gekozen optietekst; blokken zonder (geldige) override
 * blijven ongewijzigd staan, zodat de notaris ze nog kan beoordelen.
 */
export function resolveerInlineKeuzes(tekst: string, overrides: InlineKeuzeOverride[]): string {
  const perPositie = new Map(overrides.map((o) => [o.positie, o.optieIndex]));
  let positie = 0;
  return tekst.replace(INLINE_KEUZE_PATROON, (blok, optiesRuw: string) => {
    positie += 1;
    const optieIndex = perPositie.get(positie);
    if (optieIndex === undefined) return blok;
    const opties = optiesRuw.split("/").map((o) => o.trim()).filter(Boolean);
    return opties[optieIndex] ?? blok;
  });
}

// ── Versiebeheer en validatiewachtrij ────────────────────────────────────────

/** Verhoogt het minorversienummer ("1.0" → "1.1"); onbekend formaat blijft staan. */
export function bumpVersie(versie: string): string {
  const m = versie.match(/^(\d+)\.(\d+)$/);
  return m ? `${m[1]}.${Number(m[2]) + 1}` : versie;
}

const PRIORITEIT_VOLGORDE: Record<VoorstelPrioriteit, number> = { hoog: 0, midden: 1, laag: 2 };

/** Sorteert wijzigingsvoorstellen op prioriteit (hoog eerst), daarna op datum. */
export function sorteerVoorstellen(voorstellen: Wijzigingsvoorstel[]): Wijzigingsvoorstel[] {
  return [...voorstellen].sort((a, b) => {
    const prio = PRIORITEIT_VOLGORDE[a.prioriteit] - PRIORITEIT_VOLGORDE[b.prioriteit];
    return prio !== 0 ? prio : a.datum.localeCompare(b.datum);
  });
}
