// Lieferanten-Benchmark (KAR-993 P1).
//
// Anlass: Kais verglich am 30.07.2026 zwei Multi-QAFs verschiedener
// Lieferanten über den `multi_qaf`-Modus — einen VERSIONS-Vergleich. Ergebnis
// war ein 24-seitiger Bericht, mit dem er "nicht viel anfangen" konnte, weil
// jede Kennzahl darin die logische Folge des falschen Modus war: 4 von 19
// Varianten "gematcht" (die Treffer hingen an EINER Dimension bei 10 %
// Abdeckung), kein Jahres-Impact (USD gegen EUR — korrekt verweigert), 184
// Material-Zeilen "neu" gegen 60 "entfallen".
//
// ── Warum dieses Modul ohne Varianten-Matching arbeitet ────────────────────
// Ein Benchmark stellt zwei Angebote gegenüber, die NICHT dasselbe Teil
// beschreiben müssen. Damit ist jedes Zeile-gegen-Zeile-Matching sinnlos —
// und genau diese Mechanik (variant-matcher.ts, 4-stufige Kaskade) ist die
// fragilste im ganzen Paket. Der Benchmark vergleicht deshalb VERTEILUNGEN
// über alle Varianten einer Seite (Median + Spanne) gegen die der anderen.
// Nichts wird zugeordnet, also kann auch nichts falsch zugeordnet werden.
// Das ist der Grund, warum er ohne Kalibrierung für beliebige Multi-QAF-
// Generationen funktioniert — die Anforderung "es soll flexibel sein auch mit
// anderen Multi-QAF-Beispielen" (Kais, TG 9362).
//
// ── Warum ausschließlich Verhältnisse ──────────────────────────────────────
// Absolutbeträge zweier Angebote sind nicht vergleichbar: verschiedene Teile,
// Währungen (im Anlassfall USD gegen EUR), Volumina und Bezugsjahre. Ein
// VERHÄLTNIS aus zwei Positionen derselben Datei ist einheitenfrei und
// deshalb über Lieferanten hinweg lesbar — "84,5 % Materialanteil gegen
// 86,2 %" ist eine belastbare Aussage, "235 gegen 180" ist keine.
//
// Die harte Regel, die daraus folgt: ein Verhältnis wird NUR gebildet, wenn
// Zähler und Nenner in DERSELBEN Währung stehen. Sonst wäre die Zahl
// einheitenfrei aussehend, aber inhaltlich falsch — die gefährlichste Sorte
// Ergebnis. Wo das nicht gilt, liefert dieses Modul `null` MIT Begründung
// (`unavailableReason`), nie 0 und nie eine stille Auslassung. Dieselbe
// Doktrin, die container-assembly.ts bereits für `materialCostsByCurrency`
// anwendet ("never force an interpretation").
//
// Reine Berechnung, kein I/O, keine DB, keine Parser-Abhängigkeit: Eingabe
// ist eine normalisierte Variantenliste, auf die sowohl ein Multi-QAF-
// Container (VirtualVariantSummaryTotals) als auch ein Standard-Summary-QAF
// abgebildet werden kann. Deshalb ist der Benchmark format-agnostisch, ohne
// dass dieses Modul die Formate kennt.

import type { MoneyValue } from './types'

/** Warum eine Kennzahl nicht ermittelbar ist — nie stillschweigend leer. */
export type BenchmarkUnavailableReason =
  /** Die Seite hat überhaupt keine Varianten. */
  | 'no_variants'
  /** Keine einzige Variante trug Zähler UND Nenner als Zahl. */
  | 'missing_values'
  /** Zähler und Nenner standen in verschiedenen Währungen — ein Verhältnis
   * daraus wäre einheitenfrei aussehend, aber inhaltlich falsch. */
  | 'mixed_currency'
  /** Der Nenner war 0 (Division nicht definiert). */
  | 'zero_denominator'

/**
 * Eine Benchmark-Kennzahl als Verteilung über die Varianten einer Seite.
 *
 * Median statt Mittelwert: eine einzelne Ausreißer-Variante (etwa eine
 * Sonderausführung mit abweichender Fertigungstiefe) verschiebt den
 * Mittelwert, den Median nicht. `min`/`max` machen die Streuung sichtbar —
 * bei Multi-QAFs mit 10+ Varianten ist die Spanne oft die eigentliche
 * Information ("Materialanteil 58 bis 64 %" sagt mehr als "61 %").
 */
