// Diagrammvorlagen der Anzeigeebene (Spezifikation Kap. 22).
//
// Die Diagramme rechnen so wenig wie die Tabellen: Sie bekommen fertige
// Balken, fertige Beschriftungen und einen fertigen Zustand. Was hier
// entschieden wird, entscheidet sich damit einmal — und nicht je Renderer neu.
//
// Zwei Regeln der Spezifikation sind hier zu Code geworden, und beide sind
// Reaktionen auf konkrete Fehlgriffe der Vorgängerfassung:
//
//   1. **Eine Brücke, die nicht schliesst, wird nicht gezeichnet.** Sie
//      bekommt einen Fehlerzustand mit Diagnoseliste — niemals einen Balken
//      namens „Übrige". Ein solcher Balken versteckt genau den Betrag, dessen
//      Herkunft niemand kennt, und lässt das Bild vollständig aussehen.
//   2. **Eine Auswahl nennt, wovon sie eine Auswahl ist.** „Top 5 von 56 ·
//      Rest −0,42" gehört ins Bild, nicht nur ins JSON. Ohne diese Angabe liest
//      sich eine Top-Liste wie eine vollständige.
//
// Pure Funktionen, keine I/O.

import { formatNumberDe } from './format-de'

/** Rolle eines Balkens im Wasserfall. */
export type WaterfallBarKind = 'start' | 'increase' | 'decrease' | 'end'

export interface WaterfallBar {
  key: string
  labelDe: string
  kind: WaterfallBarKind
  /** Wert des Balkens: bei Schritten das Delta, bei Start und Ende der Stand. */
  value: number
  /** Untere Kante — für Schritte der aufgelaufene Stand davor. */
  base: number
  differenceIds: string[]
}

export type WaterfallState = 'ok' | 'not_reconciled' | 'not_computed'

export interface WaterfallSpec {
  state: WaterfallState
  bars: WaterfallBar[]
  /** Diagnosezeilen, wenn die Brücke nicht schliesst. */
  diagnosticsDe: string[]
  /** Ist die Skala beschnitten? Dann muss die Anzeige es kennzeichnen. */
  truncatedScale: boolean
  captionDe: string | null
}

export interface WaterfallInput {
  startLabelDe: string
  endLabelDe: string
  start: number | null
  end: number | null
  steps: Array<{ key: string; labelDe: string; delta: number; differenceIds?: string[] }>
  residual: number | null
  tolerance?: number
}

const DEFAULT_TOLERANCE = 0.005

/**
 * Wasserfall bauen.
 *
 * Ohne Anfangs- und Endwert entsteht kein Diagramm mit geschätzten Kanten,
 * sondern der Zustand „nicht berechnet". Und bleibt ein Rest über der Toleranz,
 * wird die Brücke nicht gezeichnet: Ein Bild, das schliesst, obwohl die Zahlen
 * es nicht tun, ist schlimmer als kein Bild.
 */
export function buildWaterfall(input: WaterfallInput): WaterfallSpec {
  const tolerance = input.tolerance ?? DEFAULT_TOLERANCE

  if (input.start === null || input.end === null) {
    return {
      state: 'not_computed',
      bars: [],
      diagnosticsDe: ['Anfangs- oder Endwert nicht ermittelbar — die Brücke wurde nicht gerechnet.'],
      truncatedScale: false,
      captionDe: null,
    }
  }

  // Der Rest wird immer selbst nachgerechnet, auch wenn einer mitgegeben wurde.
  // Andernfalls entschiede eine Zahl von aussen darüber, ob das Bild gezeichnet
  // wird — und ein Aufrufer, der veraltete Schritte mit einem alten Restposten
  // kombiniert, bekäme einen Wasserfall, dessen Balken nicht zum Ende führen.
  // Ein mitgegebener Rest zählt zusätzlich, nie stattdessen.
  const schrittsumme = input.steps.reduce((s, x) => s + x.delta, 0)
  const eigenerRest = input.end - input.start - schrittsumme
  const rest = Math.abs(eigenerRest) >= Math.abs(input.residual ?? 0) ? eigenerRest : (input.residual ?? 0)

  if (Math.abs(rest) > tolerance) {
    return {
      state: 'not_reconciled',
      bars: [],
      diagnosticsDe: [
        // Formatiert, nicht interpoliert: eine rohe Gleitkommazahl
        // („1.370000000000004") ist für den Leser keine Diagnose, sondern ein
        // zweites Rätsel.
        `Die Schritte erklären ${formatNumberDe(schrittsumme)} von ${formatNumberDe(input.end - input.start)}; es bleibt ein Rest von ${formatNumberDe(rest)}.`,
        'Die Brücke wird nicht gezeichnet, solange der Rest ungeklärt ist. Ein Balken „Übrige" würde genau den Betrag verstecken, dessen Herkunft niemand kennt.',
      ],
      truncatedScale: false,
      captionDe: null,
    }
  }

  const bars: WaterfallBar[] = [
    { key: 'start', labelDe: input.startLabelDe, kind: 'start', value: input.start, base: 0, differenceIds: [] },
  ]
  let lauf = input.start
  for (const s of input.steps) {
    bars.push({
      key: s.key,
      labelDe: s.labelDe,
      kind: s.delta >= 0 ? 'increase' : 'decrease',
      value: s.delta,
      base: s.delta >= 0 ? lauf : lauf + s.delta,
      differenceIds: [...(s.differenceIds ?? [])].sort(),
    })
    lauf += s.delta
  }
  bars.push({ key: 'end', labelDe: input.endLabelDe, kind: 'end', value: input.end, base: 0, differenceIds: [] })

  return {
    state: 'ok',
    bars,
    diagnosticsDe: [],
    truncatedScale: needsTruncatedScale(input.start, input.end, input.steps.map((s) => s.delta)),
    captionDe: null,
  }
}

