// ── Bron-extractie: intake-contract dossier ──────────────────────────────────
// Eén bron van waarheid voor het JSON-formaat waarmee een brondocument (te
// beginnen met het compromis) een dossier vult. De externe AI-agent (Claude)
// leest het brondocument lokaal en levert dit JSON; de dossierpagina importeert
// het client-side. Persoonsgegevens passeren zo nooit de Notary.AI-API/kennis-
// bank — ze gaan enkel naar de eigen dossieropslag van het kantoor (zie
// AGENTS.md / privacy-instructies).
//
// Deze module is puur en framework-vrij: zowel de AI-prompt (die het contract
// documenteert) als de toekomstige import-UI vertrekken van ditzelfde schema en
// dezelfde mapper, zodat ze niet uit elkaar lopen.

import {
  maakVeld,
  maakLeegDossier,
  BRON_TYPES,
  type BronType,
  type Dossier,
  type Attesten,
  type Dossiertype,
  type DossierStatus,
  type Goed,
  type Partij,
  type Veld,
  type Vennootschap,
} from "./types";
import { maakVeldUitWaarnemingen, waarnemingen, formatGoed } from "./tegenstrijdigheden";
import { euro } from "@/lib/format";
import { relatieOpties, type GoedeType, type Relatie } from "@/data/belastingen";

/** Versie van het intake-formaat; de agent zet exact deze string in "schema". */
export const DOSSIER_INTAKE_SCHEMA = "notary-ai-dossier-intake/v1";
/**
 * v2 voegt — additief op v1 — een optionele "extraWaarnemingen"-lijst toe,
 * waarmee de agent in één JSON meerdere bronnen per veld kan aanleveren (bv. een
 * prijs uit het compromis én een afwijkende prijs uit de eigendomstitel). Het
 * gezaghebbendste brontype wordt de primaire waarde, de rest blijft als
 * tegenstrijdigheid zichtbaar. v1-bestanden blijven onveranderd geldig.
 */
export const DOSSIER_INTAKE_SCHEMA_V2 = "notary-ai-dossier-intake/v2";
const BEKENDE_SCHEMAS = [DOSSIER_INTAKE_SCHEMA, DOSSIER_INTAKE_SCHEMA_V2];

const DOSSIERTYPES: Dossiertype[] = [
  "verkoop-met-krediet",
  "verkoop-zonder-krediet",
  "schenking",
  "nalatenschap",
  "aanpassing-statuten-vennootschap",
  "keuzetestament",
  "testament-gezinswoning",
];
const PARTIJ_ROLLEN: Partij["rol"][] = [
  "verkoper",
  "koper",
  "schenker",
  "begiftigde",
  "erflater",
  "erfgenaam",
];
const GESLACHTEN: NonNullable<Partij["geslacht"]>[] = ["man", "vrouw"];
const BURGERLIJKE_STATEN: NonNullable<Partij["burgerlijkeStaat"]>[] = [
  "ongehuwd",
  "gehuwd",
  "wettelijk-samenwonend",
  "uit-de-echt-gescheiden",
];
const TESTAMENT_RELATIES: NonNullable<Partij["testamentRelatie"]>[] = ["partner", "kind", "andere"];
const GOED_TYPES: NonNullable<Goed["type"]>[] = ["woning", "appartement", "grond", "ander"];
const GOEDTYPE_BELASTING: GoedeType[] = ["roerend", "onroerend", "gezinswoning"];

/** Eén partij zoals de agent ze uit een brondocument extraheert. */
export interface IntakePartij {
  naam?: string;
  voornaam?: string;
  rol?: string;
  geslacht?: string;
  geboortedatum?: string;
  geboorteplaats?: string;
  rijksregisternummer?: string;
  adres?: string;
  burgerlijkeStaat?: string;
  echtgenoot?: string;
  huwelijksplaats?: string;
  huwelijksdatum?: string;
  huwelijksstelsel?: string;
  huwelijkscontractNotaris?: string;
  huwelijkscontractPlaats?: string;
  huwelijkscontractDatum?: string;
  notarisWijzigingStelsel?: string;
  plaatsWijzigingStelsel?: string;
  datumWijzigingStelsel?: string;
  omschrijvingWijzigingStelsel?: string;
  /** Eén of meer latere wijzigingen van het huwelijksstelsel (bv. uit een CRH-opzoeking), chronologisch (oudste eerst). */
  wijzigingenStelsel?: unknown;
  testamentRelatie?: string;
}

/** Het geschonken/verkochte goed zoals geëxtraheerd. */
export interface IntakeGoed {
  adres?: string;
  kadastraleGegevens?: string;
  gemeente?: string;
  deelgemeente?: string;
  afdeling?: string;
  sectie?: string;
  perceelnummer?: string;
  /** Kadastrale sectie volgens de eigendomstitel, enkel indien apart gekend (geen fallback naar `sectie`). */
  sectieTitel?: string;
  /** Perceelnummer volgens de eigendomstitel, letterlijk (bv. "60 F 22"), enkel indien apart gekend (geen fallback naar `perceelnummer`). */
  perceelnummerTitel?: string;
  kadastraalInkomen?: number;
  type?: string;
  oppervlakte?: number;
  oppervlakteTitel?: string;
  inleidendeOmschrijving?: string;
  /** Bouwjaar van de (toegankelijke) constructies (kadastrale legger/matrice cadastrale) — zie Goed.bouwjaar. */
  bouwjaar?: number;
}

const VERKOOPDOCUMENTEN: NonNullable<Dossier["verkoopdocument"]>[] = ["compromis", "verkoopakte"];
const VERKOOP_REGIMES: NonNullable<Dossier["verkoopRegime"]>[] = ["gewoon", "wet-breyne", "lijfrente"];
const LIJFRENTE_FREQUENTIES: NonNullable<NonNullable<Dossier["lijfrente"]>["frequentie"]>[] = ["maandelijks", "trimestrieel", "jaarlijks"];
const LIJFRENTE_GENOTSRECHTEN: NonNullable<NonNullable<Dossier["lijfrente"]>["genotsrecht"]>[] = ["geen", "vruchtgebruik", "gebruik-en-bewoning"];
const GEBRUIKEN: NonNullable<Dossier["gebruik"]>[] = ["vrij", "koper-is-huurder", "verhuurd"];
const SYNDICUS_STATUSSEN: NonNullable<Dossier["syndicusStatus"]>[] = ["geantwoord", "geen-antwoord", "geen-syndicus"];
const FISCALE_REGIMES: NonNullable<Dossier["fiscaalRegime"]>[] = ["registratiebelasting", "btw", "gemengd"];

