// Multi-QAF Container Domain-Model (KAR-929 / Multi-QAF-Programm P1.1, Epic
// KAR-925).
//
// Problem this closes (30-backlog-phasenplan.md P1.1): Kadi-v2's existing
// canonical model (../types.ts QafSummary, ../summary-metrics.ts
// SummaryMetricsParse, ../compare.ts QafFileParsed) is structurally
// single-variant — one workbook, one part number, one value column per
// metric (see qaf-type-detector.ts module header: "locateAwColumn — genau
// EINE Werte-Spalte, strukturell Single-Variant"). A Multi-QAF workbook is a
// CONTAINER of several product variants sharing one material master, one set
// of manufacturing/setup-cost profiles, and a scattering of non-variant
// auxiliary columns — none of which the existing types can represent. This
// module is that container model: pure types + pure core functions
// (composite-key construction, active-state derivation, JSON
// serialization). No parsing (P1.2+), no DB migration, no ingest wiring —
// see module READMEs P1.2 (header-parser)/P1.3 (material-matrix)/P1.4
// (manufacturing-profiles)/P1.5 (formula-lineage)/P1.6 (fingerprint) for who
// actually POPULATES these types from a real workbook.
//
// Shape source: Master-Prompt §6 (00-master-prompt-multi-qaf.md) is the
// literal field list this module implements — field names below map 1:1
// onto that section's bullet points, translated into TypeScript identifiers.
// The 4 real-file analyses (10-analyse-clarwe-eu.md/-mx.md/-nafta.md/
// -ncar.md, sections C-K) are the empirical evidence for WHY each field
// needs the shape it has, cited inline below at each type that a specific
// real-file finding drove.
//
// Design discipline carried over from qaf-type-detector.ts and
// canonical-model.ts (both already-shipped Multi-QAF-programme modules):
//   - variantDimensions is NOT a closed union. Master-Prompt §6: "The
//     dimensions must remain extensible. Do not hardcode only the
//     dimensions present in these four files." VariantDefinition.dimensions
//     is a plain string-keyed Record; KNOWN_VARIANT_DIMENSION_KEYS below is
//     a documentation/typeahead aid only, never a validation whitelist (see
//     identity.ts buildCompositeCanonicalKey, which iterates it for stable
//     ordering but happily appends unknown keys too).
//   - JSON-serializable, additive, versioned — same persistence philosophy
//     as ../persistence-mapper.ts (plain objects/arrays, no Map/Set/Date
//     instances; Maps that would be natural in a richer type system
//     (quantityFactorByVariant, effectiveCostByVariant) are modeled as plain
//     `Record<string, T>` keyed by VariantDefinition.stableInternalId
//     instead, so serialization.ts needs no Map<->object bridge at all).
//   - Reuses existing engine types where the business meaning is identical
//     rather than re-declaring near-duplicates: `MoneyValue` (../types.ts)
//     for every currency-tagged amount, `FormulaProvenance` (../formula-
//     engine.ts) for the optional resolved-formula slot on
//     MultiQafFormulaValue.
//
// tdd-guard:skip — type declarations only, no logic (same category as
// ../types.ts and ../canonical-fields.types.ts). Logic lives in identity.ts,
// serialization.ts and bridge.ts, all covered by __tests__/.

import type { MoneyValue } from '../types'
import type { FormulaProvenance } from '../formula-engine'

/** Bumped on any breaking (non-additive) change to the persisted container
 * shape — see serialization.ts. Field ADDITIONS never require a bump (every
 * reader here already tolerates missing optional/array/record fields);
 * removing or repurposing a field does. */
export const MULTI_QAF_MODEL_VERSION = 1 as const

// ── Source cell provenance ──────────────────────────────────────────────────

/**
 * "Sheet!Zelle" provenance, the Multi-QAF-domain equivalent of ../types.ts's
 * `SourceRef` — kept as its own type rather than reusing SourceRef because
 * SourceRef.sheet is narrowed to the two standard-QAF sheet names
 * ('Zusammenfassung' | 'Fertigungskosten'), while Multi-QAF containers
 * reference an open set of custom sheet names (10-analyse-clarwe-eu.md B:
 * "LV Detail EU", "Rüstkosten EU", "SBM-SEKOF-FWZ", "BOM Detail EU"; // allow-customer-string
 * 10-analyse-mx.md B: "Vgl Material"; 10-analyse-nafta.md G: hidden
 * "Rüstkosten EU" with a variant set from a completely different vehicle
 * programme than the file's own active variants). */
export interface MultiQafCellRef {
  sheet: string
  /** A1 address (e.g. "Q14"), or null when the value's exact cell is not
   * individually addressable (e.g. a container-level aggregate). */
  cell: string | null
  row: number | null
  column: number | null
}

// ── Column letter helpers (also used by identity.ts / future P1.2 parser) ──

/** 1-based column index for an A1 column-letter string ("A" -> 1, "Q" -> 17,
 * "AF" -> 32). Throws on an empty/invalid string — callers always have a
 * real header cell address at hand (this is a domain-model helper, not an
 * input-validation boundary). */
