// Summary-Kennzahlen in Differenzsätze überführen (Loop 3 Schritt 2; Vorbefund
// docs/qaf-v2/loop-3-vorbefund.md, „Reihenfolge, die sich daraus ergibt").
//
// Der Übersetzer zwischen dem Summary-Vergleich und dem Katalog — der erste
// Produzent für den Bereich, den die Übersicht tatsächlich zeigt. Zwei
// Entscheidungen prägen die Datei:
//
//   1. **Keine Zelle ohne vollständige Herkunft.** `SummaryMetricDiff` kennt
//      die Zelladresse (`sourceAlt`/`sourceNeu`), aber weder Formel noch
//      Wertzustand — beides verlangt `DifferenceCell`. Eine behauptete Angabe
//      wäre eine Falschaussage in einem Provenienzfeld. Deshalb entsteht hier
//      keine Zelle; `validateTraceability` meldet jeden Satz ohne Fundstelle,
//      und `buildOverviewRun` macht die Meldung sichtbar. Die Erweiterung, die
//      `formula` und `value_state` persistiert, schließt die Lücke später.
//   2. **Was nicht übersetzbar ist, verschwindet nicht still.** Statuswerte
//      ohne Ziel im Katalog werden gezählt und benannt zurückgegeben, nicht
//      übersprungen. Heute erzeugt der Summary-Vergleich keine solchen Werte —
//      das Rechen-Trio (`nicht_berechenbar`/`nicht_anwendbar`/`blockiert`)
//      gehört dem Feld-Vergleich; sollte es je hier ankommen, wird es sichtbar.
//
// Pure Funktionen, keine I/O.

import type { DifferenceCell, DifferenceKind, DifferenceRecord, RawDifference } from './all-differences'
import type { DiffStatus } from './types'
import { METRIC_LABELS_DE, type SummaryMetricDiff, type SummaryMetricKey } from './summary-metrics'

/** Einheit für die Anzeige: Einmalbeträge in EUR, alles andere je Stück. */
export function einheitFür(key: SummaryMetricKey): string {
  return key === 'totalOneTimePayment' || key === 'oneTimeDevelopment' || key === 'oneTimeTools'
    ? 'EUR'
    : 'EUR_per_piece'
}

/** Zeile aus einem A1-Bezug — die fachliche Sortiergrundlage des Katalogs. */
const rowOf = (cell: string | null): number | null => {
  if (cell === null) return null
  const m = /(\d+)\s*$/.exec(cell)
  return m === null ? null : Number(m[1])
}

/**
 * Art der Veränderung je Status. `konstant` fehlt bewusst (keine Differenz,
 * kein Satz), das Rechen-Trio ebenso (kein Ziel — siehe Dateikopf).
 * Exportiert (Schritt 4): eine Übersetzungstabelle für alle Produzenten,
 * die `DiffStatus` sprechen — keine zweite Kopie im Fertigungs-Produzenten.
 */
export const KIND_BY_STATUS = {
  neu: 'added',
  entfallen: 'removed',
  anstieg: 'changed',
  senkung: 'changed',
  auffaellig_10: 'changed',
  auffaellig_25: 'changed',
  kritisch_50: 'changed',
  formel_geaendert: 'changed',
  formel_zu_konstante: 'changed',
} as const satisfies Partial<Record<DiffStatus, DifferenceKind>>

/**
 * Verb je Status — deterministisch, denn die Beschreibung ist der vierte
 * Sortierschlüssel der ID-Vergabe. Gleiche Schlüssel wie `KIND_BY_STATUS`,
 * vom Typsystem erzwungen. Exportiert (Schritt 4): der Fertigungs-Produzent
 * übersetzt dieselben Statuswerte mit denselben Worten.
 */
export const VERB_BY_STATUS: Record<keyof typeof KIND_BY_STATUS, string> = {
  neu: 'neu bepreist',
  entfallen: 'entfallen',
  anstieg: 'gestiegen',
  senkung: 'gesunken',
  auffaellig_10: 'auffällig verändert (mehr als 10 %)',
  auffaellig_25: 'auffällig verändert (mehr als 25 %)',
  kritisch_50: 'kritisch verändert (mehr als 50 %)',
  formel_geaendert: 'Formel geändert, Wert unverändert',
  formel_zu_konstante: 'Formel durch Konstante ersetzt',
}

function kindFor(status: DiffStatus): DifferenceKind | undefined {
  return status in KIND_BY_STATUS ? KIND_BY_STATUS[status as keyof typeof KIND_BY_STATUS] : undefined
}

export interface SummaryDifferences {
  differences: RawDifference[]
  /** Vier-Schlüssel-Tupel → Kennzahl, für die ID-Zuordnung nach der Vergabe. */
  metricKeyByTuple: Map<string, SummaryMetricKey>
  /** Statuswerte ohne Ziel im Katalog — benannt statt still übersprungen. */
  skippedDe: string[]
}