/** Volledig intake-JSON: het contract dat de agent oplevert. */
export interface DossierIntake {
  schema?: string;
  dossiertype?: string;
  verkoopdocument?: string;
  verkoopRegime?: string;
  gebruik?: string;
  zonnepanelen?: unknown;
  syndicusStatus?: unknown;
  wettelijkVoorkooprecht?: unknown;
  fiscaalRegime?: unknown;
  medeEigendomLastenBetwist?: unknown;
  referentie?: string;
  /** Brontype dat aan elk geëxtraheerd veld wordt gehangen; standaard "compromis". */
  bron?: string;
  /** Optionele verwijzing naar het brondocument (bestandsnaam/id), géén persoonsgegeven. */
  brondocumentId?: string;
  partijen?: IntakePartij[];
  goed?: IntakeGoed;
  prijs?: number;
  voorschot?: number;
  compromisdatum?: string;
  /** Contractueel bedongen aktetermijn uit het compromis: afwijkende maanden of vaste uiterste datum (ISO). */
  aktetermijn?: { maanden?: number; uiterlijkeDatum?: string };
  /** Betalingsgegevens uit het compromis (rekening koper, derdenrekening). */
  betaling?: unknown;
  /** Kredietofferte/-voornemen (bank, bedrag, aanvaard, termijnWeken). */
  kredietofferte?: unknown;
  /** Gekozen notaris per partij ({ verkoper, koper }). */
  notarissen?: unknown;
  /** Commerciële naam van het bemiddelende vastgoedkantoor/agentschap (nooit de individuele makelaar-natuurlijke persoon of diens B.I.V./I.P.I.-nummer). */
  makelaarNaam?: string;
  /** Gegevens uit de eigendomstitel ({ datum, erfdienstbaarheden }). */
  eigendomstitel?: unknown;
  /** Mede-eigendom ({ datumStatuten, datumBerichtSyndicus, lastenPerTrimester, nummerKavel, … }). */
  medeEigendom?: unknown;
  /** Lijfrentegegevens ({ bouquet, jaarlijkseRente, frequentie, genotsrecht }); enkel bij verkoopRegime "lijfrente". */
  lijfrente?: unknown;
  /** Meeverkochte roerende goederen ({ omschrijving, waarde }). */
  roerendeGoederen?: unknown;
  /** Verdeling van de aangekochte delen tussen de kopers (vrije tekst). */
  verdelingGekochteDelen?: string;
  /** Koopt de koper zijn enige eigen woning (hoofdverblijf)? Stuurt het verkooprecht. */
  koperEnigeEigenWoning?: unknown;
  /** Hypothecaire lasten uit het hypotheekattest: [{ schuldeiser, bedrag, moetGeroyeerdWorden }]. */
  hypothecaireLasten?: unknown;
  belastbareWaarde?: number;
  relatieBelasting?: string;
  goedTypeBelasting?: string;
  andereGoederenWaarde?: number;
  /** Vennootschap ({ naam, zetel, ondernemingsnummer, rechtsvorm }); enkel bij vennootschapsdossiers. */
  vennootschap?: unknown;
  attesten?: unknown;
  /** v2: bijkomende waarnemingen per veld uit andere bronnen (conflicten). */
  extraWaarnemingen?: IntakeExtraWaarneming[];
}

/** v2: één bijkomende waarneming van een conflict-gevoelig veld uit een andere bron. */
export interface IntakeExtraWaarneming {
  veld?: string;
  waarde?: unknown;
  bron?: string;
  brondocumentId?: string;
}

/** Resultaat van het inlezen van een intake-JSON. */
export interface IntakeResultaat {
  /** Het gekende dossiertype, indien geldig opgegeven. */
  dossiertype?: Dossiertype;
  /** Het op te stellen verkoopdocument (compromis/verkoopakte), indien opgegeven. */
  verkoopdocument?: Dossier["verkoopdocument"];
  /** Verkoopregime (gewoon/wet-breyne), indien opgegeven — zie Dossier.verkoopRegime. */
  verkoopRegime?: Dossier["verkoopRegime"];
  /** Gebruik/genot van het goed (vrij/koper-is-huurder/verhuurd), indien opgegeven. */
  gebruik?: Dossier["gebruik"];
  /** Zijn er zonnepanelen op het goed? (heldere-taal-clausule reclamepanelen/zonnepanelen). */
  zonnepanelen?: boolean;
  /** Antwoordde de syndicus? (heldere-taal-clausule statuten van mede-eigendom). */
  syndicusStatus?: Dossier["syndicusStatus"];
  /** Wettelijk voorkooprecht/voorkeurrecht/recht van wederinkoop? (heldere-taal-clausule §37). */
  wettelijkVoorkooprecht?: boolean;
  /** Fiscaal regime van de verkoop (heldere-taal-clausule §43). */
  fiscaalRegime?: Dossier["fiscaalRegime"];
  /** Betwist de verkoper het bedrag van de lasten/achterstallen bij mede-eigendom? */
  medeEigendomLastenBetwist?: boolean;
  /** Kantoorreferentie/dossiernummer, indien opgegeven. */
  referentie?: string;
  /** Attestgegevens (EPC, bodem, elektrische keuring), indien opgegeven. */
  attesten?: Attesten;
  /** Betalingsgegevens (rekening koper, derdenrekening), indien opgegeven. */
  betaling?: Dossier["betaling"];
  /** Gekozen notaris per partij, indien opgegeven. */
  notarissen?: Dossier["notarissen"];
  /** Commerciële naam van het bemiddelende vastgoedkantoor/agentschap, indien opgegeven. */
  makelaarNaam?: string;
  /** Gegevens uit de eigendomstitel, indien opgegeven. */
  eigendomstitel?: Dossier["eigendomstitel"];
  /** Gegevens van de mede-eigendom, indien opgegeven. */
  medeEigendom?: Dossier["medeEigendom"];
  /** Lijfrentegegevens, indien opgegeven (enkel bij verkoopRegime "lijfrente"). */
  lijfrente?: Dossier["lijfrente"];
  /** Meeverkochte roerende goederen, indien opgegeven. */
  roerendeGoederen?: Dossier["roerendeGoederen"];
  /** Verdeling van de aangekochte delen tussen de kopers, indien opgegeven. */
  verdelingGekochteDelen?: string;
  /** Koopt de koper zijn enige eigen woning? (verkooprecht), indien opgegeven. */
  koperEnigeEigenWoning?: boolean;
  /** De dossiervelden die met zekerheid uit het intake-JSON volgen. */
  velden: Partial<
    Pick<
      Dossier,
      | "partijen"
      | "goed"
      | "prijs"
      | "voorschot"
      | "compromisdatum"
      | "aktetermijn"
      | "kredietofferte"
      | "hypothecaireLasten"
      | "belastbareWaarde"
      | "relatieBelasting"
      | "goedTypeBelasting"
      | "andereGoederenWaarde"
      | "vennootschap"
    >
  >;
  /** Validatiemeldingen: overgeslagen of ongeldige velden — nooit stilzwijgend gegokt. */
  problemen: string[];
}

/**
 * Haalt het JSON-object uit door de agent geplakte tekst: strikt een
 * ```json-codeblok wanneer aanwezig, anders de tekst tussen de eerste "{" en de
 * laatste "}". Zo werkt de import ook wanneer de agent een begeleidende zin of
 * codeblok-markering meestuurt. Geeft null wanneer geen object te vinden is.
 */
export function vindJsonObject(tekst: string): string | null {
  const fence = /```(?:json)?\s*([\s\S]*?)```/i.exec(tekst);
  const kandidaat = fence ? fence[1] : tekst;
  const start = kandidaat.indexOf("{");
  const eind = kandidaat.lastIndexOf("}");
  if (start === -1 || eind === -1 || eind < start) return null;
  return kandidaat.slice(start, eind + 1);
}

function isObject(x: unknown): x is Record<string, unknown> {
  return typeof x === "object" && x !== null && !Array.isArray(x);
}

