// LOCAL CONTENT CHINA (LC-CN) sheet parser + duale LC-Rate-Nachrechnung
// (KAR-904 / P2.5 — letztes Phase-2-Item). Built from the logistics-parser.ts/
// rmr-parser.ts pattern (KAR-903/P2.4, KAR-902/P2.3): dynamic import of the
// canonical registry, label-anchor + confidence matching, a controlled
// degradation path on an unusable sheet (never throws), sourceCells/
// normalized provenance from day one, and the coreFieldsFound tri-state +
// lccnForReconciliation/FromPersistedMeta helper pair built in from the START
// (the #274/KAR-898 lesson every P2.x module since has shipped on commit one
// instead of retrofitting).
//
// ── Struktur-Entscheidung: SINGLE-RECORD label-scan, not header+row grid ───
// The Leitfaden's own worked screenshot (Abbildung 30, [49], "Blatt LC-CN")
// shows ONE Zusammenfassungsblatt with a fixed set of labeled Kostenblöcke
// (1a./1b./1c./2a./2b./SUMME HERSTELLKOSTEN/3a./3b./KALKULATION LC RATE) —
// exactly ONE record per file, not repeatable Positionsnummer rows like
// MATERIAL/SBM/RMR/LOGISTICS. [46]: "Auf Registerblatt 'LC-CN' sind keine
// Eingaben erforderlich" — the sheet is a pure, single, computed summary.
// This module therefore uses a whole-grid LABEL-SCAN (summary-parser.ts's
// labelScan pattern: locate a matching label cell, take the first non-empty
// value cell to its right, MAX_SCAN_RIGHT-bounded) against the canonical
// LC_CN registry's level:'summary' fields, instead of the header-row/
// data-block shape every row-based P2.x parser before this one uses.
//
// ── Field-Scope: 15 of the 17 registered LC_CN canonical ids ────────────────
// canonical-fields.ts's LC_CN_FIELDS already registers 17 ids (KAR-892/P1.1).
// 15 of them are level:'summary' — every one of the Blatt-LC-CN fields [49]'s
// own screenshot tabulates, extracted here. The remaining 2
// (lccn_am_cif_share, lccn_am_process_clustering) are level:'row' — they are
// the "LC-Gruppierung" columns [47] documents as being added to the MATERIAL
// and MANUFACTURING COSTS sheets ("Registerblätter MATERIAL und MANUFACTURING
// COSTS ... zusätzlich eingeblendet und befüllt"), NOT columns on the LC-CN
// sheet itself. Extracting them would require extending material-parser.ts's/
// a future manufacturing-costs row parser's OWN header scan with two more
// columns — a different module's scope, explicitly out of this PR's "Kern-
// Umfang" per the task instruction and already flagged as such in
// canonical-fields.ts's own lccn_am_process_clustering.notes ("out of this
// PR's extraction scope"). LccnFieldKey below therefore has 15 keys, and
// LCCN_FIELD_KEY_TO_CANONICAL (canonical-fields.ts) maps exactly those 15 —
// the other 2 canonical ids stay registered but unmapped by this module,
// consistent with belonging to a different sheet.
//
// ── Bindender Kais-Entscheid (10.07.2026, KAR-904-Kommentar) — LC-Rate ─────
// Der Leitfaden widerspricht sich bei der LC-Rate-Formel:
//   [47] "Formeln/Berechnungslogik": LC Rate = (1 − Importierter
//        Materialanteil / Gesamtkosten für LC Fertigung) × 100%
//   [48] Feldbeschreibung "LC Rate in %": "Verhältnis von Gesamt importierter
//        Wert (CIF) / Gesamtkosten für LC Fertigung"
// [47]s "Importierter Materialanteil" ist selbst definiert als "CIF
// importiertes Material + CIF importiertes Material vom Unterlieferant" [47]
// — wortgleich mit [48]s "Gesamt importierter Wert (CIF)" ("Summe aus
// CIF-Kosten für lokales Material (Unterlieferant) und CIF-Kosten
// importiertes Material" [48]). Beide Lesarten teilen sich also denselben
// Zähler (CIF = lccn_total_imported_value_cif) und denselben Nenner
// (Gesamtkosten = lccn_total_lc_manufacturing_cost) — sie sind rechnerische
// Komplemente (lokal = 100 − import), NICHT zwei unabhängig belegte Zahlen.
// Kais' Entscheid (bindend, siehe KAR-904-Kommentar): BEIDE Lesarten werden
// berechnet und klar beschriftet nebeneinander ausgewiesen ("LC-Rate (lokal,
// Lesart S.47)" / "Import-Quote (Lesart S.48)"), beide gegen die einzige
// tatsächlich im Excel vorhandene "LC Rate in %"-Zelle geprüft (das Blatt hat
// nur EIN LC-Rate-Feld, keine zwei — [49]s Screenshot listet nur "LC Rate in
// %" einmal unter "KALKULATION LC RATE"), mit explizitem Hinweis welche
// Lesart welches Ergebnis liefert. KEINE stille Wahl — siehe
// validateLccnLcRateReadings unten.
//
// ── 40/40-Hinweis (Leitfaden [46], AGGREGAT-Ebene, NICHT die volle
// Tier-2/Tier-3-Regel) ──────────────────────────────────────────────────────
// [46]: "Ist bei einer lokal beschafften Subkomponente (Tier 2) die LC-Rate <
// 40%, muss der CIF-Wert (Tier 3) ausgewiesen werden (40/40 LC-Regel)." Diese
// Regel operiert auf einzelnen Tier-2-Subkomponenten (MATERIAL-Zeilen-Ebene)
// — Daten, die dieser Parser (Zusammenfassungsblatt, ein Datensatz pro Datei)
// strukturell nicht hat. validateLccnLcRateReadings unten haengt an beide
// berechneten Lesarten einen rein INFORMATIVEN below40Hint an, wenn die
// jeweilige Lesart < 40% ergibt — ausdruecklich NICHT die vollstaendige
// Tier-2/Tier-3-Pruefung (die braucht Zeilen-Ebene, hier nicht verfuegbar).
//
// ── Validierungen (Kern-Umfang) ──────────────────────────────────────────────
//   (a) validateLccnTotalProductionCost: SUMME HERSTELLKOSTEN = 1a+1b+1c+2a+2b
//       — [49]s Screenshot nennt diese Formel EXPLIZIT: "SUMME HERSTELLKOSTEN*
//       (1a.+1b.+1c.+2a.+2b.)".
//   (b) validateLccnLcRateReadings: beide LC-Rate-Lesarten (lokal/import),
//       siehe "Bindender Kais-Entscheid" oben. Namespace lccn_lc_rate_local /
//       lccn_lc_rate_import.
//   (c) validateLccnLcRateComplement: die zwei BERECHNETEN Lesarten summieren
//       sich (bei denselben Inputs CIF/Gesamtkosten) rechnerisch immer zu
//       ~100% — kein unabhaengiger zweiter Beleg (siehe Herleitung oben), aber
//       ein sichtbarer, explizit ausgewiesener Konsistenz-Beleg exakt wie von
//       Kais verlangt ("als Komplemente gekennzeichnet"). Gated auf dieselben
//       Inputs wie (b) — eine Abweichung hier waere ein Implementierungsfehler
//       (z.B. falscher Nenner), kein Fachbefund; wird trotzdem als expliziter
//       Check gefuehrt statt stillschweigend angenommen, damit ein Regressions-
//       fehler sichtbar wird statt sich in zwei separat "bestanden"-Checks zu
//       verstecken.
//
// ── LC Checkliste Fertigungsprozess — Substantial vs. Other (Leitfaden Teil 3
// S.91-95) ──────────────────────────────────────────────────────────────────
// Reference table only (analog SBM_DEVICE_CATEGORY_TABLE, sbm-parser.ts
// #274/KAR-898-Muster) — Fundament fuer ein kuenftiges Lookup-Issue, NICHT in
// diesem PR live gegen Zeilen gepruft. Grund: das LC-CN-Blatt selbst traegt
// laut [49]s Screenshot AUSSCHLIESSLICH Summen (1a.-3b., KALKULATION LC RATE)
// — keine Positionsnummer-/Prozessschritt-Zeilen. Die tatsaechliche
// "Substantial vs. deklariert"-Pruefung braucht die AM (Prozessclusterung)-
// Spalte auf MATERIAL/MANUFACTURING COSTS ([47], out of this module's Sheet-
// Scope, siehe oben) — classifyLcProcess() ist daher als getestetes,
// eigenstaendiges Lookup exportiert, aber in diesem PR an keine Zeile
// verdrahtet (dokumentiertes TODO fuer eine kuenftige PR, sobald die
// MATERIAL/MANUFACTURING-COSTS-Zeilen-Ebene die AM-Spalte parst).

