// Loop 10 View-Spec-Adoption, Modul 4 (Fertigungskosten): Produktionssicht,
// Anomalien/Master-Sätze, Szenario-Grenze, Methodische Grenzen und die
// A1. Fertigungskosten-Deltas-Tabelle.
//
// Muster wie overview-run.ts/negotiation-run.ts/appendix-run.ts: ein pures
// Builder-Modul. Die Fertigungskosten-Sektionen waren die Cross-Referenz-
// dichtesten der fünf Detail-Module (view-spec-adoption-analyse.md,
// Abschnitt 3): die Schale baute vier Provenienz-Maps (source_cells,
// manual_override, raw_values, match→stepIds) plus Label-/Unsicherheits-
// Maps und reichte sie als Props durch; der Renderer löste sie pro Zeile im
// Render-Loop auf und bestimmte dort auch Richtung, Status-Label und die
// Explain-Panel-Attribute. Genau diese Ableitungen wandern hierher an die
// Build-Zeit — die ZAHLEN und Zeilen selbst kommen unverändert aus denselben
// Rohzeilen wie bisher.
//
// Die Fachregeln selbst sind bereits pure lib-Funktionen und werden hier nur
// AUFGERUFEN, nicht neu geschrieben (Analyse, Abschnitt „Nicht-Ziele"):
// buildProductionRateRows/buildMasterRateRows/scenarioAvailability
// (capability/production-view.ts), buildManufacturingExplainAttributes
// (explain-provenance.ts), isUncertainMatch (appendix-run.ts).
//
// Richtungs- und Label-Semantik (diffDirection/diffStatusLabel) ist hierher
// aus components/qaf-differences/qaf-diff-status.ts gezogen — sie ist
// Fachregel ohne Klassennamen und gehört damit in den portablen Kern; die
// Tailwind-Klassen-Seite (diffStatusClass) bleibt dort und konsumiert die
// Semantik von hier (gleiches Muster wie plausibilityBadge →
// plausibilityKindClass, Modul 3). qaf-diff-status.ts re-exportiert die
// Symbole unter den etablierten Namen für die noch nicht adoptierten
// Konsumenten (zusammenfassung-sections, Modul 5).
//
// Dieser Builder baut seine Label-/Unsicherheits-Maps selbst aus den
// Rohzeilen; die früheren Schale-Kopien (movers → summary-run.ts,
// leverInput → levers-run.ts, das direkt auf deltas.rows aufsetzt) sind
// mit Modul 5 und den Hebel-Belegen entfallen — die Adoption ist damit
// komplett (Analyse, Abschnitt „Gesamt").
//
// Bewusst KEINE erfundenen SourceRefs (gleiche Begründung wie Modul 3):
// die A1-Zeilen tragen ihre Zell-Provenienz bereits heute als
// source_cells-Referenzen (KAR-886/KAR-911) — sie wandern unverändert in
// die Zeilen-Specs; die Facts-Zahlen zählen die unmittelbar darunter
// gelisteten Zeilen.
//
// Pure Funktionen, keine I/O.

import type { QAFFieldKey, QAFRow } from '../../qaf-parser'
import {
  buildProductionRateRows,
  productionViewAvailability,
  buildMasterRateRows,
  anomaliesAvailability,
  scenarioAvailability,
  type MasterRateRow,
  type ProductionRateRow,
  type SectionAvailability,
} from './capability/production-view'
import type { FieldCandidate } from './capability/types'
import {
  isUncertainMatch,
  type AppendixPlausibility,
  type AppendixStepMatch,
} from './appendix-run'
import {
  buildManufacturingExplainAttributes,
  type ExplainAttributes,
  type RuleFindingLike,
} from './explain-provenance'
import { decodeBilingual } from './bilingual-message'
import { countPhrase, unchangedPhrase } from './section-titles'

// ── Zeilentypen (aus components/detail-sections/shared.tsx hierher gezogen,
//    dort unter den alten Namen re-exportiert — der Builder ist jetzt der
//    kanonische Ort). `Manufacturing`-Präfix, weil dies die READ-Shapes der
//    Seite sind (qaf_manufacturing_diff/qaf_manufacturing_step-Selects),
//    nicht die Engine-/Write-Typen. ─────────────────────────────────────────

