// Die `ai_ready`-Fläche (Spezifikation Kap. 17, R-21).
//
// Diese Schicht ist die Übergabe an die Sprachschicht — und sie ist bewusst so
// gebaut, dass dort nichts mehr zu rechnen ist. Der Grund steht in der
// Spezifikation als Doktrin und hier als Bauform:
//
//   **Zahlen entstehen nie im Modell.**
//
// Jede spätere Aussage ist die Umformulierung eines Fakts, der hier bereits
// fertig steht — mit Werten, Einheit, Differenz-Kennungen und Zellen. Ein
// Sprachmodell, das eine Zahl selbst bildet, bildet früher oder später eine
// falsche; ein Modell, das nur umformuliert, kann das nicht.
//
// Die zweite Doktrin ist genauso wichtig und weniger offensichtlich:
//
//   **„Nichts gefunden" und „nicht berechnet" sind zweierlei.**
//
// Ohne diese Unterscheidung schreibt die Sprachschicht „keine Auffälligkeiten",
// wo in Wahrheit ein Modul gar nicht gelaufen ist. Das ist der Fehler, der in
// einer Verhandlung am teuersten wird, weil er wie eine Entwarnung klingt.
//
// Pure Funktionen, keine I/O.

import type { DifferenceCell, DifferenceRecord } from './all-differences'
import type { Lever } from './negotiation-levers'
import type { DqFinding } from './data-quality'

/** Der Zustand einer Sektion — die Unterscheidung, um die es geht. */
export type SectionState = 'populated' | 'empty_verified' | 'not_computed'

export type FactConfidence = 'deterministic' | 'candidate'

export interface FactValue {
  key: string
  value: number | string | null
  unit: string
}

export interface Fact {
  factId: string
  /** Ein Satz, aus dem die Sprachschicht formulieren darf — ohne zu rechnen. */
  statementDe: string
  values: FactValue[]
  differenceIds: string[]
  cells: DifferenceCell[]
  /**
   * `candidate` kennzeichnet eine Vermutung — etwa eine mögliche Verlagerung
   * zwischen Material und Fertigung. Die Sprachschicht behandelt solche Fakten
   * im Konjunktiv; die Regel dafür steht im Leitfaden, nicht im Wohlwollen des
   * Prompts.
   */
  confidence: FactConfidence
}

export interface AiReadingGuide {
  deltaDefinition: string
  signSemantics: string
  nullSemantics: string
  percentScale: string
  candidateRule: string
  sectionStateRule: string
  numberRule: string
}

/**
 * Der Leitfaden, den die Sprachschicht mitbekommt.
 *
 * Er steht im Ergebnis und nicht in einer Dokumentation daneben: Was nicht in
 * der Datei steht, kommt beim Modell nicht an.
 */
export const AI_READING_GUIDE: AiReadingGuide = {
  deltaDefinition: 'Delta = aktueller Stand minus Vergabestand.',
  signSemantics: 'Ein positives Delta bedeutet teurer, ein negatives günstiger.',
  nullSemantics: 'null bedeutet nicht vorhanden oder nicht berechenbar. 0 bedeutet ausdrücklich null.',
  percentScale: 'Prozentwerte stehen als Dezimalzahl: 0,31 bedeutet 31 Prozent.',
  candidateRule:
    'Fakten mit confidence "candidate" sind Vermutungen und ausschliesslich im Konjunktiv wiederzugeben.',
  sectionStateRule:
    '"empty_verified" heisst geprüft und nichts gefunden. "not_computed" heisst nicht geprüft — dazu ist keine Aussage zulässig, auch keine beruhigende.',
  numberRule:
    'Alle Zahlen stehen in den Fakten. Eine Zahl, die dort nicht steht, darf nicht gebildet, gerundet oder umgerechnet werden.',
}

export interface AiReadySection {
  state: SectionState
  factIds: string[]
}

export interface AiReady {
  readingGuide: AiReadingGuide
  facts: Fact[]
  sections: Record<string, AiReadySection>
  /** Prüfergebnisse des Kerns — bei `false` verweigert die Sprachschicht die Zusammenfassung. */
  safeToSummarize: boolean
  blockingChecks: string[]
}

export interface FactInput {
  statementDe: string
  values: FactValue[]
  differenceIds: string[]
  confidence?: FactConfidence
}

/**
 * Fakten bilden und ihre Zellen aus dem Katalog auflösen.
 *
 * Die Zellen werden nicht mitgegeben, sondern nachgeschlagen: Eine zweite, von
 * Hand gepflegte Liste driftet weg, und dann belegt der Fakt etwas anderes, als
 * er behauptet. Ein Fakt, dessen Belege nicht auffindbar sind, entsteht gar
 * nicht — lieber eine Aussage weniger als eine unbelegte.
 */
export function buildFacts(inputs: readonly FactInput[], records: readonly DifferenceRecord[]): Fact[] {
  const byId = new Map(records.map((r) => [r.differenceId, r] as const))
  const out: Fact[] = []

  for (const input of inputs) {
    const belege = input.differenceIds.map((id) => byId.get(id))
    if (belege.some((b) => b === undefined)) continue
    const gefunden = belege as DifferenceRecord[]

    out.push({
      factId: `FACT-${String(out.length + 1).padStart(3, '0')}`,
      statementDe: input.statementDe,
      values: input.values,
      differenceIds: [...input.differenceIds].sort(),
      cells: gefunden.flatMap((r) => r.cells),
      confidence: input.confidence ?? 'deterministic',
    })
  }

  return out
}

