// A18 Management-Analyse (execution-prompt §18) — Wertstrom P7 (KAR-878/
// KAR-986). Regelbasierter Klartext-Generator: KEIN LLM, KEIN API-Call —
// jeder Satz ist eine feste deutsche Vorlage, befüllt ausschließlich mit
// bereits von diesem Modul selbst berechneten bzw. vom Aufrufer übergebenen
// Werten. Reine Funktion (ADR 019 "no DB/network/FS access in the module
// itself") wie jede andere Datei in `internal/` — der Aufrufer (UI-Panel,
// PDF-/Copilot-Export) lädt Daten, ruft diese Funktion, rendert das Ergebnis.
//
// Ehrlichkeits-Doktrin (Brief P7): ein Satz existiert NUR für einen
// tatsächlich berechenbaren Wert. Fehlt die Datenbasis, wird ein expliziter
// „nicht bewertbar, weil…"-Satz erzeugt statt eine Zahl zu erfinden oder eine
// Kategorie stillschweigend wegzulassen — jede §18-Kategorie liefert IMMER
// mindestens eine AnalysisStatement (außer „Ist/Soll-Effekte", die nur bei
// aktivem Szenarien-Vergleich überhaupt erscheint, siehe unten). `null` wird
// nie als `0` behandelt (P0-F3-Doktrin, wie der Rest dieses Moduls).
//
// Jeder Satz trägt `sourceMetric` — einen stabilen, auf eine echte Funktion/
// ein echtes Feld dieses Moduls verweisenden String — plus `values` (die
// tatsächlichen Zahlen/Strings hinter dem Satz) und `confidence`. Wo ein Satz
// direkt auf einem anderen Engine-Ergebnis aufbaut (Engpass, Timeline-Leiter,
// Validierung), wird dessen EIGENE Konfidenz/eigener Text übernommen, nie
// verdeckt oder neu erfunden ("Engine-Konfidenz + KNOWN_VALIDATION_GAPS
// übernehmen, nie überdecken" — Brief-Wortlaut).

import type { VsmConnection, VsmNode } from '@/lib/vsm-types'
import { computeBottleneckV2 } from './bottleneck'
import type { NodeCapacityOverrides } from './capacity'
import { computeInventoryCoverage } from './inventory-coverage'
import { computeCumulativeYield, type ProcessQualityInput } from './quality'
import { computeTimelineLadder, type TimelineLadderResult } from './timeline-ladder'
import { formatRound1, isStationNode, type MetricExplain } from './types'
import { KNOWN_VALIDATION_GAPS, runValidation, type ValidationGap } from './validation'
import type { ShiftModelInput } from './work-time'

export type AnalysisCategory =
  | 'demand'
  | 'takt'
  | 'bottleneck-primary'
  | 'bottleneck-secondary'
  | 'lead-time'
  | 'value-add-time'
  | 'value-add-share'
  | 'inventory-hotspot'
  | 'quality'
  | 'capacity-risk'
  | 'improvement'
  | 'open-measures'
  | 'scenario-effect'

/** `'not-computable'` is a 4th, explicit confidence state — distinct from
 * 'low' (a real result the engine itself is unsure about, e.g. a degraded
 * bottleneck) — for a statement that has NO computed value behind it at all
 * (the "nicht bewertbar"-Satz). Never inferred from an absent field; every
 * statement sets this explicitly. */
export type AnalysisConfidence = 'high' | 'medium' | 'low' | 'not-computable'

export interface AnalysisStatement {
  /** Stable id, e.g. "bottleneck-primary" — one per category, except the
   * inherently list-shaped categories (bottleneck-secondary/inventory-hotspot/
   * improvement/scenario-effect), which append an index suffix ("-1", "-2", …). */
  id: string
  category: AnalysisCategory
  /** The rendered German sentence. */
  text: string
  /** Machine-readable pointer to the function/field this statement is
   * traceable to, e.g. "computeBottleneckV2.primary". Always a real engine
   * symbol — never a free-text label. */
  sourceMetric: string
  /** The actual number(s)/string(s) referenced by `text` — the "value(s)"
   * the brief asks for, kept as a flat bag rather than a single scalar
   * because several statements cite more than one figure (e.g. utilization
   * AND over-takt percentage). `null` entries mean "known field, unknown
   * value" (never omitted to hide a gap). */
  values: Record<string, number | string | null>
  confidence: AnalysisConfidence
  /** Set only for a "nicht bewertbar" statement that maps to a documented
   * `KNOWN_VALIDATION_GAPS` entry — never fabricated; most gaps here are
   * data-basis gaps this module documents itself (see `notComputable`
   * helper), not validation-engine gaps, so this is usually absent. */
  relatedGapId?: string
}

export interface ManagementAnalysisResult {
  statements: AnalysisStatement[]
  /** Passed through verbatim from the Validierungs-Engine (A11) — same
   * static list regardless of input. Surfaced here so a report/UI needs only
   * this one result to show "N dokumentierte Modellierungs-Lücken" without a
   * second `lib/vsm-engine` import. */
  knownGaps: ValidationGap[]
  explain: MetricExplain
}

/** Minimal shape for a caller-supplied open measure (§18 "open measures").
 * This module never fetches these itself (pure, ADR 019) — a caller with a
 * real measures data source (e.g. a future workshop-actions integration)
 * assembles this array and passes it in; omitting the field entirely (not
 * an empty array) is how a caller says "no such data source is wired up
 * here", which produces the honest "nicht bewertbar" statement below rather
 * than a fabricated "0 Maßnahmen". */