export interface ManufacturingDiff {
  step_match_id: string | null
  field: string
  alt_value: number | null
  neu_value: number | null
  delta_absolute: number | null
  delta_percent: number | null
  delta_percentage_points: number | null
  status: string | null
}

export interface ManufacturingStepRow {
  id: string
  file_id: string
  process_name: string | null
  position_number: string | null
  /** KAR-886/P0.1, read back for KAR-911/P4.1's per-field provenance
   * tooltip — A1 cell reference per QAFRow field key (e.g. { fk:
   * "Fertigungskosten!W15" }), null when the row predates KAR-886. */
  source_cells?: Record<string, string> | null
  /** KAR-912/P4.2 — QAFRow.manualOverride round-tripped through raw_values
   * (lib/qaf-parser.ts QAFRow doc comment), keyed the same way as
   * source_cells. Reflects overrides as of the row's LAST parse/recompute —
   * an override set after that (before the next "Neu berechnen") is not yet
   * visible here, same "takes effect on next recompute" contract as
   * ManualPin (KAR-845). null/undefined when no field on this row was ever
   * manually overridden. */
  manual_override?: Record<string, ManualOverrideEntry> | null
  /** KAR-922/P6.2 — the row's full raw_values JSONB (QAFRow: sourceCells/
   * normalized/rawText/formulas/manualOverride), read back for the
   * consolidated Explain-Panel (§17 provenance) — a superset of
   * source_cells/manual_override above, kept as separate fields there for
   * backward compat with the existing ProvenanceTooltip callers. */
  raw_values?: QAFRow | null
}

export interface ManualOverrideEntry {
  sourceDescription: string
  setBy: string
  setAt: string
}

// ── Richtungs- und Label-Semantik (aus qaf-diff-status.ts hierher gezogen) ──

export type DiffDirection = 'up' | 'down' | 'flat'

/** Nur die Felder, die für Richtung und Einordnung gebraucht werden — nicht
 *  der ganze Zeilen-Typ der Komponente. */
export interface DiffDeltaFields {
  delta_absolute: number | null
  delta_percent: number | null
  delta_percentage_points: number | null
}

/**
 * Richtung eines Deltas.
 *
 * Die Status-Bänder der Engine (`auffaellig_*`, `kritisch_50`) sind
 * betragsbasiert und tragen keine Richtung — die kommt aus dem Vorzeichen.
 * Eine große Kosten*senkung* muss als Senkung lesen (grün ▼), nicht als
 * kritischer Anstieg.
 *
 * Die Vorrangfolge absolut → Prozentpunkte → Prozent ist bewusst: das
 * absolute Delta ist die belastbarste Größe, Prozentpunkte gelten für
 * Anteils-Felder, Prozent ist der Rückfall.
 */
export function diffDirection(d: DiffDeltaFields): DiffDirection {
  const v =
    d.delta_absolute ?? d.delta_percentage_points ?? d.delta_percent ?? 0
  if (v > 0) return 'up'
  if (v < 0) return 'down'
  return 'flat'
}

export const DIFF_STATUS_LABEL: Record<string, string> = {
  neu: 'Neu',
  entfallen: 'Entfallen',
  nicht_berechenbar: 'Nicht berechenbar',
  // P0.6 / KAR-891: distinct from nicht_berechenbar — this field carries an
  // explicit "n.a."/"entfällt" marker, it was not simply left blank.
  nicht_anwendbar: 'Nicht anwendbar',
  // R2 Pflichtfeld-Blockade in 'block'-Modus (KAR-889): a mandatory field was
  // invalid/empty — no delta is shown at all instead of a possibly-wrong number.
  blockiert: 'Blockiert',
  konstant: 'Konstant',
  anstieg: 'Anstieg',
  senkung: 'Senkung',
  auffaellig_10: 'Auffällig ±10 %',
  auffaellig_25: 'Auffällig ±25 %',
  kritisch_50: 'Kritisch ±50 %',
  // KAR-900/P2.1 (formula-engine.ts): hidden formula change with an unchanged
  // displayed value — the silent-manipulation risk Master-Prompt §12.4 calls
  // out. See compareFormulaPair's FormulaComparisonKind doc.
  formel_geaendert: 'Formel geändert',
  // A formula was removed and replaced by a hardcoded value — the classic
  // manipulation pattern from the offline tool (KAR-861).
  formel_zu_konstante: 'Formel durch Konstante ersetzt',
}