import type { Worksheet } from 'exceljs'
import type { CanonicalField } from './canonical-fields.types'
import { worksheetToGrid } from './workbook-adapter'
import { normalizeProcessName } from './normalizer'
import { matchesModuleSheetName } from './module-sheet-names'
import type { ComparisonSide } from './rule-engine'
import type { PlausibilityIssue, PlausibilitySeverity } from './plausibility'
import type { FacetDegradation } from './types'
import { MIN_SIGNAL_MAPPED_COLUMNS as MIN_SIGNAL_FOUND_LABELS } from './types'
import { LCCN_FIELD_KEY_TO_CANONICAL } from './canonical-fields'
import { byCanonicalId } from './canonical-model'
import { missingFieldReason, joinMissingReasons } from './bilingual-message'

/** EN label for an LC-CN field key, sourced from the canonical field
 * registry (KAR-906/P3.2). */
function lccnLabelEn(key: LccnFieldKey, fallbackDe: string): string {
  return byCanonicalId(LCCN_FIELD_KEY_TO_CANONICAL[key])?.labelEn ?? fallbackDe
}

// ── Record shape (15 summary-level LC_CN fields) ────────────────────────────

export interface LccnSummaryValues {
  /** 1a. Kosten lokales Material* */
  localMaterialCost: number | null
  /** 1a. davon: Kosten lokales Material ohne MGK* */
  localMaterialCostExclMgk: number | null
  /** 1a. davon: CIF-Kosten importiertes Material vom Unterlieferant* */
  cifImportedMaterialSubsupplier: number | null
  /** 1b. Kosten importiertes Material* */
  importedMaterialCost: number | null
  /** 1b. davon: CIF-Kosten importiertes Material* */
  cifImportedMaterial: number | null
  /** 1c. Rohmaterialkosten* */
  rawMaterialCost: number | null
  /** 1c. davon: Rohmaterialkosten ohne MGK* */
  rawMaterialCostExclMgk: number | null
  /** 2a. Substanzielle Fertigungskosten* */
  substantialManufacturingCost: number | null
  /** 2b. Andere Fertigungskosten* */
  otherManufacturingCost: number | null
  /** SUMME HERSTELLKOSTEN* (1a.+1b.+1c.+2a.+2b.) */
  totalProductionCost: number | null
  /** 3a. Vorrichtungen und Folgewerkzeuge fuer substanziellen Fertigungsprozess* */
  devicesSubstantial: number | null
  /** 3b. Vorrichtungen und Folgewerkzeuge fuer anderen Fertigungsprozess* */
  devicesOther: number | null
  /** KALKULATION LC RATE: Gesamt importierter Wert (CIF)* */
  totalImportedValueCif: number | null
  /** KALKULATION LC RATE: Gesamtkosten fuer LC Fertigung* */
  totalLcManufacturingCost: number | null
  /** KALKULATION LC RATE: LC Rate in % — the Excel's OWN single value, whose
   * underlying formula reading ([47] vs. [48]) is exactly the documented
   * contradiction this module never silently resolves. */
  lcRate: number | null
}

export type LccnFieldKey = keyof LccnSummaryValues

const ALL_FIELD_KEYS: readonly LccnFieldKey[] = [
  'localMaterialCost',
  'localMaterialCostExclMgk',
  'cifImportedMaterialSubsupplier',
  'importedMaterialCost',
  'cifImportedMaterial',
  'rawMaterialCost',
  'rawMaterialCostExclMgk',
  'substantialManufacturingCost',
  'otherManufacturingCost',
  'totalProductionCost',
  'devicesSubstantial',
  'devicesOther',
  'totalImportedValueCif',
  'totalLcManufacturingCost',
  'lcRate',
]

/**
 * The 3 fields that MUST be located (as LABELS — regardless of whether the
 * value cell itself is filled, same "structurally located" semantics
 * summary-metrics.ts's howLocated/confidence>0 uses) for an LC-CN sheet to be
 * usable at all: totalProductionCost (SUMME HERSTELLKOSTEN, the sheet's own
 * defining aggregate), totalImportedValueCif + totalLcManufacturingCost (the
 * two LC-Rate formula inputs both documented readings share, see module
 * header). Mirrors every other P2.x module's 3-field
 * CORE_*_FIELD_KEYS pattern, adapted from "row-identity + entity + defining
 * value" to "defining aggregate + the two shared formula inputs" for this
 * single-record sheet shape.
 *
 * Deliberately does NOT throw when missing: LC-CN is an OPTIONAL, additive
 * sheet (Type-Auswahl, [46]) — a malformed/absent LC-CN sheet must not fail
 * the whole file ingest. See parseLccnWorksheet below.
 */
export const CORE_LCCN_FIELD_KEYS: readonly LccnFieldKey[] = [
  'totalProductionCost',
  'totalImportedValueCif',
  'totalLcManufacturingCost',
]

// KAR-958/P2 (gate-audit.md B9, coreFieldsFound-Resilienz) — the shared
// MIN_SIGNAL_MAPPED_COLUMNS floor from types.ts (PR #325 review fix #5, was
// a duplicated local literal here), imported under this file's own
// MIN_SIGNAL_FOUND_LABELS name since it is applied here to the number of
// LABELS located (foundKeys.size) rather than mapped header columns — LC-CN
// is a whole-grid label scan, not a header-row parse. Below it, a degraded
// LC-CN sheet is still treated as a genuinely empty/foreign sheet.

const MAX_SCAN_RIGHT = 6

// ── Sheet detection ─────────────────────────────────────────────────────────

/**
 * True when a worksheet name identifies the LC-CN sheet. The Leitfaden's own
 * tab title is "LC-CN" ([46]/[49]: "Registerblatt 'LC-CN'" / "Blatt LC-CN") —
 * matched tolerantly on "lc-cn"/"lc cn" (dash-or-space, case-insensitive) or
 * the full "local content" phrase (Abbildung 30's own title: "LOCAL CONTENT
 * QUOTATION ANALYSIS FORM ZUSAMMENFASSUNG"), covering an EN-labelled tab
 * variant too — no other documented QAF module name contains either phrase.
 * Sheet-name alias source centralized in module-sheet-names.ts (KAR-905/P3.1).
 */
export function isLccnSheetName(name: string): boolean {
  return matchesModuleSheetName(name, 'LC_CN')
}

/** First worksheet whose name matches isLccnSheetName, or null when the
 * workbook has none (the additive gate — files without a Type=LC-CN sheet
 * are simply not parsed by this module at all). */