export function columnLetterToIndex(letters: string): number {
  const upper = letters.trim().toUpperCase()
  if (!/^[A-Z]+$/.test(upper)) throw new Error(`Invalid column letters: "${letters}"`)
  let n = 0
  for (const ch of upper) n = n * 26 + (ch.charCodeAt(0) - 64)
  return n
}

/** Inverse of columnLetterToIndex (1-based). */
export function columnIndexToLetter(index: number): string {
  if (!Number.isInteger(index) || index < 1) throw new Error(`Invalid column index: ${index}`)
  let n = index
  let out = ''
  while (n > 0) {
    const rem = (n - 1) % 26
    out = String.fromCharCode(65 + rem) + out
    n = Math.floor((n - 1) / 26)
  }
  return out
}

// ── Formula + cached value (lightweight — see FormulaProvenance note) ──────

/**
 * A formula/cached-value pair as read from one cell, for the Multi-QAF
 * container's own light-touch bookkeeping (Master-Prompt §13: "Compare
 * formulas and cached values separately. A matching cached value does not
 * make a changed formula equivalent."). Deliberately NOT the same
 * granularity as ../formula-engine.ts's full comparison pipeline
 * (buildFormulaProvenance/compareFormulaPair, hash-based structural diff) —
 * that pipeline runs at COMPARE time on two already-extracted values; this
 * type only carries what a P1.2+ parser reads once at INGEST/parse time.
 * `provenance` is populated once a caller wants the full hash-based
 * comparison (P2.6 Rekonziliation) by calling buildFormulaProvenance(raw)
 * itself — left optional/undefined here so this module does not need to
 * eagerly hash every cell it touches.
 *
 * Real-file evidence for why BOTH slots matter independently:
 * 10-analyse-mx.md D: `Zusammenfassung!AB15` = a literal number with NO
 * formula at all (`formula: null`, `cachedValue: 282.138...`) while sibling
 * cells use `=Material!R125` (`formula: '=Material!R125'`,
 * `cachedValue: <resolved number>`) — the presence/absence of `formula`
 * alone is the "manually overridden, no live calculation" signal Master-
 * Prompt §13 calls for.
 */
export interface MultiQafFormulaValue {
  formula: string | null
  cachedValue: number | string | null
  provenance?: FormulaProvenance
}

// ── Money amount ─────────────────────────────────────────────────────────

/** Re-exported alias so multi-qaf/ call sites don't need to reach into
 * ../types.ts directly for the one shared money shape (value + currency). */
export type MultiQafMoneyAmount = MoneyValue

// ── Variant dimensions ──────────────────────────────────────────────────────

/**
 * Dimension keys observed across the 4 analyzed real Multi-QAF families
 * (10-analyse-*.md section C, cross-referenced in 30-backlog-phasenplan.md
 * "Querschnitts-Wahrheiten") — NOT an exhaustive/closed set (Master-Prompt
 * §6: "The dimensions must remain extensible"). Used only as a
 * documentation/typeahead aid and by identity.ts's canonical-key builder
 * (stable ordering for the subset it recognizes, then alphabetical for
 * everything else) — never as a validation whitelist. A newly discovered
 * dimension key needs zero code change here: it is simply added to a
 * VariantDefinition.dimensions record with a fresh string key.
 */
export const KNOWN_VARIANT_DIMENSION_KEYS = [
  'project',
  'vehicle',
  'platform',
  'region',
  'driveType',
  'steeringSide',
  'ecuType',
  'rackType',
  'motor',
  'scu',
  'fitClass',
  'asilClass',
  'transmissionRatio',
  /** "LC1"/"LC2" local-content classification (10-analyse-ncar.md D:
   * "Local-Content-Stufe 1/2"). */
  'localContentLevel',
  /** "con."/"var." transmission-ratio-constancy axis (10-analyse-nafta.md C,
   * cell-literal values "con."/"var." on the "Übersetzung" row), distinct
   * from transmissionRatio itself. */
  'ratioConstancy',
] as const

export type KnownVariantDimensionKey = (typeof KNOWN_VARIANT_DIMENSION_KEYS)[number]

/** One dimension's value for one variant: the raw header text as read, the
 * normalized comparison key (identity.ts normalizeDimensionValue), and where
 * it was read from — a Multi-Row-Header-Block spreads dimensions across
 * several physical rows (10-analyse-ncar.md D: "Header über 9 Zeilen (4-12)"),
 * so per-dimension provenance (not just one shared header cell) matters. */
export interface VariantDimensionValue {
  raw: string
  normalized: string
  sourceCell: MultiQafCellRef | null
}

/** Open dimension map — see KNOWN_VARIANT_DIMENSION_KEYS doc. */
export type VariantDimensions = Readonly<Record<string, VariantDimensionValue>>

/**
 * Container-level description of one dimension AS OBSERVED IN THIS
 * WORKBOOK — which physical header row it lives in, its header label, which
 * sheet. Distinct from VariantDefinition.dimensions (the per-variant VALUE):
 * this is metadata about the dimension COLUMN/ROW itself, needed because a
 * Multi-Row-Header-Block parser (P1.2) must know "row 6 in Material is the
 * Antrieb/drive-type row" before it can read any variant's value out of it.
 */