/**
 * Fakten aus Hebeln.
 *
 * Der Hebeltext ist bereits regelbasiert und ohne Freitext — er taugt damit
 * unmittelbar als Fakt. Die Werte kommen aus dem Hebel und nicht aus dem Text,
 * damit die Sprachschicht sie nicht aus dem Satz herauslesen muss.
 */
export function factsFromLevers(levers: readonly Lever[], records: readonly DifferenceRecord[]): Fact[] {
  return buildFacts(
    levers.map((l) => ({
      statementDe: l.textDe,
      values:
        l.amount === null || l.unit === null
          ? [{ key: 'positionen', value: l.differenceIds.length, unit: 'pieces' }]
          : [
              { key: 'betrag', value: l.amount, unit: l.unit },
              { key: 'positionen', value: l.differenceIds.length, unit: 'pieces' },
            ],
      differenceIds: l.differenceIds,
    })),
    records,
  )
}

export interface SectionInput {
  /** Wurde die Sektion überhaupt gerechnet? */
  computed: boolean
  factIds: string[]
}

/**
 * Sektionszustände bestimmen.
 *
 * Die ganze Unterscheidung hängt an `computed`. Sie aus der Zahl der Fakten
 * abzuleiten wäre bequem und falsch: Null Fakten sehen bei einer geprüften und
 * bei einer übersprungenen Sektion gleich aus.
 */
export function sectionStates(sections: Record<string, SectionInput>): Record<string, AiReadySection> {
  return Object.fromEntries(
    Object.entries(sections).map(([key, s]) => [
      key,
      {
        state: !s.computed ? 'not_computed' : s.factIds.length > 0 ? 'populated' : 'empty_verified',
        factIds: [...s.factIds].sort(),
      } satisfies AiReadySection,
    ]),
  )
}

export interface ValidationCheck {
  checkId: string
  status: 'pass' | 'fail' | 'not_applicable'
}

/**
 * Die `ai_ready`-Fläche zusammensetzen.
 *
 * Steht auch nur eine Prüfung des Kerns auf `fail`, wird die Fläche nicht
 * unterdrückt — sie wird gesperrt. Die Sprachschicht sieht dann die Fakten
 * weiterhin, darf aber nicht zusammenfassen und nennt die Prüfung. Wegzulassen
 * wäre schlechter: Eine fehlende Sektion sieht aus wie ein sauberer Lauf.
 */
export function buildAiReady(
  facts: readonly Fact[],
  sections: Record<string, SectionInput>,
  checks: readonly ValidationCheck[],
): AiReady {
  const blocking = checks.filter((c) => c.status === 'fail').map((c) => c.checkId)
  return {
    readingGuide: AI_READING_GUIDE,
    facts: [...facts],
    sections: sectionStates(sections),
    safeToSummarize: blocking.length === 0,
    blockingChecks: [...blocking].sort(),
  }
}

export interface AiReadyProblem {
  kind: 'fact_without_cells' | 'section_references_unknown_fact' | 'number_not_in_facts' | 'unstated_candidate'
  where: string
  detail: string
}

/**
 * Die Fläche gegen ihre eigenen Zusagen prüfen.
 *
 * Der letzte Punkt ist der unbequemste und deshalb wichtig: Jede Zahl, die in
 * einem Fakttext steht, muss auch als Wert danebenstehen. Sonst muss die
 * Sprachschicht sie aus dem Satz herauslesen — und genau dabei entstehen die
 * Zahlendreher, die dieser Schicht die Glaubwürdigkeit nehmen.
 */
export function validateAiReady(aiReady: AiReady): AiReadyProblem[] {
  const problems: AiReadyProblem[] = []
  const known = new Set(aiReady.facts.map((f) => f.factId))

  for (const f of aiReady.facts) {
    if (f.cells.length === 0) {
      problems.push({ kind: 'fact_without_cells', where: f.factId, detail: 'Fakt ohne Fundstelle.' })
    }

    for (const zahl of zahlenIm(f.statementDe)) {
      const gedeckt = f.values.some((v) => typeof v.value === 'number' && gleich(v.value, zahl))
      if (!gedeckt) {
        problems.push({
          kind: 'number_not_in_facts',
          where: f.factId,
          detail: `Die Zahl ${zahl} steht im Satz, aber nicht in den Werten.`,
        })
      }
    }

    if (f.confidence === 'candidate' && !/\b(könnte|dürfte|deutet|vermutlich|möglicherweise)\b/i.test(f.statementDe)) {
      problems.push({
        kind: 'unstated_candidate',
        where: f.factId,
        detail: 'Vermutung ist im Satz nicht als solche erkennbar.',
      })
    }
  }

  for (const [key, s] of Object.entries(aiReady.sections)) {
    for (const id of s.factIds) {
      if (!known.has(id)) {
        problems.push({
          kind: 'section_references_unknown_fact',
          where: key,
          detail: `Verweis auf ${id}, den die Fläche nicht führt.`,
        })
      }
    }
  }

  return problems
}

/** Zahlen aus einem Satz lesen — in deutscher Schreibweise. */
function zahlenIm(text: string): number[] {
  const out: number[] = []
  for (const m of text.matchAll(/\d{1,3}(?:\.\d{3})+(?:,\d+)?|\d+(?:,\d+)?/g)) {
    const roh = m[0].replace(/\./g, '').replace(',', '.')
    const n = Number(roh)
    if (!Number.isNaN(n)) out.push(n)
  }
  return out
}

const gleich = (a: number, b: number) => Math.abs(a - b) <= 0.005