/** Geldig, strikt positief getal of undefined; anders een probleem. */
function leesBedrag(
  waarde: unknown,
  veld: string,
  problemen: string[]
): number | undefined {
  if (waarde === undefined || waarde === null) return undefined;
  if (typeof waarde !== "number" || !Number.isFinite(waarde) || waarde <= 0) {
    problemen.push(`Veld "${veld}" overgeslagen: geen geldig positief bedrag (${JSON.stringify(waarde)}).`);
    return undefined;
  }
  return waarde;
}

function leesEnum<T extends string>(
  waarde: unknown,
  toegestaan: readonly T[],
  veld: string,
  problemen: string[]
): T | undefined {
  if (waarde === undefined || waarde === null || waarde === "") return undefined;
  if (typeof waarde === "string" && (toegestaan as readonly string[]).includes(waarde)) {
    return waarde as T;
  }
  problemen.push(
    `Veld "${veld}" overgeslagen: "${String(waarde)}" is geen geldige waarde (verwacht één van: ${toegestaan.join(", ")}).`
  );
  return undefined;
}

function leesPartij(rauw: IntakePartij, index: number, problemen: string[]): Partij | undefined {
  const naam = typeof rauw.naam === "string" ? rauw.naam.trim() : "";
  const rol = leesEnum(rauw.rol, PARTIJ_ROLLEN, `partijen[${index}].rol`, problemen);
  if (!rol) return undefined;
  if (!naam) {
    problemen.push(`partijen[${index}] overgeslagen: "naam" ontbreekt.`);
    return undefined;
  }
  const partij: Partij = { naam, rol };
  if (typeof rauw.voornaam === "string" && rauw.voornaam.trim()) partij.voornaam = rauw.voornaam.trim();
  if (typeof rauw.geboortedatum === "string" && rauw.geboortedatum.trim()) partij.geboortedatum = rauw.geboortedatum.trim();
  if (typeof rauw.geboorteplaats === "string" && rauw.geboorteplaats.trim()) partij.geboorteplaats = rauw.geboorteplaats.trim();
  if (typeof rauw.rijksregisternummer === "string" && rauw.rijksregisternummer.trim()) partij.rijksregisternummer = rauw.rijksregisternummer.trim();
  if (typeof rauw.adres === "string" && rauw.adres.trim()) partij.adres = rauw.adres.trim();
  const geslacht = leesEnum(rauw.geslacht, GESLACHTEN, `partijen[${index}].geslacht`, problemen);
  if (geslacht) partij.geslacht = geslacht;
  const staat = leesEnum(rauw.burgerlijkeStaat, BURGERLIJKE_STATEN, `partijen[${index}].burgerlijkeStaat`, problemen);
  if (staat) partij.burgerlijkeStaat = staat;
  if (typeof rauw.echtgenoot === "string" && rauw.echtgenoot.trim()) partij.echtgenoot = rauw.echtgenoot.trim();
  if (typeof rauw.huwelijksplaats === "string" && rauw.huwelijksplaats.trim()) partij.huwelijksplaats = rauw.huwelijksplaats.trim();
  if (typeof rauw.huwelijksdatum === "string" && rauw.huwelijksdatum.trim()) partij.huwelijksdatum = rauw.huwelijksdatum.trim();
  if (typeof rauw.huwelijksstelsel === "string" && rauw.huwelijksstelsel.trim()) partij.huwelijksstelsel = rauw.huwelijksstelsel.trim();
  if (typeof rauw.huwelijkscontractNotaris === "string" && rauw.huwelijkscontractNotaris.trim()) partij.huwelijkscontractNotaris = rauw.huwelijkscontractNotaris.trim();
  if (typeof rauw.huwelijkscontractPlaats === "string" && rauw.huwelijkscontractPlaats.trim()) partij.huwelijkscontractPlaats = rauw.huwelijkscontractPlaats.trim();
  if (typeof rauw.huwelijkscontractDatum === "string" && rauw.huwelijkscontractDatum.trim()) partij.huwelijkscontractDatum = rauw.huwelijkscontractDatum.trim();
  if (typeof rauw.notarisWijzigingStelsel === "string" && rauw.notarisWijzigingStelsel.trim()) partij.notarisWijzigingStelsel = rauw.notarisWijzigingStelsel.trim();
  if (typeof rauw.plaatsWijzigingStelsel === "string" && rauw.plaatsWijzigingStelsel.trim()) partij.plaatsWijzigingStelsel = rauw.plaatsWijzigingStelsel.trim();
  if (typeof rauw.datumWijzigingStelsel === "string" && rauw.datumWijzigingStelsel.trim()) partij.datumWijzigingStelsel = rauw.datumWijzigingStelsel.trim();
  if (typeof rauw.omschrijvingWijzigingStelsel === "string" && rauw.omschrijvingWijzigingStelsel.trim()) partij.omschrijvingWijzigingStelsel = rauw.omschrijvingWijzigingStelsel.trim();
  const wijzigingenStelsel = leesWijzigingenStelsel(rauw.wijzigingenStelsel, index, problemen);
  if (wijzigingenStelsel) partij.wijzigingenStelsel = wijzigingenStelsel;
  const relatie = leesEnum(rauw.testamentRelatie, TESTAMENT_RELATIES, `partijen[${index}].testamentRelatie`, problemen);
  if (relatie) partij.testamentRelatie = relatie;
  return partij;
}

/**
 * Leest één of meer latere wijzigingen van het huwelijksstelsel in (bv. uit
 * een CRH-opzoeking). Elke wijziging vereist minstens notaris en datum;
 * plaats en omschrijving zijn optioneel. Nooit gokken: een onvolledig item
 * wordt overgeslagen i.p.v. aangevuld.
 */
function leesWijzigingenStelsel(
  rauw: unknown,
  index: number,
  problemen: string[]
): { notaris: string; plaats?: string; datum: string; omschrijving?: string }[] | undefined {
  if (rauw === undefined) return undefined;
  if (!Array.isArray(rauw)) {
    problemen.push(`partijen[${index}].wijzigingenStelsel overgeslagen: verwacht een lijst.`);
    return undefined;
  }
  const wijzigingen: { notaris: string; plaats?: string; datum: string; omschrijving?: string }[] = [];
  rauw.forEach((item, i) => {
    if (!isObject(item)) {
      problemen.push(`partijen[${index}].wijzigingenStelsel[${i}] overgeslagen: geen object.`);
      return;
    }
    const notaris = leesTekst(item.notaris);
    const datum = leesTekst(item.datum);
    if (!notaris || !datum) {
      problemen.push(`partijen[${index}].wijzigingenStelsel[${i}] overgeslagen: "notaris" en "datum" zijn vereist.`);
      return;
    }
    const wijziging: { notaris: string; plaats?: string; datum: string; omschrijving?: string } = { notaris, datum };
    const plaats = leesTekst(item.plaats);
    if (plaats) wijziging.plaats = plaats;
    const omschrijving = leesTekst(item.omschrijving);
    if (omschrijving) wijziging.omschrijving = omschrijving;
    wijzigingen.push(wijziging);
  });
  return wijzigingen.length > 0 ? wijzigingen : undefined;
}