/**
 * Das Tupel, über das ein Satz nach `assignDifferenceIds` wiedergefunden wird.
 *
 * Die Vergabe sortiert — eine Zuordnung über den Index würde die Kennungen
 * verwürfeln, sobald die Eingabereihenfolge sich ändert. Die vier
 * Sortierschlüssel sind die einzige Brücke, die das übersteht.
 */
export function sortKeyTuple(
  d: Pick<DifferenceRecord, 'area' | 'anchorRow' | 'kind' | 'descriptionDe'>,
): string {
  return `${d.area}|${d.anchorRow ?? ''}|${d.kind}|${d.descriptionDe}`
}

/**
 * Eine DifferenceCell aus der Diff-Provenienz — oder keine: Zelle nur, wenn
 * Blatt, Zelladresse und Wertzustand vollständig vorliegen (Weg 4).
 */
function zelle(
  fileRole: 'award' | 'current',
  source: string | null,
  prov: SummaryMetricDiff['provenanceAlt'],
): DifferenceCell[] {
  if (prov === undefined || source === null || prov.sheet === null) return []
  return [{ fileRole, sheet: prov.sheet, cell: source, formula: prov.formula, valueState: prov.valueState }]
}

export function differencesFromSummary(diffs: readonly SummaryMetricDiff[]): SummaryDifferences {
  const differences: RawDifference[] = []
  const metricKeyByTuple = new Map<string, SummaryMetricKey>()
  const skippedDe: string[] = []

  for (const d of diffs) {
    // Kein Unterschied, kein Satz — ein Katalog, in dem jede Zeile steht, ist
    // eine Kopie des Blatts. `formulaFinding` ist bei `konstant` nie gesetzt:
    // die beiden Wert-gleich-Fälle überschreiben den Status, der dritte trägt
    // ein Wertband (summary-metrics.ts, Formel-Zuweisung).
    if (d.status === 'konstant') continue

    const kind = kindFor(d.status)
    if (kind === undefined) {
      skippedDe.push(
        `${METRIC_LABELS_DE[d.metricKey]}: Status „${d.status}" hat kein Ziel im Katalog — kein Satz erzeugt.`,
      )
      continue
    }

    const raw: RawDifference = {
      area: 'SUM',
      kind,
      // Orthogonal zu `kind`: bei `formel_geaendert_wert_geaendert` trägt der
      // Satz sein Wertband UND den Formelbefund — keines verdrängt das andere.
      formulaFindingKind: d.formulaFinding?.kind ?? null,
      descriptionDe: `${METRIC_LABELS_DE[d.metricKey]}: ${VERB_BY_STATUS[d.status as keyof typeof KIND_BY_STATUS]}`,
      valueAward: d.altValue,
      valueCurrent: d.neuValue,
      delta: d.deltaAbsolute,
      // Bei `added`/`removed` erklärt die Art selbst das fehlende Delta — die
      // drei Rechen-Gründe beschreiben eine gescheiterte Rechnung und bleiben
      // leer.
      deltaNullReason: null,
      unit: einheitFür(d.metricKey),
      // Entscheidung 1 im Dateikopf: keine Zelle ohne VOLLSTÄNDIGE Herkunft.
      // Seit der Provenance-Migration liefert der Diff sie je Seite mit —
      // Blatt, Zelle, Formel und Wertzustand. Fehlt eines davon (Bestands-
      // zeilen, kein Grid), entsteht weiterhin keine Zelle, und
      // `validateTraceability` meldet den Satz ohne Fundstelle.
      cells: [
        ...zelle('award', d.sourceAlt, d.provenanceAlt),
        ...zelle('current', d.sourceNeu, d.provenanceNeu),
      ],
      relatedDq: [],
      anchorRow: rowOf(d.sourceNeu) ?? rowOf(d.sourceAlt),
    }

    differences.push(raw)
    metricKeyByTuple.set(sortKeyTuple(raw), d.metricKey)
  }

  return { differences, metricKeyByTuple, skippedDe }
}

/**
 * Kennungen je Kennzahl aus dem fertigen Katalog — über die Sortierschlüssel,
 * nicht über den Index (siehe `sortKeyTuple`).
 */
export function differenceIdsByMetricKey(
  records: readonly DifferenceRecord[],
  metricKeyByTuple: ReadonlyMap<string, SummaryMetricKey>,
): Map<SummaryMetricKey, string[]> {
  const out = new Map<SummaryMetricKey, string[]>()
  for (const r of records) {
    const key = metricKeyByTuple.get(sortKeyTuple(r))
    if (key === undefined) continue
    out.set(key, [...(out.get(key) ?? []), r.differenceId])
  }
  return out
}