export interface VariantDimensionDescriptor {
  key: string
  headerRow: number | null
  label: string | null
  sheet: string | null
}

// ── Variant active state ────────────────────────────────────────────────────

/**
 * Master-Prompt §10 column classification, active/inactive/reserved slice
 * (the remaining 8 non-variant classifications live in ColumnClassification
 * below). Derivation (identity.ts deriveActiveState), calibrated against the
 * MX empirical pattern (10-analyse-mx.md C: "10 von 25 Slots real befüllt
 * mit Volumen, 12 von 25 mit Identität, 13 von 25 nutzlos-leer" — i.e. of 25
 * reserved slot positions, 12 carry a variant identity, of those 10 also
 * carry volume>0, and 13 carry neither):
 *   - 'active':   has identity AND annualVolume/peakVolume/lifetimeVolume>0.
 *   - 'inactive': has identity but no positive volume on any volume field
 *     (MX slots 4/8, Q column: "Header nur, kein Volumen").
 *   - 'reserved': no identity at all — a numbered slot position with no
 *     header data filled in (MX slots 13-25: "vollständig leer (nur
 *     Index-Zahl in Row 2)").
 */
export type VariantActiveState = 'active' | 'inactive' | 'reserved'

// ── VariantDefinition (Master-Prompt §6) ────────────────────────────────────

export interface VariantDefinition {
  /** Stable across re-parses of the SAME workbook and across template
   * reorders/renames (Master-Prompt §9) — NOT the column letter (a variant
   * cannot be identified only by its column, per §9). Generated as a
   * deterministic function of compositeCanonicalKey where possible, falling
   * back to a positional id ("slot-<n>") only for reserved/no-identity slots
   * that have nothing else to key off — see identity.ts. */
  stableInternalId: string
  /** A1 column letter this variant's data currently occupies in the
   * SUMMARY/Zusammenfassung sheet (e.g. "Q", "AF") — positional, NOT part of
   * the identity (§9), kept purely for provenance/UI display. */
  originalColumn: string
  originalColumnIndex: number
  /** The slot/variant number as printed in the workbook, if any — the
   * wide-slot pattern's Row-2 index ("1".."25", with gaps: 10-analyse-mx.md
   * C) or an explicit variant code ("V201_LC1": 10-analyse-ncar.md D,
   * structural pattern — see synthetic-fixtures.ts for the invented code
   * used in this PR's fixtures). Distinct from compositeCanonicalKey —
   * Master-Prompt §9 explicitly forbids using this alone as identity, since
   * it is reassigned on reorder. */
  originalVariantNumber: string | null
  /** Raw header text(s) as read, one entry per header row that contributed
   * (Multi-Row-Header-Block, up to 9 rows deep per 10-analyse-ncar.md D). */
  originalLabels: readonly string[]
  normalizedLabels: readonly string[]
  /** Deterministic identity built from `dimensions` (identity.ts
   * buildCompositeCanonicalKey) — survives column reorder/rename (§9),
   * unlike originalColumn/originalVariantNumber above. */
  compositeCanonicalKey: string
  dimensions: VariantDimensions
  annualVolume: number | null
  peakVolume: number | null
  lifetimeVolume: number | null
  currency: string | null
  activeState: VariantActiveState
  sourceReferences: readonly MultiQafCellRef[]
  /** 0..1 heuristic (same "deliberately coarse, not calibrated" category as
   * qaf-type-detector.ts's MultiQafDetectionResult.confidence) — how
   * confident the parser that produced this definition is in the identity +
   * activeState classification above. */
  confidence: number
}

// ── VariantMatrixRow (Master-Prompt §6 "VariantMatrix") ─────────────────────

/** Master-Prompt §17/§22 "hidden helper sheet"/"reconciliation" outcome for
 * one shared-material-master row. Not an exhaustive validation engine (that
 * is P2.6/P1.3) — a coarse classification a P1.3 parser assigns while
 * reading the row, so container-level consumers can filter/flag without
 * re-deriving it. */
export type VariantMatrixRowValidationStatus =
  | 'ok'
  | 'missing_unit_cost'
  | 'missing_factor_for_all_variants'
  | 'formula_error_cached'
  | 'orphaned_no_summary_link'
  | 'needs_review'

/**
 * One shared-cost-item row from the material master, with its per-variant
 * quantity-factor and effective-cost matrix (Master-Prompt §6/§11.2,
 * 10-analyse-clarwe-eu.md D, 10-analyse-mx.md F, 10-analyse-nafta.md D,
 * 10-analyse-ncar.md E — all 4 files independently confirm the same
 * "shared position list + per-variant factor column" pattern).
 * quantityFactorByVariant/effectiveCostByVariant are keyed by
 * VariantDefinition.stableInternalId (plain Record, not a Map — see module
 * header "additive... JSON-serializable" note); a variant with NO entry for
 * a given row means "not applicable to this variant" (10-analyse-nafta.md D:
 * a material position tied to only one of the two platform families has
 * factors only in that family's variant columns, none in the other
 * family's) — distinct from an entry with value `0`.
 */
