// Multi-QAF-Variante <-> Standard-QAF-Vergleich (KAR-943 / Multi-QAF-Programm
// P3.2, Epic KAR-925).
//
// Master-Prompt §15 Szenario B: EINE explizit gewählte Multi-QAF-Variante wird
// als virtuelles Standard-QAF durch die BESTEHENDE Standard-Vergleichs-Engine
// geschickt — NIE ein ganzer Container gegen ein Standard-QAF (that is a
// category error the bridge itself cannot paper over: a MultiQafContainer has
// N variants, a standard `compareQafPair` call is for exactly ONE part
// number). compare-flow.ts's `runMultiQafCompareFlow` (KAR-942) is the
// sibling MQ<->MQ orchestrator; THIS module is the MQ-variant<->standard one —
// deliberately a SEPARATE function/result type, not an overload of either,
// because the two comparisons answer genuinely different questions with a
// genuinely different honest-coverage matrix (see below).
//
// ── Why this is NOT "just call compareQafPair with a Partial<QafFileParsed>"
// ────────────────────────────────────────────────────────────────────────────
// bridge.ts's `toCanonicalInputs(variant)` returns `Partial<QafFileParsed>`
// with ONLY `summary`/`summaryMetrics` populated — it deliberately never
// fabricates `ref`/`steps` (see that module's own header: `steps` has NO
// per-variant equivalent in the Multi-QAF domain model at all, manufacturing
// cost is a SHARED profile selected via a VariantProfileBinding, not an
// independent per-variant process list, Master-Prompt §12). The KAR-929
// review warning this module exists to resolve: a downstream consumer that
// takes that Partial and feeds it to `compareQafPair` UNCHECKED either
// crashes (TypeScript would need a cast past `QafFileParsed`'s mandatory
// `ref`/`steps`) or, worse, silently computes something WRONG if a caller
// "fixed" the type error by fabricating `steps: []`: `checkPlausibility`'s
// currency check (`distinctCurrencies(altSteps)` vs. `distinctCurrencies(
// neuSteps)`) would then report a spurious `currency_change` on EVERY SINGLE
// comparison (empty set vs. the real side's actual currencies almost always
// differ in SIZE alone), and `matchStepsWithPins` would report the standard
// file's entire process list as `new_step`/`removed_step` — neither is a
// real finding, both are artifacts of feeding the step-matching engine an
// empty array that means "no steps to compare", not "steps genuinely
// unchanged". This module NEVER does that: it only ever calls the specific
// standard-engine functions that operate on `QafSummary`/`SummaryMetricsParse`
// alone (`checkSummaryIdentityPlausibility`, extracted from `checkPlausibility`
// in this same PR for exactly this reuse; `diffSummaryMetrics`, called
// unmodified — its own `undefined`/`null`-tolerant, "skip both-absent, `neu`/
// `entfallen` for one-sided" contract already IS the honest degradation this
// task requires, no wrapping needed).
//
// ── Module-Bedienbarkeits-Matrix (task-mandated "which standard-compare
// modules run for real vs. degrade honestly") ──────────────────────────────
//   REAL (this module actually calls the standard engine's own function):
//     - Summary-Identitäts-Vergleich    checkVariantVsStandardIdentity, this
//                                       module's own thin wrapper around
//                                       checkSummaryIdentityPlausibility
//                                       (variant_changed/quotation_date_order
//                                       — the fields bridge.ts's
//                                       virtualVariantSummary maps for REAL).
//                                       part_number_mismatch/part_name_changed
//                                       are EXCLUDED (KAR-943 adversarial-
//                                       review F1): bridge.ts's own
//                                       SYNTHETIC_IDENTITY_FIELDS marks
//                                       partNumber/partName as synthetic
//                                       placeholders, never a real customer
//                                       identity — comparing them 1:1 against
//                                       a real standard QAF's identity would
//                                       false-alarm on practically every
//                                       legitimate comparison. partNumber's
//                                       exclusion is replaced by an always-
//                                       present informational finding
//                                       (`part_number_comparison_not_
//                                       applicable`); see
//                                       checkVariantVsStandardIdentity's own
//                                       doc comment below.
//     - SummaryMetrics-Vergleich        diffSummaryMetrics (the 8
//                                       VirtualVariantSummaryTotals-backed
//                                       canonical metrics bridge.ts maps for
//                                       real diff genuinely; the 12 metrics
//                                       bridge.ts leaves ABSENT surface
//                                       honestly as one-sided 'neu'/'entfallen'
//                                       findings via that function's own
//                                       existing, unmodified contract — never
//                                       a fabricated 0-diff).
//   STRUCTURALLY DEGRADED (this comparison never had a source for these —
//   `DEGRADED_MODULES` below, own finding class, never crashes, never
//   silently reports "no findings" as if it had checked):
//     - manufacturing_steps   Fertigungs-Prozessvergleich (matchStepsWithPins/
//                              diffSteps) — no per-variant process list exists
//                              (bridge.ts's own "steps" reasoning above).
//     - material/sbm/rmr/logistics/lccn/co2e   no per-variant detail-sheet row
//                              source exists in the Multi-QAF domain model
//                              (bridge.ts's own "DELIBERATELY LEFT ABSENT"
//                              list for the same reason).
//     - reconciliation/business_rules/rule_engine   every one of these reads
//                              `steps` (checkReconciliation/evaluateBusinessRules/
//                              evaluateRuleEngine all take a `steps` field) —
//                              same structural gap as manufacturing_steps.
//                              (The variant's OWN internal consistency IS
//                              independently checked, just not by THIS
//                              module — see "Relationship to
//                              variant-reconciliation.ts" below.)
//     - root_cause             computeRootCause's `StepDiffSummary[]` input is
//                              derived from `stepComparisons` — no source.
//     - workbook_safety        `WorkbookSafetyResult` is captured once at
//                              INGEST for a real uploaded file
//                              (runPreParseWorkbookSafetyCheck) — a virtual
//                              variant is a computed projection, not a
//                              re-parsed workbook, so it has none of its own
//                              (the container's OWN workbookSafety, if any,
//                              already surfaced when that Multi-QAF file was
//                              ingested — not re-derived or re-reported here).
//
// ── Relationship to variant-reconciliation.ts (KAR-940) ─────────────────────
// `reconcileVirtualVariant`/`reconcileContainer` already independently verify
// ONE container's own internal consistency (Master-Prompt §13) — that is a
// DIFFERENT question ("does this variant's own summary agree with its own
// detail data") from what this module answers ("how does this variant compare
// against a chosen standard QAF"). This module does not re-run or duplicate
// that reconciliation; a caller that also wants it calls `reconcileContainer`
// separately (compare-flow.ts's `runMultiQafCompareFlow` already does the
// analogous thing for the MQ<->MQ case, calling `reconcileContainer` per side
// alongside — not instead of — the cross-container diffs).
//
// ── Fail-closed reviewRequired propagation (task requirement 3) ─────────────
// `reviewRequired` is `true` when EITHER:
//   (a) `reviewStatusForMultiQafContainer(container) === 'review_required'`
//       (container-assembly.ts's own gate — ANY container warning with
//       `severity: 'critical'` OR `reviewRelevant: true`, e.g. a canonical-key
//       collision or an ambiguous cost-profile binding anywhere in the
//       container, not necessarily tied to THIS variant); OR
//   (b) THIS variant's own `warnings` (container-assembly.ts's
//       `variantWarningsFor` — includes the KAR-936 aggregation-gate
//       findings `material_variant_fuzzy_match_below_aggregation_gate` /
//       `material_variant_fuzzy_matched_to_summary`, both `reviewRelevant:
//       true`) carry a `severity: 'critical'` or `reviewRelevant: true` entry
//       — i.e. this SPECIFIC variant's material data is fuzzy-matched or
//       below the KAR-936 financial-aggregation gate; OR
//   (c) `summaryIdentityIssues` (post-F1-filter, see
//       checkVariantVsStandardIdentity above) carries a `severity: 'kritisch'`
//       entry (KAR-943 adversarial-review F3 — this was silently ignored
//       before this fix, the one class of "the comparison itself found
//       something critical" that never flipped `reviewRequired`).
// Never suppressed, never downgraded — the comparison still RUNS and returns
// a full result (never blocks), but `reviewRequired`/`variantWarnings`/
// `containerReviewWarnings` make the uncertainty visible on the result
// itself, same "run but flag" discipline `diffContainers`'/
// `runMultiQafCompareFlow`'s own `reviewRequired` already establish.
//
// tdd-guard:skip — this file's own orchestration body is a thin, deterministic
// composition of already-unit-tested pure functions (generateVirtualVariants,
// toCanonicalInputs, checkSummaryIdentityPlausibility, diffSummaryMetrics,
// reviewStatusForMultiQafContainer) plus a fixed, versioned degraded-module
// catalog — covered by variant-vs-standard.test.ts (synthetic container +
// synthetic QafFileParsed) and variant-vs-standard.real-files.test.ts
// (env-gated real-file regression), same category as compare-flow.ts's own
// tdd-guard:skip.

