// Der Differenzkatalog und seine Rückverfolgung (Spezifikation Kap. 7.6, R-13).
//
// Jede festgestellte Differenz — Summenzeile, Materialposition, Station,
// Werkzeug, Prämisse — bekommt einen Satz mit eigener Kennung und ihren
// Fundstellen. Das ist die Ebene, auf der später jede Kachel, jede Folie und
// jede Aussage belegt wird: Aussage → Differenz-Kennungen → Zellen.
//
// Zwei Eigenschaften machen den Katalog überhaupt brauchbar:
//
//   1. Die Kennungen sind stabil. Zwei Läufe über dieselben Dateien vergeben
//      dieselben — sonst zeigt jeder Verweis aus einer Folie nach dem nächsten
//      Lauf woandershin.
//   2. Kein Satz ohne Fundstelle. Eine Differenz, die man nicht nachschlagen
//      kann, ist in einer Verhandlung nichts wert.
//
// Pure Funktionen, keine I/O.

import type { ValueState } from './cell-state'
import type { FormulaComparisonKind } from './formula-engine'
import type { DiffStatus } from './types'

/** Bereich, aus dem die Differenz stammt. Die Reihenfolge ist die Sortierordnung. */
export type DifferenceArea = 'IDT' | 'SUM' | 'MAT' | 'MFG' | 'SBM'

export const DIFFERENCE_AREAS: readonly DifferenceArea[] = ['IDT', 'SUM', 'MAT', 'MFG', 'SBM']

/**
 * Art der Veränderung — ein geschlossenes Vokabular.
 *
 * Bewusst geschlossen: Freitext an dieser Stelle macht den Katalog
 * unauswertbar, und die Spezifikation verlangt Regeln statt Formulierungen.
 */
export type DifferenceKind =
  | 'added'
  | 'removed'
  | 'changed'
  | 'renamed'
  | 'merged'
  | 'split'
  | 'moved'
  | 'newly_filled'
  | 'cleared'

/**
 * Formelbefund im Katalog — die drei berichtspflichtigen Arten des
 * Formelvergleichs. Per `Extract` an die kanonische Quelle
 * (formula-engine.ts) gebunden statt als dritte Kopie der Literale geführt.
 */
export type FormulaFindingKind = Extract<
  FormulaComparisonKind,
  'formel_geaendert_wert_gleich' | 'formel_geaendert_wert_geaendert' | 'formel_zu_konstante'
>

/**
 * Grund für ein fehlendes Delta — Teilmenge von `DiffStatus` (types.ts),
 * damit Katalog und Feld-Vergleich dieselben Wörter benutzen.
 */
export type DeltaNullReason = Extract<DiffStatus, 'nicht_berechenbar' | 'nicht_anwendbar' | 'blockiert'>

export interface DifferenceCell {
  fileRole: 'award' | 'current'
  sheet: string
  cell: string
  formula: string | null
  valueState: ValueState
}

export interface DifferenceRecord {
  differenceId: string
  area: DifferenceArea
  kind: DifferenceKind
  /**
   * Formelbefund — orthogonal zu `kind`, nach dem Muster des
   * Summary-Vergleichs: bei `formel_geaendert_wert_geaendert` behält der
   * Vergleich das Wertband und setzt den Formelbefund dazu
   * (summary-metrics.ts, Formel-Zuweisung). Formel- und Wertänderung treten
   * gleichzeitig auf; ein einziges Vokabular müsste eine der beiden Aussagen
   * verwerfen — genau die stille Verkürzung, die Master-Prompt §12.4
   * verbietet. `null` heißt: kein Formelbefund erhoben oder keiner
   * vorhanden. Der menschenlesbare Text bleibt beim Plausibilitäts-Befund
   * (dort persistiert); hier steht nur das geschlossene, auswertbare
   * Vokabular.
   */
  formulaFindingKind: FormulaFindingKind | null
  descriptionDe: string
  valueAward: number | string | null
  valueCurrent: number | string | null
  delta: number | null
  /**
   * Grund, warum kein Delta vorliegt. `nicht_berechenbar`, `nicht_anwendbar`
   * und `blockiert` sind Gründe für `delta === null`, keine Änderungsarten —
   * sie gehören deshalb nicht in `kind`. Invariante: nur gesetzt, wenn
   * `delta === null`; `validateTraceability` meldet den Widerspruch.
   */
  deltaNullReason: DeltaNullReason | null
  unit: string
  cells: DifferenceCell[]
  /** Kennungen der Datenqualitäts-Befunde, die dieselbe Stelle betreffen. */
  relatedDq: string[]
  /**
   * Zeile, an der die Differenz hängt — die deterministische Sortiergrundlage
   * innerhalb eines Bereichs. `null` für Differenzen ohne Zeilenbezug.
   */
  anchorRow: number | null
}