function leesGoed(rauw: IntakeGoed, problemen: string[]): Goed | undefined {
  const adres = typeof rauw.adres === "string" ? rauw.adres.trim() : "";
  if (!adres) {
    problemen.push('Veld "goed" overgeslagen: "adres" ontbreekt (vereist voor een goed).');
    return undefined;
  }
  const goed: Goed = { adres };
  if (typeof rauw.kadastraleGegevens === "string" && rauw.kadastraleGegevens.trim()) goed.kadastraleGegevens = rauw.kadastraleGegevens.trim();
  if (typeof rauw.gemeente === "string" && rauw.gemeente.trim()) goed.gemeente = rauw.gemeente.trim();
  if (typeof rauw.deelgemeente === "string" && rauw.deelgemeente.trim()) goed.deelgemeente = rauw.deelgemeente.trim();
  if (typeof rauw.oppervlakteTitel === "string" && rauw.oppervlakteTitel.trim()) goed.oppervlakteTitel = rauw.oppervlakteTitel.trim();
  if (typeof rauw.inleidendeOmschrijving === "string" && rauw.inleidendeOmschrijving.trim()) goed.inleidendeOmschrijving = rauw.inleidendeOmschrijving.trim();
  if (typeof rauw.afdeling === "string" && rauw.afdeling.trim()) goed.afdeling = rauw.afdeling.trim();
  if (typeof rauw.sectie === "string" && rauw.sectie.trim()) goed.sectie = rauw.sectie.trim();
  if (typeof rauw.perceelnummer === "string" && rauw.perceelnummer.trim()) goed.perceelnummer = rauw.perceelnummer.trim();
  if (typeof rauw.sectieTitel === "string" && rauw.sectieTitel.trim()) goed.sectieTitel = rauw.sectieTitel.trim();
  if (typeof rauw.perceelnummerTitel === "string" && rauw.perceelnummerTitel.trim()) goed.perceelnummerTitel = rauw.perceelnummerTitel.trim();
  const ki = leesBedrag(rauw.kadastraalInkomen, "goed.kadastraalInkomen", problemen);
  if (ki !== undefined) goed.kadastraalInkomen = ki;
  const opp = leesBedrag(rauw.oppervlakte, "goed.oppervlakte", problemen);
  if (opp !== undefined) goed.oppervlakte = opp;
  const type = leesEnum(rauw.type, GOED_TYPES, "goed.type", problemen);
  if (type) goed.type = type;
  const bouwjaar = leesBedrag(rauw.bouwjaar, "goed.bouwjaar", problemen);
  if (bouwjaar !== undefined) goed.bouwjaar = bouwjaar;
  return goed;
}

/** Leest één tekstveld van een attest, leeg/ongeldig → undefined. */
function leesTekst(waarde: unknown): string | undefined {
  return typeof waarde === "string" && waarde.trim() ? waarde.trim() : undefined;
}

/** Leest de attestgegevens (EPC, bodem, elektrische keuring) defensief in. */
function leesAttesten(rauw: unknown, problemen: string[]): Attesten | undefined {
  if (!isObject(rauw)) {
    problemen.push('Veld "attesten" overgeslagen: verwacht een object.');
    return undefined;
  }
  const attesten: Attesten = {};
  if (isObject(rauw.epc)) {
    const e = rauw.epc;
    const epc = {
      datum: leesTekst(e.datum),
      nummer: leesTekst(e.nummer),
      label: leesTekst(e.label),
      geldigTot: leesTekst(e.geldigTot),
      verbruik: typeof e.verbruik === "number" ? e.verbruik : undefined,
      energiedeskundige: leesTekst(e.energiedeskundige),
    };
    if (Object.values(epc).some((v) => v !== undefined)) attesten.epc = epc;
  }
  if (isObject(rauw.bodem)) {
    const bodem = {
      datum: leesTekst(rauw.bodem.datum),
      inhoud: leesTekst(rauw.bodem.inhoud),
      refertenummer: leesTekst(rauw.bodem.refertenummer),
      // Expliciet negatief feit (geen bodemvervuilende activiteiten): enkel
      // "geen" wordt gedragen, nooit afgeleid uit `inhoud` (nooit gokken).
      activiteiten: rauw.bodem.activiteiten === "geen" ? ("geen" as const) : undefined,
    };
    if (Object.values(bodem).some(Boolean)) attesten.bodem = bodem;
  }
  if (isObject(rauw.elektrischeKeuring)) {
    const k = rauw.elektrischeKeuring;
    const keuring = { organisme: leesTekst(k.organisme), datum: leesTekst(k.datum), conclusie: leesTekst(k.conclusie) };
    if (Object.values(keuring).some(Boolean)) attesten.elektrischeKeuring = keuring;
  }
  if (isObject(rauw.stedenbouw)) {
    const s = rauw.stedenbouw;
    const stb = {
      datum: leesTekst(s.datum),
      bestemming: leesTekst(s.bestemming),
      vergunningen: leesTekst(s.vergunningen),
      // Expliciete negatieve feiten uit de inlichtingen: enkel "geen" wordt
      // gedragen (elke andere waarde = open laten, nooit gokken).
      onteigeningRooilijn: s.onteigeningRooilijn === "geen" ? ("geen" as const) : undefined,
      erfgoed: s.erfgoed === "geen" ? ("geen" as const) : undefined,
    };
    if (Object.values(stb).some(Boolean)) attesten.stedenbouw = stb;
  }
  if (isObject(rauw.asbest)) {
    const a = rauw.asbest;
    const asb = { datum: leesTekst(a.datum), nummer: leesTekst(a.nummer), conclusie: leesTekst(a.conclusie) };
    if (Object.values(asb).some(Boolean)) attesten.asbest = asb;
  }
  if (isObject(rauw.overstroming)) {
    const o = rauw.overstroming;
    const ov = { pScore: leesTekst(o.pScore), gScore: leesTekst(o.gScore), datum: leesTekst(o.datum), conclusie: leesTekst(o.conclusie) };
    if (Object.values(ov).some(Boolean)) attesten.overstroming = ov;
  }
  if (rauw.natuurBos === "geen") attesten.natuurBos = "geen";
  if (rauw.herstelvordering === "geen") attesten.herstelvordering = "geen";
  if (isObject(rauw.maatregelenregister)) {
    const m = rauw.maatregelenregister;
    const reg = {
      datumRaadpleging: leesTekst(m.datumRaadpleging),
      inhoud: m.inhoud === "blanco" ? ("blanco" as const) : undefined,
    };
    if (Object.values(reg).some(Boolean)) attesten.maatregelenregister = reg;
  }
  if (rauw.stookolietank === "geen") attesten.stookolietank = "geen";
  if (rauw.leegstand === "geen") attesten.leegstand = "geen";
  if (rauw.pfas === "geen") attesten.pfas = "geen";
  return Object.keys(attesten).length > 0 ? attesten : undefined;
}

/** Leest de betalingsgegevens (rekening koper, derdenrekening) defensief in. */
function leesBetaling(rauw: unknown, problemen: string[]): Dossier["betaling"] {
  if (!isObject(rauw)) {
    problemen.push('Veld "betaling" overgeslagen: verwacht een object.');
    return undefined;
  }
  const betaling = {
    rekeningKoper: leesTekst(rauw.rekeningKoper),
    naamRekeningKoper: leesTekst(rauw.naamRekeningKoper),
    rekeningDerden: leesTekst(rauw.rekeningDerden),
    naamRekeningDerden: leesTekst(rauw.naamRekeningDerden),
  };
  return Object.values(betaling).some(Boolean) ? betaling : undefined;
}