import { generateVirtualVariants, reviewStatusForMultiQafContainer } from './container-assembly'
import { SYNTHETIC_IDENTITY_FIELDS, toCanonicalInputs } from './bridge'
import type { MultiQafContainer, MultiQafWarning } from './types'
import { checkSummaryIdentityPlausibility, type PlausibilityIssue } from '../plausibility'
import { diffSummaryMetrics, type SummaryMetricDiff } from '../summary-metrics'
import { DEFAULT_ENGINE_CONFIG } from '../engine-config'
import type { QafFileParsed } from '../compare'
import type { QafFileRef } from '../baseline'
import type { QafSummary, QafSummaryKey } from '../types'

/** Bumped only on a breaking (non-additive) change to the persisted result
 * shape — same discipline `MULTI_QAF_COMPARISON_RESULT_VERSION` (compare-
 * flow.ts) already documents. Field ADDITIONS never require a bump. */
export const MULTI_QAF_VARIANT_VS_STANDARD_RESULT_VERSION = 1 as const

/** Stable, machine-greppable keys for every standard-engine module this
 * comparison structurally cannot serve — see module header "Module-
 * Bedienbarkeits-Matrix". Fixed vocabulary (not derived from QafFileParsed's
 * field names 1:1) so a UI/test can match on it without parsing message
 * text. */