export function findLccnWorksheet(wb: { worksheets: Worksheet[] }): Worksheet | null {
  return wb.worksheets.find((w) => isLccnSheetName(w.name)) ?? null
}

// ── Canonical registry bridge (dynamic import — Client-bundle discipline,
// KAR-893 lesson "Bundle-Lehre aus #272": canonical-model.ts/canonical-
// fields.ts must stay OUT of the static import graph, exactly like every
// other P2.x parser's loadXRegistry()) ──────────────────────────────────────

interface LabelMatch {
  key: LccnFieldKey
  /** 1.0 exact labelDe/labelEn match, 0.9 alias match — same two-tier
   * rationale material-parser.ts's module header documents, reused verbatim
   * here. */
  confidence: number
}

interface LccnRegistryCtx {
  /** LC_CN module, level:'summary' slice of the canonical registry — scoped
   * to the 15 fields this module actually extracts (see module header "Field-
   * Scope"); the 2 level:'row' ids never enter this label-scan at all. */
  registry: readonly CanonicalField[]
  idToKey: Record<string, LccnFieldKey>
  findByAliasFn: (
    label: string,
    lang: 'de' | 'en' | undefined,
    registry: readonly CanonicalField[],
  ) => CanonicalField[]
}

let lccnRegistryPromise: Promise<LccnRegistryCtx> | null = null

async function loadLccnRegistry(): Promise<LccnRegistryCtx> {
  if (!lccnRegistryPromise) {
    lccnRegistryPromise = (async () => {
      const [{ byModule, findByAlias }, { LCCN_FIELD_KEY_TO_CANONICAL }] = await Promise.all([
        import('./canonical-model'),
        import('./canonical-fields'),
      ])
      const registry = byModule('LC_CN').filter((f) => f.level === 'summary')
      const idToKey = Object.fromEntries(
        Object.entries(LCCN_FIELD_KEY_TO_CANONICAL).map(([key, canonicalId]) => [canonicalId, key as LccnFieldKey]),
      ) as Record<string, LccnFieldKey>

      return { registry, idToKey, findByAliasFn: findByAlias }
    })()
  }
  return lccnRegistryPromise
}

function matchLabelCellSync(cellText: string, ctx: LccnRegistryCtx): LabelMatch | null {
  if (cellText === '') return null

  for (const field of ctx.registry) {
    if (cellText === field.labelDe || cellText === field.labelEn) {
      const key = ctx.idToKey[field.id]
      if (key) return { key, confidence: 1 }
    }
  }

  const hits = ctx.findByAliasFn(cellText, undefined, ctx.registry)
  const distinctKeys = new Set(hits.map((f) => ctx.idToKey[f.id]).filter((k): k is LccnFieldKey => k !== undefined))
  if (distinctKeys.size === 1) {
    const [key] = distinctKeys
    return { key, confidence: 0.9 }
  }

  return null
}

/** Match one already-whitespace-normalized cell string against the LC_CN
 * summary-level canonical field registry. Exported for the same reason every
 * other module's matchXHeaderColumn is: single-cell callers (tests) that
 * don't want to manage the registry cache themselves. */
export async function matchLccnLabelCell(cellText: string): Promise<LabelMatch | null> {
  const ctx = await loadLccnRegistry()
  return matchLabelCellSync(cellText, ctx)
}