/** Leest de gekozen notarissen ({ verkoper, koper }) defensief in. */
function leesNotarissen(rauw: unknown, problemen: string[]): Dossier["notarissen"] {
  if (!isObject(rauw)) {
    problemen.push('Veld "notarissen" overgeslagen: verwacht een object.');
    return undefined;
  }
  const notarissen = { verkoper: leesTekst(rauw.verkoper), koper: leesTekst(rauw.koper) };
  return Object.values(notarissen).some(Boolean) ? notarissen : undefined;
}

/** Leest de eigendomstitel ({ datum, erfdienstbaarheden }) defensief in. */
function leesEigendomstitel(rauw: unknown, problemen: string[]): Dossier["eigendomstitel"] {
  if (!isObject(rauw)) {
    problemen.push('Veld "eigendomstitel" overgeslagen: verwacht een object.');
    return undefined;
  }
  const titel = { datum: leesTekst(rauw.datum), erfdienstbaarheden: leesTekst(rauw.erfdienstbaarheden) };
  return Object.values(titel).some(Boolean) ? titel : undefined;
}

/** Leest de mede-eigendomgegevens (incl. kavelbeschrijving en basisakte) defensief in. */
function leesMedeEigendom(rauw: unknown, problemen: string[]): Dossier["medeEigendom"] {
  if (!isObject(rauw)) {
    problemen.push('Veld "medeEigendom" overgeslagen: verwacht een object.');
    return undefined;
  }
  const lasten = leesBedrag(rauw.lastenPerTrimester, "medeEigendom.lastenPerTrimester", problemen);
  const me = {
    datumStatuten: leesTekst(rauw.datumStatuten),
    datumBerichtSyndicus: leesTekst(rauw.datumBerichtSyndicus),
    ...(lasten !== undefined ? { lastenPerTrimester: lasten } : {}),
    nummerKavel: leesTekst(rauw.nummerKavel),
    liggingInGebouw: leesTekst(rauw.liggingInGebouw),
    verdieping: leesTekst(rauw.verdieping),
    beschrijvingPrivatief: leesTekst(rauw.beschrijvingPrivatief),
    exclusiefGenot: leesTekst(rauw.exclusiefGenot),
    aandeelGemeneDelen: leesTekst(rauw.aandeelGemeneDelen),
    perceelsidentificatie: leesTekst(rauw.perceelsidentificatie),
    datumBasisakte: leesTekst(rauw.datumBasisakte),
    notarisBasisakte: leesTekst(rauw.notarisBasisakte),
    plaatsNotarisBasisakte: leesTekst(rauw.plaatsNotarisBasisakte),
  };
  return Object.values(me).some((v) => v !== undefined) ? me : undefined;
}

/** Leest de lijfrentegegevens (bouquet, rente, frequentie, genotsrecht) defensief in. */
function leesLijfrente(rauw: unknown, problemen: string[]): Dossier["lijfrente"] {
  if (!isObject(rauw)) {
    problemen.push('Veld "lijfrente" overgeslagen: verwacht een object.');
    return undefined;
  }
  const bouquet = leesBedrag(rauw.bouquet, "lijfrente.bouquet", problemen);
  const jaarlijkseRente = leesBedrag(rauw.jaarlijkseRente, "lijfrente.jaarlijkseRente", problemen);
  const frequentie = leesEnum(rauw.frequentie, LIJFRENTE_FREQUENTIES, "lijfrente.frequentie", problemen);
  const genotsrecht = leesEnum(rauw.genotsrecht, LIJFRENTE_GENOTSRECHTEN, "lijfrente.genotsrecht", problemen);
  const lr = {
    ...(bouquet !== undefined ? { bouquet } : {}),
    ...(jaarlijkseRente !== undefined ? { jaarlijkseRente } : {}),
    ...(frequentie ? { frequentie } : {}),
    ...(genotsrecht ? { genotsrecht } : {}),
  };
  return Object.keys(lr).length > 0 ? lr : undefined;
}

/** Leest de meeverkochte roerende goederen defensief in. */
function leesRoerendeGoederen(rauw: unknown, problemen: string[]): Dossier["roerendeGoederen"] {
  if (!isObject(rauw)) {
    problemen.push('Veld "roerendeGoederen" overgeslagen: verwacht een object.');
    return undefined;
  }
  const waarde = leesBedrag(rauw.waarde, "roerendeGoederen.waarde", problemen);
  const rg = {
    omschrijving: leesTekst(rauw.omschrijving),
    ...(waarde !== undefined ? { waarde } : {}),
  };
  return Object.values(rg).some((v) => v !== undefined) ? rg : undefined;
}

/**
 * Leest de hypothecaire lasten (uit het hypotheekattest) defensief in. Elke
 * regel vereist schuldeiser, bedrag én een expliciete moetGeroyeerdWorden —
 * of een last gelicht moet worden is een juridisch feit dat nooit wordt gegokt.
 */
function leesHypothecaireLasten(
  rauw: unknown,
  problemen: string[]
): { schuldeiser: string; bedrag: number; moetGeroyeerdWorden: boolean }[] | undefined {
  if (!Array.isArray(rauw)) {
    problemen.push('Veld "hypothecaireLasten" overgeslagen: verwacht een lijst.');
    return undefined;
  }
  const lasten: { schuldeiser: string; bedrag: number; moetGeroyeerdWorden: boolean }[] = [];
  rauw.forEach((item, i) => {
    if (!isObject(item)) {
      problemen.push(`hypothecaireLasten[${i}] overgeslagen: geen object.`);
      return;
    }
    const schuldeiser = leesTekst(item.schuldeiser);
    const bedrag = leesBedrag(item.bedrag, `hypothecaireLasten[${i}].bedrag`, problemen);
    if (!schuldeiser || bedrag === undefined) {
      problemen.push(`hypothecaireLasten[${i}] overgeslagen: "schuldeiser" en "bedrag" zijn vereist.`);
      return;
    }
    if (typeof item.moetGeroyeerdWorden !== "boolean") {
      problemen.push(
        `hypothecaireLasten[${i}] overgeslagen: "moetGeroyeerdWorden" (true/false) is vereist — nooit gokken of een last gelicht moet worden.`
      );
      return;
    }
    lasten.push({ schuldeiser, bedrag, moetGeroyeerdWorden: item.moetGeroyeerdWorden });
  });
  return lasten.length > 0 ? lasten : undefined;
}

/** Leest de kredietofferte (bank, bedrag, aanvaard, termijnWeken) defensief in. */
function leesKredietofferte(
  rauw: unknown,
  problemen: string[]
): { bank: string; bedrag: number; aanvaard: boolean; termijnWeken?: number } | undefined {
  if (!isObject(rauw)) {
    problemen.push('Veld "kredietofferte" overgeslagen: verwacht een object.');
    return undefined;
  }
  const bedrag = leesBedrag(rauw.bedrag, "kredietofferte.bedrag", problemen);
  if (bedrag === undefined) {
    problemen.push('Veld "kredietofferte" overgeslagen: "bedrag" (positief getal) is vereist.');
    return undefined;
  }
  const termijn = leesBedrag(rauw.termijnWeken, "kredietofferte.termijnWeken", problemen);
  return {
    bank: leesTekst(rauw.bank) ?? "",
    bedrag,
    aanvaard: rauw.aanvaard === true,
    ...(termijn !== undefined ? { termijnWeken: termijn } : {}),
  };
}