export type VariantVsStandardDegradedModuleKey =
  | 'manufacturing_steps'
  | 'material'
  | 'sbm'
  | 'rmr'
  | 'logistics'
  | 'lccn'
  | 'co2e'
  | 'reconciliation'
  | 'business_rules'
  | 'rule_engine'
  | 'root_cause'
  | 'workbook_safety'

/**
 * ONE degraded-module finding — its own finding class (task requirement:
 * "eigene Befund-Klasse 'nicht verfügbar für Multi-QAF-Variante'"), distinct
 * from both a `PlausibilityIssue` (which implies a check RAN and found
 * something) and a `SummaryMetricDiff` `'nicht_berechenbar'` status (which
 * implies a per-metric computation was attempted). `status` is a single
 * fixed literal by design — there is only ever one honest status for a
 * structurally-unavailable module, never a computed severity.
 */
export interface VariantVsStandardDegradedModuleFinding {
  moduleKey: VariantVsStandardDegradedModuleKey
  status: 'not_available_for_multi_qaf_variant'
  messageDe: string
  messageEn: string
}

/** Fixed, ordered (by moduleKey) catalog — see module header "Module-
 * Bedienbarkeits-Matrix" for why each entry is structurally, not just
 * currently, unavailable. Computed once (module-level constant, not
 * re-built per call) since it never depends on the container/variant/
 * standardFile arguments — every Multi-QAF-variant-vs-standard comparison
 * degrades the SAME modules for the SAME reason. */
