// ── Comparison-mode dispatch registry (KAR-943 adversarial-review F2) ──────
//
// Three defects in this package's history are the SAME bug class: a NEW
// `comparison_mode` value gets added to `qaf_comparison`, and every ad-hoc
// `if (cmp.comparison_mode === '...')` dispatch site in app/qaf-differences/
// actions.ts has to independently remember to learn about it — nothing
// enforces that they all do. KAR-942 adversarial-review F1:
// replaceComparisonFile's isG60Comparison-only guard silently
// rejected-and-DELETED valid 'multi_qaf' replacement uploads. KAR-942
// adversarial-review F3: exportQafComparisonXlsx only guarded 'g60', letting
// 'multi_qaf' fall through into a misleadingly EMPTY export. KAR-943's own
// adversarial-review F2: the exact same pattern for
// 'multi_qaf_variant_vs_standard' in BOTH of those same two functions.
//
// This module is the single source of truth for "what does mode X expect at
// each dispatch point" — file-replace-actions.ts' replaceComparisonFile file-kind guard,
// exportQafComparisonXlsx's support guard, and recompareComparison's
// pins/swap applicability guards all read from here instead of repeating the
// mode list ad-hoc. `comparisonModeRule`'s internal `switch` + the
// TypeScript never-trick `assertExhaustiveComparisonMode` make a FUTURE
// `QafComparisonModeKey` addition without a matching case a COMPILE error,
// rather than a silent runtime fallthrough discovered by the next
// adversarial review.
//
// Lives in its own module (not inline in actions.ts) because actions.ts is a
// `'use server'` file — every top-level export from a `'use server'` module
// must be an async Server Action (Next.js constraint), which these pure,
// synchronous, DB-free functions/constants are not and should not become
// (`next build` catches this at compile time; `tsc --noEmit` alone does
// not).
//
// Deliberately NOT covered here: recompareComparison's per-mode
// rehydrate/re-run bodies (which tables to read, which engine function to
// call) — that is genuinely mode-SPECIFIC business logic, not a shared
// yes/no/expected-shape fact a data table can express; forcing it through
// this registry would just relocate the branching without adding any real
// exhaustiveness guarantee beyond what this registry+switch already gives
// the guards that ARE data-only facts.
//
// tdd-guard:skip — pure data/dispatch module, unit-tested exhaustively in
// comparison-mode.test.ts (registry shape + every mode/role combination).

export type QafComparisonModeKey =
  | 'g60'
  | 'multi_qaf'
  | 'multi_qaf_variant_vs_standard'
  | 'summary'
  /** KAR-993: Lieferanten-Benchmark — zwei Angebote VERSCHIEDENER Lieferanten
   * gegenübergestellt, nicht zwei Stände desselben Angebots. Vergleicht
   * ausschließlich Verhältnisse (Kostenstruktur, Aufschlag-Kette) und ordnet
   * dabei keine Varianten zu; siehe supplier-benchmark.ts. */
  | 'supplier_benchmark'

/** The `ingestQafUpload().kind` literals a replacement upload can have — kept
 * as its own small alias (rather than importing actions.ts's unexported
 * `IngestResult` type, which would also create an import cycle back into the
 * `'use server'` file this module exists to be independent of). */
export type IngestedQafFileKind = 'g60' | 'summary' | 'multi_qaf'