export interface BenchmarkRatio {
  /** Median über alle beitragenden Varianten, als Anteil (0.845 = 84,5 %). */
  median: number | null
  min: number | null
  max: number | null
  /** Anzahl Varianten, die zu dieser Kennzahl beigetragen haben. */
  contributingVariants: number
  /** Anzahl Varianten, die übersprungen wurden (fehlende Werte, Währungs-
   * Konflikt, Nenner 0) — macht Teilabdeckung sichtbar statt sie zu glätten. */
  skippedVariants: number
  /** Gesetzt genau dann, wenn `median === null`. */
  unavailableReason: BenchmarkUnavailableReason | null
}

/** Kostenpositionen einer Variante. Deckungsgleich mit den Feldern von
 * `VirtualVariantSummaryTotals` (multi-qaf/types.ts), damit ein Container ohne
 * Umrechnung darauf abgebildet werden kann; ein Standard-Summary-QAF wird als
 * Liste mit genau einem Eintrag übergeben. */
export interface BenchmarkVariantInput {
  variantId: string
  materialCosts: MoneyValue | null
  manufacturingCosts: MoneyValue | null
  totalProductionCosts: MoneyValue | null
  scrap: MoneyValue | null
  otherSurcharges: MoneyValue | null
  setupCostAllocation: MoneyValue | null
  toolingAndFixtureCost: MoneyValue | null
  offerBasePrice: MoneyValue | null
  offerPrice: MoneyValue | null
}

/** Eine Benchmark-Seite (ein Lieferant/Angebot). */
export interface BenchmarkSideInput {
  /** Anzeigename. Bewusst vom Aufrufer gesetzt und hier nicht interpretiert —
   * dieses Modul kennt keine Lieferantennamen. */
  label: string
  variants: readonly BenchmarkVariantInput[]
}

/** Block A — Kostenstruktur: woraus besteht der Preis? */
export interface BenchmarkCostStructure {
  /** Materialkosten / Angebotspreis. PRIMÄRE Kennzahl.
   *
   * Bezug auf den Angebotspreis statt auf die Summe Herstellkosten, weil
   * letztere im persistierten Multi-QAF-Container durchgängig fehlt (am
   * 30.07. an zwei Realdateien gemessen: 0 von 25 bzw. 0 von 11 Varianten
   * tragen `totalProductionCosts`, während der Angebotspreis auf 12 bzw. 11
   * vorliegt). Eine Kennzahl, deren Bezugsgröße nie da ist, ist keine
   * Kennzahl. Die Herstellkosten-Bezüge bleiben darunter erhalten und füllen
   * sich automatisch, sobald der Parser die Zeile liefert. */
  materialShareOfOfferPrice: BenchmarkRatio
  /** Fertigungskosten / Angebotspreis. PRIMÄRE Kennzahl, siehe oben. */
  manufacturingShareOfOfferPrice: BenchmarkRatio
  /** Rüstkosten-Umlage / Angebotspreis. PRIMÄRE Kennzahl, siehe oben. */
  setupShareOfOfferPrice: BenchmarkRatio
  /** Materialkosten / Summe Herstellkosten. Heute meist nicht ermittelbar —
   * siehe materialShareOfOfferPrice. */
  materialShareOfProduction: BenchmarkRatio
  /** Fertigungskosten / Summe Herstellkosten. */
  manufacturingShareOfProduction: BenchmarkRatio
  /** Ausschuss / Angebotspreis. */
  scrapShareOfOfferPrice: BenchmarkRatio
  /** Sonstige Zuschläge / Angebotspreis. */
  surchargeShareOfOfferPrice: BenchmarkRatio
  /** Rüstkosten-Umlage / Summe Herstellkosten. */
  setupShareOfProduction: BenchmarkRatio
}

/** Block C — Aufschlag-Kette: wie kommt der Preis auf die Kostenbasis?
 *
 * Der Anlassfall zeigt, warum das die stärkste Kennzahl ist: beide Angebote
 * landeten bei ca. 334, einmal aus 332 Gesamtkosten (+0,5 %) und einmal aus
 * 258 (+29,8 %). Wer Endpreise vergleicht, sieht Gleichstand; die Kostenbasis
 * lag 29 % auseinander. */