export const DEGRADED_MODULES: readonly VariantVsStandardDegradedModuleFinding[] = [
  {
    moduleKey: 'manufacturing_steps',
    status: 'not_available_for_multi_qaf_variant',
    messageDe:
      'Fertigungs-Prozessvergleich nicht verfügbar für Multi-QAF-Variante — eine Variante hat keine eigene Fertigungskosten-Prozessliste, Fertigungskosten stammen aus einem gemeinsam genutzten Profil (SharedCostProfile), nicht aus unabhängigen Prozesszeilen (siehe bridge.ts).',
    messageEn:
      'Manufacturing process comparison is not available for a Multi-QAF variant — a variant has no process list of its own; manufacturing cost comes from a shared profile (SharedCostProfile), not from independent process rows (see bridge.ts).',
  },
  {
    moduleKey: 'material',
    status: 'not_available_for_multi_qaf_variant',
    messageDe:
      'MATERIAL-Detail-Vergleich nicht verfügbar für Multi-QAF-Variante — kein per-Varianten-Materialzeilen-Äquivalent im aktuellen Domänenmodell (siehe bridge.ts, materialRows).',
    messageEn:
      'MATERIAL detail comparison is not available for a Multi-QAF variant — no per-variant material-row equivalent exists in the current domain model (see bridge.ts, materialRows).',
  },
  {
    moduleKey: 'sbm',
    status: 'not_available_for_multi_qaf_variant',
    messageDe: 'SBM-DEVICES-FWZ-Vergleich nicht verfügbar für Multi-QAF-Variante — keine Quelle im Domänenmodell.',
    messageEn: 'SBM-DEVICES-FWZ comparison is not available for a Multi-QAF variant — no source in the domain model.',
  },
  {
    moduleKey: 'rmr',
    status: 'not_available_for_multi_qaf_variant',
    messageDe: 'RAW-MATERIAL-RISKS-Vergleich nicht verfügbar für Multi-QAF-Variante — keine Quelle im Domänenmodell.',
    messageEn: 'RAW MATERIAL RISKS comparison is not available for a Multi-QAF variant — no source in the domain model.',
  },
  {
    moduleKey: 'logistics',
    status: 'not_available_for_multi_qaf_variant',
    messageDe: 'LOGISTICS-&-CUSTOM-Vergleich nicht verfügbar für Multi-QAF-Variante — keine Quelle im Domänenmodell.',
    messageEn: 'LOGISTICS & CUSTOM comparison is not available for a Multi-QAF variant — no source in the domain model.',
  },
  {
    moduleKey: 'lccn',
    status: 'not_available_for_multi_qaf_variant',
    messageDe: 'LC-CN-Validierung nicht verfügbar für Multi-QAF-Variante — keine Quelle im Domänenmodell.',
    messageEn: 'LC-CN validation is not available for a Multi-QAF variant — no source in the domain model.',
  },
  {
    moduleKey: 'co2e',
    status: 'not_available_for_multi_qaf_variant',
    messageDe: 'CO2e-Material-Validierung nicht verfügbar für Multi-QAF-Variante — keine Quelle im Domänenmodell.',
    messageEn: 'CO2e material validation is not available for a Multi-QAF variant — no source in the domain model.',
  },
  {
    moduleKey: 'reconciliation',
    status: 'not_available_for_multi_qaf_variant',
    messageDe:
      'Summen-Rekonziliation (Prozesszeilen ↔ Summary) nicht verfügbar für diesen Vergleich — benötigt Fertigungskosten-Prozesszeilen, die eine Multi-QAF-Variante nicht hat. Die interne Konsistenz DIESER Variante wird separat über die Multi-QAF-Rekonziliation (reconcileContainer/reconcileVirtualVariant) geprüft, nicht durch diesen Vergleich.',
    messageEn:
      'Sum reconciliation (process rows vs. summary) is not available for this comparison — it needs manufacturing process rows, which a Multi-QAF variant does not have. This variant’s own internal consistency is checked separately via the Multi-QAF reconciliation (reconcileContainer/reconcileVirtualVariant), not by this comparison.',
  },
  {
    moduleKey: 'business_rules',
    status: 'not_available_for_multi_qaf_variant',
    messageDe: 'Business-Rule-Nachrechnung nicht verfügbar für Multi-QAF-Variante — benötigt Prozesszeilen.',
    messageEn: 'Business-rule recomputation is not available for a Multi-QAF variant — it needs process rows.',
  },
  {
    moduleKey: 'rule_engine',
    status: 'not_available_for_multi_qaf_variant',
    messageDe: 'Fehlerreport-Regel-Engine (R1-R6) nicht verfügbar für Multi-QAF-Variante — benötigt Prozesszeilen.',
    messageEn: 'The error-report rule engine (R1-R6) is not available for a Multi-QAF variant — it needs process rows.',
  },
  {
    moduleKey: 'root_cause',
    status: 'not_available_for_multi_qaf_variant',
    messageDe: 'Root-Cause-Analyse nicht verfügbar für Multi-QAF-Variante — benötigt Prozesszeilen-Deltas.',
    messageEn: 'Root-cause analysis is not available for a Multi-QAF variant — it needs process-row deltas.',
  },
  {
    moduleKey: 'workbook_safety',
    status: 'not_available_for_multi_qaf_variant',
    messageDe:
      'Untrusted-Excel-Hardening-Befunde nicht verfügbar für diese Variante — eine virtuelle Variante ist eine berechnete Projektion, kein erneut geparstes Workbook (der Container selbst hat seine eigenen, beim Ingest erfassten Befunde).',
    messageEn:
      'Untrusted-Excel-hardening findings are not available for this variant — a virtual variant is a computed projection, not a re-parsed workbook (the container itself has its own findings, captured at ingest).',
  },
]