interface ComparisonModeFileRoleRule {
  /** The upload kinds a REPLACEMENT for this role may have —
   * `replaceComparisonFile` rejects (and cleans up) anything not listed.
   *
   * A SET rather than a single kind because of 'supplier_benchmark'
   * (KAR-993): a benchmark compares two offers that need not share a format
   * — Multi-QAF against Multi-QAF, Multi-QAF against Summary, or Summary
   * against Summary are all legitimate, because the benchmark only ever
   * reads per-variant cost positions and never matches variants across the
   * two sides. The four pre-existing modes each keep exactly one entry, so
   * their behaviour is unchanged. */
  expectedKinds: readonly IngestedQafFileKind[]
  /** Bilingual rejection message when the uploaded kind does not match. */
  wrongKindError: string
  /** Whether a replacement upload for this role must carry the SAME part
   * number as the comparison it joins (`qaf_comparison.part_number`).
   *
   * Explicit flag rather than derived from `expectedKinds` containing
   * 'summary': that derivation held only as long as every Summary-accepting
   * role also wanted the part-number binding. 'supplier_benchmark' breaks it
   * — it accepts Summary files but compares offers for DIFFERENT parts on
   * purpose, so requiring equal part numbers would reject exactly the case
   * the mode exists for. Making it a registry fact keeps the guard readable
   * at the call site and stops the next mode from having to rediscover this. */
  partNumberMustMatch: boolean
}

export interface ComparisonModeRule {
  /** File-role rules — `alt` = `qaf_comparison.baseline_file_id`, `neu` =
   * `comparison_file_id` (same ALT/NEU convention this whole package uses). */
  alt: ComparisonModeFileRoleRule
  neu: ComparisonModeFileRoleRule
  /** Whether `exportQafComparisonXlsx` can produce a workbook for this mode. */
  exportSupported: boolean
  /** Bilingual message `exportQafComparisonXlsx` returns when
   * `exportSupported` is false. Always set together with `exportSupported:
   * false` — never read otherwise. */
  exportNotSupportedError?: string
  /** Whether `recompareComparison`'s `pins` option (ManualPin, Summary-only
   * step-matching) is a meaningful concept for this mode. */
  pinsSupported: boolean
  pinsNotSupportedError?: string
  /** Whether `recompareComparison`'s `swap` option (exchange ALT/NEU file
   * roles) is a meaningful concept for this mode. */
  swapSupported: boolean
  swapNotSupportedError?: string
}

