// Der Lauf hinter der Übersichts-Sektion (Spezifikation Kap. 21, View-Phase V1).
//
// Wozu: Die Anzeigeebene war fertig, aber an keine Datenquelle angeschlossen —
// `buildKpiTiles`, `buildWaterfall` und `validationBanner` hatten zusammen null
// Aufrufe ausserhalb von Tests. Dieses Modul ist das fehlende Bindeglied: aus
// zwei geöffneten Arbeitsmappen entsteht, was die Übersicht zeigt.
//
// Drei Entscheidungen prägen die Datei:
//
//   1. **Die Brückenschritte kommen aus der Zeilenrollen-Registry**, nicht aus
//      einer Liste hier. `additiveRowSpecs()` ist laut eigener Dokumentation
//      „die einzige zulässige Grundlage für Preisbrücke und
//      Additivitätsprüfung. Wer eine Brücke aus etwas anderem baut, baut wieder
//      F-01." Eine zweite Liste hier wäre genau diese zweite Wahrheit.
//   2. **Gerechnet wird nichts selbst.** Deltas, Zustände und die Frage, ob die
//      Brücke schliesst, entscheiden `diffSummaryMetrics` und `buildWaterfall`.
//      Bleibt ein Rest, zeichnet die Anzeige die Diagnose — dieses Modul
//      korrigiert das nicht weg.
//   3. **Fehlende Blätter sind ein Befund, kein Grund zu schweigen.** Ohne
//      Preisblock gibt es keinen Lauf, aber eine benannte Ursache.
//
// Pure Funktionen, keine I/O: die Mappen kommen geöffnet herein.

import type { Worksheet } from 'exceljs'
import { parseSummarySheetFromWorkbook } from './workbook-adapter'
import {
  diffSummaryMetrics,
  METRIC_LABELS_DE,
  type SummaryMetricDiff,
  type SummaryMetricKey,
  type SummaryMetricsParse,
} from './summary-metrics'
import { additiveRowSpecs } from './summary-row-registry'
import { buildCatalog, buildTraceabilityIndex, validateTraceability } from './all-differences'
import { differencesFromSummary, differenceIdsByMetricKey, einheitFür } from './differences-from-summary'
import { buildWaterfall, type WaterfallSpec } from './chart-specs'
import {
  buildKpiTiles,
  validationBanner,
  type KpiTile,
  type KpiTileInput,
  type ValidationBanner,
} from './view-specs'

type Mappe = { worksheets: Worksheet[] }

export interface OverviewRunInput {
  /** Preisblock des Vergabestands, `null` wenn nicht lesbar. */
  award: SummaryMetricsParse | null
  current: SummaryMetricsParse | null
  /** Dateiname des Vergabestands, wie er in der Kopfzeile stehen soll. */
  awardFileNameDe: string
  currentFileNameDe: string
  /**
   * Engine-Config-Treue (Review-Befund #502): der Lauf rechnet mit der
   * Formel-Engine-Einstellung, unter der der Vergleich PERSISTIERT wurde
   * (resolvePersistedEngineConfig-Doktrin, wie compare.ts) — nicht blind
   * mit dem heutigen Default. Sonst könnten Live-Provenienz und
   * persistierte Anzeigezahlen bei einem Config-Flip auseinanderlaufen.
   * Optional mit Default true (= bisheriges Verhalten für Aufrufer ohne
   * persistierte Config, z. B. den Frisch-Parse-Weg).
   */
  formulaEngineEnabled?: boolean
}

export interface OverviewRun {
  tiles: KpiTile[]
  waterfall: WaterfallSpec
  validation: ValidationBanner
  /** Einheit der Preisgrössen, aus der Währung der Datei. */
  unit: string
  /**
   * Was nicht gelesen werden konnte, im Klartext. Leer heisst: vollständig.
   * Eine leere Ansicht ohne Begründung sieht aus wie ein sauberer Lauf —
   * deshalb steht hier, was fehlt (U-04).
   */
  degradationsDe: string[]
  /**
   * Rückverfolgungs-Lücken aus `validateTraceability`, im Klartext und
   * aggregiert. Getrennt von `degradationsDe`: dort steht, was nicht gelesen
   * werden konnte — hier, was gelesen wurde, aber (noch) ohne nachschlagbare
   * Fundstelle bleibt. Leer heisst: jede Differenz trägt mindestens eine Zelle.
   */
  traceabilityDe: string[]
  /**
   * Der Summary-Diff dieses Laufs — dieselben Zeilen, aus denen Kacheln und
   * Waterfall oben gebaut sind. Nach aussen gereicht, damit ein Aufrufer
   * weitere Darstellungen (Kostenstruktur, Klassik-Brücke) aus GENAU dieser
   * einen Quelle speisen kann statt aus einem zweiten, unabhängig
   * berechneten Stand — die Ein-Quellen-Invariante von summary-view.ts
   * („one metric can never show two different percentages on one screen"),
   * Review-Befund der zusammengeführten Ansicht (10.08.2026).
   */
  summaryDiffs: SummaryMetricDiff[]
  /**
   * Die SUM-Katalog-Sätze dieses Laufs samt vergebener Kennungen (Loop 6
   * Etappe 1b): der kanonische Export braucht die DifferenceRecords und die
   * metricKey-Zuordnung je Kennung — exponiert aus GENAU dem Katalog, den
   * dieser Lauf ohnehin baut (kein zweiter buildCatalog beim Aufrufer).
   */
  summaryRecords: import('./all-differences').DifferenceRecord[]
  summaryDifferenceIdsByMetricKey: ReadonlyMap<SummaryMetricKey, string[]>
}