export type VariantVsStandardReviewRequiredReason =
  | 'container_review_required'
  | 'variant_material_below_aggregation_gate'
  | 'summary_identity_critical_issue'

// ── Synthetic-identity-aware summary-identity check (KAR-943 adversarial-
// review F1) ─────────────────────────────────────────────────────────────
//
// checkSummaryIdentityPlausibility is ALT/NEU-generic — it has no idea one
// side is a bridged Multi-QAF variant whose `partNumber`/`partName` are
// SYNTHETIC placeholders (bridge.ts's own `SYNTHETIC_IDENTITY_FIELDS`
// marker, next to the mapping that makes them synthetic). Calling it
// unfiltered here would compare `standardFile`'s REAL part number against
// the variant's synthetic `compositeCanonicalKey` (`region=eu|drivetype=
// awd`-shaped) — a mismatch on PRACTICALLY EVERY legitimate comparison,
// reported as `severity: 'kritisch'`. This wrapper is the honest boundary:
// it reuses checkSummaryIdentityPlausibility for real reuse (never a
// duplicated copy of its logic), then drops any issue keyed to a
// bridge.ts-marked synthetic field — using that marker, never re-deriving
// or guessing which fields are synthetic from field names or content shape.

/** Always-present, structural (never data-dependent — same discipline
 * `DEGRADED_MODULES` already establishes) informational replacement for the
 * dropped `part_number_mismatch` check: a caller/UI would otherwise have no
 * way to tell "no part-number issue" (checked, found nothing) apart from
 * "part-number check never ran for this comparison" (structurally
 * inapplicable). `partName`'s dropped issue gets no dedicated replacement —
 * it was already only ever `hinweis` severity, not the `kritisch`
 * false-alarm class this finding exists to explain. */
const PART_NUMBER_COMPARISON_NOT_APPLICABLE_ISSUE: PlausibilityIssue = {
  type: 'part_number_comparison_not_applicable',
  severity: 'hinweis',
  field: 'partNumber',
  explanation:
    'Sachnummern-Vergleich nicht anwendbar — Multi-QAF-Variante trägt synthetische Identität (die Sachnummer ist ein zusammengesetzter Varianten-Schlüssel, keine echte Kunden-Sachnummer; siehe bridge.ts SYNTHETIC_IDENTITY_FIELDS).',
  explanationEn:
    'Part-number comparison is not applicable — the Multi-QAF variant carries a synthetic identity (the part number is a composite variant key, not a real customer part number; see bridge.ts SYNTHETIC_IDENTITY_FIELDS).',
}

/**
 * Summary-identity comparison for the variant-vs-standard case (KAR-943
 * adversarial-review F1). Reuses `checkSummaryIdentityPlausibility` for real
 * reuse, but excludes any issue on a `bridge.ts`-marked
 * `SYNTHETIC_IDENTITY_FIELDS` field (`partNumber`/`partName`) before
 * returning — comparing a bridged variant's synthetic placeholder identity
 * against a real standard QAF's real identity is not a genuine finding.
 * `partNumber`'s dropped issue is replaced by the always-present
 * `PART_NUMBER_COMPARISON_NOT_APPLICABLE_ISSUE` (never conditional on
 * whether the raw check would have fired — structurally true for every
 * variant-vs-standard comparison, same "structural, not computed" discipline
 * `DEGRADED_MODULES` already establishes). `variant`/`quotationDate` issues
 * (real per-variant source data, see bridge.ts) pass through unchanged.
 * Exported for direct unit coverage independent of the full compare
 * pipeline.
 */