export const COMPARISON_MODE_RULES: Record<QafComparisonModeKey, ComparisonModeRule> = {
  g60: {
    alt: {
      expectedKinds: ['g60'],
      wrongKindError: 'Dieser Vergleich braucht ein G60-Detail-QAF (.xlsm mit Kostenreitern).',
      partNumberMustMatch: false,
    },
    neu: {
      expectedKinds: ['g60'],
      wrongKindError: 'Dieser Vergleich braucht ein G60-Detail-QAF (.xlsm mit Kostenreitern).',
      partNumberMustMatch: false,
    },
    exportSupported: false,
    exportNotSupportedError: 'G60-Detailvergleiche verwenden einen eigenen Export — exportQafG60ComparisonXlsx.',
    pinsSupported: false,
    pinsNotSupportedError: 'Manuelles Matching gilt für Summary-Vergleiche (G60 vergleicht Kostenreiter namensgleich).',
    swapSupported: true,
  },
  multi_qaf: {
    alt: {
      expectedKinds: ['multi_qaf'],
      wrongKindError:
        'Dieser Vergleich braucht eine Multi-QAF-Datei (Container mit mehreren Varianten). / This comparison needs a Multi-QAF file (container with multiple variants).',
      partNumberMustMatch: false,
    },
    neu: {
      expectedKinds: ['multi_qaf'],
      wrongKindError:
        'Dieser Vergleich braucht eine Multi-QAF-Datei (Container mit mehreren Varianten). / This comparison needs a Multi-QAF file (container with multiple variants).',
      partNumberMustMatch: false,
    },
    // KAR-949: Multi-QAF comparisons now have their own dedicated export
    // (exportMultiQafComparisonXlsx, lib/qaf-differences/internal/multi-qaf/
    // export.ts buildMultiQafExportWorkbook) — same "own export, registry
    // stays false for the STANDARD export path" precedent 'g60' already
    // establishes just above. `exportSupported` here still correctly means
    // "does exportQafComparisonXlsx (the Summary-shaped export) support this
    // mode" — it never will, 'multi_qaf' has no qaf_manufacturing_step rows
    // (see exportQafComparisonXlsx's own guard comment).
    exportSupported: false,
    exportNotSupportedError:
      'Multi-QAF-Vergleiche verwenden einen eigenen Export — exportMultiQafComparisonXlsx. / Multi-QAF comparisons use a dedicated export — exportMultiQafComparisonXlsx.',
    pinsSupported: false,
    pinsNotSupportedError: 'Manuelles Matching gilt für Summary-Vergleiche (Multi-QAF nutzt Varianten-Match-Overrides).',
    swapSupported: true,
  },
  multi_qaf_variant_vs_standard: {
    // baseline_file_id (role 'alt') is the chosen STANDARD QAF (Summary-
    // shaped) — see app/qaf-differences/actions.ts's
    // createVariantVsStandardComparison.
    alt: {
      expectedKinds: ['summary'],
      wrongKindError:
        'Dieser Vergleich braucht ein Standard-Summary-QAF für die Standard-Seite. / This comparison needs a standard Summary QAF for the standard side.',
      partNumberMustMatch: true,
    },
    // comparison_file_id (role 'neu') is the Multi-QAF CONTAINER the selected
    // variant is bridged from — see createVariantVsStandardComparison.
    neu: {
      expectedKinds: ['multi_qaf'],
      wrongKindError:
        'Dieser Vergleich braucht eine Multi-QAF-Datei für die Container-Seite (Varianten-Auswahl). / This comparison needs a Multi-QAF file for the container side (variant selection).',
      partNumberMustMatch: false,
    },
    // KAR-949: same "own dedicated export, registry stays false for the
    // STANDARD export path" precedent as 'g60'/'multi_qaf' above —
    // exportVariantVsStandardComparisonXlsx (buildVariantVsStandardExportWorkbook).
    exportSupported: false,
    exportNotSupportedError:
      'Multi-QAF-Variante-vs-Standard-Vergleiche verwenden einen eigenen Export — exportVariantVsStandardComparisonXlsx. / Multi-QAF-variant-vs-standard comparisons use a dedicated export — exportVariantVsStandardComparisonXlsx.',
    pinsSupported: false,
    pinsNotSupportedError: 'Manuelles Matching gilt für Summary-Vergleiche (nicht für Multi-QAF-Variante-vs-Standard).',
    swapSupported: false,
    swapNotSupportedError:
      'Rollentausch ist für Multi-QAF-Variante-vs-Standard nicht sinnvoll (eine Seite ist eine Container+Varianten-Auswahl, keine austauschbare Datei-Rolle).',
  },
  summary: {
    alt: {
      expectedKinds: ['summary'],
      wrongKindError: 'Dieser Vergleich braucht ein Summary-QAF (Zusammenfassungs-Blatt).',
      partNumberMustMatch: true,
    },
    neu: {
      expectedKinds: ['summary'],
      wrongKindError: 'Dieser Vergleich braucht ein Summary-QAF (Zusammenfassungs-Blatt).',
      partNumberMustMatch: true,
    },
    exportSupported: true,
    pinsSupported: true,
    swapSupported: true,
  },
  // KAR-993: Lieferanten-Benchmark.
  supplier_benchmark: {
    // Format-agnostisch auf BEIDEN Seiten — der einzige Modus, der mehr als
    // einen `expectedKinds`-Eintrag je Rolle trägt. Der Benchmark liest nur
    // Kostenpositionen je Variante; ein Standard-QAF ist dabei schlicht eine
    // Seite mit einer Variante. G60 ist NICHT dabei: das Detail-QAF trägt
    // Kostenreiter statt eines Summary-Blattes mit Positionszeilen, aus dem
    // sich die Anteile bilden ließen.
    alt: {
      expectedKinds: ['multi_qaf', 'summary'],
      wrongKindError:
        'Für einen Lieferanten-Benchmark braucht diese Seite ein Multi-QAF oder ein Standard-Summary-QAF (kein G60-Detail-QAF). / A supplier benchmark needs a Multi-QAF or a standard Summary QAF on this side (not a G60 detail QAF).',
      partNumberMustMatch: false,
    },
    neu: {
      expectedKinds: ['multi_qaf', 'summary'],
      wrongKindError:
        'Für einen Lieferanten-Benchmark braucht diese Seite ein Multi-QAF oder ein Standard-Summary-QAF (kein G60-Detail-QAF). / A supplier benchmark needs a Multi-QAF or a standard Summary QAF on this side (not a G60 detail QAF).',
      partNumberMustMatch: false,
    },
    // Eigener Export analog g60/multi_qaf — der Summary-Export-Pfad
    // (exportQafComparisonXlsx) setzt qaf_manufacturing_step-Zeilen voraus,
    // die ein Benchmark nie schreibt.
    exportSupported: false,
    exportNotSupportedError:
      'Lieferanten-Benchmarks verwenden einen eigenen Export. / Supplier benchmarks use a dedicated export.',
    // Es wird nichts zugeordnet, also gibt es auch nichts manuell zu pinnen —
    // das ist die zentrale Design-Eigenschaft des Modus, nicht eine Lücke.
    pinsSupported: false,
    pinsNotSupportedError:
      'Ein Benchmark ordnet keine Varianten zu — manuelles Matching ist hier gegenstandslos. / A benchmark matches no variants; manual matching does not apply.',
    // ALT/NEU ist beim Benchmark reine Anzeige-Reihenfolge (links/rechts),
    // keine Baseline-Semantik — Tauschen ist deshalb unschädlich und nützlich.
    swapSupported: true,
  },
}