/** Der Endstand der Brücke: was der Lieferant am Ende aufruft. */
const BRÜCKEN_TOTAL: SummaryMetricKey = 'quotationPrice'

/**
 * Kennzahlen der Übersicht.
 *
 * Bewusst wenige: Die Kachelreihe soll die Frage „was ist teurer geworden und
 * wie sehr" beantworten, nicht den Preisblock nachbauen — dafür gibt es die
 * Tabellen.
 */
const KACHELN: ReadonlyArray<{ key: SummaryMetricKey; formulaNoteDe?: string }> = [
  { key: 'quotationPrice' },
  { key: 'totalProductionCosts' },
  { key: 'materialCosts' },
  { key: 'manufacturingCosts' },
  {
    key: 'totalOneTimePayment',
    formulaNoteDe: 'Einmalzahlung, nicht im Stückpreis enthalten',
  },
]

/**
 * Den Preisblock aus einer Arbeitsmappe lesen.
 *
 * Der zweite Weg in denselben Lauf führt über `summaryMetricsFromRows`: Die
 * Originaldateien sind nach dem Ingest nicht mehr adressierbar (`qaf_file`
 * führt keinen Storage-Pfad), ein bestehender Vergleich lässt sich aber aus
 * seinen persistierten Zeilen zurückbauen. Deshalb nimmt `buildOverviewRun`
 * den Preisblock entgegen statt die Mappe — beide Quellen münden hier.
 */
export function preisblockAusMappe(mappe: Mappe): SummaryMetricsParse | null {
  return parseSummarySheetFromWorkbook(mappe).summaryMetrics
}

function kachelEingaben(
  diffs: readonly SummaryMetricDiff[],
  ids: ReadonlyMap<SummaryMetricKey, string[]>,
): KpiTileInput[] {
  const byKey = new Map(diffs.map((d) => [d.metricKey, d] as const))

  return KACHELN.filter((k) => byKey.has(k.key)).map(({ key, formulaNoteDe }) => {
    const d = byKey.get(key)!
    return {
      key,
      labelDe: METRIC_LABELS_DE[key],
      valueAward: d.altValue,
      valueCurrent: d.neuValue,
      unit: einheitFür(key),
      path: `summary.${key}`,
      // Leer nur, wo es keine Differenz gibt — eine unveränderte Kennzahl
      // behauptet keine und braucht deshalb keine Kennung.
      differenceIds: ids.get(key) ?? [],
      ...(formulaNoteDe === undefined ? {} : { formulaNoteDe }),
    }
  })
}

/**
 * Die Preisbrücke aus den additiven Zeilen.
 *
 * Zeilen ohne Veränderung bleiben draussen: Ein Balken der Höhe null trägt
 * keine Aussage, kostet aber Platz, den die tragenden Schritte brauchen. Die
 * Summe verändert er nicht — bliebe dadurch ein Rest, wäre er ohnehin schon
 * vorher da gewesen.
 */
/**
 * Der Betrag, um den eine Zeile die Brücke bewegt.
 *
 * `deltaAbsolute` bleibt leer, sobald eine Seite keinen Wert trägt — und genau
 * diese Zeilen bewegen die Summe am stärksten. Ein Posten, der im Vergabestand
 * bepreist war und jetzt leer steht, ist eine Senkung um seinen vollen Betrag;
 * umgekehrt eine erstmalige Bepreisung. Wer beide wegfiltert, bekommt eine
 * Brücke, die nicht schliesst, ohne zu wissen warum.
 *
 * Die Zeile existiert in beiden Fällen im Blatt: `diffSummaryMetrics` überspringt
 * Zeilen, die auf beiden Seiten leer sind — was hier ankommt, trägt mindestens
 * einen Wert. Dieselbe Doktrin gilt auf der Materialebene („Wegfall auf null" als
 * Spiegelfall zur erstmaligen Bepreisung).
 */
function bewegung(d: SummaryMetricDiff): number | null {
  if (d.deltaAbsolute !== null) return d.deltaAbsolute
  if (d.altValue !== null && d.neuValue === null) return -d.altValue
  if (d.altValue === null && d.neuValue !== null) return d.neuValue
  return null
}

/** Kennzeichnet die Zeilen, bei denen eine Seite gar keinen Wert trägt. */
function schrittLabel(d: SummaryMetricDiff): string {
  const name = METRIC_LABELS_DE[d.metricKey]
  if (d.deltaAbsolute !== null) return name
  return d.neuValue === null ? `${name} (entfallen)` : `${name} (neu bepreist)`
}