export interface OpenMeasureInput {
  id: string
  title: string
  status?: string
}

/** §18 "current-state and future-state effects" — structurally the SAME
 * shape as `components/wertstrom/vsm-scenario-compare.ts`'s `KpiDeltaRow`
 * (Wertstrom P5), deliberately re-declared here rather than imported: this
 * module must stay independent of `components/` (see README "Dependencies"),
 * the same purity reason `ShiftModelInput`/`TaktInput`/`ProcessQualityInput`
 * are the engine's own parameter types instead of reading a components/
 * concept directly. Because the fields are structurally identical, a caller
 * can pass `computeKpiDeltaRows(...)`'s return value straight through with
 * zero cast/mapping. */
export type ScenarioEffectDirection = 'improvement' | 'deterioration' | 'unchanged' | 'not-comparable' | 'info'

export interface ScenarioEffectRow {
  key: string
  label: string
  unit: 'percent' | 'seconds' | 'days'
  currentValue: number | null
  scenarioValue: number | null
  absoluteDelta: number | null
  percentDelta: number | null
  direction: ScenarioEffectDirection
}

/** Structurally identical to `components/wertstrom/vsm-scenario-compare.ts`'s
 * `ContextMismatch` (P5) — deliberately re-declared here (not imported),
 * same "engine must not depend on components/" reasoning as
 * `ScenarioEffectRow` mirroring `KpiDeltaRow` above. A caller that already
 * computed `describeContextMismatch(ist.context, szenario.context)` (both
 * `vsm-management-analysis-view.tsx` and `vsm-export-dialog.tsx` already
 * have both raw contexts in scope at the point they assemble
 * `scenarioComparison`) can pass that result straight through with zero
 * cast/mapping. */
export interface ScenarioContextMismatch {
  differentTakt: boolean
  differentShiftModel: boolean
  differentDemand: boolean
  any: boolean
}

export interface ManagementAnalysisContext {
  taktTimeSec?: number | null
  demandUnitsPerDay?: number
  shiftModel?: ShiftModelInput
  capacityOverridesByNodeId?: Record<string, NodeCapacityOverrides>
  /** See `OpenMeasureInput` doc comment — omit entirely (not `[]`) when no
   * measures data source is wired up for this caller. */
  openMeasures?: OpenMeasureInput[]
  /** Wertstrom P8.1 fix (K2, RLS-Sichtbarkeits-Ehrlichkeit — PR #363
   * adversarial review, CONFIRMED critical/major): only consulted when
   * `openMeasures` itself is `undefined` — picks a HONEST, more specific
   * "nicht bewertbar"-Satz than the generic "keine Datenquelle" one.
   * `'not-visible'`: a measures data source IS wired up (a project is
   * linked) but the caller could not confirm whether any linked measure
   * exists — RLS-typical (project-owner-only policy, current viewer is not
   * the owner), never rendered as a fabricated "0 Maßnahmen"
   * (components/wertstrom/vsm-measures.ts `deriveOpenMeasureInputs`
   * computes this). `'load-error'`: the underlying query failed outright. A
   * caller that DOES pass a real `openMeasures` array should never also set
   * this. */
  openMeasuresIssue?: 'not-visible' | 'load-error'
  /** §18 "current-state and future-state effects" — only produced when this
   * is supplied (an active Szenarien-Vergleich, P5). Pass a label for the
   * compared side (e.g. the scenario's title) plus
   * `computeKpiDeltaRows(...)`'s own result. This module does not recompute
   * or re-judge the comparison itself, only turns already-computed rows into
   * sentences.
   *
   * `contextMismatch` (P7 fix): the SAME P5 "Abweichung der Kontexte EHRLICH
   * ausweisen, nicht still mischen!" doctrine `vsm-scenario-compare-view.tsx`
   * already renders as a banner — omitted here entirely, every §18 Ist/Soll-
   * effect sentence used to claim strict `'high'` confidence even when Ist
   * and Szenario were computed under a different Kundentakt/Arbeitszeitmodell/
   * Bedarf. Optional so a caller that genuinely has no mismatch info (should
   * not exist today, both real callers compute it) degrades to the old
   * behavior rather than throwing. */
  scenarioComparison?: { scenarioLabel: string; rows: ScenarioEffectRow[]; contextMismatch?: ScenarioContextMismatch }
}

const EXPLAIN: MetricExplain = {
  formula:
    'Regelbasierte Satzvorlagen je §18-Kategorie, befüllt ausschließlich aus computeBottleneckV2/computeTimelineLadder/computeInventoryCoverage/computeCumulativeYield/runValidation (alle A1-A11, dieses Modul) sowie optionalem Aufrufer-Kontext (Kundenbedarf/Takt/Arbeitszeitmodell/offene Maßnahmen/Szenario-Vergleich). Kein LLM, kein externer Aufruf.',
  dataBasis: 'VsmNode[] + VsmConnection[] + ManagementAnalysisContext (alle Felder optional — fehlende Basis erzeugt einen "nicht bewertbar"-Satz, nie eine erfundene Zahl).',
  exclusions: [
    'Jeder Satz ist eine feste deutsche Vorlage — keine freie Textgenerierung, kein Modell-Aufruf.',
    '„Offene Maßnahmen" hat in diesem Repo heute keine verknüpfte Datenquelle (kein Aufrufer übergibt `openMeasures`) — dokumentierte Lücke, siehe PRODUCT_SPEC.md Wertstrom-P7-Abschnitt.',
    '„Ist/Soll-Effekte" erscheint nur, wenn der Aufrufer `scenarioComparison` übergibt (aktiver Szenarien-Vergleich, P5) — keine Platzhalter-Sätze ohne aktiven Vergleich.',
  ],
}

