// ── AUT-O3 · Perceptielaag-contract (laag 1) ─────────────────────────────────
// De sleutel tot autonomie: Notary.AI leest de bronstukken ZELF en vult het
// dossier, i.p.v. te vertrouwen op een externe agent die de intake-JSON
// aanlevert (vandaag buiten scope — zie ARCHITECTURE.md). Dit bestand legt het
// CONTRACT van die perceptielaag vast en koppelt het aan wat al bestaat; de
// eigenlijke extractors per brondocumenttype (regex/LLM) zijn AUT-S1 en vullen
// het (nu lege) register.
//
// De pijplijn: Brondocument[] → (extractor per bronType) → GeextraheerdeWaarneming[]
//   → naarIntakeWaarnemingen → de BESTAANDE merge (tegenstrijdigheden.ts:
//   maakVeldUitWaarnemingen, met bronprioriteit + conflictdetectie) → Dossier.
// De perceptielaag herhaalt die merge dus NIET; ze levert er de waarnemingen
// voor aan. De kwaliteit van de perceptie voedt bovendien AUT-O1
// (`perceptieGronden`), zodat een onvolledig of tegenstrijdig ingelezen dossier
// nooit blind autonoom verder handelt.
//
// ── Ontwerpbeslissingen (de dragende Opus-keuzes) ───────────────────────────
// 1. WAARNEMING, NIET WAARHEID. Een extractor levert WAARNEMINGEN (feit + bron +
//    zekerheid), geen definitieve waarden. Het samenvoegen en het beslechten van
//    tegenstrijdigheden gebeurt in de bestaande, gezaghebbende laag
//    (bronPrioriteit) — de perceptie mag nooit stilzwijgend één bron "winnen".
// 2. GEEN EXTRACTOR → GEEN GOK. Een brondocument zonder geregistreerde extractor
//    belandt in `onbekendeBronnen` (zichtbaar, escaleerbaar), nooit in een
//    verzonnen waarneming. Default-deny, net als AUT-O1.
// 3. GDPR: PERCEPTIE IS LOKAAL. `Brondocument.tekst` bevat persoonsgegevens en
//    mag het lokale milieu NOOIT verlaten; enkel de geanonimiseerde structuur
//    (placeholders) stroomt verder, zoals de bestaande handoff-aanpak. De
//    `ref`/`brondocumentRef` is een pseudoniem (bestandsnaam/id), geen PII.

import type { BronType } from "./types";
import type { Zekerheidsniveau, Zekerheidsgrond } from "./autonomie";
import type { IntakeExtraWaarneming } from "./intake";
import type { GebruikslogEvent } from "../gebruikslog";

/** Eén ruw bronstuk zoals de perceptielaag het ziet. `tekst` kan
 * persoonsgegevens bevatten → strikt lokaal (zie ontwerpbeslissing 3). */
export interface Brondocument {
  /** Pseudoniem/verwijzing (bestandsnaam of id), GEEN persoonsgegevens. */
  ref: string;
  /** Gedetecteerd/opgegeven brontype; bepaalt welke extractor draait. */
  bronType: BronType;
  /** Ruwe tekstinhoud van het stuk (lokaal; verlaat het milieu niet). */
  tekst: string;
}

/** Eén door een extractor waargenomen feit uit één bronstuk. */
export interface GeextraheerdeWaarneming {
  /** Dossierveld-pad, bv. "prijs", "goed.adres", "partijen[0].naam". */
  veldpad: string;
  waarde: unknown;
  bronType: BronType;
  brondocumentRef?: string;
  /** Hoe zeker de extractor van deze waarneming is (voedt AUT-O1). */
  zekerheid: Zekerheidsniveau;
}

/** Een extractor voor één brondocumenttype (AUT-S1 implementeert deze). */
export type Extractor = (doc: Brondocument) => GeextraheerdeWaarneming[];

/** Register van extractors per brontype. Leeg tot AUT-S1 het vult. */
export type PerceptieRegister = Partial<Record<BronType, Extractor>>;