function normalizeCell(v: unknown): string {
  return String(v ?? '')
    .replace(/[\r\n]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
}

function colLetter(col0: number): string {
  let n = col0 + 1
  let out = ''
  while (n > 0) {
    const rem = (n - 1) % 26
    out = String.fromCharCode(65 + rem) + out
    n = Math.floor((n - 1) / 26)
  }
  return out
}

function a1(row0: number, col0: number): string {
  return `${colLetter(col0)}${row0 + 1}`
}

function toNum(v: unknown): number | null {
  if (v === null || v === undefined || v === '') return null
  // `Number(new Date(...))` liefert den Millisekunden-Epoch, nicht NaN — eine
  // Zahlenspalte mit datums-/zeitartigem Zellformat käme sonst als
  // Milliardenwert in der Kalkulation an, ohne Fehler und ohne Warnung. Seit
  // der BIFF-Lesepfad `cellDates: true` setzt, liefern auch .xls-Dateien echte
  // Date-Objekte; 964 von 1076 Realdateien tragen Datumszellen, ein Großteil
  // davon in genau den Blättern, die dieser Parser liest.
  if (v instanceof Date) return null
  const n = Number(v)
  return isNaN(n) ? null : n
}

function isLabelLikeCell(v: unknown): boolean {
  const s = normalizeCell(v)
  return s !== '' && !isFinite(Number(s))
}

// ── Parse-level diagnostics ─────────────────────────────────────────────────

export interface LccnParseMeta {
  /** Arithmetic mean of per-field confidence over located labels — 1.0 for an
   * intact sheet, lower once alias-only matches enter the mix. */
  parseConfidence: number
  /** Number of the 15 summary fields whose label was located (regardless of
   * whether the value cell itself was filled). */
  mappedFieldCount: number
  /** True once every CORE_LCCN_FIELD_KEYS label was located. False means at
   * least one core label is missing — since KAR-958/P2, `values` is NOT
   * unconditionally all-null in that case anymore (see
   * MIN_SIGNAL_FOUND_LABELS / `degradation` below). */
  coreFieldsFound: boolean
  /** KAR-958/P2 — set only when `coreFieldsFound` is false AND at least
   * MIN_SIGNAL_FOUND_LABELS labels were located (partial extraction
   * attempted). `undefined` on every intact parse and on a genuinely
   * empty/foreign sheet. */
  degradation?: FacetDegradation
}

export interface LccnParseResult {
  values: LccnSummaryValues
  sourceCells: Partial<Record<LccnFieldKey, string>>
  normalized: Partial<Record<LccnFieldKey, number>>
  meta: LccnParseMeta
}

function emptyValues(): LccnSummaryValues {
  const v = {} as LccnSummaryValues
  for (const key of ALL_FIELD_KEYS) v[key] = null
  return v
}

/**
 * Parse an already-located LC-CN worksheet — whole-grid label-scan (see
 * module header "Struktur-Entscheidung"), not a header-row/data-block shape.
 * Pure aside from the lazy registry import. Never throws on a degraded/
 * unusable sheet (see CORE_LCCN_FIELD_KEYS doc comment) — returns an
 * all-null, flagged result instead, so a malformed LC-CN sheet cannot fail
 * the whole file ingest.
 */
export async function parseLccnWorksheet(ws: Worksheet): Promise<LccnParseResult> {
  const grid = worksheetToGrid(ws) as unknown[][]
  if (!grid || grid.length === 0) {
    return { values: emptyValues(), sourceCells: {}, normalized: {}, meta: { parseConfidence: 0, mappedFieldCount: 0, coreFieldsFound: false } }
  }

  const ctx = await loadLccnRegistry()
  const values = emptyValues()
  const sourceCells: Partial<Record<LccnFieldKey, string>> = {}
  const normalized: Partial<Record<LccnFieldKey, number>> = {}
  const confidences: number[] = []
  const foundKeys = new Set<LccnFieldKey>()

  for (let r = 0; r < grid.length; r++) {
    const row = (grid[r] as unknown[]) ?? []
    for (let c = 0; c < row.length; c++) {
      const text = normalizeCell(row[c])
      if (text === '') continue
      const match = matchLabelCellSync(text, ctx)
      if (!match || foundKeys.has(match.key)) continue // first match wins, same as every other parser's usedKeys guard

      for (let k = c + 1; k <= c + MAX_SCAN_RIGHT && k < row.length; k++) {
        const cell = row[k]
        if (isLabelLikeCell(cell)) break // hit the next label before any value — nothing to read for this field
        const num = toNum(cell)
        if (num === null) continue
        values[match.key] = num
        sourceCells[match.key] = a1(r, k)
        normalized[match.key] = num
        break
      }

      // The label itself was structurally located even when no numeric value
      // followed it (e.g. a not-yet-quoted block) — counts toward
      // coreFieldsFound/mappedFieldCount/parseConfidence regardless, same
      // "label found vs. value present are independent questions" reasoning
      // template-fingerprint.ts's summaryLocatedKeys doc comment documents.
      foundKeys.add(match.key)
      confidences.push(match.confidence)
    }
  }

  const coreFieldsFound = CORE_LCCN_FIELD_KEYS.every((k) => foundKeys.has(k))
  // KAR-958/P2 (gate-audit.md B9): below the minimum-signal floor, treated
  // exactly as before this PR (genuinely empty/foreign sheet, all-null
  // values). At/above it, a missing core label no longer discards every
  // already-located label — `values`/`sourceCells`/`normalized` were already
  // populated in the scan loop above, so the fix here is to stop discarding
  // them, not to extract anything further.
  if (foundKeys.size < MIN_SIGNAL_FOUND_LABELS) {
    return {
      values: emptyValues(),
      sourceCells: {},
      normalized: {},
      meta: { parseConfidence: 0, mappedFieldCount: foundKeys.size, coreFieldsFound: false },
    }
  }

  const parseConfidence = confidences.length > 0 ? confidences.reduce((a, b) => a + b, 0) / confidences.length : 0
  const degradation: FacetDegradation | undefined = coreFieldsFound
    ? undefined
    : {
        facet: 'lccn',
        reason: 'PARSE_FAILED',
        sheet: ws.name,
        message: `LC-CN-Kernfelder fehlen: ${CORE_LCCN_FIELD_KEYS.filter((k) => !foundKeys.has(k)).join(', ')}.`,
      }
  return {
    values,
    sourceCells,
    normalized,
    meta: {
      parseConfidence,
      mappedFieldCount: foundKeys.size,
      coreFieldsFound,
      ...(degradation ? { degradation } : {}),
    },
  }
}

/**
 * Locate + parse the LC-CN sheet from an already-loaded workbook. Returns
 * null when the workbook has no LC-CN sheet at all (the additive gate —
 * distinct from "sheet present but too degraded", which returns an all-null
 * LccnParseResult with meta.coreFieldsFound:false instead).
 */
export async function parseLccnSheet(wb: { worksheets: Worksheet[] }): Promise<LccnParseResult | null> {
  const ws = findLccnWorksheet(wb)
  if (!ws) return null
  return parseLccnWorksheet(ws)
}

/**
 * Derive the tri-state `LccnParseResult | null` value callers (actions.ts's
 * ingestQafUpload, and via it QafFileParsed.lccn) must persist/thread from a
 * `parseLccnSheet` result — the single source of truth for that derivation,
 * mirroring rmrRowsForReconciliation/logisticsRowsForReconciliation (the
 * established #274/KAR-898-lesson pattern this module builds in from day one
 * instead of retrofitting).
 */
export function lccnForReconciliation(parsed: LccnParseResult | null): LccnParseResult | null {
  if (parsed === null) return null
  if (!parsed.meta.coreFieldsFound) return null
  return parsed
}

/**
 * The shape actions.ts's ingestQafUpload persists on `qaf_file.g60_meta.lccn`
 * — `null` when no LC-CN sheet was found at all, otherwise the parsed record
 * plus the same LccnParseMeta a live LccnParseResult carries. Identical shape
 * to LccnParseResult itself (a single-record parser has no separate "rows +
 * meta" split to make) — kept as its own named type for symmetry with every
 * other module's PersistedXMeta and so a JSONB-shape change is visible at the
 * type level independent of the live-parse type.
 */
export type PersistedLccnMeta = LccnParseResult

/**
 * Persisted-JSONB counterpart to lccnForReconciliation above (KAR-899-style
 * rehydration path, threaded through from the start per task instruction) —
 * `undefined` when `qaf_file.g60_meta.lccn` carries no key at all (pre-
 * KAR-904/P2.5 file, or a file whose Type was never LC-CN), `null`/the
 * record otherwise, delegated to lccnForReconciliation so the live-parse and
 * rehydrated-JSONB entry points cannot drift.
 */
export function lccnFromPersistedMeta(meta: PersistedLccnMeta | null | undefined): LccnParseResult | null | undefined {
  if (meta === undefined) return undefined
  if (meta === null) return null
  return lccnForReconciliation(meta)
}

// ── Validierung (a): SUMME HERSTELLKOSTEN = 1a+1b+1c+2a+2b (Leitfaden [49]) ─

export type LccnValidationCheckId =
  | 'lccn_total_production_cost'
  | 'lccn_lc_rate_local'
  | 'lccn_lc_rate_import'
  | 'lccn_lc_rate_complement'
export type LccnValidationStatus = 'bestanden' | 'abweichung' | 'nicht_pruefbar'

export interface LccnValidationResult {
  checkId: LccnValidationCheckId
  side: ComparisonSide
  status: LccnValidationStatus
  expected: number | null
  actual: number | null
  deltaAbsolute: number | null
  deltaPercent: number | null
  /** Set only for status === 'nicht_pruefbar' — why no verdict could be reached. */
  reason?: string
  /** EN counterpart of `reason` (KAR-906/P3.2). Set whenever `reason` is. */
  reasonEn?: string
  messageDe?: string
  messageEn?: string
  /** 40/40-Hinweis (informativ, Leitfaden [46], AGGREGAT-Ebene — siehe Modul-
   * Header) — set only on lccn_lc_rate_local/lccn_lc_rate_import when the
   * respective expected rate is < 40%. */
  below40Hint?: string
  /** EN counterpart of `below40Hint` (KAR-906/P3.2). */
  below40HintEn?: string
}

/** Central tolerance config for this module — same STARTWERT-Fachentscheid
 * philosophy as reconciliation.ts's RECONCILIATION_CONFIG / every other P2.x
 * module's own local config (0.5% relativ + 1 AW/Prozentpunkt absolut,
 * KAR-896-Muster). */
export interface LccnValidationConfig {
  /** Fraction, e.g. 0.005 = 0.5%. */
  relativeTolerance: number
  /** Absolute floor (currency minor unit for the cost check, percentage
   * points for the LC-Rate checks), applied via max(relative, absolute). */
  absoluteToleranceMinor: number
}

export const LCCN_VALIDATION_CONFIG: LccnValidationConfig = {
  relativeTolerance: 0.005,
  absoluteToleranceMinor: 1,
}

function fmt(n: number): string {
  return n.toFixed(4)
}

function withinTolerance(expected: number, actual: number, cfg: LccnValidationConfig): boolean {
  const threshold = Math.max(Math.abs(expected) * cfg.relativeTolerance, cfg.absoluteToleranceMinor)
  return Math.abs(actual - expected) <= threshold
}

function notPruefbar(
  checkId: LccnValidationCheckId,
  side: ComparisonSide,
  reason: string,
  reasonEn: string,
): LccnValidationResult {
  return {
    checkId,
    side,
    status: 'nicht_pruefbar',
    expected: null,
    actual: null,
    deltaAbsolute: null,
    deltaPercent: null,
    reason,
    reasonEn,
  }
}

/**
 * SUMME HERSTELLKOSTEN == 1a.+1b.+1c.+2a.+2b. (localMaterialCost +
 * importedMaterialCost + rawMaterialCost + substantialManufacturingCost +
 * otherManufacturingCost), Leitfaden [49] — the formula is quoted verbatim in
 * the sheet's own screenshot caption. Gated: returns null — no result at all,
 * not even nicht_pruefbar — when totalProductionCost itself is null (nothing
 * to check yet). A missing component is nicht_pruefbar (not silently treated
 * as 0 — a genuinely-zero cost block is a valid input, an unparsed one is
 * not, same "missing != zero" distinction every other P2.x formula check in
 * this file family makes).
 */
export function validateLccnTotalProductionCost(
  values: LccnSummaryValues,
  side: ComparisonSide,
  cfg: LccnValidationConfig = LCCN_VALIDATION_CONFIG,
): LccnValidationResult | null {
  const checkId: LccnValidationCheckId = 'lccn_total_production_cost'
  const actual = values.totalProductionCost
  if (actual === null) return null

  const components: Array<[LccnFieldKey, string]> = [
    ['localMaterialCost', 'Kosten lokales Material (1a.)'],
    ['importedMaterialCost', 'Kosten importiertes Material (1b.)'],
    ['rawMaterialCost', 'Rohmaterialkosten (1c.)'],
    ['substantialManufacturingCost', 'Substanzielle Fertigungskosten (2a.)'],
    ['otherManufacturingCost', 'Andere Fertigungskosten (2b.)'],
  ]
  const missing = components.filter(([key]) => values[key] === null)
  if (missing.length > 0) {
    const joined = joinMissingReasons(missing.map(([key, label]) => missingFieldReason(label, lccnLabelEn(key, label), false)))
    return notPruefbar(checkId, side, joined.de, joined.en)
  }

  const expected =
    values.localMaterialCost! +
    values.importedMaterialCost! +
    values.rawMaterialCost! +
    values.substantialManufacturingCost! +
    values.otherManufacturingCost!
  const deltaAbsolute = Number((actual - expected).toFixed(6))
  const deltaPercent = expected !== 0 ? Number((deltaAbsolute / Math.abs(expected)).toFixed(6)) : null
  const ok = withinTolerance(expected, actual, cfg)
  const messageDe = `SUMME HERSTELLKOSTEN (${side}): Excel weist ${fmt(actual)} aus, die unabhaengige Nachrechnung (1a.+1b.+1c.+2a.+2b.) ergibt ${fmt(expected)} (Delta ${fmt(deltaAbsolute)}) — Abweichung ausserhalb der Toleranz (${(cfg.relativeTolerance * 100).toFixed(1)} % / min. ${cfg.absoluteToleranceMinor}).`
  const messageEn = `Total production costs (${side}): Excel states ${fmt(actual)}, the independent recomputation (1a.+1b.+1c.+2a.+2b.) yields ${fmt(expected)} (delta ${fmt(deltaAbsolute)}) — deviation exceeds tolerance (${(cfg.relativeTolerance * 100).toFixed(1)}% / min. ${cfg.absoluteToleranceMinor}).`
  return {
    checkId,
    side,
    status: ok ? 'bestanden' : 'abweichung',
    expected,
    actual,
    deltaAbsolute,
    deltaPercent,
    ...(ok ? {} : { messageDe, messageEn }),
  }
}

// ── Validierung (b)+(c): duale LC-Rate + Komplement (bindender Kais-Entscheid) ─

function below40HintFor(
  rateLabelDe: string,
  rateLabelEn: string,
  expectedRate: number,
): { de: string; en: string } | undefined {
  if (expectedRate >= 40) return undefined
  return {
    de:
      `40/40-Hinweis (Leitfaden S.46, AGGREGAT-Ebene, informativ): ${rateLabelDe} liegt bei ${fmt(expectedRate)} % ` +
      `unter der 40%-Schwelle — bei einer einzelnen Tier-2-Subkomponente wuerde dies die CIF-Wert-Ausweisungspflicht ` +
      `(Tier 3) ausloesen. Diese Pruefung laeuft auf Zusammenfassungs-Ebene (ein Datensatz pro Datei), nicht auf ` +
      `einzelnen Tier-2-Subkomponenten — keine Aussage ueber eine konkrete Subkomponente.`,
    en:
      `40/40 note (Leitfaden p.46, AGGREGATE level, informational): ${rateLabelEn} is at ${fmt(expectedRate)}% ` +
      `below the 40% threshold — for a single tier-2 subcomponent this would trigger the CIF-value disclosure ` +
      `requirement (tier 3). This check runs at the summary level (one record per file), not on ` +
      `individual tier-2 subcomponents — no statement about any specific subcomponent.`,
  }
}

/**
 * Beide dokumentierten LC-Rate-Lesarten ([47] vs. [48], siehe Modul-Header
 * "Bindender Kais-Entscheid") — berechnet aus denselben zwei Feldern (CIF =
 * totalImportedValueCif, Gesamtkosten = totalLcManufacturingCost), beide
 * gegen die EINE tatsaechliche Excel-Zelle (lcRate) geprueft, IMMER BEIDE
 * zurueckgegeben (nie nur die "passende"). Gated: returns [] — keine
 * Ergebnisse, nicht einmal nicht_pruefbar — wenn lcRate selbst null ist
 * (nichts zu pruefen). Fehlende CIF/Gesamtkosten-Inputs ergeben
 * nicht_pruefbar fuer BEIDE Lesarten (dieselbe Datenbasis fehlt beiden).
 */
export function validateLccnLcRateReadings(
  values: LccnSummaryValues,
  side: ComparisonSide,
  cfg: LccnValidationConfig = LCCN_VALIDATION_CONFIG,
): LccnValidationResult[] {
  const actual = values.lcRate
  if (actual === null) return []

  const missing: { de: string; en: string }[] = []
  if (values.totalImportedValueCif === null) {
    missing.push(missingFieldReason('Gesamt importierter Wert (CIF)', lccnLabelEn('totalImportedValueCif', 'Gesamt importierter Wert (CIF)'), false))
  }
  if (values.totalLcManufacturingCost === null) {
    missing.push(missingFieldReason('Gesamtkosten fuer LC Fertigung', lccnLabelEn('totalLcManufacturingCost', 'Gesamtkosten fuer LC Fertigung'), false))
  }
  if (missing.length > 0) {
    const joined = joinMissingReasons(missing)
    return [
      notPruefbar('lccn_lc_rate_local', side, joined.de, joined.en),
      notPruefbar('lccn_lc_rate_import', side, joined.de, joined.en),
    ]
  }
  // Denominator-guard: Gesamtkosten == 0 would make both readings undefined
  // (division by zero) — nicht_pruefbar rather than Infinity/NaN.
  if (values.totalLcManufacturingCost === 0) {
    const reason = 'Gesamtkosten fuer LC Fertigung ist 0 — LC-Rate rechnerisch nicht bestimmbar (Division durch 0).'
    const reasonEn = 'Total costs for LC manufacturing is 0 — LC rate cannot be computed (division by zero).'
    return [
      notPruefbar('lccn_lc_rate_local', side, reason, reasonEn),
      notPruefbar('lccn_lc_rate_import', side, reason, reasonEn),
    ]
  }

  const cif = values.totalImportedValueCif!
  const total = values.totalLcManufacturingCost!
  const expectedImport = (cif / total) * 100
  const expectedLocal = 100 - expectedImport

  function judgeRate(
    checkId: LccnValidationCheckId,
    readingLabelDe: string,
    readingLabelEn: string,
    formulaNoteDe: string,
    expected: number,
  ): LccnValidationResult {
    const deltaAbsolute = Number((actual! - expected).toFixed(6))
    const deltaPercent = expected !== 0 ? Number((deltaAbsolute / Math.abs(expected)).toFixed(6)) : null
    const ok = withinTolerance(expected, actual!, cfg)
    const messageDe = `${readingLabelDe} (${side}): Excel-Zelle "LC Rate in %" weist ${fmt(actual!)} % aus, die Nachrechnung nach ${formulaNoteDe} ergibt ${fmt(expected)} % (Delta ${fmt(deltaAbsolute)} Prozentpunkte) — Abweichung ausserhalb der Toleranz. Hinweis: der Leitfaden dokumentiert ZWEI widerspruechliche Formulierungen (S.47 vs. S.48) fuer dieses eine Excel-Feld — dies ist die komplementaere "${readingLabelDe}"-Lesart, siehe auch die komplementaere Lesart im parallelen Check.`
    const messageEn = `${readingLabelEn} (${side}): the Excel "LC Rate in %" cell states ${fmt(actual!)}%, recomputation per ${formulaNoteDe} yields ${fmt(expected)}% (delta ${fmt(deltaAbsolute)} percentage points) — deviation exceeds tolerance. Note: the Leitfaden documents TWO contradictory formulations (p.47 vs. p.48) for this single Excel field — this is the complementary "${readingLabelEn}" reading, see the complementary reading in the parallel check.`
    const below40Hint = below40HintFor(readingLabelDe, readingLabelEn, expected)
    return {
      checkId,
      side,
      status: ok ? 'bestanden' : 'abweichung',
      expected,
      actual,
      deltaAbsolute,
      deltaPercent,
      ...(ok ? {} : { messageDe, messageEn }),
      ...(below40Hint ? { below40Hint: below40Hint.de, below40HintEn: below40Hint.en } : {}),
    }
  }

  return [
    judgeRate(
      'lccn_lc_rate_local',
      'LC-Rate (lokal, Lesart S.47)',
      'LC rate (local content, p.47 reading)',
      'Leitfaden S.47 ("(1 − Importierter Materialanteil / Gesamtkosten fuer LC Fertigung) × 100%")',
      expectedLocal,
    ),
    judgeRate(
      'lccn_lc_rate_import',
      'Import-Quote (Lesart S.48)',
      'Import quota (p.48 reading)',
      'Leitfaden S.48 ("Verhaeltnis von Gesamt importierter Wert (CIF) / Gesamtkosten fuer LC Fertigung")',
      expectedImport,
    ),
  ]
}

/**
 * Komplement-Konsistenz: die zwei BERECHNETEN Lesarten (nicht zwei
 * unabhaengige Excel-Werte — das Blatt hat nur EIN "LC Rate in %"-Feld, siehe
 * Modul-Header) summieren sich bei denselben Inputs rechnerisch immer zu
 * ~100%. Kein unabhaengiger Fachbefund, aber ein sichtbarer, explizit
 * ausgewiesener Beleg genau wie von Kais verlangt ("als Komplemente
 * gekennzeichnet") — eine Abweichung hier waere ein Implementierungsfehler
 * (z.B. divergierender Nenner zwischen den zwei judgeRate-Aufrufen), kein
 * Fachbefund, wird aber trotzdem als expliziter, sichtbarer Check gefuehrt.
 * Gated auf dieselbe Datenbasis wie validateLccnLcRateReadings (CIF +
 * Gesamtkosten befuellt) — unabhaengig davon, ob die Excel-Zelle lcRate
 * selbst befuellt ist (die Komplement-Eigenschaft der BERECHNETEN Werte
 * braucht kein drittes Excel-Feld).
 */
export function validateLccnLcRateComplement(
  values: LccnSummaryValues,
  side: ComparisonSide,
  cfg: LccnValidationConfig = LCCN_VALIDATION_CONFIG,
): LccnValidationResult | null {
  const checkId: LccnValidationCheckId = 'lccn_lc_rate_complement'
  if (values.totalImportedValueCif === null || values.totalLcManufacturingCost === null) return null
  if (values.totalLcManufacturingCost === 0) {
    return notPruefbar(
      checkId,
      side,
      'Gesamtkosten fuer LC Fertigung ist 0 — Komplement-Pruefung nicht moeglich.',
      'Total costs for LC manufacturing is 0 — complement check not possible.',
    )
  }

  const expectedImport = (values.totalImportedValueCif / values.totalLcManufacturingCost) * 100
  const expectedLocal = 100 - expectedImport
  const sum = expectedLocal + expectedImport // == 100 by construction; see doc comment
  const expected = 100
  const deltaAbsolute = Number((sum - expected).toFixed(6))
  const ok = withinTolerance(expected, sum, cfg)
  const messageDe = `LC-Rate-Komplement (${side}): LC-Rate lokal (${fmt(expectedLocal)} %) + Import-Quote (${fmt(expectedImport)} %) ergeben ${fmt(sum)} % statt der erwarteten 100 % (Delta ${fmt(deltaAbsolute)}) — pruefen, ob CIF-Wert/Gesamtkosten konsistent erfasst wurden.`
  const messageEn = `LC-rate complement (${side}): local LC rate (${fmt(expectedLocal)}%) + import quota (${fmt(expectedImport)}%) sum to ${fmt(sum)}% instead of the expected 100% (delta ${fmt(deltaAbsolute)}) — review whether CIF value / total costs were captured consistently.`
  return {
    checkId,
    side,
    status: ok ? 'bestanden' : 'abweichung',
    expected,
    actual: sum,
    deltaAbsolute,
    deltaPercent: Number((deltaAbsolute / 100).toFixed(6)),
    ...(ok ? {} : { messageDe, messageEn }),
  }
}

export interface LccnValidationInput {
  side: ComparisonSide
  /** Tri-state, identical contract to every other P2.x module's own rows
   * input: undefined = no LC-CN parse attempted (all checks omitted
   * entirely, not even as nicht_pruefbar); null = a parse was attempted but
   * no usable LC-CN sheet was found (file-level nicht_pruefbar per check);
   * LccnSummaryValues = the parsed record (individual fields can legitimately
   * be null). */
  lccnValues?: LccnSummaryValues | null
}

function fileLevelNotPruefbar(checkId: LccnValidationCheckId, side: ComparisonSide): LccnValidationResult {
  return notPruefbar(
    checkId,
    side,
    'Kein LC-CN-Sheet in dieser Datei erkannt (oder zu stark abweichend) — Nachrechnung nicht moeglich.',
    'No LC-CN sheet detected in this file (or too degraded) — recomputation not possible.',
  )
}

/** Orchestrator over one side's lccnValues tri-state — mirrors
 * evaluateRmrValidation/evaluateLogisticsValidation exactly, just across the
 * 4 LC-CN check ids instead of a per-row loop (this sheet is one record). */
export function evaluateLccnValidation(
  input: LccnValidationInput,
  config: LccnValidationConfig = LCCN_VALIDATION_CONFIG,
): LccnValidationResult[] {
  const { side, lccnValues } = input
  if (lccnValues === undefined) return []
  if (lccnValues === null) {
    return (
      ['lccn_total_production_cost', 'lccn_lc_rate_local', 'lccn_lc_rate_import', 'lccn_lc_rate_complement'] as const
    ).map((checkId) => fileLevelNotPruefbar(checkId, side))
  }

  const results: LccnValidationResult[] = []
  const costResult = validateLccnTotalProductionCost(lccnValues, side, config)
  if (costResult) results.push(costResult)
  results.push(...validateLccnLcRateReadings(lccnValues, side, config))
  const complementResult = validateLccnLcRateComplement(lccnValues, side, config)
  if (complementResult) results.push(complementResult)
  return results
}

// ── Persistence bridge ───────────────────────────────────────────────────────

export function lccnValidationResultToPlausibilityIssue(r: LccnValidationResult): PlausibilityIssue | null {
  if (r.status === 'bestanden') return null

  if (r.status === 'nicht_pruefbar') {
    return {
      type: `${r.checkId}_nicht_pruefbar`,
      severity: 'hinweis' as PlausibilitySeverity,
      step: r.side,
      explanation: r.reason ?? 'Nachrechnung nicht moeglich (unvollstaendige Datenbasis).',
      explanationEn: r.reasonEn,
    }
  }

  const explanation = r.below40Hint ? `${r.messageDe ?? ''} ${r.below40Hint}` : (r.messageDe ?? '')
  const explanationEn = r.messageEn
    ? r.below40HintEn
      ? `${r.messageEn} ${r.below40HintEn}`
      : r.messageEn
    : undefined
  return {
    type: r.checkId,
    severity: 'pruefen' as PlausibilitySeverity,
    step: r.side,
    explanation,
    explanationEn,
  }
}

export function checkLccnValidation(
  input: LccnValidationInput,
  config: LccnValidationConfig = LCCN_VALIDATION_CONFIG,
): PlausibilityIssue[] {
  return evaluateLccnValidation(input, config)
    .map(lccnValidationResultToPlausibilityIssue)
    .filter((x): x is PlausibilityIssue => x !== null)
}

// ── LC Checkliste Fertigungsprozess (Leitfaden Teil 3, S.91-95) —
// Substantial vs. Other Manufacturing Process ───────────────────────────────
//
// Reference table only (see module header) — Fundament fuer ein kuenftiges
// Lookup-Issue, NICHT in diesem PR live gegen Zeilen geprueft (das LC-CN-
// Blatt selbst traegt keine Prozessschritt-Zeilen, siehe Modul-Header).
// Transcribed verbatim from the 5-page LC-Checkliste ([91]-[95]) — every
// Prozessschritt the Leitfaden lists is included (97 individual entries
// across ~17 Kategorien; the backlog/task estimate of "~90 Eintraege"
// undercounts the real table slightly, same as SBM_DEVICE_CATEGORY_TABLE's
// own "~40 undercounts to 90" note — this constant reports the real,
// measured count, verified by the >= 90 test in lccn-parser.test.ts).

export type LcProcessClassificationTag = 'substantial' | 'other'

export interface LcProcessClassificationEntry {
  category: string
  prozessschritt: string
  classification: LcProcessClassificationTag
}

function pe(category: string, prozessschritt: string, tag: 'S' | 'O'): LcProcessClassificationEntry {
  return { category, prozessschritt, classification: tag === 'S' ? 'substantial' : 'other' }
}

/** Leitfaden Teil 3, S.91: 1. Urformen, 2. Umformen, 3. Trennen (Beginn). */
const LC_PROCESS_TABLE_PAGE_91: readonly LcProcessClassificationEntry[] = [
  pe('1. Urformen', 'Gießen', 'S'),
  pe('1. Urformen', 'Druckguss', 'S'),
  pe('1. Urformen', 'Schäumen', 'S'),
  pe('1. Urformen', 'Spritzguss-Verfahren/Spritzformen', 'S'),
  pe('1. Urformen', 'Vacuumformen', 'S'),
  pe('2. Umformen', 'Walzen/rollen', 'S'),
  pe('2. Umformen', 'Biegen/krümmen/abwinkeln', 'S'),
  pe('2. Umformen', 'Abkanten', 'S'),
  pe('2. Umformen', 'Falzen', 'S'),
  pe('2. Umformen', 'Eindrücken/einformen', 'S'),
  pe('2. Umformen', 'Rollbiegen', 'S'),
  pe('2. Umformen', 'Airbagkissen/-luftsack zusammenlegen/falten', 'S'),
  pe('2. Umformen', 'Streckformen', 'S'),
  pe('2. Umformen', 'Fliesspressen/Strangpressen', 'S'),
  pe('2. Umformen', 'Heißwickeln', 'S'),
  pe('2. Umformen', 'Formen', 'S'),
  pe('2. Umformen', 'Pressen/prägen', 'S'),
  pe('3. Trennen', 'Oberflächenbehandlung', 'S'),
  pe('3. Trennen — Zerteilen', 'Aluminium schneiden', 'S'),
  pe('3. Trennen — Zerteilen', 'Leder zuschneiden', 'S'),
]

/** Leitfaden Teil 3, S.92: Zerteilen (Forts.), Spanende Bearbeitung, Abtragen/
 * Reinigen, 4. Fügen (Beginn). */
const LC_PROCESS_TABLE_PAGE_92: readonly LcProcessClassificationEntry[] = [
  pe('3. Trennen — Zerteilen', 'Airbagstoff/-gewebe schneiden', 'S'),
  pe('3. Trennen — Zerteilen', 'Sitzbezüge schneiden', 'S'),
  pe('3. Trennen — Zerteilen', 'Schlauch abschneiden', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Zerspanen', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Abbohren', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Polieren', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Entgraten', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Bohren', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Maschinelles bearbeiten/zerspanen', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Fräsen', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Nutzen fräsen (Leiterplatten)', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Einstechen', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Abschleifen', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Sägen', 'S'),
  pe('3. Trennen — Spanende Bearbeitung', 'Stanzen', 'S'),
  pe('3. Trennen — Abtragen, Reinigen', 'Reinigen', 'O'),
  pe('3. Trennen — Abtragen, Reinigen', 'Sandstrahlen', 'S'),
  pe('3. Trennen — Abtragen, Reinigen', 'Kugelstrahlen', 'S'),
  pe('3. Trennen — Abtragen, Reinigen', 'Spülen/waschen/säubern', 'O'),
  pe('4. Fügen', 'Löten', 'S'),
  pe('4. Fügen', 'Kleben', 'S'),
  pe('4. Fügen', 'Beledern des Lenkrades (kleben)', 'S'),
  pe('4. Fügen', 'Außenhülle schweißen', 'S'),
]

/** Leitfaden Teil 3, S.93: 4. Fügen (Forts.), An-/Einpressen, Zusammensetzen. */
const LC_PROCESS_TABLE_PAGE_93: readonly LcProcessClassificationEntry[] = [
  pe('4. Fügen', 'Aufschmelzlöten', 'S'),
  pe('4. Fügen', 'Nieten', 'S'),
  pe('4. Fügen', 'Heften/nähen', 'S'),
  pe('4. Fügen', 'Ultraschall schweißen', 'S'),
  pe('4. Fügen', 'Schweißen', 'S'),
  pe('4. Fügen — An-, Einpressen', 'Lager einpressen', 'S'),
  pe('4. Fügen — An-, Einpressen', 'Einkapselung', 'S'),
  pe('4. Fügen — An-, Einpressen', 'Einpressen', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Zusammenbauen', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Montage Baugruppe/Einzelteile', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Emblem befestigen', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Endmontage', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Endbearbeiten', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Schlauch Montage', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Mit Leder einfassen und vernähen', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Anbringen/anflanschen', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Leiterplatten bestücken', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Vormontage', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Substanzielle Montage', 'S'),
  pe('4. Fügen — Zusammensetzen', 'SMT-Bestückung', 'S'),
  pe('4. Fügen — Zusammensetzen', 'Schalter Montage', 'S'),
]

/** Leitfaden Teil 3, S.94: 5. Beschichten/Oberflächenveredelung, 6. Stoff-
 * eigenschaften verändern, Wärmebehandeln, Messtechnik. */
const LC_PROCESS_TABLE_PAGE_94: readonly LcProcessClassificationEntry[] = [
  pe('5. Beschichten', 'Oberflächenbehandlung', 'S'),
  pe('5. Beschichten', 'Auftragen/beschichten', 'S'),
  pe('5. Beschichten', 'Galvanisieren', 'S'),
  pe('5. Beschichten', 'Belabeln', 'O'),
  pe('5. Beschichten', 'Markieren (z.B. von Schweißpunkten)', 'O'),
  pe('5. Beschichten', 'Lackieren', 'S'),
  pe('5. Beschichten', 'Beschichten', 'S'),
  pe('5. Beschichten', 'Pulverbeschichten', 'S'),
  pe('5. Beschichten', 'Bedrucken', 'S'),
  pe('6. Stoffeigenschaften verändern', 'Eloxieren', 'S'),
  // "Vulkanisieren" trägt im Original ein kleines "x" statt "X" — laut [94]
  // unklar ob inhaltliche Abweichung oder reiner Darstellungsfehler; hier
  // konsistent als Substantial übernommen (die einzige im Original markierte
  // Spalte), Diskrepanz dokumentiert statt stillschweigend geglättet.
  pe('6. Stoffeigenschaften verändern', 'Vulkanisieren (Original: kleines "x" statt "X" — Darstellungsabweichung, siehe [94])', 'S'),
  pe('Wärmebehandeln', 'Abkühlen', 'S'),
  pe('Wärmebehandeln', 'Kalthärten', 'S'),
  pe('Wärmebehandeln', 'Härten', 'S'),
  pe('Wärmebehandeln', 'Erhitzen', 'S'),
  pe('Wärmebehandeln', 'Warmhärten', 'S'),
  pe('Wärmebehandeln', 'Abkühlen/abschrecken', 'S'),
  pe('Messtechnik', 'Rüsten, justieren', 'O'),
  pe('Messtechnik', 'Kalibrieren', 'O'),
  pe('Messtechnik', 'Vermessen', 'O'),
]

/** Leitfaden Teil 3, S.95: Qualität, Logistik, Sonstige Prozesse (Ende der
 * LC-Checkliste). */
const LC_PROCESS_TABLE_PAGE_95: readonly LcProcessClassificationEntry[] = [
  pe('Qualität', 'Prüfen', 'S'),
  pe('Qualität', 'Endmontage Test', 'S'),
  pe('Qualität', 'Endabnahme', 'S'),
  pe('Qualität', 'Helium Test/Dichtheitsprüfung mit Helium', 'S'),
  pe('Qualität', '(Sicht)Prüfung', 'S'),
  pe('Qualität', 'Dichtigkeitsprüfung', 'S'),
  pe('Qualität', 'Q-Prüfung', 'S'),
  pe('Logistik', 'Logistik', 'S'),
  pe('Logistik', 'Verpacken', 'S'),
  pe('Logistik', 'Transport', 'S'),
  pe('Sonstige Prozesse', 'Werkzeugwechsel', 'S'),
  pe('Sonstige Prozesse', 'Systemintegration', 'S'),
  pe('Sonstige Prozesse', 'Reparatur', 'S'),
]

/** Full LC-Checkliste Fertigungsprozess table, Leitfaden Teil 3 S.91-95, all
 * 5 pages concatenated in source order. */
export const LC_PROCESS_CLASSIFICATION_TABLE: readonly LcProcessClassificationEntry[] = [
  ...LC_PROCESS_TABLE_PAGE_91,
  ...LC_PROCESS_TABLE_PAGE_92,
  ...LC_PROCESS_TABLE_PAGE_93,
  ...LC_PROCESS_TABLE_PAGE_94,
  ...LC_PROCESS_TABLE_PAGE_95,
]

/** Prozessschritt -> entry, keyed by normalizeProcessName (case/umlaut/
 * whitespace-insensitive) — built once at module load, same "computed
 * constant" pattern sbm-parser.ts's SBM_DEVICE_CATEGORY_INDEX uses.
 * "Oberflächenbehandlung" legitimately repeats across two categories (3.
 * Trennen / 5. Beschichten) with the SAME classification (Substantial) both
 * times, so a single flat map is safe — verified by the "no duplicate
 * Prozessschritt text mapping to different classifications" test. */
const LC_PROCESS_CLASSIFICATION_INDEX: ReadonlyMap<string, LcProcessClassificationEntry> = new Map(
  LC_PROCESS_CLASSIFICATION_TABLE.map((entry) => [normalizeProcessName(entry.prozessschritt), entry]),
)

export interface LcProcessClassificationResult {
  entry: LcProcessClassificationEntry | null
  /** True when the Prozessschritt text has no known match against
   * LC_PROCESS_CLASSIFICATION_TABLE — a "pruefen"-style hint for downstream
   * consumers, NEVER an error (same "unknown != wrong" contract
   * classifySbmToolDeviceType documents). */
  needsReview: boolean
}

/**
 * Classify a Fertigungsprozess-Bezeichnung against the LC-Checkliste
 * reference table (Leitfaden Teil 3, S.91-95). Pure, never throws. Returns
 * null for a blank Prozessschritt (nothing to classify yet).
 *
 * NOT wired to any row in this PR — the LC-CN sheet itself carries no
 * Prozessschritt rows (see module header); this is the Fundament for a
 * future lookup-issue once a MATERIAL/MANUFACTURING-COSTS row parser
 * extracts the AM (Prozessclusterung)-column ([47], lccn_am_process_
 * clustering) this table is meant to be checked against. See
 * checkLcProcessDeclarationAgainstClassification below for the shape that
 * future wiring would use.
 */
export function classifyLcProcess(prozessschritt: string): LcProcessClassificationResult | null {
  const key = normalizeProcessName(prozessschritt)
  if (key === '') return null
  const entry = LC_PROCESS_CLASSIFICATION_INDEX.get(key) ?? null
  return { entry, needsReview: entry === null }
}

/**
 * Documented TODO (not wired in this PR — see classifyLcProcess doc comment
 * and module header "LC Checkliste Fertigungsprozess"): the shape a future
 * MATERIAL/MANUFACTURING-COSTS row-level AM-Prozessclusterung check would
 * use — given a Prozessschritt-Bezeichnung and the declared AM value
 * ('Substantial manufacturing' | 'Other manufacturing', [47] dropdown), flags
 * a mismatch as a review-worthy PlausibilityIssue. Exported now (pure,
 * tested) so the future row-level wiring is a one-line call-site addition,
 * not a new function to design from scratch.
 */
export function checkLcProcessDeclarationAgainstClassification(
  prozessschritt: string,
  declaredAm: 'Substantial manufacturing' | 'Other manufacturing',
  side: ComparisonSide,
  rowIndex: number,
): PlausibilityIssue | null {
  const result = classifyLcProcess(prozessschritt)
  if (result === null || result.entry === null) return null // unknown Prozessschritt: needsReview is surfaced by the caller separately, never fabricated as a mismatch here

  const declaredSubstantial = declaredAm === 'Substantial manufacturing'
  const tableSaysSubstantial = result.entry.classification === 'substantial'
  if (declaredSubstantial === tableSaysSubstantial) return null

  return {
    type: 'lccn_process_classification_mismatch',
    severity: 'pruefen' as PlausibilitySeverity,
    step: side,
    explanation: `Prozessschritt "${prozessschritt}" (Zeile ${rowIndex + 1}, ${side}) ist als "${declaredAm}" deklariert, die LC-Checkliste (Leitfaden S.91-95, Kategorie "${result.entry.category}") klassifiziert diesen Prozessschritt als "${tableSaysSubstantial ? 'Substantial' : 'Other'} Manufacturing Process" — bitte pruefen.`,
    explanationEn: `Process step "${prozessschritt}" (row ${rowIndex + 1}, ${side}) is declared as "${declaredAm}", but the LC checklist (Leitfaden p.91-95, category "${result.entry.category}") classifies this process step as "${tableSaysSubstantial ? 'Substantial' : 'Other'} Manufacturing Process" — please review.`,
  }
}