export interface BenchmarkMarkupChain {
  /** (Angebotsbasispreis − Summe Herstellkosten) / Summe Herstellkosten. */
  productionToOfferBase: BenchmarkRatio
  /** (Angebotspreis − Angebotsbasispreis) / Angebotsbasispreis. */
  offerBaseToOfferPrice: BenchmarkRatio
  /** (Angebotspreis − Summe Herstellkosten) / Summe Herstellkosten —
   * der Gesamtaufschlag über die ganze Kette. */
  productionToOfferPrice: BenchmarkRatio
}

export interface BenchmarkSideResult {
  label: string
  /** Alle in den Kostenpositionen beobachteten Währungen, sortiert. Mehr als
   * eine heißt nicht "Fehler", sondern erklärt, warum einzelne Kennzahlen
   * `mixed_currency` sind. */
  currencies: readonly string[]
  variantCount: number
  costStructure: BenchmarkCostStructure
  markupChain: BenchmarkMarkupChain
}

export interface SupplierBenchmarkResult {
  left: BenchmarkSideResult
  right: BenchmarkSideResult
  /** Hinweise zur Lesbarkeit des Vergleichs — bewusst keine Blocker, aber
   * sichtbar, damit niemand eine Zahl für belastbarer hält als sie ist. */
  notices: readonly BenchmarkNotice[]
}

export interface BenchmarkNotice {
  kind:
    /** Die Seiten rechnen in verschiedenen Währungen. Für Verhältnisse
     * unschädlich (genau deshalb sind es Verhältnisse), aber es heißt: kein
     * Absolutbetrag der beiden Seiten darf verglichen werden. */
    | 'currencies_differ_between_sides'
    /** Eine Seite trägt mehrere Währungen (z. B. Fertigung an zwei
     * Standorten) — einzelne Kennzahlen können dadurch entfallen. */
    | 'multiple_currencies_within_side'
    /** Eine Kennzahl ist auf einer Seite ermittelbar, auf der anderen nicht:
     * die Gegenüberstellung ist an dieser Stelle einseitig. */
    | 'metric_only_on_one_side'
    /** Eine Seite hat deutlich weniger beitragende Varianten als die andere. */
    | 'coverage_imbalance'
  messageDe: string
  messageEn: string
}

// ── Interna ────────────────────────────────────────────────────────────────

const EMPTY_RATIO = (reason: BenchmarkUnavailableReason): BenchmarkRatio => ({
  median: null,
  min: null,
  max: null,
  contributingVariants: 0,
  skippedVariants: 0,
  unavailableReason: reason,
})

function median(sorted: readonly number[]): number {
  const mid = Math.floor(sorted.length / 2)
  return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
}

/** Ein einzelnes Verhältnis aus zwei Geldwerten derselben Variante.
 *
 * Gibt `null` zurück, wenn es nicht bildbar ist, und sagt über
 * `skipReason` WARUM — der Aufrufer aggregiert die Gründe, damit die
 * Kennzahl am Ende begründet leer ist statt einfach leer. */