function statement(
  id: string,
  category: AnalysisCategory,
  text: string,
  sourceMetric: string,
  values: Record<string, number | string | null>,
  confidence: AnalysisConfidence,
  relatedGapId?: string,
): AnalysisStatement {
  return { id, category, text, sourceMetric, values, confidence, ...(relatedGapId ? { relatedGapId } : {}) }
}

// ── 1. Kundenbedarf ─────────────────────────────────────────────────────

function demandStatement(context: ManagementAnalysisContext | undefined): AnalysisStatement {
  const demand = context?.demandUnitsPerDay
  if (demand == null) {
    return statement(
      'demand',
      'demand',
      'Der Kundenbedarf ist nicht bewertbar, weil kein Kunde-Node mit hinterlegtem Bedarf (Stück pro Tag) vorliegt.',
      'context.demandUnitsPerDay',
      { demandUnitsPerDay: null },
      'not-computable',
    )
  }
  return statement(
    'demand',
    'demand',
    `Der Kundenbedarf beträgt ${demand} Stück pro Tag.`,
    'context.demandUnitsPerDay',
    { demandUnitsPerDay: demand },
    'high',
  )
}

// ── 2. Takt ──────────────────────────────────────────────────────────────

function taktStatement(context: ManagementAnalysisContext | undefined): AnalysisStatement {
  const takt = context?.taktTimeSec
  if (takt == null || !(takt > 0)) {
    return statement(
      'takt',
      'takt',
      'Der Kundentakt ist nicht bewertbar, weil kein verknüpftes Projekt mit hinterlegtem Kundentakt vorliegt.',
      'context.taktTimeSec',
      { taktTimeSec: null },
      'not-computable',
    )
  }
  return statement('takt', 'takt', `Der Kundentakt beträgt ${formatRound1(takt)} Sekunden.`, 'context.taktTimeSec', { taktTimeSec: takt }, 'high')
}

// ── 3./4. Engpässe (primär + sekundär) ──────────────────────────────────

function bottleneckStatements(nodes: VsmNode[], context: ManagementAnalysisContext | undefined): AnalysisStatement[] {
  const bottleneck = computeBottleneckV2(nodes, context?.taktTimeSec ?? null, context?.capacityOverridesByNodeId)
  const out: AnalysisStatement[] = []

  if (!bottleneck.primary || !bottleneck.reasonDe) {
    out.push(
      statement(
        'bottleneck-primary',
        'bottleneck-primary',
        'Der primäre Engpass ist nicht bewertbar, weil weniger als zwei Prozess-/Maschinen-Nodes mit berechenbarer effektiver Zykluszeit vorliegen.',
        'computeBottleneckV2.primary',
        { nodeId: null, nodeName: null },
        'not-computable',
      ),
    )
  } else {
    // reasonDe is ALREADY a complete, traceable German sentence produced by
    // the engine (§8.8 example-sentence style) — reused verbatim rather than
    // re-derived, so the analysis text can never drift from the number the
    // engine itself stands behind.
    out.push(
      statement(
        'bottleneck-primary',
        'bottleneck-primary',
        bottleneck.reasonDe,
        'computeBottleneckV2.primary.reasonDe',
        {
          nodeId: bottleneck.primary.nodeId,
          nodeName: bottleneck.primary.nodeName,
          utilizationPct: bottleneck.primary.utilizationPct,
          overTaktPct: bottleneck.primary.overTaktPct,
        },
        bottleneck.confidence,
      ),
    )
  }

  if (bottleneck.degraded) {
    out.push(
      statement(
        'bottleneck-secondary',
        'bottleneck-secondary',
        'Sekundäre Engpässe sind nicht bewertbar, weil bereits der primäre Engpass nicht im vollen Modell berechnet werden konnte (siehe primärer Engpass).',
        'computeBottleneckV2.secondary',
        { count: null },
        'not-computable',
      ),
    )
  } else if (bottleneck.secondary.length === 0) {
    out.push(
      statement(
        'bottleneck-secondary',
        'bottleneck-secondary',
        'Keine weiteren Prozesse liegen nahe am Kundentakt (Beobachtungs-Schwelle 80% Auslastung).',
        'computeBottleneckV2.secondary',
        { count: 0 },
        bottleneck.confidence,
      ),
    )
  } else {
    const names = bottleneck.secondary.map((c) => `${c.nodeName} (${formatRound1(c.utilizationPct)}%)`).join(', ')
    const plural = bottleneck.secondary.length === 1 ? 'Ein weiterer Prozess liegt' : `${bottleneck.secondary.length} weitere Prozesse liegen`
    out.push(
      statement(
        'bottleneck-secondary',
        'bottleneck-secondary',
        `${plural} nahe am Kundentakt (Auslastung ≥ 80%): ${names}.`,
        'computeBottleneckV2.secondary',
        { count: bottleneck.secondary.length, nodeNames: bottleneck.secondary.map((c) => c.nodeName).join('|') },
        bottleneck.confidence,
      ),
    )
  }

  return out
}

