// Persistierte Fertigungs-Feld-Diffs in Differenzsätze überführen
// (Loop 3 Schritt 4; Vorbefund docs/qaf-v2/loop-3-vorbefund.md).
//
// Der zweite datenbankfähige Produzent des Katalogs. Drei Entscheidungen:
//
//   1. **Input ist das DB-Zeilen-Subset**, nach dem Muster von
//      `summary-view.ts` (`SummaryDiffRowData`): `qaf_manufacturing_diff`
//      führt den fertigen Feld-Diff — hier wird nichts neu gerechnet.
//      Ein zweiter Eingang für den Mappen-Pfad (`FieldDiff[]` aus `diffSteps`)
//      kann später dazukommen, analog zu den zwei Eingängen der Übersicht.
//   2. **Keine Zelle ohne vollständige Herkunft (Weg 4).** Die Tabelle führt
//      `source_alt`/`source_neu` als Adressen, aber weder Formel noch
//      Wertzustand. Es entsteht keine Zelle; `validateTraceability` meldet
//      die Sätze ohne Fundstelle beim Verbraucher.
//   3. **Das Rechen-Trio erzeugt keine Sätze — und verschwindet nicht still.**
//      Die Engine selbst verweigert dort die Differenz-Aussage:
//      `nicht_anwendbar` ist laut differ.ts ausdrücklich „not a structural
//      add/removal, just a state that can't be diffed numerically";
//      `blockiert` heißt „no delta is computed at all … instead of showing a
//      possibly-wrong number" (types.ts), und der Befund läuft bereits als
//      Plausibilitäts-Issue; `nicht_berechenbar` (beide Seiten leer) enthält
//      keine Aussage, die unterdrückt werden könnte. Ein Katalog-Satz würde
//      eine Aussage erfinden, die der Vergleich nicht getroffen hat. Deshalb:
//      `nicht_anwendbar`/`blockiert` werden je Zeile benannt zurückgegeben
//      (`withheldDe`), `nicht_berechenbar` und `konstant` bleiben satzlos.
//      Konsequenz, ehrlich benannt: `deltaNullReason` bleibt auf diesem Pfad
//      ungesetzt — das Feld ist Vokabular für Produzenten, die eine echte
//      Differenz mit fehlendem Delta führen (heute keiner).
//
// Formelbefund auf dem DB-Pfad: `status` persistiert die beiden
// überschreibenden Arten (`formel_geaendert` → Wert war gleich,
// `formel_zu_konstante`). Der dritte Fall (`formel_geaendert_wert_geaendert`)
// ist in-memory-only (kein Spare-Column, types.ts) und auf diesem Pfad
// grundsätzlich nicht rekonstruierbar — er fehlt hier, benannt statt still.
//
// Pure Funktionen, keine I/O.

import type { QAFFieldKey } from '@/lib/qaf-parser'
import type { DifferenceRecord, RawDifference } from './all-differences'
import type { DiffStatus } from './types'
import { KIND_BY_STATUS, VERB_BY_STATUS, sortKeyTuple } from './differences-from-summary'
import { STEP_FIELD_LABELS } from './rule-engine'

/** Subset der `qaf_manufacturing_diff`-Spalten, das dieser Produzent liest. */
export interface ManufacturingDiffRowData {
  field: string
  alt_value: number | null
  neu_value: number | null
  delta_absolute: number | null
  status: string | null
  source_alt: string | null
  source_neu: string | null
}

/** Stations-Kontext aus `qaf_step_match` — für Beschreibung und Ankerzeile. */
export interface ManufacturingDiffInput {
  diff: ManufacturingDiffRowData
  /** Stationsname, wie die Anzeige ihn führt. */
  stationDe: string
  /** Blattzeile der Station, Fallback wenn die Quellzellen keine tragen. */
  stationAnchorRow: number | null
  /** Match-Einstufung aus `qaf_step_match` — wird der Tabellen-`matchType`. */
  matchStatus?: string | null
}

export interface ManufacturingTableName {
  stationDe: string
  feldLabelDe: string
  matchStatus: string | null
}

export interface ManufacturingDifferences {
  differences: RawDifference[]
  /**
   * Vier-Schlüssel-Tupel → Anzeige-Namen. `TableRow` kennt kein
   * `descriptionDe` — Station und Feld erreichen die Tabelle nur über die
   * names-Map von `rowsFromRecords`, zugeordnet nach der ID-Vergabe über
   * dieselben Sortierschlüssel wie beim SUM-Produzenten.
   */
  nameByTuple: Map<string, ManufacturingTableName>
  /** Zeilen, zu denen die Engine die Differenz-Aussage verweigert (n.a./blockiert). */
  withheldDe: string[]
  /** Statuswerte ohne Ziel im Katalog — benannt statt still übersprungen. */
  skippedDe: string[]
}