export interface VariantMatrixRow {
  /** Stable identity for this material position — NOT just the row number
   * (Master-Prompt §11.1 "row identity"; 10-analyse-clarwe-eu.md J.10:
   * position number + part designation, not raw row index, since rows can
   * be inserted/deleted/reordered between file versions, §8). */
  canonicalComponentIdentity: string
  sourceRow: number
  unitCost: MultiQafMoneyAmount
  procurementCurrency: string | null
  offerCurrency: string | null
  exchangeRate: number | null
  logisticsOrDuty: MultiQafMoneyAmount | null
  materialOverhead: MultiQafMoneyAmount | null
  quantityFactorByVariant: Readonly<Record<string, number>>
  effectiveCostByVariant: Readonly<Record<string, number | null>>
  formulaAndCachedValue: MultiQafFormulaValue | null
  sourceCells: readonly MultiQafCellRef[]
  validationStatus: VariantMatrixRowValidationStatus
}

// ── SharedCostProfile / VariantProfileBinding (Master-Prompt §6/§12) ───────

export type SharedCostProfileKind =
  | 'manufacturing'
  | 'volumeBand'
  | 'lShape'
  | 'iShape'
  | 'setupCost'
  | 'location'
  | 'currency'
  | 'commonTooling'

/**
 * A shared cost structure multiple variants reference instead of each
 * carrying its own independent process list (Master-Prompt §12,
 * 10-analyse-nafta.md E: all 5 variants in that file reference the identical
 * absolute manufacturing-cost cell; 10-analyse-mx.md G: L-Shape is a
 * live SUMIF, I-Shape is a hardcoded literal outside the formula chain —
 * `values`/`formulaAndCachedValue` together capture both cases without
 * forcing a single representation). `values` is intentionally an open,
 * free-form record (not a fixed field set) because profile CONTENT varies
 * widely by kind — a manufacturing profile carries cycle-time/labor-rate/
 * machine-hour-rate, a volumeBand carries only a threshold, a currency
 * profile carries only an exchange rate. P1.4 (Fertigungs-/Rüst-Profile
 * parser) defines the actual key vocabulary per kind; this module only
 * defines the container shape.
 */
export interface SharedCostProfile {
  profileId: string
  kind: SharedCostProfileKind
  label: string | null
  sheet: string
  values: Readonly<Record<string, number | string | null>>
  formulaAndCachedValue: MultiQafFormulaValue | null
  sourceReferences: readonly MultiQafCellRef[]
  confidence: number
}

export type ProfileBindingEvidenceKind = 'cellReference' | 'literal' | 'formula'

/**
 * Which variant uses which profile, and why (Master-Prompt §6: "A change to
 * the profile and a change to the binding must be reported separately").
 * `bindingEvidence.kind` distinguishes the 3 real binding mechanisms found
 * across the 4 files: 10-analyse-ncar.md F/E-parallel — hardcoded absolute
 * cellReference per variant (`=Fertigungskosten!$U$34`, manually set, NOT a
 * formula-derived lookup); 10-analyse-mx.md D — literal (AB column's
 * Fertigungskosten value has no source cell at all, `46.388...` typed
 * directly); 10-analyse-clarwe-eu.md F — formula (`='LV Detail EU'!$U$52+
 * 'Rüstkosten EU'!<col>15`, a live additive formula, not a single reference).
 */
export interface VariantProfileBinding {
  variantId: string
  profileId: string
  bindingEvidence: {
    kind: ProfileBindingEvidenceKind
    source: MultiQafCellRef | null
    detail: string
  }
}

// ── Column classification (Master-Prompt §10) ───────────────────────────────

export type ColumnClassificationKind =
  | 'active_product_variant'
  | 'inactive_product_variant'
  | 'reserved_placeholder'
  | 'benchmark_scenario'
  | 'comparison_scenario'
  | 'delta_column'
  | 'percentage_delta_column'
  | 'comment_column'
  | 'helper_calculation'
  | 'total'
  | 'unknown'

/** One classification signal (Master-Prompt §10: "Use: header semantics,
 * formula lineage, participation in the material matrix, connection to
 * summary totals, volume data, variant dimensions, presence in the
 * manufacturing mapping, known template profile, neighbouring-column
 * context"). `signal` is a short greppable tag (free string, same
 * "convention over enum" choice as ../canonical-fields.types.ts
 * CanonicalFieldEvidence.source), `detail` the human-readable evidence. */
export interface ColumnClassificationEvidence {
  signal: string
  detail: string
}

/**
 * Full classification of one candidate column — the Master-Prompt §10
 * "each candidate column must be classified as one of" list. Used both for
 * the definitive per-column scan report AND (filtered by kind) to populate
 * MultiQafContainer.auxiliaryScenarios/helperColumns below, so this single
 * type serves double duty rather than forcing two near-identical shapes.
 * `relatedVariantIds` is set for delta/percentage-delta columns that derive
 * from exactly one or two other (real or benchmark) variants — e.g.
 * 10-analyse-mx.md D's benchmark-vs-lead-variant delta column, a pure
 * difference formula relating a comparison-scenario column to two real
 * variant slots.
 */