export function checkVariantVsStandardIdentity(standardSummary: QafSummary, variantSummary: QafSummary): readonly PlausibilityIssue[] {
  const rawIssues = checkSummaryIdentityPlausibility(standardSummary, variantSummary)
  const realIssues = rawIssues.filter((issue) => !issue.field || !SYNTHETIC_IDENTITY_FIELDS.includes(issue.field as QafSummaryKey))
  return [...realIssues, PART_NUMBER_COMPARISON_NOT_APPLICABLE_ISSUE]
}

/** Pure predicate backing reviewRequired reason (c) below (KAR-943
 * adversarial-review F3: "reviewRequired ignoriert summaryIdentityIssues —
 * selbst 'kritisch'-Befunde flippen baseline_status nicht"). Fail-closed:
 * ANY `severity: 'kritisch'` entry in the (already synthetic-field-filtered)
 * `summaryIdentityIssues` must flip `reviewRequired`, never silently
 * absorbed. After the F1 fix above, `checkVariantVsStandardIdentity` itself
 * never emits `kritisch` (its one source, `part_number_mismatch`, is always
 * dropped) — this predicate stays independently correct and tested so a
 * FUTURE `checkSummaryIdentityPlausibility` addition that IS kritisch and
 * NOT synthetic-field-scoped still surfaces here, rather than requiring a
 * second fix the day that happens. */
export function hasCriticalIdentityIssue(issues: readonly PlausibilityIssue[]): boolean {
  return issues.some((issue) => issue.severity === 'kritisch')
}

export interface VariantVsStandardComparisonResult {
  modelVersion: typeof MULTI_QAF_VARIANT_VS_STANDARD_RESULT_VERSION
  variantId: string
  /** The chosen standard QAF this variant was compared against — `ref` only
   * (not the full `QafFileParsed`), same "keep the persisted envelope small,
   * the caller already has the full file" discipline `compare-flow.ts`'s
   * `MultiQafComparisonResult` follows for its own refs. */
  standardRef: QafFileRef
  /** `checkVariantVsStandardIdentity(standardFile.summary, <bridged variant
   * summary>)` — see module header "Module-Bedienbarkeits-Matrix" and this
   * function's own doc comment for the synthetic-field exclusion (KAR-943
   * adversarial-review F1). A `kritisch` entry here flips `reviewRequired`
   * (reason `summary_identity_critical_issue`, F3). */
  summaryIdentityIssues: readonly PlausibilityIssue[]
  /** `diffSummaryMetrics(standardFile.summaryMetrics, <bridged variant
   * summaryMetrics>)` — empty when `standardFile.summaryMetrics` itself is
   * absent (same guard `compareQafPair` already applies), never a fabricated
   * diff. */
  summaryMetricsDiff: readonly SummaryMetricDiff[]
  /** Fixed catalog, see `DEGRADED_MODULES` — always present, always the same
   * 12 entries (structural, not data-dependent). */
  degradedModules: readonly VariantVsStandardDegradedModuleFinding[]
  /** See module header "Fail-closed reviewRequired propagation". */
  reviewRequired: boolean
  reviewRequiredReasons: readonly VariantVsStandardReviewRequiredReason[]
  /** This variant's OWN warnings (container-assembly.ts's `variantWarningsFor`
   * output on `VirtualQafVariant.warnings`) — includes the KAR-936
   * aggregation-gate findings when this variant's material data triggered
   * one. Always present (possibly empty), so a caller never has to
   * re-fetch the container to see WHY `reviewRequired` fired for reason (b).
   */
  variantWarnings: readonly MultiQafWarning[]
  /** Container-level warnings that are themselves review-relevant
   * (`severity === 'critical' || reviewRelevant === true`) — reason (a) for
   * `reviewRequired`, kept separate from `variantWarnings` so a caller can
   * tell "this variant's own data is uncertain" apart from "something ELSE
   * in this container is uncertain, unrelated to this specific variant". */
  containerReviewWarnings: readonly MultiQafWarning[]
}