// ── 5.–7. DLZ / VA-Zeit / VA-Anteil ─────────────────────────────────────

/** C1-Fix: confidence war bisher einzig an `totalLeadTimeIncludesInventory`
 * gekoppelt (nur: war Bestand berechenbar?) — ob VA-/NVA-Prozesszeit,
 * Wartezeit oder Transportzeit überhaupt GEMESSEN wurden, floss nie ein, was
 * einem Wertstrom ohne eine einzige gemessene Zykluszeit trotzdem 'high'
 * gab. Leiht sich dasselbe measuredNodeCount===totalNodeCount-Muster, das
 * value-add-time/value-add-share zwei Blöcke weiter unten in DIESER
 * Funktion bereits nutzen — jetzt auch für lead-time. Ein Snapshot, dessen
 * gesamte Durchlaufzeit AUSSCHLIESSLICH aus unmeasured-Zeros besteht (kein
 * einziger Node hat je etwas beigetragen, auch kein Bestand), ist
 * 'not-computable' — nicht 'high' für eine fabrizierte '0 Sekunden'-Aussage. */
function leadTimeConfidence(ladder: TimelineLadderResult): AnalysisConfidence {
  // P8.2a (Baustein 1 "isVaNode-unknown-Alignment"): `unknownTimeSec` MUST be
  // included here too — before this fix, a vaClass='unknown' node's
  // measured/total counts lived inside `nonVaProcessTimeSec`; now that it is
  // its own bucket, omitting it here would silently SHRINK the measured-
  // population denominator (fullyMeasuredProcess/anyProcessMeasured), a
  // quiet confidence drift for any Wertstrom containing such a node. For a
  // Wertstrom with none (unknownTimeSec.totalNodeCount === 0), this is a
  // pure no-op — `.every()`/`.some()` are unaffected by an added
  // vacuously-true/false entry.
  const processCategories = [ladder.vaTimeSec, ladder.unknownTimeSec, ladder.nonVaProcessTimeSec, ladder.waitingTimeSec, ladder.transportTimeSec]
  const fullyMeasuredProcess = processCategories.every((c) => c.measuredNodeCount === c.totalNodeCount)
  const anyProcessMeasured = processCategories.some((c) => c.measuredNodeCount > 0)
  // Nur ein NODE-basierter Bestandsbeitrag zählt als "real" — bei 0
  // Bestands-Nodes liefert computeInventoryTime absichtlich ein triviales,
  // nicht-fabriziertes `seconds: 0` (siehe timeline-ladder.ts), das darf
  // hier nicht als "hat echt beigetragen" durchgehen (sonst bleibt der
  // komplett leere Snapshot fälschlich bei 'high' hängen).
  const inventoryContributes = ladder.inventoryTime.inventoryNodeCount > 0 && ladder.inventoryTime.seconds != null

  if (!anyProcessMeasured && !inventoryContributes) return 'not-computable'
  if (fullyMeasuredProcess && ladder.totalLeadTimeIncludesInventory) return 'high'
  if (!anyProcessMeasured) return 'low'
  return 'medium'
}

function timelineStatements(nodes: VsmNode[], context: ManagementAnalysisContext | undefined): AnalysisStatement[] {
  const ladder = computeTimelineLadder(nodes, context?.demandUnitsPerDay, context?.shiftModel)
  const out: AnalysisStatement[] = []

  const leadTimeConf = leadTimeConfidence(ladder)
  if (leadTimeConf === 'not-computable') {
    // C1-Fix: dieselbe "nicht bewertbar, weil…"-Konvention wie jede andere
    // Kategorie in diesem Modul — kein Zeigen einer fabrizierten "0
    // Sekunden" mit nur einer niedrigeren Confidence-Badge; `values` nullt
    // den Wert (P0-F3, "known field, unknown value"), statt den best-effort
    // Best-Effort-0 stillschweigend als Zahl auszuweisen.
    out.push(
      statement(
        'lead-time',
        'lead-time',
        'Die gesamte Durchlaufzeit ist nicht bewertbar, weil weder Prozess-, Warte- noch Transportzeit gemessen wurde und keine Bestandszeit berechenbar ist.',
        'computeTimelineLadder.totalLeadTimeSec',
        { totalLeadTimeSec: null },
        'not-computable',
      ),
    )
  } else {
    const leadTimeCaveat = ladder.totalLeadTimeIncludesInventory
      ? ''
      : ' (ohne Bestandszeit — kein Kundenbedarf und/oder Arbeitszeitmodell hinterlegt, die tatsächliche Durchlaufzeit ist vermutlich höher)'
    out.push(
      statement(
        'lead-time',
        'lead-time',
        `Die gesamte Durchlaufzeit beträgt ${formatRound1(ladder.totalLeadTimeSec)} Sekunden${leadTimeCaveat}.`,
        'computeTimelineLadder.totalLeadTimeSec',
        { totalLeadTimeSec: ladder.totalLeadTimeSec, includesInventory: ladder.totalLeadTimeIncludesInventory ? 'true' : 'false' },
        leadTimeConf,
      ),
    )
  }

  if (ladder.vaTimeSec.measuredSec == null) {
    out.push(
      statement(
        'value-add-time',
        'value-add-time',
        'Die wertschöpfende Zeit ist nicht bewertbar, weil kein wertschöpfend klassifizierter Prozess-Node eine gemessene Zykluszeit trägt.',
        'computeTimelineLadder.vaTimeSec.measuredSec',
        { vaTimeSec: null },
        'not-computable',
      ),
    )
  } else {
    const fullyMeasured = ladder.vaTimeSec.measuredNodeCount === ladder.vaTimeSec.totalNodeCount
    out.push(
      statement(
        'value-add-time',
        'value-add-time',
        `Die wertschöpfende Zeit beträgt ${formatRound1(ladder.vaTimeSec.measuredSec)} Sekunden (gemessen über ${ladder.vaTimeSec.measuredNodeCount} von ${ladder.vaTimeSec.totalNodeCount} wertschöpfenden Nodes).`,
        'computeTimelineLadder.vaTimeSec.measuredSec',
        { vaTimeSec: ladder.vaTimeSec.measuredSec, measuredNodeCount: ladder.vaTimeSec.measuredNodeCount, totalNodeCount: ladder.vaTimeSec.totalNodeCount },
        fullyMeasured ? 'high' : 'medium',
      ),
    )
  }

  if (ladder.processCycleEfficiencyPct == null) {
    out.push(
      statement(
        'value-add-share',
        'value-add-share',
        'Der wertschöpfende Anteil (Process Cycle Efficiency) ist nicht bewertbar, weil keine wertschöpfende Zeit gemessen wurde.',
        'computeTimelineLadder.processCycleEfficiencyPct',
        { pcePct: null },
        'not-computable',
      ),
    )
  } else {
    const fullyMeasured = ladder.vaTimeSec.measuredNodeCount === ladder.vaTimeSec.totalNodeCount
    out.push(
      statement(
        'value-add-share',
        'value-add-share',
        `Nur ${formatPceHonestly(ladder.processCycleEfficiencyPct)} Prozent der gesamten Durchlaufzeit sind wertschöpfend.`,
        'computeTimelineLadder.processCycleEfficiencyPct',
        { pcePct: ladder.processCycleEfficiencyPct },
        fullyMeasured ? 'high' : 'medium',
      ),
    )
  }

  return out
}