export interface ColumnClassification {
  column: string
  columnIndex: number
  kind: ColumnClassificationKind
  confidence: number
  evidence: readonly ColumnClassificationEvidence[]
  relatedVariantIds?: readonly string[]
}

// ── Virtual variant (Master-Prompt §6/§18) ──────────────────────────────────

export interface VirtualVariantMaterialRow {
  canonicalComponentIdentity: string
  unitCost: MultiQafMoneyAmount
  quantityFactor: number | null
  effectiveCost: MultiQafMoneyAmount
  sourceCells: readonly MultiQafCellRef[]
}

/** Master-Prompt §13 "For every virtual variant, independently verify"
 * list — the per-variant summary totals a P2.6 reconciliation pass checks
 * against detail-sheet sums. Deliberately narrower than the full ../types.ts
 * QafSummary/SummaryMetricsParse vocabulary (see bridge.ts module header for
 * exactly which of these two shapes maps onto which canonical field, and
 * which canonical fields have no Multi-QAF-side source at all). */
export interface VirtualVariantSummaryTotals {
  variantId: string
  materialCosts: MultiQafMoneyAmount | null
  /** Per-currency breakdown of the same material rows `materialCosts` sums
   * (KAR-935 adversarial review F1 fix) — always populated when the variant
   * has at least one material row with a known cost, REGARDLESS of whether
   * `materialCosts` itself is null. One entry per distinct currency observed
   * across `materialRows[].effectiveCost` (plus an entry with `currency:
   * null` when at least one row's currency itself is unknown). When this
   * array has more than one entry with a non-null currency, `materialCosts`
   * is null (Master-Prompt §7 "never force an interpretation" — summing
   * mismatched currencies numerically would silently fabricate a wrong
   * total labeled with whichever currency happened to be found first, which
   * is exactly the bug this field's introduction fixes) and a
   * 'mixed_currency_material_costs' warning is attached to this variant's
   * own `.warnings`. */
  materialCostsByCurrency: readonly MultiQafMoneyAmount[]
  manufacturingCosts: MultiQafMoneyAmount | null
  totalProductionCosts: MultiQafMoneyAmount | null
  toolingAndFixtureCost: MultiQafMoneyAmount | null
  setupCostAllocation: MultiQafMoneyAmount | null
  /** Master-Prompt §13 lists "scrap" as a single line (unlike the canonical
   * registry's materialScrap/manufacturingScrap split) — see bridge.ts for
   * how this single value maps onto the two canonical metric keys. */
  scrap: MultiQafMoneyAmount | null
  otherSurcharges: MultiQafMoneyAmount | null
  offerBasePrice: MultiQafMoneyAmount | null
  /** KAR-951 F1 fix (review finding): the Summary sheet's OWN
   * "ANGEBOTSBASISPREIS inkl. Umlage" row — the live-formula final base
   * price INCLUDING the vorrichtungs-Umlage (fixture-cost allocation)
   * surcharge. This is a DIFFERENT, real-file-observed row from
   * `offerBasePrice`'s own "ANGEBOTSBASISPREIS" anchor (row 34 vs. row 38
   * in the KAR-951 livetest corpus), NOT a duplicate/alias of it — in that
   * corpus the "ANGEBOTSBASISPREIS" row is empty on all 26 variants (so
   * `offerBasePrice` honestly stays `nicht_ermittelbar` there) while THIS
   * row is the one that actually changes when a supplier only edits the
   * Umlage surcharge. Deliberately NOT merged onto `offerBasePrice` or
   * `offerPrice` (see profile-parser.ts's `SUMMARY_MONEY_ROW_LABEL_
   * SYNONYMS.offerBasePriceInclAllocation` for the label anchor, and
   * bridge.ts's own module header for why this field has no canonical
   * ../summary-metrics.ts equivalent to map onto). */
  offerBasePriceInclAllocation: MultiQafMoneyAmount | null
  offerPrice: MultiQafMoneyAmount | null
}

/**
 * Master-Prompt §6 VirtualQafVariant: "Create a normalized single-variant
 * representation from: shared container data, the variant definition,
 * variant-specific material quantities, selected manufacturing profile,
 * selected setup-cost profile, selected tooling data, calculated summary
 * values." This is the ONE object per active variant that bridge.ts's
 * toCanonicalInputs() turns into (a partial) ../compare.ts QafFileParsed —
 * see that module for the exact, honestly-scoped mapping.
 */
export interface VirtualQafVariant {
  /** == definition.stableInternalId; duplicated at the top level so callers
   * that only hold a VirtualQafVariant[] (not the whole container) don't
   * need to reach into `.definition` for the join key. */
  variantId: string
  /** Ties back to the source container without an actual object reference /
   * circular structure (== container.templateFingerprint.structuralHash,
   * null when the container itself has none yet). */
  containerFingerprint: string | null
  definition: VariantDefinition
  materialRows: readonly VirtualVariantMaterialRow[]
  selectedManufacturingProfile: SharedCostProfile | null
  selectedSetupCostProfile: SharedCostProfile | null
  selectedToolingProfile: SharedCostProfile | null
  summaryTotals: VirtualVariantSummaryTotals
  warnings: readonly MultiQafWarning[]
}