export interface RunVariantVsStandardCompareOptions {
  /** Passed to `diffSummaryMetrics`'s own `formulaEngineEnabled` parameter —
   * defaults to `DEFAULT_ENGINE_CONFIG.formulaEngine.enabled` (matches every
   * other caller's default in this package, e.g. `compareQafPair`). */
  formulaEngineEnabled?: boolean
}

/**
 * Master-Prompt §15 Szenario B: run the standard comparison engine's REAL-
 * bedienbare modules (Summary identity + SummaryMetrics) for ONE explicitly
 * chosen, ACTIVE Multi-QAF variant against an existing standard QAF
 * (`standardFile`, already parsed/rehydrated by the caller — this function
 * never re-parses a workbook, same discipline `runMultiQafCompareFlow`
 * already establishes for containers). Every standard-engine module that
 * structurally cannot run for a virtual variant (steps-dependent modules —
 * see module header) degrades honestly via the fixed `DEGRADED_MODULES`
 * catalog rather than crashing or silently reporting nothing.
 *
 * Fail-closed on the variant selection itself (task requirement: "validiert
 * Variante existiert+aktiv, sonst klare Meldung"): throws when `variantId`
 * does not resolve to a KNOWN variant in `container` at all, or resolves to
 * one that is not ACTIVE (inactive/reserved variants carry no volume/identity
 * a caller should compare against a standard QAF — Master-Prompt §6's binary
 * active/inactive split). Never silently falls back to "compare the first
 * active variant" or similar — an invalid selection is a caller bug that must
 * surface, not be papered over (same "an id that does not resolve throws"
 * discipline `diffContainers`/`matchVariantsWithOverrides` already establish
 * elsewhere in this package).
 *
 * `alt`/`neu` role assignment (both `checkSummaryIdentityPlausibility` and
 * `diffSummaryMetrics` are ALT/NEU-shaped): `standardFile` is ALT (the
 * existing baseline this variant is measured AGAINST), the bridged virtual
 * variant is NEU (the thing being checked) — same "ALT = reference baseline,
 * NEU = candidate under review" convention the rest of this package's
 * standard-compare path already uses.
 */