/** C15-Fix: `formatRound1` rounds to 1 decimal — a real, non-zero PCE below
 * 0.05% (e.g. 0,000174%, a normal case when inventory time dwarfs a short
 * cycle time) collapses to the displayed text "0", visually identical to a
 * genuine zero. Used at both PCE-percentage sentence sites below (value-add-
 * share + the Wertschöpfungshebel improvement statement) — the underlying
 * `values.pcePct` field always keeps the exact number regardless (P0-F3
 * "null ≠ 0" doctrine extended to "tiny-real ≠ 0"); only the rendered TEXT
 * gets this honesty guard. */
function formatPceHonestly(pcePct: number): string {
  if (pcePct > 0 && pcePct < 0.05) return 'unter 0,1'
  return formatRound1(pcePct)
}

// ── 8. Bestands-Hotspots ────────────────────────────────────────────────

function inventoryHotspotStatements(nodes: VsmNode[], context: ManagementAnalysisContext | undefined): AnalysisStatement[] {
  const inventoryNodes = nodes.filter((n) => n.type === 'inventory')
  if (inventoryNodes.length === 0) {
    return [
      statement(
        'inventory-hotspot',
        'inventory-hotspot',
        'Der Wertstrom enthält keine Bestands-/Puffer-Nodes.',
        'nodes.filter(type=inventory)',
        { inventoryNodeCount: 0 },
        'high',
      ),
    ]
  }

  const demand = context?.demandUnitsPerDay
  if (demand == null) {
    return [
      statement(
        'inventory-hotspot',
        'inventory-hotspot',
        'Bestands-Hotspots sind nicht bewertbar, weil kein Kundenbedarf hinterlegt ist (Reichweite nicht berechenbar).',
        'computeInventoryCoverage',
        { inventoryNodeCount: inventoryNodes.length },
        'not-computable',
      ),
    ]
  }

  const ranked = inventoryNodes
    .map((n) => ({ node: n, coverage: computeInventoryCoverage(n.quantity, { unitsPerDay: demand }, context?.shiftModel) }))
    .filter((r) => r.coverage.coverageDays != null)
    .sort((a, b) => (b.coverage.coverageDays as number) - (a.coverage.coverageDays as number))

  if (ranked.length === 0) {
    return [
      statement(
        'inventory-hotspot',
        'inventory-hotspot',
        'Bestands-Hotspots sind nicht bewertbar, weil kein Bestands-Node eine erfasste Menge trägt.',
        'computeInventoryCoverage',
        { inventoryNodeCount: inventoryNodes.length },
        'not-computable',
      ),
    ]
  }

  const top = ranked[0]
  const runnerUps = ranked.slice(1, 3).map((r) => `${r.node.name} (${formatRound1(r.coverage.coverageDays)} Tage)`)
  const runnerUpText = runnerUps.length > 0 ? `, gefolgt von ${runnerUps.join(', ')}` : ''
  return [
    statement(
      'inventory-hotspot',
      'inventory-hotspot',
      `Der größte Bestands-Hotspot ist „${top.node.name}" mit ${formatRound1(top.coverage.coverageDays)} Tagen Reichweite${runnerUpText}.`,
      'computeInventoryCoverage',
      { topNodeId: top.node.id, topNodeName: top.node.name, topCoverageDays: top.coverage.coverageDays, hotspotCount: ranked.length },
      'high',
    ),
  ]
}