/** Ein Satz, bevor er seine Kennung bekommt. */
export type RawDifference = Omit<DifferenceRecord, 'differenceId'>

/**
 * Der fünfte Sortierschlüssel: die erste Fundstelle, fachlich statt
 * redaktionell. Material- und Fertigungszeilen tragen mehrere Felder je Zeile —
 * dort sind Bereich, Ankerzeile und Art regelmäßig gleich, und die
 * Eindeutigkeit hinge sonst allein an `descriptionDe`, einem Anzeigetext, den
 * eine Umformulierung ändert. Blatt und Zelle ändern sich zwischen zwei Läufen
 * über dieselben Dateien nicht. Ohne Fundstelle (Weg 4: bewusst leere `cells`)
 * bleibt der Schlüssel leer und das bisherige Verhalten unverändert.
 */
const fundstellenSchlüssel = (d: RawDifference): string =>
  d.cells.length === 0 ? '' : `${d.cells[0].sheet}!${d.cells[0].cell}`

/**
 * Kennungen vergeben.
 *
 * Sortiert wird nach Bereich, dann Ankerzeile, dann Art, Beschreibung und
 * erster Fundstelle. Die Zeile ist der tragende Teil: Sie ändert sich zwischen
 * zwei Läufen über dieselben Dateien nicht, während jede Reihenfolge, die sich
 * aus der Verarbeitung ergibt, das sehr wohl kann. Die Fundstelle trennt den
 * kompletten Gleichstand der ersten vier — sonst entschiede dort die
 * Eingabereihenfolge.
 */
export function assignDifferenceIds(differences: RawDifference[]): DifferenceRecord[] {
  const areaRank = new Map(DIFFERENCE_AREAS.map((a, i) => [a, i] as const))
  const counters = new Map<DifferenceArea, number>()

  return [...differences]
    .sort(
      (a, b) =>
        (areaRank.get(a.area) ?? 99) - (areaRank.get(b.area) ?? 99) ||
        (a.anchorRow ?? Number.MAX_SAFE_INTEGER) - (b.anchorRow ?? Number.MAX_SAFE_INTEGER) ||
        a.kind.localeCompare(b.kind) ||
        a.descriptionDe.localeCompare(b.descriptionDe) ||
        fundstellenSchlüssel(a).localeCompare(fundstellenSchlüssel(b)),
    )
    .map((d) => {
      const n = (counters.get(d.area) ?? 0) + 1
      counters.set(d.area, n)
      return { ...d, differenceId: `D-${d.area}-${String(n).padStart(3, '0')}` }
    })
}

export interface TraceabilityEntry {
  /** Differenzen, auf denen die Aussage beruht. */
  differenceIds: string[]
  /** Zellen, die diese Differenzen belegen — aufgelöst, damit die Kette lückenlos ist. */
  cells: DifferenceCell[]
  /** Datenqualitäts-Befunde, die die Aussage einschränken. */
  dq: string[]
}

export interface TraceabilityIndex {
  /** Managementaussagen und Kacheln. */
  statements: Record<string, TraceabilityEntry>
  /** Sichten und Folien. */
  views: Record<string, TraceabilityEntry>
}

/**
 * Rückverfolgung aufbauen.
 *
 * Die Zellen werden aus den Differenzsätzen aufgelöst und nicht noch einmal
 * mitgegeben: Eine zweite, von Hand gepflegte Liste driftet unweigerlich von
 * der ersten weg, und dann belegt der Index etwas anderes als die Differenz
 * behauptet.
 */