/** Status ohne Richtungsbezug — sie bekommen keinen Pfeil, weil „Neu" oder
 *  „Blockiert" weder steigt noch fällt. */
export const STRUCTURAL_STATUS: ReadonlySet<string> = new Set([
  'neu',
  'entfallen',
  'konstant',
  'nicht_berechenbar',
  'nicht_anwendbar',
  'blockiert',
  'formel_geaendert',
  'formel_zu_konstante',
])

/** Beschriftung einer Diff-Zelle; Pfeil nur, wo Richtung überhaupt Sinn ergibt. */
export function diffStatusLabel(
  status: string | null,
  dir: DiffDirection,
): string {
  if (!status) return ''
  const base = DIFF_STATUS_LABEL[status] ?? status
  if (STRUCTURAL_STATUS.has(status)) return base
  return dir === 'up' ? `${base} ▲` : dir === 'down' ? `${base} ▼` : base
}

// ── Regel-Befund-Auflösung (aus der Schale hierher gezogen) ─────────────────
//
// KAR-922/P6.2: best-effort rule-engine-finding lookup for the Explain-
// Panel's "comparison rule" attribute — a plausibility issue whose `field`
// matches this result's field, scoped to the same step label when the
// issue carries one (R2/R3 findings do; reconciliation/structural findings
// don't). First match wins — same "one finding is enough to explain the
// status" pragmatism the existing A4-Plausibilität list already applies.
// KAR-924/Teil 1: explanation is decoded to BOTH `de` and `en` (see the
// original doc comment in qaf-comparison-detail.tsx — moved verbatim).

export function ruleFindingResolver(
  plausibility: readonly Pick<
    AppendixPlausibility,
    'issue_type' | 'step_label' | 'field' | 'explanation'
  >[],
): (field: string, stepLabel: string | null) => RuleFindingLike | null {
  return (field, stepLabel) => {
    const hit = plausibility.find(
      (p) =>
        p.field === field &&
        (p.step_label === null || p.step_label === stepLabel),
    )
    if (!hit) return null
    const decoded = decodeBilingual(hit.explanation)
    return {
      rule: hit.issue_type,
      explanation: decoded.de,
      explanationEn: decoded.en,
    }
  }
}

// ── Run ─────────────────────────────────────────────────────────────────────

export interface ManufacturingDeltaRowSpec {
  diff: ManufacturingDiff
  /** Anzeige-Label des zugeordneten Prozessschritts — "—" ohne Match oder
   * ohne auflösbares Label, exakt der bisherige Zell-Fallback. */
  stepLabelDe: string
  uncertain: boolean
  /** KAR-911/P4.1: per-field source-cell provenance — this diff row's
   * matched ALT/NEU step, then the field's A1 cell on each side
   * (source_cells is keyed by QAFRow field, same field space as d.field). */
  altCell: string | null
  neuCell: string | null
  /** KAR-912/P4.2: same lookup shape as altCell/neuCell above, for the
   * "manuell zugeordnet" provenance line. */
  altOverride: ManualOverrideEntry | null
  neuOverride: ManualOverrideEntry | null
  dir: DiffDirection
  /** diffStatusLabel(status, dir) — "" wenn die Zeile keinen Status trägt
   * (der Renderer zeigt das Pill nur bei vorhandenem Status, wie bisher). */
  statusLabelDe: string
  /** KAR-922/P6.2: consolidated Explain-Panel — the full §17 attribute set
   * (see explain-provenance.ts), jetzt zur Build-Zeit statt im Render-Loop. */
  explainAttrs: ExplainAttributes
}

export interface ManufacturingRun {
  production: {
    rows: ProductionRateRow[]
    availability: SectionAvailability
    factsDe: string[]
    /** Sektion-count wie bisher: `rows.length || undefined` — 0 zeigt keinen
     * Zähler. */
    count: number | undefined
  }
  anomalies: {
    /** NUR belegte Zeilen (r.alt || r.neu) — die Tabelle zeigte nie andere;
     * der Filter lief bisher doppelt (Facts + tbody) im Renderer. */
    rows: MasterRateRow[]
    availability: SectionAvailability
    factsDe: string[]
  }
  scenario: {
    availability: SectionAvailability
    factsDe: string[]
  }
  limits: {
    /** Praktisch immer false, weil die Szenario-Grenze strukturell
     * unbefüllbar ist (scenarioAvailability-Doku) — als berechnetes Feld
     * gepinnt statt im Renderer entschieden. */
    hidden: boolean
    reasons: string[]
    factsDe: string[]
  }
  deltas: {
    rows: ManufacturingDeltaRowSpec[]
    count: number
    factsDe: string[]
    anyUncertain: boolean
    /** Konstante Zeilen-übergreifende Provenienz-Angaben der
     * ProvenanceTooltip-Spalte — je Vergleich, nicht je Zeile. */
    altParseConfidence: number | null
    neuParseConfidence: number | null
    engineVersionParser: string | null
  }
}