/**
 * Eén registratie in het extractorregister (lib/dossier/extractors/, AUT-F4):
 * een nieuwe extractor toevoegen = één bestand in die map dat een
 * `…Extractors`-lijst exporteert; de barrel-codegen neemt ze op.
 */
export interface ExtractorRegistratie {
  bronType: BronType;
  extractor: Extractor;
}

/**
 * Vouwt de geregistreerde extractors tot het PerceptieRegister. Een dubbele
 * registratie voor hetzelfde brontype is een programmeerfout en faalt luid —
 * twee extractors voor één bron zouden elkaars waarnemingen stilzwijgend
 * verdringen.
 */
export function maakPerceptieRegister(registraties: readonly ExtractorRegistratie[]): PerceptieRegister {
  const register: PerceptieRegister = {};
  for (const { bronType, extractor } of registraties) {
    if (register[bronType]) {
      throw new Error(`Dubbele extractor geregistreerd voor brontype "${bronType}".`);
    }
    register[bronType] = extractor;
  }
  return register;
}

/** Uitkomst van het inlezen van alle bronstukken. */
export interface ExtractieResultaat {
  waarnemingen: GeextraheerdeWaarneming[];
  /** Refs van bronstukken zonder geregistreerde extractor — nooit geraden. */
  onbekendeBronnen: string[];
}

/**
 * Laat over elk bronstuk de bijhorende extractor lopen en verzamelt de
 * waarnemingen. Een bronstuk zonder extractor in het register wordt niet
 * geraden maar als `onbekendeBronnen` teruggegeven (default-deny). Puur:
 * dezelfde documenten + register → dezelfde uitvoer (mits de extractors puur
 * zijn).
 */
export function extraheerUitBrondocumenten(
  documenten: readonly Brondocument[],
  register: PerceptieRegister
): ExtractieResultaat {
  const waarnemingen: GeextraheerdeWaarneming[] = [];
  const onbekendeBronnen: string[] = [];
  for (const doc of documenten) {
    const extractor = register[doc.bronType];
    if (!extractor) {
      onbekendeBronnen.push(doc.ref);
      continue;
    }
    waarnemingen.push(...extractor(doc));
  }
  return { waarnemingen, onbekendeBronnen };
}

/**
 * Zet de geëxtraheerde waarnemingen om naar het bestaande
 * `IntakeExtraWaarneming`-formaat, zodat de reeds bestaande intake-merge
 * (`maakVeldUitWaarnemingen`, met bronprioriteit + tegenstrijdigheidsdetectie)
 * ze verwerkt. Zo blijft er één merge-/conflictmechanisme in de codebase.
 */
export function naarIntakeWaarnemingen(extractie: ExtractieResultaat): IntakeExtraWaarneming[] {
  return extractie.waarnemingen.map((w) => ({
    veld: w.veldpad,
    waarde: w.waarde,
    bron: w.bronType,
    brondocumentId: w.brondocumentRef,
  }));
}

/**
 * Leidt uit de perceptiekwaliteit de gronden af voor het AUT-O1-zekerheidsmodel:
 * - een verwacht veld dat met zekerheid "zeker" is waargenomen → positieve grond;
 * - een verwacht veld dat ontbreekt → blokkerende negatieve grond (kan niet
 *   blind verder);
 * - een verwacht veld dat enkel met lage zekerheid ("onzeker") is waargenomen,
 *   of waarvoor meerdere bronnen tegenstrijdige waarden gaven → negatieve grond;
 * - een onbekend (niet-inleesbaar) bronstuk → blokkerende negatieve grond.
 * `vereisteVeldpaden` zijn de velden die voor de beoogde handeling nodig zijn.
 */