const rowOf = (cell: string | null): number | null => {
  if (cell === null) return null
  const m = /(\d+)\s*$/.exec(cell)
  return m === null ? null : Number(m[1])
}

const WITHHELD: ReadonlySet<string> = new Set(['nicht_anwendbar', 'blockiert'])

function feldLabel(field: string): string {
  return STEP_FIELD_LABELS[field as QAFFieldKey] ?? field
}

export function differencesFromManufacturing(
  rows: readonly ManufacturingDiffInput[],
): ManufacturingDifferences {
  const differences: RawDifference[] = []
  const nameByTuple = new Map<string, ManufacturingTableName>()
  const withheldDe: string[] = []
  const skippedDe: string[] = []

  for (const { diff, stationDe, stationAnchorRow, matchStatus } of rows) {
    // Kein Unterschied, kein Satz; beidseitig leer enthält keine Aussage.
    if (diff.status === null || diff.status === 'konstant' || diff.status === 'nicht_berechenbar') continue

    if (WITHHELD.has(diff.status)) {
      withheldDe.push(
        `${stationDe} · ${feldLabel(diff.field)}: Status „${diff.status}" — die Engine trifft hier keine Differenz-Aussage, der Katalog erfindet keine.`,
      )
      continue
    }

    const kind = KIND_BY_STATUS[diff.status as keyof typeof KIND_BY_STATUS]
    if (kind === undefined) {
      skippedDe.push(
        `${stationDe} · ${feldLabel(diff.field)}: Status „${diff.status}" hat kein Ziel im Katalog — kein Satz erzeugt.`,
      )
      continue
    }

    const raw: RawDifference = {
      area: 'MFG',
      kind,
      // Auf dem DB-Pfad aus `status` rekonstruierbar: die zwei überschreibenden
      // Arten. Der dritte Fall ist in-memory-only und hier nie sichtbar.
      formulaFindingKind:
        diff.status === 'formel_geaendert'
          ? 'formel_geaendert_wert_gleich'
          : diff.status === 'formel_zu_konstante'
            ? 'formel_zu_konstante'
            : null,
      descriptionDe: `${stationDe} · ${feldLabel(diff.field)}: ${VERB_BY_STATUS[diff.status as keyof typeof KIND_BY_STATUS]}`,
      valueAward: diff.alt_value,
      valueCurrent: diff.neu_value,
      delta: diff.delta_absolute,
      // Kein Satz dieses Produzenten braucht einen Delta-Grund: das Trio
      // erzeugt keine Sätze (Dateikopf, Entscheidung 3), und bei added/removed
      // erklärt die Art das fehlende Delta selbst.
      deltaNullReason: null,
      // Einheiten stehen im Feld-Label ([s], [%], [BW/h] …). Ein eigenes
      // unit-Vokabular je QAF-Feld wäre eine kleine Registry — die wird nicht
      // hier im Vorbeigehen erfunden ("keine parallele kanonische Feldregistry").
      unit: '-',
      cells: [], // Weg 4: keine Zelle ohne Formel + Wertzustand (Dateikopf, 2).
      relatedDq: [],
      anchorRow: rowOf(diff.source_neu) ?? rowOf(diff.source_alt) ?? stationAnchorRow,
    }
    differences.push(raw)
    nameByTuple.set(sortKeyTuple(raw), {
      stationDe,
      feldLabelDe: feldLabel(diff.field),
      matchStatus: matchStatus ?? null,
    })
  }

  return { differences, nameByTuple, withheldDe, skippedDe }
}

/**
 * Anzeige-Namen je Kennung, für `rowsFromRecords` — Zuordnung über die
 * Sortierschlüssel, nicht über den Index. Station und Feld heißen in beiden
 * Ständen gleich (das Match hat sie zusammengeführt); `matchType` trägt die
 * Match-Einstufung, wo sie bekannt ist, sonst die Katalog-Art.
 */
export function tableNamesByDifferenceId(
  records: readonly DifferenceRecord[],
  nameByTuple: ReadonlyMap<string, ManufacturingTableName>,
): Map<string, { award: string[]; current: string[]; matchType: string; bucket?: string | null }> {
  const out = new Map<string, { award: string[]; current: string[]; matchType: string; bucket?: string | null }>()
  for (const r of records) {
    const n = nameByTuple.get(sortKeyTuple(r))
    if (n === undefined) continue
    out.set(r.differenceId, {
      award: [n.stationDe, n.feldLabelDe],
      current: [n.stationDe, n.feldLabelDe],
      matchType: n.matchStatus ?? r.kind,
    })
  }
  return out
}

/** Für Tests und Verbraucher: die Statuswerte, die hier bewusst satzlos bleiben. */
export const MANUFACTURING_SATZLOS: ReadonlyArray<DiffStatus> = [
  'konstant',
  'nicht_berechenbar',
  'nicht_anwendbar',
  'blockiert',
]
