// Die Datenverträge der Anzeigeebene (Spezifikation Teil D, Kap. 22 und 26).
//
// Leitgedanke der Spezifikation: ein kanonischer Datenbaum, ein Renderer je
// Sektion. Die Anzeige rechnet nichts selbst. Jede angezeigte Zahl trägt einen
// Verweis in den Kern, und alle Renderer — Webansicht, Foliensatz,
// Arbeitsmappe, Offline-Ansicht — lesen dieselbe Vorlage.
//
// Der Grund ist nicht Ordnungsliebe. Sobald eine der vier Ausgaben selbst
// rechnet, laufen sie auseinander, und zwar unbemerkt: Niemand vergleicht eine
// Folie Zeile für Zeile mit dem Bildschirm. Genau das ist in der
// Vorgängerfassung passiert.
//
// Zwei Invarianten sind hier zu Code geworden:
//
//   U-02  Keine Anzeigezahl ohne Verweis in den Kern.
//   U-03  Anzeigesummen sind Kernsummen — im Client wird nicht vor dem
//         Summieren gerundet.
//
// Pure Funktionen, keine I/O.

import type { DifferenceCell, DifferenceRecord } from './all-differences'
import type { DqFinding } from './data-quality'
import type { SectionState } from './ai-ready'

/**
 * Verweis auf die Stelle im Kern, aus der eine Anzeigezahl stammt.
 *
 * Der Pfad ist die Adresse im Ergebnisbaum, die Zellen sind der Nachweis in den
 * Arbeitsmappen. Beides gehört zusammen: Der Pfad sagt, woher die Anzeige den
 * Wert hat, die Zellen sagen, woher der Kern ihn hat.
 */
export interface SourceRef {
  path: string
  cells: DifferenceCell[]
  differenceIds: string[]
}

export type StatusChip = 'critical' | 'warning' | 'neutral' | 'good'

export interface KpiTile {
  key: string
  labelDe: string
  value: number | null
  unit: string
  deltaAbsolute: number | null
  /** Anteil, nicht Prozentzahl: 0,31 bedeutet 31 Prozent. */
  deltaRatio: number | null
  status: StatusChip
  sourceRef: SourceRef
  /**
   * Hinweis, der bei rechnerischen Grössen sichtbar mitläuft — etwa dass die
   * Lifetime-Kachel eine Bewertungsgrösse ist und keine bestätigte Einsparung.
   */
  formulaNoteDe: string | null
}

export interface KpiTileInput {
  key: string
  labelDe: string
  valueAward: number | null
  valueCurrent: number | null
  unit: string
  path: string
  differenceIds: string[]
  formulaNoteDe?: string
  /** Schwellen für den Status-Chip, als Anteil. */
  thresholds?: { warning: number; critical: number }
}

const DEFAULT_THRESHOLDS = { warning: 0.02, critical: 0.1 }

/**
 * Statusfarbe aus der Abweichung.
 *
 * Aus Schwellen und nicht aus dem Einzelfall — sonst bekommt dieselbe
 * Abweichung je nach Kachel eine andere Farbe, und die Ampel wird zur Deko.
 */
export function statusFor(deltaRatio: number | null, thresholds = DEFAULT_THRESHOLDS): StatusChip {
  if (deltaRatio === null) return 'neutral'
  if (deltaRatio <= -thresholds.warning) return 'good'
  if (deltaRatio >= thresholds.critical) return 'critical'
  if (deltaRatio >= thresholds.warning) return 'warning'
  return 'neutral'
}

/**
 * Kacheln bauen.
 *
 * Das Delta wird hier gerechnet und nicht im Renderer: Vier Renderer, die
 * dasselbe rechnen, rechnen es irgendwann verschieden.
 */
export function buildKpiTiles(inputs: readonly KpiTileInput[], records: readonly DifferenceRecord[]): KpiTile[] {
  const byId = new Map(records.map((r) => [r.differenceId, r] as const))

  return inputs.map((i) => {
    const deltaAbsolute =
      i.valueAward === null || i.valueCurrent === null ? null : i.valueCurrent - i.valueAward
    const deltaRatio =
      deltaAbsolute === null || i.valueAward === null || i.valueAward === 0 ? null : deltaAbsolute / i.valueAward

    const belege = i.differenceIds.map((id) => byId.get(id)).filter((r): r is DifferenceRecord => r !== undefined)

    return {
      key: i.key,
      labelDe: i.labelDe,
      value: i.valueCurrent,
      unit: i.unit,
      deltaAbsolute,
      deltaRatio,
      status: statusFor(deltaRatio, i.thresholds),
      sourceRef: {
        path: i.path,
        cells: belege.flatMap((r) => r.cells),
        differenceIds: [...i.differenceIds].sort(),
      },
      formulaNoteDe: i.formulaNoteDe ?? null,
    }
  })
}

/**
 * Summe einer Anzeigespalte.
 *
 * Erst summieren, dann runden — nie umgekehrt. Die Reihenfolge ist der
 * Unterschied zwischen einer Spaltensumme, die zur Kernsumme passt, und einer,
 * die um ein paar Cent daneben liegt und jede Rückfrage auslöst (U-03).
 */
export function displaySum(values: readonly (number | null)[], decimals: number): number {
  const summe = values.reduce<number>((s, v) => s + (v ?? 0), 0)
  const faktor = 10 ** decimals
  return Math.round(summe * faktor) / faktor
}