// ── 9. Qualitätsverluste ─────────────────────────────────────────────────

/** Shared by the Qualitäts- and Kapazitätsrisiken-Sätze: the engine's
 * `ProcessQualityInput` has no VsmNode-field-scoped constructor of its own
 * (see quality.ts module doc — `reworkRatePct`/`reworkTimeSec` are not
 * VsmNode fields today), so both call sites build the SAME scrap-only
 * projection from station nodes (process/machine — the only types with a
 * meaningful `scrapRate` in today's editor UI) rather than duplicating the
 * filter/map inline twice. */
function deriveQualityInputs(nodes: VsmNode[]): ProcessQualityInput[] {
  return nodes.filter((n) => isStationNode(n) && n.scrapRate != null).map((n) => ({ nodeId: n.id, scrapRatePct: n.scrapRate }))
}

function qualityStatement(nodes: VsmNode[]): AnalysisStatement {
  const qualityInputs = deriveQualityInputs(nodes)

  if (qualityInputs.length === 0) {
    return statement(
      'quality',
      'quality',
      'Qualitätsverluste sind nicht bewertbar, weil kein Prozess-/Maschinen-Node eine Ausschussrate hinterlegt hat.',
      'computeCumulativeYield',
      { cumulativeYieldPct: null },
      'not-computable',
    )
  }

  const yieldResult = computeCumulativeYield(qualityInputs)
  if (yieldResult.cumulativeYieldPct == null) {
    return statement(
      'quality',
      'quality',
      'Qualitätsverluste sind nicht bewertbar, weil keine der erfassten Ausschussraten auswertbar ist.',
      'computeCumulativeYield.cumulativeYieldPct',
      { cumulativeYieldPct: null },
      'not-computable',
    )
  }

  const fullyKnown = yieldResult.nodesWithKnownScrap === yieldResult.nodesTotal
  return statement(
    'quality',
    'quality',
    `Die kumulierte Ausbeute über die Prozesskette beträgt ${formatRound1(yieldResult.cumulativeYieldPct)} Prozent (Ausschussrate bekannt bei ${yieldResult.nodesWithKnownScrap} von ${yieldResult.nodesTotal} Prozess-/Maschinen-Nodes). Nacharbeit ist in dieser Zahl nicht enthalten — keine Datenbasis für Nacharbeitsrate/-zeit im Editor.`,
    'computeCumulativeYield.cumulativeYieldPct',
    { cumulativeYieldPct: yieldResult.cumulativeYieldPct, nodesWithKnownScrap: yieldResult.nodesWithKnownScrap, nodesTotal: yieldResult.nodesTotal },
    fullyKnown ? 'high' : 'medium',
  )
}

// ── 10. Kapazitätsrisiken ─────────────────────────────────────────────────

function capacityRiskStatement(nodes: VsmNode[], connections: VsmConnection[], context: ManagementAnalysisContext | undefined): AnalysisStatement {
  const qualityInputs = deriveQualityInputs(nodes)
  const validation = runValidation(nodes, connections, {
    taktTimeSec: context?.taktTimeSec ?? undefined,
    demandUnitsPerDay: context?.demandUnitsPerDay,
    shiftModel: context?.shiftModel,
    qualityInputs,
    capacityOverridesByNodeId: context?.capacityOverridesByNodeId,
  })
  const capacityIssues = validation.issues.filter((i) => i.category === 'capacity')

  if (capacityIssues.length === 0) {
    return statement(
      'capacity-risk',
      'capacity-risk',
      'Die Validierung meldet aktuell keine Kapazitätsrisiken (Kategorie "capacity").',
      'runValidation.issues[category=capacity]',
      { count: 0 },
      'high',
    )
  }

  const criticalCount = capacityIssues.filter((i) => i.severity === 'critical').length
  const preview = capacityIssues
    .slice(0, 3)
    .map((i) => i.whatDe)
    .join(' ')
  const more = capacityIssues.length > 3 ? ` (+${capacityIssues.length - 3} weitere)` : ''
  return statement(
    'capacity-risk',
    'capacity-risk',
    `${capacityIssues.length} Kapazitätsrisiko(-en) erkannt: ${preview}${more}`,
    'runValidation.issues[category=capacity]',
    { count: capacityIssues.length, criticalCount, warningCount: capacityIssues.length - criticalCount },
    criticalCount > 0 ? 'high' : 'medium',
  )
}

// ── 11. Verbesserungshebel ────────────────────────────────────────────────