function ratioOf(
  numerator: MoneyValue | null,
  denominator: MoneyValue | null
): { value: number | null; skipReason: BenchmarkUnavailableReason | null } {
  if (!numerator || !denominator || numerator.value === null || denominator.value === null) {
    return { value: null, skipReason: 'missing_values' }
  }
  // Die zentrale Regel dieses Moduls: ein Verhältnis über Währungsgrenzen
  // sieht einheitenfrei aus, ist aber inhaltlich falsch.
  //
  // Beide Codes `null` gilt AUSDRÜCKLICH als übereinstimmend. Grund: Zähler
  // und Nenner stammen immer aus derselben Variante, und eine Variante ist
  // eine Spalte EINES Angebots — sie hat strukturell genau eine
  // Angebotswährung (Zusammenfassung, Zeile "Währungen"). Zwei Positionen
  // derselben Spalte in verschiedenen Währungen wären ein Datenfehler, nicht
  // der Normalfall. `null` heißt hier "nicht ausgelesen", nicht "unbekannt
  // welche" — und ein Verhältnis zweier Beträge derselben, nur unbenannten
  // Einheit ist korrekt.
  //
  // Das war zunächst fail-closed (`null` = Konflikt) und hat den Benchmark am
  // 30.07. an Realdaten vollständig blockiert: der Multi-QAF-Container
  // persistiert `currency: null` auf fast allen Summary-Positionen, obwohl die
  // Währung in der Datei steht. Ein bekannter Code gegen `null` bleibt ein
  // Konflikt liegt deshalb nur vor, wenn BEIDE Codes bekannt UND verschieden
  // sind. Ein fehlender Name widerspricht einem vorhandenen nicht.
  //
  // Warum das für die hier verwendeten Positionen strukturell trägt: die
  // Summary-Positionen bilden eine SUMMENKETTE (Material + Fertigung = Summe
  // Herstellkosten, ... , = Angebotspreis). Eine Kette, die addiert wird,
  // steht zwingend in einer Währung — sonst wäre schon die Datei falsch.
  // Diese Regel gilt deshalb ausdrücklich NUR für Summary-Positionen. Sollte
  // der Benchmark später Fertigungsblatt-Werte lesen (Block B: Lohnsatz, MSS),
  // ist dort mit "Angaben in Beschaffungswährung" gegen "Angaben in
  // Angebotswährung" ein echter Zwei-Währungs-Fall im selben Blatt — der
  // braucht dann eine eigene, strengere Prüfung und darf diese hier nicht
  // wiederverwenden.
  if (
    numerator.currency !== null &&
    denominator.currency !== null &&
    numerator.currency !== denominator.currency
  ) {
    return { value: null, skipReason: 'mixed_currency' }
  }
  if (denominator.value === 0) {
    return { value: null, skipReason: 'zero_denominator' }
  }
  return { value: numerator.value / denominator.value, skipReason: null }
}

/** Aggregiert ein Verhältnis über alle Varianten einer Seite. */
function aggregateRatio(
  variants: readonly BenchmarkVariantInput[],
  pick: (v: BenchmarkVariantInput) => { numerator: MoneyValue | null; denominator: MoneyValue | null }
): BenchmarkRatio {
  if (variants.length === 0) return EMPTY_RATIO('no_variants')

  const values: number[] = []
  const reasons = new Set<BenchmarkUnavailableReason>()
  let skipped = 0

  for (const v of variants) {
    const { numerator, denominator } = pick(v)
    const { value, skipReason } = ratioOf(numerator, denominator)
    if (value === null) {
      skipped++
      if (skipReason) reasons.add(skipReason)
      continue
    }
    values.push(value)
  }

  if (values.length === 0) {
    // Begründung nach Schwere: ein Währungs-Konflikt ist die
    // erklärungsbedürftigere Ursache als ein schlicht fehlender Wert, deshalb
    // gewinnt er in der Meldung.
    const reason: BenchmarkUnavailableReason = reasons.has('mixed_currency')
      ? 'mixed_currency'
      : reasons.has('zero_denominator')
        ? 'zero_denominator'
        : 'missing_values'
    return { ...EMPTY_RATIO(reason), skippedVariants: skipped }
  }

  const sorted = [...values].sort((a, b) => a - b)
  return {
    median: median(sorted),
    min: sorted[0],
    max: sorted[sorted.length - 1],
    contributingVariants: values.length,
    skippedVariants: skipped,
    unavailableReason: null,
  }
}

/** Differenz-Verhältnis: (a − b) / b, als Aufschlag lesbar. Nutzt dieselbe
 * Währungsregel wie `ratioOf`, indem es den Zähler als synthetischen
 * MoneyValue in der Währung des Nenners bildet — nur wenn beide Währungen
 * übereinstimmen, sonst bleibt es `mixed_currency`. */
function markupOf(
  target: MoneyValue | null,
  base: MoneyValue | null
): { numerator: MoneyValue | null; denominator: MoneyValue | null } {
  if (!target || !base || target.value === null || base.value === null) {
    return { numerator: null, denominator: base ?? null }
  }
  // WICHTIG: dieselbe Bedingung wie in `ratioOf` — ein Konflikt liegt nur vor,
  // wenn BEIDE Codes bekannt und verschieden sind. Stand hier zunächst
  // `currency === null` mit drin, was nach der Lockerung in `ratioOf` einen
  // stillen Rechenfehler ergab: der Weiterreichungs-Pfad übergibt `target`
  // OHNE Differenz, und ratioOf rechnete daraus `target / base` — also 100,5 %
  // statt 0,5 % Aufschlag (am 30.07. an Realdaten aufgefallen). Die Regel muss
  // an beiden Stellen identisch sein, sonst rechnet der Aufschlag-Pfad etwas
  // anderes als der Anteils-Pfad.
  if (
    target.currency !== null &&
    base.currency !== null &&
    target.currency !== base.currency
  ) {
    // Konflikt an ratioOf weiterreichen, statt hier zu entscheiden — so gibt
    // es genau EINE Stelle, die über Währungen urteilt.
    return { numerator: target, denominator: base }
  }
  return {
    numerator: { value: target.value - base.value, currency: base.currency },
    denominator: base,
  }
}