export function perceptieGronden(
  extractie: ExtractieResultaat,
  vereisteVeldpaden: readonly string[]
): Zekerheidsgrond[] {
  const gronden: Zekerheidsgrond[] = [];

  for (const ref of extractie.onbekendeBronnen) {
    gronden.push({ omschrijving: `Bronstuk "${ref}" kon niet worden ingelezen (geen extractor).`, positief: false, blokkerend: true });
  }

  for (const { veldpad, beoordeling } of beoordeelVereisteVelden(extractie, vereisteVeldpaden)) {
    if (beoordeling === "ontbreekt") {
      gronden.push({ omschrijving: `Vereist veld "${veldpad}" ontbreekt in de bronstukken.`, positief: false, blokkerend: true });
    } else if (beoordeling === "tegenstrijdig") {
      gronden.push({ omschrijving: `Vereist veld "${veldpad}" is tegenstrijdig tussen bronnen.`, positief: false, blokkerend: true });
    } else if (beoordeling === "onzeker") {
      gronden.push({ omschrijving: `Vereist veld "${veldpad}" is slechts met lage zekerheid waargenomen.`, positief: false });
    } else {
      gronden.push({ omschrijving: `Vereist veld "${veldpad}" is met zekerheid waargenomen.`, positief: true });
    }
  }

  return gronden;
}

/** Beoordeling van één vereist veldpad over alle waarnemingen heen. */
type VeldBeoordeling = "ontbreekt" | "tegenstrijdig" | "onzeker" | "zeker";

/**
 * Beoordeelt elk vereist veldpad: ontbreekt het in de bronstukken, spreken de
 * bronnen elkaar tegen, is het enkel met lage zekerheid waargenomen, of is het
 * zeker? Eén classificatie voor de zekerheidsgronden (perceptieGronden) én de
 * observability-signalen (gebruikslogEventsUitPerceptie), zodat die twee
 * beoordelingen nooit stil uit elkaar kunnen groeien.
 */
function beoordeelVereisteVelden(
  extractie: ExtractieResultaat,
  vereisteVeldpaden: readonly string[]
): { veldpad: string; beoordeling: VeldBeoordeling }[] {
  const perVeld = new Map<string, GeextraheerdeWaarneming[]>();
  for (const w of extractie.waarnemingen) {
    const lijst = perVeld.get(w.veldpad) ?? [];
    lijst.push(w);
    perVeld.set(w.veldpad, lijst);
  }
  return vereisteVeldpaden.map((veldpad) => {
    const ws = perVeld.get(veldpad) ?? [];
    const beoordeling: VeldBeoordeling =
      ws.length === 0
        ? "ontbreekt"
        : new Set(ws.map((w) => JSON.stringify(w.waarde))).size > 1
          ? "tegenstrijdig"
          : ws.some((w) => w.zekerheid === "onzeker")
            ? "onzeker"
            : "zeker";
    return { veldpad, beoordeling };
  });
}

/**
 * Observability-signalen (AUT-F5) uit een perceptierun, klaar voor
 * `registreerGebruik`: welke bronnen niet inleesbaar waren en welke vereiste
 * velden ontbreken of tegenstrijdig zijn — om de volgende AUT-S1-extractors te
 * prioriteren. GDPR: de waarde is het bronTYPE of het veldpad, nooit de ref
 * (een bestandsnaam kan een partijnaam bevatten) en nooit een waargenomen
 * waarde. Puur: registreren doet de aanroeper.
 */
export function gebruikslogEventsUitPerceptie(
  documenten: readonly Brondocument[],
  extractie: ExtractieResultaat,
  vereisteVeldpaden: readonly string[]
): Omit<GebruikslogEvent, "tijdstip">[] {
  const events: Omit<GebruikslogEvent, "tijdstip">[] = [];

  for (const ref of extractie.onbekendeBronnen) {
    const doc = documenten.find((d) => d.ref === ref);
    events.push({ type: "perceptie_onbekende_bron", waarde: doc?.bronType ?? "onbekend" });
  }

  for (const { veldpad, beoordeling } of beoordeelVereisteVelden(extractie, vereisteVeldpaden)) {
    if (beoordeling === "ontbreekt") {
      events.push({ type: "perceptie_veld_ontbreekt", waarde: veldpad });
    } else if (beoordeling === "tegenstrijdig") {
      events.push({ type: "perceptie_veld_tegenstrijdig", waarde: veldpad });
    }
  }

  return events;
}