/**
 * Lohnt eine beschnittene Skala?
 *
 * Wenn die Schritte gegenüber den Ständen verschwindend klein sind, ist eine
 * Skala ab null unlesbar. Beschneiden ist dann erlaubt — aber nur, wenn die
 * Anzeige es kennzeichnet, sonst wirkt ein Prozent wie eine Verdopplung.
 */
function needsTruncatedScale(start: number, end: number, deltas: number[]): boolean {
  const spanne = Math.max(...deltas.map(Math.abs), 0)
  const niveau = Math.max(Math.abs(start), Math.abs(end))
  return niveau > 0 && spanne > 0 && spanne / niveau < 0.2
}

export interface MoversSelection {
  /** Wie viele Zeilen gezeigt werden. */
  shown: number
  /** Wie viele es insgesamt gibt. */
  total: number
  /** Summe der gezeigten Zeilen. */
  shownSum: number
  /** Summe des nicht gezeigten Rests. */
  residualSum: number
  unit: string
}

export interface MoversSpec {
  rows: Array<{ key: string; labelDe: string; value: number; differenceIds: string[] }>
  selection: MoversSelection
  /** Der Satz, der im Bild steht — nicht nur in den Daten. */
  selectionNoteDe: string
}

/**
 * Die Auswahl mitsamt ihrer Herkunft.
 *
 * Der Hinweis ist Pflicht: Ohne ihn liest sich eine Top-Liste wie eine
 * vollständige, und der ausgeblendete Rest verschwindet aus der Diskussion.
 */
export function buildMovers(
  all: ReadonlyArray<{ key: string; labelDe: string; value: number; differenceIds?: string[] }>,
  topN: number,
  unit: string,
  decimals = 2,
): MoversSpec {
  const sortiert = [...all].sort((a, b) => Math.abs(b.value) - Math.abs(a.value) || a.key.localeCompare(b.key))
  const gezeigt = sortiert.slice(0, Math.max(0, topN))
  const rest = sortiert.slice(Math.max(0, topN))

  const runden = (v: number) => {
    const f = 10 ** decimals
    return Math.round(v * f) / f
  }
  const shownSum = runden(gezeigt.reduce((s, x) => s + x.value, 0))
  const residualSum = runden(rest.reduce((s, x) => s + x.value, 0))

  return {
    rows: gezeigt.map((x) => ({
      key: x.key,
      labelDe: x.labelDe,
      value: x.value,
      differenceIds: [...(x.differenceIds ?? [])].sort(),
    })),
    selection: { shown: gezeigt.length, total: sortiert.length, shownSum, residualSum, unit },
    selectionNoteDe:
      rest.length === 0
        ? `Alle ${sortiert.length} Positionen gezeigt.`
        : `Top ${gezeigt.length} von ${sortiert.length} · Rest ${residualSum >= 0 ? '+' : ''}${residualSum}`,
  }
}

export type SectionEmptyState = 'empty_no_findings' | 'not_applicable' | 'not_computed'

export interface EmptyStateSpec {
  tone: 'positive' | 'neutral' | 'warning'
  messageDe: string
}

/**
 * Leermeldungen — stilles Leer-Rendern ist untersagt (U-04).
 *
 * Die drei Fälle sehen auf dem Bildschirm gleich aus und bedeuten Verschiedenes.
 * „Keine Auffälligkeiten" bei einer nie gerechneten Sektion ist die gefährlichste
 * Falschaussage des ganzen Berichts, weil sie beruhigt.
 */
export function emptyState(state: SectionEmptyState, detail: { checksPassed?: number; reasonDe?: string } = {}): EmptyStateSpec {
  switch (state) {
    case 'empty_no_findings':
      return {
        tone: 'positive',
        messageDe:
          detail.checksPassed === undefined
            ? 'Keine Auffälligkeiten.'
            : `Keine Auffälligkeiten — ${detail.checksPassed} Prüfungen bestanden.`,
      }
    case 'not_applicable':
      return {
        tone: 'neutral',
        messageDe:
          detail.reasonDe === undefined
            ? 'Für diesen Vergleich nicht anwendbar.'
            : `Für diesen Vergleich nicht anwendbar: ${detail.reasonDe}`,
      }
    default:
      return {
        tone: 'warning',
        messageDe:
          'Nicht berechnet — zu dieser Sektion ist keine Aussage möglich, auch keine beruhigende.' +
          (detail.reasonDe === undefined ? '' : ` Grund: ${detail.reasonDe}`),
      }
  }
}