function collectCurrencies(variants: readonly BenchmarkVariantInput[]): string[] {
  const set = new Set<string>()
  for (const v of variants) {
    for (const m of [
      v.materialCosts,
      v.manufacturingCosts,
      v.totalProductionCosts,
      v.scrap,
      v.otherSurcharges,
      v.setupCostAllocation,
      v.toolingAndFixtureCost,
      v.offerBasePrice,
      v.offerPrice,
    ]) {
      if (m?.currency) set.add(m.currency)
    }
  }
  return [...set].sort()
}

function buildSide(input: BenchmarkSideInput): BenchmarkSideResult {
  const variants = input.variants
  return {
    label: input.label,
    currencies: collectCurrencies(variants),
    variantCount: variants.length,
    costStructure: {
      materialShareOfOfferPrice: aggregateRatio(variants, (v) => ({
        numerator: v.materialCosts,
        denominator: v.offerPrice,
      })),
      manufacturingShareOfOfferPrice: aggregateRatio(variants, (v) => ({
        numerator: v.manufacturingCosts,
        denominator: v.offerPrice,
      })),
      setupShareOfOfferPrice: aggregateRatio(variants, (v) => ({
        numerator: v.setupCostAllocation,
        denominator: v.offerPrice,
      })),
      materialShareOfProduction: aggregateRatio(variants, (v) => ({
        numerator: v.materialCosts,
        denominator: v.totalProductionCosts,
      })),
      manufacturingShareOfProduction: aggregateRatio(variants, (v) => ({
        numerator: v.manufacturingCosts,
        denominator: v.totalProductionCosts,
      })),
      scrapShareOfOfferPrice: aggregateRatio(variants, (v) => ({
        numerator: v.scrap,
        denominator: v.offerPrice,
      })),
      surchargeShareOfOfferPrice: aggregateRatio(variants, (v) => ({
        numerator: v.otherSurcharges,
        denominator: v.offerPrice,
      })),
      setupShareOfProduction: aggregateRatio(variants, (v) => ({
        numerator: v.setupCostAllocation,
        denominator: v.totalProductionCosts,
      })),
    },
    markupChain: {
      productionToOfferBase: aggregateRatio(variants, (v) =>
        markupOf(v.offerBasePrice, v.totalProductionCosts)
      ),
      offerBaseToOfferPrice: aggregateRatio(variants, (v) => markupOf(v.offerPrice, v.offerBasePrice)),
      productionToOfferPrice: aggregateRatio(variants, (v) =>
        markupOf(v.offerPrice, v.totalProductionCosts)
      ),
    },
  }
}

const RATIO_LABELS_DE: Record<string, string> = {
  materialShareOfOfferPrice: 'Materialanteil am Angebotspreis',
  manufacturingShareOfOfferPrice: 'Fertigungsanteil am Angebotspreis',
  setupShareOfOfferPrice: 'Rüstkostenanteil am Angebotspreis',
  materialShareOfProduction: 'Materialanteil an Herstellkosten',
  manufacturingShareOfProduction: 'Fertigungsanteil an Herstellkosten',
  scrapShareOfOfferPrice: 'Ausschussanteil am Angebotspreis',
  surchargeShareOfOfferPrice: 'Zuschlagsanteil am Angebotspreis',
  setupShareOfProduction: 'Rüstkostenanteil an Herstellkosten',
  productionToOfferBase: 'Aufschlag Herstellkosten auf Basispreis',
  offerBaseToOfferPrice: 'Aufschlag Basispreis auf Angebotspreis',
  productionToOfferPrice: 'Gesamtaufschlag Herstellkosten auf Angebotspreis',
}