// ── Container-level warnings ────────────────────────────────────────────────

export type MultiQafWarningSeverity = 'info' | 'warning' | 'critical'

/** Master-Prompt §6 "warnings and confidence" / §16 external-link-safety /
 * §17 hidden-sheet findings — one bilingual-ready structured warning.
 * `code` is a stable, machine-greppable identifier (e.g.
 * 'orphaned_variant_column', 'external_link_present',
 * 'formula_column_range_inconsistency' — see 10-analyse-clarwe-eu.md I.4 for
 * the real-file finding that last code names) so UI/tests can match on it
 * without parsing message text. */
export interface MultiQafWarning {
  code: string
  severity: MultiQafWarningSeverity
  message: string
  messageEn?: string
  sourceReferences: readonly MultiQafCellRef[]
  /** stableInternalId(s) of the VariantDefinition(s) this warning concerns,
   * when the producer knows them at construction time (KAR-935 adversarial
   * review F5 fix). This is the STRUCTURAL replacement for the old
   * "message.startsWith('Variante X:')" convention `variantWarningsFor`
   * (container-assembly.ts) used to rely on exclusively — that string-prefix
   * match never covers a warning phrased differently (e.g. identity.ts's own
   * `detectCanonicalKeyCollisions`, which lists every colliding
   * stableInternalId in its message but does not start with the prefix).
   * Omitted (not `[]`) for warnings that are genuinely workbook-level/not
   * attributable to specific variants — an empty array would falsely claim
   * "checked, matches none" for a warning that was never variant-scoped to
   * begin with. Producers SHOULD set this going forward; the DE message-
   * prefix match in `variantWarningsFor` remains a legacy fallback for
   * warnings that don't set it yet. */
  variantIds?: readonly string[]
  /** True when this warning represents a genuine identity/aggregation
   * ambiguity a human should look at before the container's numbers are
   * trusted (KAR-935 adversarial review F4 fix) — canonical-key collisions,
   * material variants that could not be matched to any Summary variant
   * identity, ambiguous cost-profile bindings. Deliberately a SEPARATE axis
   * from `severity`: these cases are not necessarily "critical" in the
   * blocking-failure sense `severity` already carries (no producer in this
   * package has ever needed that), they are "ambiguous, needs a judgment
   * call" — conflating the two would either water down `severity: 'critical'`
   * or leave `review_status` gating on a branch no producer ever reaches
   * (exactly the dead-code bug this field fixes: actions.ts's
   * `review_status` gate used to check `severity === 'critical'` only). */
  reviewRelevant?: boolean
}

// ── Container-level summary aggregation ─────────────────────────────────────

/**
 * Master-Prompt §6 "summary aggregation" bullet — per-variant summary
 * totals as read from the container, for container-level reconciliation
 * (§13/§14). Explicitly NOT the §19 "aggregate commercial impact" (that is
 * P3.3, gated behind the §19 validity checks this module does not
 * implement) — `perVariant` here is a flat per-variant record list, never
 * summed across variants by this module.
 */
export interface MultiQafSummaryAggregation {
  perVariant: readonly VirtualVariantSummaryTotals[]
  /** Free-text notes about formula lineage anomalies discovered while
   * building the aggregation (e.g. 10-analyse-clarwe-eu.md C.3's SUMIF-over-
   * 28-vs-AVERAGE-over-26-columns inconsistency) — populated by P1.5
   * (Formel-Lineage-Modul); always empty from this module's own pure
   * functions, which only define the shape. */
  formulaLineageNotes: readonly string[]
}

// ── Template fingerprint ────────────────────────────────────────────────────

export type MultiQafTemplateFamily =
  /** QAF 8.1 base + bespoke Multi-QAF sheets bolted on (not a dedicated
   * M-QAF marker version — 10-analyse-clarwe-eu.md never found an
   * "M-QAF Version" marker, the Multi-QAF-ness is purely structural). */
  | 'qaf_8_1_custom_multi'
  | 'm_qaf_1_0'
  | 'm_qaf_2_0'
  /** No family/profile ever matched (KAR-934/P1.6, multi-qaf/template-
   * fingerprint.ts) — distinct from `'unknown'` below, which means "this
   * facet was never computed at all" (serialization.ts's own EMPTY_
   * default/normalizeDeserializedContainer fallback). `'unknown_multi_qaf'`
   * means the opposite: qaf-type-detector.ts's detectMultiQaf DID find
   * Multi-QAF-shaped evidence (confirmed/probable/ambiguous), a fingerprint
   * WAS attempted, and it still matched none of the three known families
   * above — Master-Prompt §8: "Never silently treat an unknown Multi-QAF
   * template as identical to a known one." Kept as its own value so a caller
   * can tell "we never looked" apart from "we looked and genuinely don't
   * recognize this shape" without inspecting any other field. */
  | 'unknown_multi_qaf'
  | 'unknown'