/** `qaf_comparison.comparison_mode` is a nullable, unconstrained TEXT column
 * (supabase-migration-qaf-differences.sql) — the plain Summary-vs-Summary
 * comparison has never had its own literal (historically the implicit
 * "else" of every ad-hoc check in actions.ts). This is the single place that
 * turns the persisted `string | null` into the closed `QafComparisonModeKey`
 * union every dispatch site should read from now on. */
export function normalizeComparisonMode(raw: string | null | undefined): QafComparisonModeKey {
  if (
    raw === 'g60' ||
    raw === 'multi_qaf' ||
    raw === 'multi_qaf_variant_vs_standard' ||
    raw === 'supplier_benchmark'
  ) {
    return raw
  }
  return 'summary'
}

/** TypeScript never-trick exhaustiveness check (task requirement) — a
 * FUTURE `QafComparisonModeKey` addition without a matching `case` in
 * `comparisonModeRule`'s switch below fails to COMPILE, rather than silently
 * falling through a dispatch site the way KAR-942 F1/F3 and KAR-943's own F2
 * did at runtime. */
function assertExhaustiveComparisonMode(mode: never): never {
  throw new Error(`Unhandled QafComparisonModeKey: ${String(mode)}`)
}

/** Single lookup point every dispatch site (replaceComparisonFile,
 * exportQafComparisonXlsx, recompareComparison's pins/swap guards) should
 * call instead of re-deriving/guessing the mode from `comparison_mode`
 * directly. */
export function comparisonModeRule(raw: string | null | undefined): ComparisonModeRule {
  const mode = normalizeComparisonMode(raw)
  switch (mode) {
    case 'g60':
      return COMPARISON_MODE_RULES.g60
    case 'multi_qaf':
      return COMPARISON_MODE_RULES.multi_qaf
    case 'multi_qaf_variant_vs_standard':
      return COMPARISON_MODE_RULES.multi_qaf_variant_vs_standard
    case 'summary':
      return COMPARISON_MODE_RULES.summary
    case 'supplier_benchmark':
      return COMPARISON_MODE_RULES.supplier_benchmark
    default:
      return assertExhaustiveComparisonMode(mode)
  }
}