const RATIO_LABELS_EN: Record<string, string> = {
  materialShareOfOfferPrice: 'Material share of offer price',
  manufacturingShareOfOfferPrice: 'Manufacturing share of offer price',
  setupShareOfOfferPrice: 'Setup-cost share of offer price',
  materialShareOfProduction: 'Material share of production cost',
  manufacturingShareOfProduction: 'Manufacturing share of production cost',
  scrapShareOfOfferPrice: 'Scrap share of offer price',
  surchargeShareOfOfferPrice: 'Surcharge share of offer price',
  setupShareOfProduction: 'Setup-cost share of production cost',
  productionToOfferBase: 'Markup production cost to base price',
  offerBaseToOfferPrice: 'Markup base price to offer price',
  productionToOfferPrice: 'Total markup production cost to offer price',
}

function buildNotices(left: BenchmarkSideResult, right: BenchmarkSideResult): BenchmarkNotice[] {
  const notices: BenchmarkNotice[] = []

  const leftCur = left.currencies.join(', ') || '—'
  const rightCur = right.currencies.join(', ') || '—'
  const sameCurrencySet =
    left.currencies.length === right.currencies.length &&
    left.currencies.every((c, i) => c === right.currencies[i])

  if (!sameCurrencySet && left.currencies.length > 0 && right.currencies.length > 0) {
    notices.push({
      kind: 'currencies_differ_between_sides',
      messageDe: `Die Seiten rechnen in verschiedenen Währungen (${leftCur} gegen ${rightCur}). Die Anteile und Aufschläge unten sind davon unberührt — Absolutbeträge der beiden Seiten dürfen aber nicht verglichen werden.`,
      messageEn: `The sides are denominated differently (${leftCur} vs ${rightCur}). The shares and markups below are unaffected, but absolute amounts must not be compared across sides.`,
    })
  }

  for (const side of [left, right]) {
    if (side.currencies.length > 1) {
      notices.push({
        kind: 'multiple_currencies_within_side',
        messageDe: `${side.label} trägt mehrere Währungen (${side.currencies.join(', ')}) — etwa bei Fertigung an mehreren Standorten. Kennzahlen, die zwei Positionen unterschiedlicher Währung verbinden würden, bleiben deshalb offen.`,
        messageEn: `${side.label} carries several currencies (${side.currencies.join(', ')}), e.g. manufacturing across sites. Metrics that would combine positions in different currencies are left unavailable.`,
      })
    }
  }

  type MetricKey = keyof BenchmarkCostStructure | keyof BenchmarkMarkupChain
  const blocks: readonly (readonly [MetricKey, BenchmarkRatio, BenchmarkRatio])[] = [
    ...(Object.keys(left.costStructure) as (keyof BenchmarkCostStructure)[]).map(
      (k) => [k, left.costStructure[k], right.costStructure[k]] as const
    ),
    ...(Object.keys(left.markupChain) as (keyof BenchmarkMarkupChain)[]).map(
      (k) => [k, left.markupChain[k], right.markupChain[k]] as const
    ),
  ]

  for (const [key, l, r] of blocks) {
    const lOk = l.median !== null
    const rOk = r.median !== null
    if (lOk !== rOk) {
      const availableSide = lOk ? left.label : right.label
      const missingSide = lOk ? right.label : left.label
      const reason = (lOk ? r : l).unavailableReason
      notices.push({
        kind: 'metric_only_on_one_side',
        messageDe: `„${RATIO_LABELS_DE[key] ?? key}" liegt nur für ${availableSide} vor; für ${missingSide} nicht ermittelbar (${reason}). Kein Vergleich an dieser Stelle.`,
        messageEn: `"${RATIO_LABELS_EN[key] ?? key}" is available for ${availableSide} only; not determinable for ${missingSide} (${reason}). No comparison here.`,
      })
    }
  }

  return notices
}

/**
 * Stellt zwei Angebote als Benchmark gegenüber.
 *
 * Rein, deterministisch, ohne I/O. Vergleicht ausschließlich Verhältnisse
 * innerhalb je einer Seite und ordnet dabei keine Varianten zu — siehe
 * Modul-Header für die Begründung beider Entscheidungen.
 */
export function buildSupplierBenchmark(
  left: BenchmarkSideInput,
  right: BenchmarkSideInput
): SupplierBenchmarkResult {
  const leftResult = buildSide(left)
  const rightResult = buildSide(right)
  return {
    left: leftResult,
    right: rightResult,
    notices: buildNotices(leftResult, rightResult),
  }
}