/**
 * Known/modified/unknown verdict for a Multi-QAF template-fingerprint
 * classification (template-fingerprint.ts's own MultiQafFamilyClassification
 * — declared HERE, not there, so toMultiQafContainerFingerprint can persist
 * it on MultiQafTemplateFingerprint below without a circular import; that
 * module re-exports this type verbatim for its own callers). 'unknown' means
 * either the family itself is unresolved (family === 'unknown_multi_qaf')
 * or a resolved family had no usable variant structure to assess
 * known/modified against (template-fingerprint.ts's own "hard-unknown"
 * branch) — see that module's fingerprintMultiQafTemplate doc.
 */
export type MultiQafFamilyClassification = 'known' | 'modified' | 'unknown'

/**
 * Structural + semantic fingerprint (Master-Prompt §8: "Build and store a
 * structural and semantic fingerprint for every detected template. Never
 * silently treat an unknown Multi-QAF template as identical to a known
 * one."). Deliberately lighter than ../template-fingerprint.ts's full
 * TemplateFingerprintResult (that module fingerprints STANDARD QAF
 * templates at a per-field-mapping-coverage granularity) — P1.6 extends
 * this shape once a Multi-QAF-specific fingerprinting pass exists; this
 * module only defines the container-level slot for it.
 */
export interface MultiQafTemplateFingerprint {
  family: MultiQafTemplateFamily
  /** Opaque structural hash (sheet names + variant-column count + header-row
   * count, etc.) — computed by a P1.6 fingerprinting function; null until
   * then. Never guessed/fabricated by this module's own pure functions. */
  structuralHash: string | null
  variantColumnCount: number
  headerRowCount: number
  /** Matched known-template name (e.g. an entry name from a future
   * KNOWN_MULTI_QAF_TEMPLATE_PROFILES list, mirroring ../template-
   * fingerprint.ts's KNOWN_TEMPLATE_PROFILES) — null when no known profile
   * matched (Master-Prompt §8: a template profile "must not be the sole
   * parsing mechanism", i.e. this being null must never block parsing). */
  knownProfile: string | null
  /** template-fingerprint.ts's own MultiQafTemplateFingerprintResult.
   * classification, carried through verbatim — null only when this
   * container field was never computed at all (KAR-934 adversarial review
   * F2 fix: `family` alone is NOT sufficient for a downstream reader to tell
   * "confidently recognized" apart from "marker matched but structurally
   * unconfirmed" — a persisted family with no accompanying classification
   * must never be read as "known"). */
  classification: MultiQafFamilyClassification | null
}

// ── Source workbook metadata + shared metadata ──────────────────────────────

export interface MultiQafSourceWorkbookMeta {
  /** Kept ONLY for audit-trail display (Master-Prompt §20 "original
   * source-file hash" + file name for human readability) — NEVER read by any
   * detection/classification logic in this module or qaf-type-detector.ts
   * (Master-Prompt §7: "Do not detect Multi-QAF only from the filename",
   * a discipline this module extends to itself: fileName is provenance-only
   * data, not a signal input to any function here). */
  fileName: string | null
  fileHash: string | null
  sheetNames: readonly string[]
  hiddenSheetNames: readonly string[]
}

/** One shared (non-variant-specific) metadata value read from the container
 * (supplier, project, quotation date, ...) with its source cell. */
export interface MultiQafMetadataValue {
  value: string | null
  sourceCell: MultiQafCellRef | null
}

// ── MultiQafContainer (Master-Prompt §6) ────────────────────────────────────

export type MultiQafLanguage = 'de' | 'en' | 'mixed' | 'unknown'

/**
 * The top-level Multi-QAF domain object — everything ../compare.ts's
 * StandardQafAdapter path has no concept of. Field order below mirrors
 * Master-Prompt §6's bullet list order exactly for easy cross-reading.
 */