function improvementStatements(nodes: VsmNode[], context: ManagementAnalysisContext | undefined): AnalysisStatement[] {
  const out: AnalysisStatement[] = []
  const bottleneck = computeBottleneckV2(nodes, context?.taktTimeSec ?? null, context?.capacityOverridesByNodeId)
  if (bottleneck.primary && bottleneck.primary.overTaktPct != null && bottleneck.primary.overTaktPct > 0) {
    out.push(
      statement(
        `improvement-${out.length + 1}`,
        'improvement',
        `Größter Hebel: Engpass „${bottleneck.primary.nodeName}" entschärfen (aktuell ${formatRound1(bottleneck.primary.utilizationPct)}% Auslastung, ${formatRound1(bottleneck.primary.overTaktPct)}% über Kundentakt) — z. B. durch Kapazitätserhöhung, Parallelisierung oder Taktanpassung.`,
        'computeBottleneckV2.primary',
        { nodeName: bottleneck.primary.nodeName, overTaktPct: bottleneck.primary.overTaktPct },
        bottleneck.confidence,
      ),
    )
  }

  const demand = context?.demandUnitsPerDay
  if (demand != null) {
    const ranked = nodes
      .filter((n) => n.type === 'inventory')
      .map((n) => ({ node: n, coverage: computeInventoryCoverage(n.quantity, { unitsPerDay: demand }, context?.shiftModel) }))
      .filter((r) => r.coverage.coverageDays != null && r.coverage.coverageDays > 0)
      .sort((a, b) => (b.coverage.coverageDays as number) - (a.coverage.coverageDays as number))
    if (ranked.length > 0) {
      const top = ranked[0]
      out.push(
        statement(
          `improvement-${out.length + 1}`,
          'improvement',
          `Bestandshebel: „${top.node.name}" bindet ${formatRound1(top.coverage.coverageDays)} Tage Reichweite — größter Bestands-Hebel im Wertstrom.`,
          'computeInventoryCoverage',
          { nodeName: top.node.name, coverageDays: top.coverage.coverageDays },
          'high',
        ),
      )
    }
  }

  const ladder = computeTimelineLadder(nodes, context?.demandUnitsPerDay, context?.shiftModel)
  if (ladder.processCycleEfficiencyPct != null && ladder.processCycleEfficiencyPct < 100) {
    out.push(
      statement(
        `improvement-${out.length + 1}`,
        'improvement',
        `Wertschöpfungshebel: nur ${formatPceHonestly(ladder.processCycleEfficiencyPct)}% der Durchlaufzeit sind wertschöpfend — Potenzial zur Reduktion von Warte-/Bestands-/Transportzeit.`,
        'computeTimelineLadder.processCycleEfficiencyPct',
        { pcePct: ladder.processCycleEfficiencyPct },
        'medium',
      ),
    )
  }

  if (out.length === 0) {
    return [
      statement(
        'improvement-1',
        'improvement',
        'Verbesserungshebel sind nicht bewertbar, weil weder Engpass- noch Bestands- noch Wertschöpfungsdaten für diesen Wertstrom vorliegen.',
        'computeBottleneckV2 / computeInventoryCoverage / computeTimelineLadder',
        {},
        'not-computable',
      ),
    ]
  }
  return out
}

// ── 12. Offene Maßnahmen ──────────────────────────────────────────────────

function openMeasuresStatement(context: ManagementAnalysisContext | undefined): AnalysisStatement {
  const measures = context?.openMeasures
  if (measures === undefined) {
    // K2-Fix (RLS-Sichtbarkeits-Ehrlichkeit): a more specific "nicht
    // bewertbar"-Satz when the caller can name WHY — never "0 offene
    // Maßnahmen" for a result it could not actually confirm.
    if (context?.openMeasuresIssue === 'not-visible') {
      return statement(
        'open-measures',
        'open-measures',
        'Offene Maßnahmen sind nicht bewertbar, weil sie für diese Ansicht nicht einsehbar sind (kein Zugriff auf das verknüpfte Projekt).',
        'context.openMeasuresIssue',
        { count: null },
        'not-computable',
      )
    }
    if (context?.openMeasuresIssue === 'load-error') {
      return statement(
        'open-measures',
        'open-measures',
        'Offene Maßnahmen konnten nicht geladen werden.',
        'context.openMeasuresIssue',
        { count: null },
        'not-computable',
      )
    }
    return statement(
      'open-measures',
      'open-measures',
      'Offene Maßnahmen sind nicht bewertbar, weil keine Maßnahmen-Datenquelle mit diesem Wertstrom verknüpft ist.',
      'context.openMeasures',
      { count: null },
      'not-computable',
    )
  }
  if (measures.length === 0) {
    return statement('open-measures', 'open-measures', 'Keine offenen Maßnahmen hinterlegt.', 'context.openMeasures', { count: 0 }, 'high')
  }
  const titles = measures.map((m) => m.title).join(', ')
  return statement(
    'open-measures',
    'open-measures',
    `${measures.length} offene Maßnahme(n) hinterlegt: ${titles}.`,
    'context.openMeasures',
    { count: measures.length },
    'high',
  )
}

// ── 13. Ist/Soll-Effekte ──────────────────────────────────────────────────

/** C2-Fix: same generic "was actually differs" phrasing as
 * `vsm-scenario-compare-view.tsx`'s `describeMismatchParts` (P5) — mirrored,
 * not imported (this module must stay independent of components/). */
function describeMismatchPartsDe(mismatch: ScenarioContextMismatch): string {
  const parts: string[] = []
  if (mismatch.differentTakt) parts.push('einen unterschiedlichen Kundentakt')
  if (mismatch.differentShiftModel) parts.push('ein unterschiedliches Arbeitszeitmodell')
  if (mismatch.differentDemand) parts.push('eine unterschiedliche Bedarfsrate')
  if (parts.length <= 1) return parts[0] ?? ''
  return `${parts.slice(0, -1).join(', ')} und ${parts[parts.length - 1]}`
}