export function buildManufacturingRun(input: {
  diffs: readonly ManufacturingDiff[]
  steps: readonly ManufacturingStepRow[]
  stepMatches: readonly AppendixStepMatch[]
  /** Dieselbe gemergte Liste (DB-Zeilen + live-berechnete Fingerprint-/
   * Override-Befunde), die die Seite auch an buildAppendixRun und die
   * plausibility-Prop gibt — Regel-Befunde je A1-Zeile lösen sich daraus. */
  plausibility: readonly AppendixPlausibility[]
  baselineFileId: string | null
  comparisonFileId: string | null
  baselineSummaryFieldCandidates: readonly FieldCandidate<number>[]
  comparisonSummaryFieldCandidates: readonly FieldCandidate<number>[]
  baselineFileName: string | null
  comparisonFileName: string | null
  baselineManufacturingParseConfidence: number | null
  comparisonManufacturingParseConfidence: number | null
  baselineTemplateProfile: string | null
  comparisonTemplateProfile: string | null
  engineVersion: Record<string, unknown> | null
  engineVersionParser: string | null
  createdAt: string | null
}): ManufacturingRun {
  // §6/§7/§11 — dieselben lib-Aufrufe, die bisher in der Schale standen.
  const productionRows = buildProductionRateRows(
    input.steps,
    input.baselineFileId,
    input.comparisonFileId,
  )
  const production = productionViewAvailability(productionRows)
  const masterRateRows = buildMasterRateRows(
    input.baselineSummaryFieldCandidates,
    input.comparisonSummaryFieldCandidates,
  )
  const anomalies = anomaliesAvailability(masterRateRows)
  const scenario = scenarioAvailability(productionRows.length)
  const populatedMasterRateRows = masterRateRows.filter((r) => r.alt || r.neu)

  // Methodische Grenzen — Aggregation aus dem Renderer hierher.
  const limitReasons = [
    production.available ? null : production.reason,
    anomalies.available ? null : anomalies.reason,
    scenario.available ? null : scenario.reason,
  ].filter((r): r is string => typeof r === 'string' && r.length > 0)
  const unavailableCount = [
    production.available,
    anomalies.available,
    scenario.available,
  ].filter((a) => !a).length

  // A1-Zeilen — die bisherigen Schale-Maps, jetzt lokal im Builder.
  const stepLabelById = new Map(
    input.steps.map((s) => [
      s.id,
      s.process_name || s.position_number || s.id.slice(0, 8),
    ]),
  )
  const stepSourceCellsById = new Map(
    input.steps.map((s) => [s.id, s.source_cells ?? null]),
  )
  const stepManualOverrideById = new Map(
    input.steps.map((s) => [s.id, s.manual_override ?? null]),
  )
  const stepRawValuesById = new Map(
    input.steps.map((s) => [s.id, s.raw_values ?? null]),
  )
  const matchStepIdsById = new Map(
    input.stepMatches.map((m) => [
      m.id,
      { alt: m.alt_step_id, neu: m.neu_step_id },
    ]),
  )
  const matchLabelById = new Map<string, string>()
  const matchUncertainById = new Map<string, boolean>()
  for (const m of input.stepMatches) {
    const label =
      (m.neu_step_id && stepLabelById.get(m.neu_step_id)) ||
      (m.alt_step_id && stepLabelById.get(m.alt_step_id)) ||
      '—'
    matchLabelById.set(m.id, label)
    matchUncertainById.set(m.id, isUncertainMatch(m))
  }

  const ruleFindingFor = ruleFindingResolver(input.plausibility)

  const deltaRows: ManufacturingDeltaRowSpec[] = input.diffs.map((d) => {
    const dir = diffDirection(d)
    const uncertain = d.step_match_id
      ? (matchUncertainById.get(d.step_match_id) ?? false)
      : false
    const stepIds = d.step_match_id
      ? matchStepIdsById.get(d.step_match_id)
      : null
    const altCell =
      (stepIds?.alt && stepSourceCellsById.get(stepIds.alt)?.[d.field]) || null
    const neuCell =
      (stepIds?.neu && stepSourceCellsById.get(stepIds.neu)?.[d.field]) || null
    const altOverride =
      (stepIds?.alt && stepManualOverrideById.get(stepIds.alt)?.[d.field]) ||
      null
    const neuOverride =
      (stepIds?.neu && stepManualOverrideById.get(stepIds.neu)?.[d.field]) ||
      null
    // Für die Regel-Befund-Auflösung zählt das AUFGELÖSTE Label (kann "—"
    // sein, wenn das Match kein benennbares Step-Label hat) bzw. null ohne
    // Match — exakt die bisherige Unterscheidung im Renderer.
    const stepLabel = d.step_match_id
      ? (matchLabelById.get(d.step_match_id) ?? null)
      : null
    const explainAttrs = buildManufacturingExplainAttributes({
      fieldKey: d.field as QAFFieldKey,
      alt: {
        fileName: input.baselineFileName,
        row: (stepIds?.alt && stepRawValuesById.get(stepIds.alt)) || null,
        moduleConfidence: input.baselineManufacturingParseConfidence,
        templateProfile: input.baselineTemplateProfile,
      },
      neu: {
        fileName: input.comparisonFileName,
        row: (stepIds?.neu && stepRawValuesById.get(stepIds.neu)) || null,
        moduleConfidence: input.comparisonManufacturingParseConfidence,
        templateProfile: input.comparisonTemplateProfile,
      },
      diff: {
        deltaAbsolute: d.delta_absolute,
        deltaPercent: d.delta_percent,
        status: d.status,
      },
      ruleFinding: ruleFindingFor(d.field, stepLabel),
      engineVersion: input.engineVersion,
      timestamp: input.createdAt,
    })
    return {
      diff: d,
      stepLabelDe: stepLabel ?? '—',
      uncertain,
      altCell,
      neuCell,
      altOverride,
      neuOverride,
      dir,
      statusLabelDe: diffStatusLabel(d.status, dir),
      explainAttrs,
    }
  })

  const changedCount = input.diffs.filter(
    (d) => (d.delta_absolute ?? 0) !== 0,
  ).length

  return {
    production: {
      rows: productionRows,
      availability: production,
      factsDe: [
        productionRows.length > 0
          ? countPhrase({
              from: productionRows.length,
              to: productionRows.length,
              nounDe: 'Stationen mit Raten',
            })
          : 'keine Raten in den Dateien',
      ],
      count: productionRows.length || undefined,
    },
    anomalies: {
      rows: populatedMasterRateRows,
      availability: anomalies,
      factsDe: [
        anomalies.available
          ? countPhrase({
              from: populatedMasterRateRows.length,
              to: populatedMasterRateRows.length,
              nounDe: 'Master-Sätze belegt',
            })
          : 'keine Master-Sätze gefunden',
      ],
    },
    scenario: {
      availability: scenario,
      // Der Abschnitt steht immer, füllen lässt er sich nur mit den nötigen
      // Angaben — Begründung im bisherigen Renderer-Kommentar (ScenarioSection).
      factsDe: ['für diesen Vergleichstyp nicht verfügbar'],
    },
    limits: {
      hidden: limitReasons.length === 0,
      reasons: limitReasons,
      factsDe: [
        countPhrase({
          from: unavailableCount,
          to: unavailableCount,
          nounDe: 'Abschnitte nicht befüllbar',
        }),
      ],
    },
    deltas: {
      rows: deltaRows,
      count: input.diffs.length,
      factsDe: [
        changedCount > 0
          ? countPhrase({
              from: changedCount,
              to: changedCount,
              nounDe: 'Schritte mit Abweichung',
            })
          : unchangedPhrase('Alle Prozessschritte'),
      ],
      anyUncertain: deltaRows.some((r) => r.uncertain),
      altParseConfidence: input.baselineManufacturingParseConfidence,
      neuParseConfidence: input.comparisonManufacturingParseConfidence,
      engineVersionParser: input.engineVersionParser,
    },
  }
}