function brückenEingabe(
  diffs: readonly SummaryMetricDiff[],
  awardLabelDe: string,
  currentLabelDe: string,
  ids: ReadonlyMap<SummaryMetricKey, string[]>,
) {
  const byKey = new Map(diffs.map((d) => [d.metricKey, d] as const))
  const total = byKey.get(BRÜCKEN_TOTAL)

  const steps = additiveRowSpecs()
    .map((spec) => byKey.get(spec.metricKey))
    .filter((d): d is SummaryMetricDiff => d !== undefined)
    .map((d) => ({ d, delta: bewegung(d) }))
    .filter((x): x is { d: SummaryMetricDiff; delta: number } => x.delta !== null && x.delta !== 0)
    .map(({ d, delta }) => ({
      key: d.metricKey,
      labelDe: schrittLabel(d),
      delta,
      differenceIds: ids.get(d.metricKey) ?? [],
    }))

  return {
    startLabelDe: awardLabelDe,
    endLabelDe: currentLabelDe,
    start: total?.altValue ?? null,
    end: total?.neuValue ?? null,
    steps,
    // Nicht mitgegeben: `buildWaterfall` rechnet den Rest selbst nach. Ein von
    // aussen gereichter Rest könnte einen Wasserfall zeichnen lassen, dessen
    // Balken nicht zum Ende führen.
    residual: null,
  }
}

/**
 * Aus zwei Arbeitsmappen die Übersicht bauen.
 *
 * Fehlt der Preisblock in einer der beiden, entsteht kein halber Lauf: Die
 * Brücke steht auf „nicht gerechnet", die Kachelreihe bleibt leer, und der
 * Grund steht in `degradationsDe`.
 */
export function buildOverviewRun(input: OverviewRunInput): OverviewRun {
  const { award: alt, current: neu } = input

  const degradationsDe: string[] = []
  if (alt === null) degradationsDe.push(`Im Vergabestand (${input.awardFileNameDe}) ist kein Preisblock lesbar.`)
  if (neu === null) degradationsDe.push(`Im aktuellen Stand (${input.currentFileNameDe}) ist kein Preisblock lesbar.`)

  if (alt === null || neu === null) {
    return {
      tiles: [],
      waterfall: {
        state: 'not_computed',
        bars: [],
        diagnosticsDe: degradationsDe,
        truncatedScale: false,
        captionDe: null,
      },
      // Der fehlende Preisblock ist selbst der fehlgeschlagene Check: Solange
      // er steht, darf niemand den Lauf zusammenfassen.
      validation: validationBanner(['Preisblock nicht lesbar']),
      unit: 'EUR_per_piece',
      degradationsDe,
      traceabilityDe: [],
      summaryDiffs: [],
      summaryRecords: [],
      summaryDifferenceIdsByMetricKey: new Map(),
    }
  }

  const diffs = diffSummaryMetrics(alt, neu, input.formulaEngineEnabled ?? true)

  // Der Katalog zum Lauf: Sätze aus dem Summary-Vergleich, Kennungen über die
  // Sortierschlüssel zurück an die Kennzahlen. Die Prüfung läuft mit — ein
  // Meldewesen ohne Aufrufer war der Kernbefund des Loop-3-Vorbefunds.
  const summe = differencesFromSummary(diffs)
  const catalog = buildCatalog(summe.differences)
  const ids = differenceIdsByMetricKey(catalog.records, summe.metricKeyByTuple)

  const referenced = {
    statements: Object.fromEntries([...ids].map(([key, kennungen]) => [`summary.${key}`, kennungen])),
    views: {},
  }
  const probleme = validateTraceability(
    catalog.records,
    buildTraceabilityIndex(catalog.records, referenced.statements, referenced.views),
    referenced,
  )
  const ohneZellen = probleme.filter((p) => p.kind === 'difference_without_cells')
  const sonstige = probleme.filter((p) => p.kind !== 'difference_without_cells')
  const traceabilityDe = [
    ...(ohneZellen.length > 0
      ? [
          `${ohneZellen.length} ${ohneZellen.length === 1 ? 'Differenz' : 'Differenzen'} ohne Zellnachweis — der Summary-Pfad führt weder Formel noch Wertzustand; die Fundstellen folgen, sobald beides persistiert ist.`,
        ]
      : []),
    ...sonstige.map((p) => `${p.where}: ${p.detail}`),
    ...summe.skippedDe,
  ]

  return {
    tiles: buildKpiTiles(kachelEingaben(diffs, ids), catalog.records),
    waterfall: buildWaterfall(brückenEingabe(diffs, input.awardFileNameDe, input.currentFileNameDe, ids)),
    validation: validationBanner([]),
    unit: 'EUR_per_piece',
    degradationsDe,
    traceabilityDe,
    summaryDiffs: diffs,
    summaryRecords: catalog.records,
    summaryDifferenceIdsByMetricKey: ids,
  }
}