function scenarioEffectStatements(context: ManagementAnalysisContext | undefined): AnalysisStatement[] {
  const comparison = context?.scenarioComparison
  if (!comparison) return []

  const mismatch = comparison.contextMismatch
  const out: AnalysisStatement[] = []

  // C2-Fix: P5-Doktrin ("Abweichung der Kontexte EHRLICH ausweisen, nicht
  // still mischen!") übernommen — bisher konnte scenarioComparison einen
  // Kontext-Mismatch (unterschiedlicher Kundentakt/Arbeitszeitmodell/Bedarf
  // zwischen Ist und Soll) gar nicht ausdrücken, jeder Delta-Satz bekam
  // pauschal 'high'. Ein expliziter Vorbehalts-Satz VOR den Effekt-Sätzen,
  // plus Confidence-Herabstufung unten — analog zum P5-Banner-Text in
  // vsm-scenario-compare-view.tsx, statt den Mismatch stillschweigend zu
  // unterschlagen.
  if (mismatch?.any) {
    out.push(
      statement(
        'scenario-effect-context-caveat',
        'scenario-effect',
        `Vergleich unter abweichenden Rahmenbedingungen: Ist und Szenario „${comparison.scenarioLabel}" nutzen ${describeMismatchPartsDe(mismatch)}. Die folgenden Effekte sind jede Seite für sich korrekt berechnet, aber nur eingeschränkt vergleichbar.`,
        'context.scenarioComparison.contextMismatch',
        // `values` is `Record<string, number | string | null>` (no boolean)
        // — same 'true'/'false' string convention totalLeadTimeIncludesInventory
        // already uses above in this file.
        { differentTakt: String(mismatch.differentTakt), differentShiftModel: String(mismatch.differentShiftModel), differentDemand: String(mismatch.differentDemand) },
        'high',
      ),
    )
  }

  comparison.rows.forEach((row, i) => {
    const id = `scenario-effect-${i + 1}`
    if (row.absoluteDelta == null) {
      out.push(
        statement(
          id,
          'scenario-effect',
          `${row.label} ist zwischen Ist und Szenario „${comparison.scenarioLabel}" nicht vergleichbar (fehlende Datenbasis auf mindestens einer Seite).`,
          `computeKpiDeltaRows[${row.key}]`,
          { key: row.key, currentValue: row.currentValue, scenarioValue: row.scenarioValue },
          'not-computable',
        ),
      )
      return
    }
    const unitSuffix = row.unit === 'percent' ? ' Prozentpunkte' : row.unit === 'seconds' ? ' Sekunden' : ' Tage'
    const directionText =
      row.direction === 'improvement' ? 'verbessert sich' : row.direction === 'deterioration' ? 'verschlechtert sich' : row.direction === 'unchanged' ? 'bleibt unverändert' : 'verändert sich'
    // C3-Fix: „verbessert sich ... um -X" behauptet wörtlich eine
    // Verbesserung um einen negativen Betrag (traf 3 der 5 KPI-Zeilen, deren
    // Verbesserung ein NEGATIVES Delta ist — Auslastung/DLZ/Bestand). Für
    // gerichtete Zeilen (improvement/deterioration/unchanged) trägt allein
    // das Verb die Richtung, der Betrag wird als Math.abs() gelesen. Nur
    // 'info'-Zeilen (kein Richtungs-Verb, z. B. VA-Zeit — mehr/weniger
    // wertschöpfende Zeit ist für sich genommen weder gut noch schlecht)
    // behalten das Vorzeichen am Betrag selbst, weil dort sonst gar kein
    // Signal für Zu-/Abnahme existiert.
    const isInfo = row.direction === 'info'
    const magnitude = isInfo ? row.absoluteDelta : Math.abs(row.absoluteDelta)
    const sign = isInfo && row.absoluteDelta > 0 ? '+' : ''
    const percentSuffix = row.percentDelta != null ? ` (${row.percentDelta > 0 ? '+' : ''}${formatRound1(row.percentDelta)}%)` : ''
    out.push(
      statement(
        id,
        'scenario-effect',
        `${row.label} ${directionText} im Szenario „${comparison.scenarioLabel}" um ${sign}${formatRound1(magnitude)}${unitSuffix}${percentSuffix}.`,
        `computeKpiDeltaRows[${row.key}]`,
        { key: row.key, currentValue: row.currentValue, scenarioValue: row.scenarioValue, absoluteDelta: row.absoluteDelta, percentDelta: row.percentDelta, direction: row.direction },
        mismatch?.any ? 'medium' : 'high',
      ),
    )
  })

  return out
}

/**
 * A18 Management-Analyse (§18). Rule-based, deterministic, no LLM. Every
 * §18 category is represented by at least one `AnalysisStatement`
 * (`scenario-effect` is the one exception — it is entirely absent, not a
 * "nicht bewertbar" placeholder, when `context.scenarioComparison` is not
 * supplied, per the brief's "nur bei aktivem Szenarien-Vergleich").
 */
export function computeManagementAnalysis(nodes: VsmNode[], connections: VsmConnection[], context?: ManagementAnalysisContext): ManagementAnalysisResult {
  const statements: AnalysisStatement[] = [
    demandStatement(context),
    taktStatement(context),
    ...bottleneckStatements(nodes, context),
    ...timelineStatements(nodes, context),
    ...inventoryHotspotStatements(nodes, context),
    qualityStatement(nodes),
    capacityRiskStatement(nodes, connections, context),
    ...improvementStatements(nodes, context),
    openMeasuresStatement(context),
    ...scenarioEffectStatements(context),
  ]

  return { statements, knownGaps: KNOWN_VALIDATION_GAPS, explain: EXPLAIN }
}