export function buildTraceabilityIndex(
  records: readonly DifferenceRecord[],
  statements: Record<string, string[]>,
  views: Record<string, string[]>,
): TraceabilityIndex {
  const byId = new Map(records.map((r) => [r.differenceId, r] as const))

  const resolve = (ids: string[]): TraceabilityEntry => {
    const found = ids.map((id) => byId.get(id)).filter((r): r is DifferenceRecord => r !== undefined)
    return {
      differenceIds: [...ids].sort(),
      cells: found.flatMap((r) => r.cells),
      dq: [...new Set(found.flatMap((r) => r.relatedDq))].sort(),
    }
  }

  return {
    statements: Object.fromEntries(Object.entries(statements).map(([k, ids]) => [k, resolve(ids)])),
    views: Object.fromEntries(Object.entries(views).map(([k, ids]) => [k, resolve(ids)])),
  }
}

export interface TraceabilityProblem {
  kind:
    | 'unknown_difference_id'
    | 'difference_without_cells'
    | 'statement_without_differences'
    | 'delta_reason_contradiction'
  where: string
  detail: string
}

/**
 * Die Rückverfolgung gegen ihre eigenen Versprechen prüfen.
 *
 * Ein Index, der auf eine nicht existierende Differenz zeigt, ist schlimmer als
 * keiner: Er sieht vollständig aus. Deshalb wird hier ausdrücklich beides
 * geprüft — dass jede genannte Kennung existiert und dass jede Differenz
 * wenigstens eine Fundstelle trägt.
 */
export function validateTraceability(
  records: readonly DifferenceRecord[],
  index: TraceabilityIndex,
  referenced: { statements: Record<string, string[]>; views: Record<string, string[]> },
): TraceabilityProblem[] {
  const known = new Set(records.map((r) => r.differenceId))
  const problems: TraceabilityProblem[] = []

  for (const r of records) {
    if (r.cells.length === 0) {
      problems.push({
        kind: 'difference_without_cells',
        where: r.differenceId,
        detail: 'Differenz ohne Fundstelle — nicht nachschlagbar.',
      })
    }
    // Ein Grund für ein fehlendes Delta neben einem vorhandenen Delta ist
    // eine widersprüchliche Aussage — sie darf nicht still im Katalog stehen.
    if (r.deltaNullReason !== null && r.delta !== null) {
      problems.push({
        kind: 'delta_reason_contradiction',
        where: r.differenceId,
        detail: `Grund „${r.deltaNullReason}" für ein fehlendes Delta, aber das Delta ist ${r.delta}.`,
      })
    }
  }

  for (const [gruppe, eintraege] of [
    ['statements', referenced.statements],
    ['views', referenced.views],
  ] as const) {
    for (const [key, ids] of Object.entries(eintraege)) {
      for (const id of ids) {
        if (!known.has(id)) {
          problems.push({
            kind: 'unknown_difference_id',
            where: `${gruppe}.${key}`,
            detail: `Verweis auf ${id}, den der Katalog nicht führt.`,
          })
        }
      }
      if (ids.length === 0) {
        problems.push({
          kind: 'statement_without_differences',
          where: `${gruppe}.${key}`,
          detail: 'Aussage ohne Beleg.',
        })
      }
    }
  }

  // Der Index selbst muss dieselben Kennungen tragen wie die Eingabe — sonst
  // ist er beim Aufbau stillschweigend gefiltert worden.
  for (const [gruppe, eintraege] of [
    ['statements', index.statements],
    ['views', index.views],
  ] as const) {
    for (const [key, entry] of Object.entries(eintraege)) {
      const soll = (referenced[gruppe][key] ?? []).length
      if (entry.differenceIds.length !== soll) {
        problems.push({
          kind: 'unknown_difference_id',
          where: `${gruppe}.${key}`,
          detail: `Index führt ${entry.differenceIds.length} Kennungen, verwiesen wurde auf ${soll}.`,
        })
      }
    }
  }

  return problems
}

export interface DifferenceCatalog {
  records: DifferenceRecord[]
  count: number
  countsByArea: Record<DifferenceArea, number>
}

export function buildCatalog(differences: RawDifference[]): DifferenceCatalog {
  const records = assignDifferenceIds(differences)
  const countsByArea = Object.fromEntries(DIFFERENCE_AREAS.map((a) => [a, 0])) as Record<DifferenceArea, number>
  for (const r of records) countsByArea[r.area] += 1
  return { records, count: records.length, countsByArea }
}