/** Leest de vennootschapsgegevens ({ naam, zetel, ondernemingsnummer, rechtsvorm }). */
function leesVennootschap(rauw: unknown, problemen: string[]): Vennootschap | undefined {
  if (!isObject(rauw)) {
    problemen.push('Veld "vennootschap" overgeslagen: verwacht een object.');
    return undefined;
  }
  const naam = leesTekst(rauw.naam);
  if (!naam) {
    problemen.push('Veld "vennootschap" overgeslagen: "naam" (maatschappelijke benaming) is vereist.');
    return undefined;
  }
  const zetel = leesTekst(rauw.zetel);
  const ondernemingsnummer = leesTekst(rauw.ondernemingsnummer);
  const rechtsvorm = leesTekst(rauw.rechtsvorm);
  return {
    naam,
    ...(zetel ? { zetel } : {}),
    ...(ondernemingsnummer ? { ondernemingsnummer } : {}),
    ...(rechtsvorm ? { rechtsvorm } : {}),
  };
}

/**
 * Leest een intake-JSON (zoals de AI-agent het uit een brondocument extraheert)
 * en zet het om naar dossiervelden. Defensief: elk ongeldig of onbekend veld
 * wordt overgeslagen en als probleem gemeld — er wordt nooit een waarde gegokt.
 * Het brontype wordt aan elk veld gehangen (standaard "compromis"), zodat de
 * bestaande tegenstrijdigheden- en brongezag-logica blijft werken.
 */
export function dossierVeldenUitIntake(intake: unknown): IntakeResultaat {
  const problemen: string[] = [];
  const velden: IntakeResultaat["velden"] = {};

  if (!isObject(intake)) {
    return { velden, problemen: ['Ongeldige invoer: verwacht een JSON-object volgens het intake-schema.'] };
  }
  const data = intake as DossierIntake;

  if (typeof data.schema !== "string" || !BEKENDE_SCHEMAS.includes(data.schema)) {
    problemen.push(
      `Onbekend of ontbrekend "schema" (verwacht "${DOSSIER_INTAKE_SCHEMA}" of "${DOSSIER_INTAKE_SCHEMA_V2}", kreeg ${JSON.stringify(data.schema)}). Velden worden alsnog ingelezen, controleer het formaat.`
    );
  }

  const bron: BronType =
    typeof data.bron === "string" && (BRON_TYPES as string[]).includes(data.bron)
      ? (data.bron as BronType)
      : "compromis";
  if (data.bron !== undefined && bron !== data.bron) {
    problemen.push(`Onbekend brontype "${String(data.bron)}" — teruggevallen op "compromis".`);
  }
  const brondocumentId =
    typeof data.brondocumentId === "string" && data.brondocumentId.trim()
      ? data.brondocumentId.trim()
      : undefined;
  const veld = <T,>(waarde: T) => maakVeld(waarde, bron, brondocumentId);

  const dossiertype = leesEnum(data.dossiertype, DOSSIERTYPES, "dossiertype", problemen);
  const verkoopdocument = leesEnum(data.verkoopdocument, VERKOOPDOCUMENTEN, "verkoopdocument", problemen);
  const verkoopRegime = leesEnum(data.verkoopRegime, VERKOOP_REGIMES, "verkoopRegime", problemen);
  const gebruik = leesEnum(data.gebruik, GEBRUIKEN, "gebruik", problemen);
  let zonnepanelen: boolean | undefined;
  if (typeof data.zonnepanelen === "boolean") zonnepanelen = data.zonnepanelen;
  else if (data.zonnepanelen !== undefined) problemen.push('Veld "zonnepanelen" overgeslagen: verwacht true of false.');
  const syndicusStatus = leesEnum(data.syndicusStatus, SYNDICUS_STATUSSEN, "syndicusStatus", problemen);
  let wettelijkVoorkooprecht: boolean | undefined;
  if (typeof data.wettelijkVoorkooprecht === "boolean") wettelijkVoorkooprecht = data.wettelijkVoorkooprecht;
  else if (data.wettelijkVoorkooprecht !== undefined) problemen.push('Veld "wettelijkVoorkooprecht" overgeslagen: verwacht true of false.');
  const fiscaalRegime = leesEnum(data.fiscaalRegime, FISCALE_REGIMES, "fiscaalRegime", problemen);
  let medeEigendomLastenBetwist: boolean | undefined;
  if (typeof data.medeEigendomLastenBetwist === "boolean") medeEigendomLastenBetwist = data.medeEigendomLastenBetwist;
  else if (data.medeEigendomLastenBetwist !== undefined)
    problemen.push('Veld "medeEigendomLastenBetwist" overgeslagen: verwacht true of false.');
  const referentie = leesTekst(data.referentie);
  const attesten = data.attesten !== undefined ? leesAttesten(data.attesten, problemen) : undefined;
  const betaling = data.betaling !== undefined ? leesBetaling(data.betaling, problemen) : undefined;
  const notarissen = data.notarissen !== undefined ? leesNotarissen(data.notarissen, problemen) : undefined;
  const makelaarNaam = leesTekst(data.makelaarNaam);
  const eigendomstitel = data.eigendomstitel !== undefined ? leesEigendomstitel(data.eigendomstitel, problemen) : undefined;
  const medeEigendom = data.medeEigendom !== undefined ? leesMedeEigendom(data.medeEigendom, problemen) : undefined;
  const lijfrente = data.lijfrente !== undefined ? leesLijfrente(data.lijfrente, problemen) : undefined;
  const roerendeGoederen = data.roerendeGoederen !== undefined ? leesRoerendeGoederen(data.roerendeGoederen, problemen) : undefined;
  const verdelingGekochteDelen = leesTekst(data.verdelingGekochteDelen);
  let koperEnigeEigenWoning: boolean | undefined;
  if (typeof data.koperEnigeEigenWoning === "boolean") koperEnigeEigenWoning = data.koperEnigeEigenWoning;
  else if (data.koperEnigeEigenWoning !== undefined)
    problemen.push('Veld "koperEnigeEigenWoning" overgeslagen: verwacht true of false.');

  if (Array.isArray(data.partijen)) {
    const partijen = data.partijen
      .map((p, i) => (isObject(p) ? leesPartij(p, i, problemen) : undefined))
      .filter((p): p is Partij => p !== undefined);
    if (partijen.length > 0) velden.partijen = veld(partijen);
  }

  if (isObject(data.goed)) {
    const goed = leesGoed(data.goed, problemen);
    if (goed) velden.goed = veld(goed);
  }

  const prijs = leesBedrag(data.prijs, "prijs", problemen);
  if (prijs !== undefined) velden.prijs = veld(prijs);

  const voorschot = leesBedrag(data.voorschot, "voorschot", problemen);
  if (voorschot !== undefined) velden.voorschot = veld(voorschot);

  // De compromisdatum is de rekenbasis van de termijnbewaking (opvolging.ts)
  // en moet dus strikt ISO (JJJJ-MM-DD) zijn — vrije datumteksten worden
  // overgeslagen met een expliciet probleem, zodat de agent het formaat
  // corrigeert in plaats van dat de termijnen stilletjes wegvallen.
  if (typeof data.compromisdatum === "string" && /^\d{4}-\d{2}-\d{2}$/.test(data.compromisdatum.trim())) {
    velden.compromisdatum = veld(data.compromisdatum.trim());
  } else if (data.compromisdatum !== undefined) {
    problemen.push('Veld "compromisdatum" overgeslagen: geen geldige ISO-datum (JJJJ-MM-DD).');
  }

  // Contractueel bedongen aktetermijn (punt 42): afwijkende maanden of een
  // vaste uiterste datum; ongeldige waarden expliciet melden, nooit gokken.
  if (isObject(data.aktetermijn)) {
    const a = data.aktetermijn as Record<string, unknown>;
    const maanden = typeof a.maanden === "number" && a.maanden > 0 ? a.maanden : undefined;
    const uiterlijkeDatum =
      typeof a.uiterlijkeDatum === "string" && /^\d{4}-\d{2}-\d{2}$/.test(a.uiterlijkeDatum.trim())
        ? a.uiterlijkeDatum.trim()
        : undefined;
    if (maanden !== undefined || uiterlijkeDatum !== undefined) {
      velden.aktetermijn = veld({ ...(maanden !== undefined ? { maanden } : {}), ...(uiterlijkeDatum !== undefined ? { uiterlijkeDatum } : {}) });
    } else {
      problemen.push('Veld "aktetermijn" overgeslagen: verwacht { maanden: positief getal } en/of { uiterlijkeDatum: "JJJJ-MM-DD" }.');
    }
  }

  const belastbareWaarde = leesBedrag(data.belastbareWaarde, "belastbareWaarde", problemen);
  if (belastbareWaarde !== undefined) velden.belastbareWaarde = veld(belastbareWaarde);

  const relatie = leesEnum<Relatie>(data.relatieBelasting, relatieOpties, "relatieBelasting", problemen);
  if (relatie) velden.relatieBelasting = veld(relatie);

  const goedType = leesEnum<GoedeType>(data.goedTypeBelasting, GOEDTYPE_BELASTING, "goedTypeBelasting", problemen);
  if (goedType) velden.goedTypeBelasting = veld(goedType);

  const andere = leesBedrag(data.andereGoederenWaarde, "andereGoederenWaarde", problemen);
  if (andere !== undefined) velden.andereGoederenWaarde = veld(andere);

  if (data.vennootschap !== undefined) {
    const vennootschap = leesVennootschap(data.vennootschap, problemen);
    if (vennootschap) velden.vennootschap = veld(vennootschap);
  }

  if (data.kredietofferte !== undefined) {
    const offerte = leesKredietofferte(data.kredietofferte, problemen);
    if (offerte) velden.kredietofferte = veld(offerte);
  }

  if (data.hypothecaireLasten !== undefined) {
    const lasten = leesHypothecaireLasten(data.hypothecaireLasten, problemen);
    if (lasten) velden.hypothecaireLasten = veld(lasten);
  }

  // v2: bijkomende waarnemingen per veld uit andere bronnen (conflicten).
  if (data.extraWaarnemingen !== undefined) {
    if (Array.isArray(data.extraWaarnemingen)) {
      data.extraWaarnemingen.forEach((rauw, i) => {
        if (isObject(rauw)) verwerkExtraWaarneming(rauw as IntakeExtraWaarneming, i, velden, problemen);
        else problemen.push(`extraWaarnemingen[${i}] overgeslagen: geen object.`);
      });
    } else {
      problemen.push('Veld "extraWaarnemingen" overgeslagen: verwacht een lijst.');
    }
  }

  return {
    dossiertype,
    verkoopdocument,
    verkoopRegime,
    gebruik,
    zonnepanelen,
    syndicusStatus,
    wettelijkVoorkooprecht,
    fiscaalRegime,
    medeEigendomLastenBetwist,
    referentie,
    attesten,
    betaling,
    notarissen,
    makelaarNaam,
    eigendomstitel,
    medeEigendom,
    lijfrente,
    roerendeGoederen,
    verdelingGekochteDelen,
    koperEnigeEigenWoning,
    velden,
    problemen,
  };
}