export interface EvidenceRow {
  fileRole: 'award' | 'current'
  sheet: string
  cell: string
  formula: string | null
  valueState: DifferenceCell['valueState']
  /** Datenqualitäts-Befunde, die dieselbe Stelle betreffen. */
  findingIds: string[]
}

/**
 * Das Zellnachweis-Panel — im ganzen Modul dieselbe Auflösung.
 *
 * Eine Zeile aufzuklappen muss überall dasselbe zeigen: welche Datei, welches
 * Blatt, welche Zelle, welche Formel, welcher Zustand, welche Befunde. Vier
 * Sektionen mit vier eigenen Panels wären vier Gelegenheiten, es verschieden zu
 * machen.
 */
export function evidenceFor(
  record: DifferenceRecord,
  findings: readonly DqFinding[] = [],
): EvidenceRow[] {
  // Die Datei gehört in den Schlüssel. Beide Stände tragen dieselben
  // Zellbezüge — ein Befund im aktuellen Stand hinge sonst auch an der
  // gleichnamigen Zelle des Vergabestands und behauptete dort etwas, das dort
  // nicht gilt.
  const findingsByCell = new Map<string, string[]>()
  const add = (key: string, id: string) => findingsByCell.set(key, [...(findingsByCell.get(key) ?? []), id])
  for (const f of findings) {
    for (const c of f.affectedCells) {
      const rollen = c.fileRole === 'both' ? (['award', 'current'] as const) : ([c.fileRole] as const)
      for (const rolle of rollen) add(`${rolle}|${c.sheet}!${c.cell}`, f.findingId)
    }
  }

  return record.cells.map((c) => ({
    fileRole: c.fileRole,
    sheet: c.sheet,
    cell: c.cell,
    formula: c.formula,
    valueState: c.valueState,
    findingIds: [
      ...new Set([...(findingsByCell.get(`${c.fileRole}|${c.sheet}!${c.cell}`) ?? []), ...record.relatedDq]),
    ].sort(),
  }))
}

export interface SectionHeader {
  key: string
  labelDe: string
  state: SectionState
  /** Wird der Zustand sichtbar gerendert? Bei „nicht berechnet" ist das Pflicht (U-04). */
  noticeDe: string | null
}

const STATE_NOTICE: Record<SectionState, string | null> = {
  populated: null,
  empty_verified: 'Geprüft, keine Abweichungen gefunden.',
  not_computed: 'Nicht berechnet — zu dieser Sektion ist keine Aussage möglich, auch keine beruhigende.',
}

/**
 * Kopfzeilen der Sektionen.
 *
 * Der Hinweis bei „nicht berechnet" ist keine Höflichkeit, sondern der Kern der
 * Invariante U-04: Eine leer wirkende Sektion, die nie gerechnet wurde, liest
 * sich sonst als Entwarnung.
 */
export function sectionHeaders(sections: Record<string, { labelDe: string; state: SectionState }>): SectionHeader[] {
  return Object.entries(sections)
    .map(([key, s]) => ({ key, labelDe: s.labelDe, state: s.state, noticeDe: STATE_NOTICE[s.state] }))
    .sort((a, b) => a.key.localeCompare(b.key))
}

export interface ValidationBanner {
  visible: boolean
  failedChecks: string[]
  /** Solange das Banner steht, ist die KI-Zusammenfassung gesperrt (Kap. 17.4). */
  summaryBlocked: boolean
  messageDe: string | null
}

/**
 * Das Fehlerbanner des Kopfbereichs.
 *
 * Es blockiert die Zusammenfassung sichtbar, statt sie stillschweigend
 * wegzulassen — eine fehlende Zusammenfassung erklärt sich niemandem von selbst.
 */
export function validationBanner(failedChecks: readonly string[]): ValidationBanner {
  const failed = [...failedChecks].sort()
  return {
    visible: failed.length > 0,
    failedChecks: failed,
    summaryBlocked: failed.length > 0,
    messageDe:
      failed.length === 0
        ? null
        : `${failed.length} Prüfung${failed.length === 1 ? '' : 'en'} des Kerns fehlgeschlagen: ${failed.join(', ')}. Die Zusammenfassung bleibt gesperrt, bis das behoben ist.`,
  }
}

export interface ViewProblem {
  kind: 'number_without_source' | 'sum_mismatch'
  where: string
  detail: string
}

/**
 * Die Anzeigevorlage gegen U-02 und U-03 prüfen.
 *
 * Eine Kachel ohne Verweis ist eine Zahl, deren Herkunft niemand nachschlagen
 * kann. Und eine Anzeigesumme, die nicht der Kernsumme entspricht, ist der
 * Anfang jeder Diskussion darüber, welcher Zahl man glauben soll.
 */
export function validateViewSpec(
  tiles: readonly KpiTile[],
  sums: readonly { where: string; displayed: number; core: number }[] = [],
): ViewProblem[] {
  const problems: ViewProblem[] = []

  for (const t of tiles) {
    if (t.value === null && t.deltaAbsolute === null) continue
    if (t.sourceRef.differenceIds.length === 0 && t.sourceRef.cells.length === 0) {
      problems.push({
        kind: 'number_without_source',
        where: t.key,
        detail: 'Kachel zeigt einen Wert ohne Verweis in den Kern.',
      })
    }
  }

  for (const s of sums) {
    if (Math.abs(s.displayed - s.core) > 0.005) {
      problems.push({
        kind: 'sum_mismatch',
        where: s.where,
        detail: `Anzeige ${s.displayed}, Kern ${s.core}.`,
      })
    }
  }

  return problems
}