export interface MultiQafContainer {
  sourceWorkbook: MultiQafSourceWorkbookMeta
  detectedTemplateFamily: MultiQafTemplateFamily
  /** Raw matched M-QAF version marker text (qaf-type-detector.ts S1 signal,
   * e.g. `"M-QAF Version 2.0 "` with a real trailing space — 10-analyse-
   * mx.md I.1), or null when no marker was found (the split-column-pattern
   * source file has none — detectedTemplateFamily alone carries that file's
   * classification). */
  multiQafVersion: string | null
  /** The underlying (non-Multi) QAF template version this container is
   * built on top of (e.g. "8.1") — distinct from multiQafVersion. */
  underlyingQafVersion: string | null
  language: MultiQafLanguage
  /** Distinct currencies observed anywhere in the container (material unit
   * costs, quotation currency, manufacturing profile currency, ...) — a
   * Multi-QAF container is NOT guaranteed single-currency (10-analyse-
   * nafta.md C: the file's own region-suggesting name diverges from its
   * actual manufacturing-site currency; content is CNY throughout). */
  currencies: readonly string[]
  sharedMetadata: Readonly<Record<string, MultiQafMetadataValue>>
  variantDimensions: readonly VariantDimensionDescriptor[]
  activeVariants: readonly VariantDefinition[]
  /** Master-Prompt §6 only names a binary active/inactive split at the
   * container level (unlike §10's 3-way active/inactive/reserved column
   * taxonomy) — this bucket holds every VariantDefinition whose own
   * `activeState` is EITHER 'inactive' OR 'reserved'; the definition's own
   * `activeState` field (identity.ts deriveActiveState) is what
   * disambiguates "has identity but no volume" from "no identity at all"
   * within it. A parser is free to omit fully-'reserved' (no-identity-at-
   * all) slots from this array entirely rather than materializing one
   * VariantDefinition per empty slot position — both representations are
   * valid; __tests__/synthetic-fixtures.ts demonstrates the fuller
   * (materialized) form for the wide-slot-pattern fixture. */
  inactiveVariants: readonly VariantDefinition[]
  /** Master-Prompt §10 benchmark_scenario/comparison_scenario columns —
   * e.g. 10-analyse-mx.md D's hardcoded benchmark column (own summed cost
   * chain but no material/manufacturing basis in this workbook) and its
   * paired pure-delta comparison column (relates two REAL variants, no own
   * data basis). */
  auxiliaryScenarios: readonly ColumnClassification[]
  /** Master-Prompt §10 delta_column/percentage_delta_column/comment_column/
   * helper_calculation columns — e.g. 10-analyse-mx.md D's AC/AD/AE/AW/AX/AU
   * helper-delta clusters around the AB/AV scenario columns above. */
  helperColumns: readonly ColumnClassification[]
  sharedMaterialMaster: readonly VariantMatrixRow[]
  sharedManufacturingProfiles: readonly SharedCostProfile[]
  sharedToolingData: readonly SharedCostProfile[]
  setupCostProfiles: readonly SharedCostProfile[]
  /** Not itself a Master-Prompt §6 MultiQafContainer bullet (the master
   * prompt's own architecture diagram in §5 lists "Variant-profile bindings"
   * as a peer box alongside variant definitions/matrix/shared components/
   * shared cost profiles under the container) — included here so the
   * container is a single self-contained, serializable unit rather than
   * requiring a caller to separately thread bindings alongside it. */
  variantProfileBindings: readonly VariantProfileBinding[]
  summaryAggregation: MultiQafSummaryAggregation
  templateFingerprint: MultiQafTemplateFingerprint
  warnings: readonly MultiQafWarning[]
  /** 0..1 heuristic — overall container-parse confidence (same "coarse, not
   * calibrated" category as VariantDefinition.confidence /
   * qaf-type-detector.ts's MultiQafDetectionResult.confidence). */
  confidence: number
  /** KAR-951 — the Summary-sheet-sourced VirtualVariantSummaryTotals fields
   * (scrap/otherSurcharges/offerBasePrice/offerBasePriceInclAllocation/
   * offerPrice — the 4th grew to 5 with the KAR-951 F1 adversarial-review
   * fix), keyed by `VariantDefinition.stableInternalId`, WITH each value's
   * source cell.
   * Populated once by container-assembly.ts's Summary-row extraction step
   * (profile-parser.ts `locateSummaryMoneyRow`/`extractSummaryMoneyRow`) and
   * read purely by `generateVirtualVariants` (same "container's own
   * already-finalized fields, no workbook re-read" contract every other
   * `generateVirtualVariants` input already follows) — this is what lets
   * `generateVirtualVariants` be called a second time, from an
   * already-assembled container, without re-reading the workbook (see
   * `resolveMultiQafContainerIngest`'s own second call). Kept as its OWN
   * sibling container field rather than merged directly into
   * VirtualVariantSummaryTotals: that type's money fields carry no per-field
   * cell provenance at all today, and it is constructed as a plain object
   * literal across ~30 test files in this package — adding a required field
   * there would ripple through every one of them for a provenance detail
   * only summary-totals-differ.ts's `sourceRefs` actually needs. A variant
   * id absent from this record, or a per-metric value `null`, means that
   * metric's row could not be located on the Summary sheet at all, or this
   * variant had no value on an otherwise-located row — never fabricated. */
  summaryMoneyRowsByVariant: Readonly<Record<string, VirtualVariantSummaryMoneyRows>>
}

/** See MultiQafContainer.summaryMoneyRowsByVariant's own doc comment. */
export interface SummaryMoneyRowSnapshot {
  amount: MultiQafMoneyAmount
  sourceCell: MultiQafCellRef
}

/** See MultiQafContainer.summaryMoneyRowsByVariant's own doc comment. */
export interface VirtualVariantSummaryMoneyRows {
  scrap: SummaryMoneyRowSnapshot | null
  otherSurcharges: SummaryMoneyRowSnapshot | null
  offerBasePrice: SummaryMoneyRowSnapshot | null
  /** KAR-951 F1 fix — the Summary sheet's own "ANGEBOTSBASISPREIS inkl.
   * Umlage" row (final base price INCLUDING the vorrichtungs-Umlage
   * surcharge), a DIFFERENT row from `offerBasePrice`'s own
   * "ANGEBOTSBASISPREIS" anchor — see VirtualVariantSummaryTotals.
   * offerBasePriceInclAllocation's own doc comment for why the two are
   * never conflated. */
  offerBasePriceInclAllocation: SummaryMoneyRowSnapshot | null
  offerPrice: SummaryMoneyRowSnapshot | null
}