/** Voegt twee attestensets samen: nieuwe deelvelden overschrijven bestaande per attest. */
export function voegAttestenSamen(bestaand: Attesten | undefined, nieuw: Attesten | undefined): Attesten | undefined {
  if (!nieuw) return bestaand;
  if (!bestaand) return nieuw;
  const samen: Attesten = {};
  if (bestaand.epc || nieuw.epc) samen.epc = { ...bestaand.epc, ...nieuw.epc };
  if (bestaand.bodem || nieuw.bodem) samen.bodem = { ...bestaand.bodem, ...nieuw.bodem };
  if (bestaand.elektrischeKeuring || nieuw.elektrischeKeuring)
    samen.elektrischeKeuring = { ...bestaand.elektrischeKeuring, ...nieuw.elektrischeKeuring };
  if (bestaand.stedenbouw || nieuw.stedenbouw) samen.stedenbouw = { ...bestaand.stedenbouw, ...nieuw.stedenbouw };
  if (bestaand.asbest || nieuw.asbest) samen.asbest = { ...bestaand.asbest, ...nieuw.asbest };
  if (bestaand.overstroming || nieuw.overstroming)
    samen.overstroming = { ...bestaand.overstroming, ...nieuw.overstroming };
  if (bestaand.maatregelenregister || nieuw.maatregelenregister)
    samen.maatregelenregister = { ...bestaand.maatregelenregister, ...nieuw.maatregelenregister };
  const natuurBos = nieuw.natuurBos ?? bestaand.natuurBos;
  if (natuurBos) samen.natuurBos = natuurBos;
  const herstelvordering = nieuw.herstelvordering ?? bestaand.herstelvordering;
  if (herstelvordering) samen.herstelvordering = herstelvordering;
  const stookolietank = nieuw.stookolietank ?? bestaand.stookolietank;
  if (stookolietank) samen.stookolietank = stookolietank;
  const leegstand = nieuw.leegstand ?? bestaand.leegstand;
  if (leegstand) samen.leegstand = leegstand;
  const pfas = nieuw.pfas ?? bestaand.pfas;
  if (pfas) samen.pfas = pfas;
  return samen;
}

/**
 * Verwerkt één v2-extra-waarneming en voegt ze samen met het overeenkomstige
 * veld (de gezaghebbendste bron primeert, afwijkingen blijven als alternatief).
 * Enkel conflict-gevoelige velden worden ondersteund — partijen niet.
 */