export function runVariantVsStandardCompare(
  container: MultiQafContainer,
  variantId: string,
  standardFile: QafFileParsed,
  options: RunVariantVsStandardCompareOptions = {},
): VariantVsStandardComparisonResult {
  const isActive = container.activeVariants.some((d) => d.stableInternalId === variantId)
  const isInactive = container.inactiveVariants.some((d) => d.stableInternalId === variantId)
  if (!isActive && !isInactive) {
    throw new Error(
      `runVariantVsStandardCompare: variant id "${variantId}" does not exist in this container's activeVariants/inactiveVariants — the caller must select a variantId returned by generateVirtualVariants(container) for this same container.`,
    )
  }
  if (!isActive) {
    throw new Error(
      `runVariantVsStandardCompare: variant id "${variantId}" exists but is inactive/reserved — only an ACTIVE Multi-QAF variant (Master-Prompt §6 activeVariants) may be compared against a standard QAF; an inactive/reserved slot carries no volume/identity to compare.`,
    )
  }

  const virtualVariants = generateVirtualVariants(container)
  const variant = virtualVariants.find((v) => v.variantId === variantId)
  if (!variant) {
    // Unreachable in practice (generateVirtualVariants produces exactly one
    // VirtualQafVariant per active+inactive VariantDefinition) — fail-closed
    // rather than silently proceeding with `undefined` if that invariant is
    // ever violated by a future container-assembly.ts change.
    throw new Error(
      `runVariantVsStandardCompare: variant id "${variantId}" is a known active VariantDefinition but generateVirtualVariants(container) produced no matching VirtualQafVariant — container-assembly.ts invariant violated.`,
    )
  }

  const bridged = toCanonicalInputs(variant)
  // bridge.ts's toCanonicalInputs always sets both fields (see that module's
  // own doc) — non-null assertion documents that contract rather than
  // widening this function's own return type to a Partial.
  const variantSummary = bridged.summary!
  const variantSummaryMetrics = bridged.summaryMetrics!

  const summaryIdentityIssues = checkVariantVsStandardIdentity(standardFile.summary, variantSummary)
  const formulaEngineEnabled = options.formulaEngineEnabled ?? DEFAULT_ENGINE_CONFIG.formulaEngine.enabled
  const summaryMetricsDiff = standardFile.summaryMetrics
    ? diffSummaryMetrics(standardFile.summaryMetrics, variantSummaryMetrics, formulaEngineEnabled)
    : []

  const containerReviewWarnings = container.warnings.filter((w) => w.severity === 'critical' || w.reviewRelevant === true)
  const variantBelowGate = variant.warnings.some((w) => w.severity === 'critical' || w.reviewRelevant === true)
  const containerReviewRequired = reviewStatusForMultiQafContainer(container) === 'review_required'
  // F3: a kritisch summary-identity issue must flip reviewRequired too — see
  // `hasCriticalIdentityIssue`'s own doc comment for why this stays correct
  // even though the F1 fix above means checkVariantVsStandardIdentity itself
  // never currently produces one.
  const criticalIdentityIssue = hasCriticalIdentityIssue(summaryIdentityIssues)

  const reviewRequiredReasons: VariantVsStandardReviewRequiredReason[] = []
  if (containerReviewRequired) reviewRequiredReasons.push('container_review_required')
  if (variantBelowGate) reviewRequiredReasons.push('variant_material_below_aggregation_gate')
  if (criticalIdentityIssue) reviewRequiredReasons.push('summary_identity_critical_issue')

  return {
    modelVersion: MULTI_QAF_VARIANT_VS_STANDARD_RESULT_VERSION,
    variantId,
    standardRef: standardFile.ref,
    summaryIdentityIssues,
    summaryMetricsDiff,
    degradedModules: DEGRADED_MODULES,
    reviewRequired: containerReviewRequired || variantBelowGate || criticalIdentityIssue,
    reviewRequiredReasons,
    variantWarnings: variant.warnings,
    containerReviewWarnings,
  }
}

// ── Versioned JSON envelope (same discipline as compare-flow.ts /
// serialization.ts) ─────────────────────────────────────────────────────────

interface VariantVsStandardComparisonResultEnvelope {
  modelVersion: number
  result: unknown
}

/** Serialize a `VariantVsStandardComparisonResult` to a versioned JSON string
 * — same envelope discipline as `serializeMultiQafComparisonResult`
 * (compare-flow.ts). */
export function serializeVariantVsStandardComparisonResult(result: VariantVsStandardComparisonResult): string {
  const envelope: VariantVsStandardComparisonResultEnvelope = { modelVersion: MULTI_QAF_VARIANT_VS_STANDARD_RESULT_VERSION, result }
  return JSON.stringify(envelope)
}

/**
 * Parse + rebuild a `VariantVsStandardComparisonResult` from a JSON string
 * produced by `serializeVariantVsStandardComparisonResult`. Throws under the
 * same conditions as `deserializeMultiQafComparisonResult` (bad envelope /
 * unsupported modelVersion / missing key) — same "only ever written by this
 * module's own serializer, never hand-crafted" contract, no deep
 * per-field normalization.
 */
export function deserializeVariantVsStandardComparisonResult(json: string): VariantVsStandardComparisonResult {
  const parsed: unknown = JSON.parse(json)
  if (parsed === null || typeof parsed !== 'object') {
    throw new Error('Invalid Multi-QAF variant-vs-standard comparison result JSON: expected an object envelope')
  }
  const envelope = parsed as Partial<VariantVsStandardComparisonResultEnvelope>
  if (envelope.modelVersion !== MULTI_QAF_VARIANT_VS_STANDARD_RESULT_VERSION) {
    throw new Error(
      `Unsupported Multi-QAF variant-vs-standard comparison result modelVersion: ${String(envelope.modelVersion)} (expected ${MULTI_QAF_VARIANT_VS_STANDARD_RESULT_VERSION})`,
    )
  }
  if (!('result' in envelope) || envelope.result === undefined) {
    throw new Error('Invalid Multi-QAF variant-vs-standard comparison result JSON: missing "result" key')
  }
  return envelope.result as VariantVsStandardComparisonResult
}