function verwerkExtraWaarneming(
  rauw: IntakeExtraWaarneming,
  i: number,
  velden: IntakeResultaat["velden"],
  problemen: string[]
): void {
  const pad = `extraWaarnemingen[${i}]`;
  const bron =
    typeof rauw.bron === "string" && (BRON_TYPES as string[]).includes(rauw.bron)
      ? (rauw.bron as BronType)
      : undefined;
  if (!bron) {
    problemen.push(`${pad} overgeslagen: ongeldig of ontbrekend brontype.`);
    return;
  }
  const brondocumentId =
    typeof rauw.brondocumentId === "string" && rauw.brondocumentId.trim() ? rauw.brondocumentId.trim() : undefined;
  const w = <T,>(waarde: T) => maakVeld(waarde, bron, brondocumentId);

  switch (rauw.veld) {
    case "prijs":
    case "voorschot":
    case "belastbareWaarde":
    case "andereGoederenWaarde": {
      const n = leesBedrag(rauw.waarde, `${pad}.waarde`, problemen);
      if (n !== undefined) velden[rauw.veld] = combineerVeld(velden[rauw.veld], w(n), euro);
      break;
    }
    case "compromisdatum": {
      if (typeof rauw.waarde === "string" && rauw.waarde.trim()) {
        velden.compromisdatum = combineerVeld(velden.compromisdatum, w(rauw.waarde.trim()), (d) => d);
      } else {
        problemen.push(`${pad}.waarde overgeslagen: geen geldige datumtekst.`);
      }
      break;
    }
    case "relatieBelasting": {
      const r = leesEnum<Relatie>(rauw.waarde, relatieOpties, `${pad}.waarde`, problemen);
      if (r) velden.relatieBelasting = combineerVeld(velden.relatieBelasting, w(r), (x) => x);
      break;
    }
    case "goedTypeBelasting": {
      const g = leesEnum<GoedeType>(rauw.waarde, GOEDTYPE_BELASTING, `${pad}.waarde`, problemen);
      if (g) velden.goedTypeBelasting = combineerVeld(velden.goedTypeBelasting, w(g), (x) => x);
      break;
    }
    case "goed": {
      if (isObject(rauw.waarde)) {
        const goed = leesGoed(rauw.waarde as IntakeGoed, problemen);
        if (goed) velden.goed = combineerVeld(velden.goed, w(goed), formatGoed);
      } else {
        problemen.push(`${pad}.waarde overgeslagen: "goed" verwacht een object.`);
      }
      break;
    }
    default:
      problemen.push(
        `${pad} overgeslagen: veld "${String(rauw.veld)}" ondersteunt geen extra waarnemingen (enkel prijs, voorschot, belastbareWaarde, andereGoederenWaarde, compromisdatum, relatieBelasting, goedTypeBelasting, goed).`
      );
  }
}

/** Combineert een bestaand en een nieuw veld tot één veld met brongezag + alternatieven. */
function combineerVeld<T>(
  bestaand: Veld<T> | undefined,
  nieuw: Veld<T> | undefined,
  format: (waarde: T) => string
): Veld<T> | undefined {
  if (!nieuw) return bestaand;
  if (!bestaand) return nieuw;
  return maakVeldUitWaarnemingen([...waarnemingen(bestaand), ...waarnemingen(nieuw)], format);
}

/**
 * Voegt geïmporteerde intake-velden samen met een bestaand dossier: per veld
 * worden de bestaande en de nieuwe waarneming gecombineerd, waarbij de bron met
 * het hoogste gezag (authentieke bron > onderhandse bron) de primaire waarde
 * wordt en een afwijkende bron als alternatief behouden blijft — net wat de
 * tegenstrijdighedencontrole nadien als conflict toont. De format-functies zijn
 * uitgelijnd op berekenTegenstrijdigheden, zodat échte verschillen ook effectief
 * als tegenstrijdigheid verschijnen. De aanroeper herberekent nadien de
 * ontbrekende stukken en tegenstrijdigheden en zet de tijdstempel.
 */
export function voegIntakeVeldenSamen(
  dossier: Dossier,
  velden: IntakeResultaat["velden"]
): Dossier {
  const tekst = (waarde: string) => waarde;
  // Veld-agnostische merge aan de runtime-grens van de import: welke velden
  // bij welk dossiertype horen, bewaakt de discriminated union bij het bouwen
  // en narrowen in getypeerde code — deze functie combineert generiek wat de
  // intake aanlevert met wat er al staat (het hoogste brongezag wint). De cast
  // is de ene, bewuste uitzondering: voor een dossiertype zonder een bepaald
  // veld levert de intake dat veld in de praktijk ook niet aan.
  return {
    ...dossier,
    partijen: combineerVeld(dossier.partijen, velden.partijen, (ps) => JSON.stringify(ps)) ?? dossier.partijen,
    goed: combineerVeld(dossier.goed, velden.goed, formatGoed),
    prijs: combineerVeld(dossier.prijs, velden.prijs, euro),
    voorschot: combineerVeld(dossier.voorschot, velden.voorschot, euro),
    compromisdatum: combineerVeld(dossier.compromisdatum, velden.compromisdatum, tekst),
    belastbareWaarde: combineerVeld(dossier.belastbareWaarde, velden.belastbareWaarde, euro),
    relatieBelasting: combineerVeld(dossier.relatieBelasting, velden.relatieBelasting, tekst),
    goedTypeBelasting: combineerVeld(dossier.goedTypeBelasting, velden.goedTypeBelasting, tekst),
    andereGoederenWaarde: combineerVeld(dossier.andereGoederenWaarde, velden.andereGoederenWaarde, euro),
  } as Dossier;
}

// ── Intake-resultaat → volledig Dossier-object (één bron van waarheid) ───────

/**
 * De optionele top-level dossiervelden die één-op-één uit het intake-resultaat
 * overgenomen worden. Eén lijst, op één plaats: een nieuw intake-veld hoeft
 * enkel hier (en in IntakeResultaat/Dossier) te bestaan om overal — app én
 * tests — door te stromen. Voorheen stond dezelfde spread-opsomming in vier
 * testbestanden en de dossierpagina, en moest élk nieuw veld vijfmaal worden
 * toegevoegd (vergeten = veld dat stilzwijgend nooit doorstroomt).
 */
const INTAKE_TOPVELDEN = [
  "verkoopdocument",
  "verkoopRegime",
  "referentie",
  "gebruik",
  "zonnepanelen",
  "syndicusStatus",
  "wettelijkVoorkooprecht",
  "fiscaalRegime",
  "medeEigendomLastenBetwist",
  "attesten",
  "betaling",
  "notarissen",
  "makelaarNaam",
  "eigendomstitel",
  "medeEigendom",
  "lijfrente",
  "roerendeGoederen",
  "verdelingGekochteDelen",
  "koperEnigeEigenWoning",
] as const satisfies readonly (keyof IntakeResultaat & keyof Dossier)[];

/**
 * Bouwt een volledig Dossier uit een intake-resultaat: de vaste basisvelden,
 * alle gekende optionele top-level velden (INTAKE_TOPVELDEN) en de
 * bron-getrackte velden (r.velden). `partijen` krijgt een lege default die
 * door r.velden wordt overschreven zodra de intake partijen bevat.
 */
export function maakDossierUitIntakeResultaat(
  r: IntakeResultaat,
  basis: {
    id: string;
    status: DossierStatus;
    aangemaakt: string;
    aangepast: string;
    /** Terugvaltype wanneer het intake-JSON geen (geldig) dossiertype droeg. */
    standaardDossiertype?: Dossiertype;
  }
): Dossier {
  const dossier: Dossier = maakLeegDossier(r.dossiertype ?? basis.standaardDossiertype ?? "verkoop-zonder-krediet", {
    id: basis.id,
    status: basis.status,
    partijen: maakVeld([], "manueel"),
    ontbrekendeStukken: [],
    tegenstrijdigheden: [],
    aangemaakt: basis.aangemaakt,
    aangepast: basis.aangepast,
  });
  // Generieke toewijzing aan de runtime-grens (zelfde uitzondering als in
  // voegIntakeVeldenSamen): de intake levert in de praktijk enkel velden aan
  // die bij het dossiertype horen; de union bewaakt de getypeerde code.
  for (const veld of INTAKE_TOPVELDEN) {
    if (r[veld] !== undefined) {
      (dossier as unknown as Record<string, unknown>)[veld] = r[veld];
    }
  }
  Object.assign(dossier, r.velden);
  return dossier;
}
