// Verhandlungshebel aus Regeln (Spezifikation Kap. 7.9, R-20; Befund F-16).
//
// In der Vorgängerfassung standen unter „Hebel" Sätze ohne Betrag und ohne
// Quelle. Damit kann niemand in eine Verhandlung gehen: Ein Hinweis, dessen
// Wirkung unbeziffert bleibt, wandert in der Vorbereitung nach hinten, egal wie
// berechtigt er ist.
//
// Deshalb gilt hier eine Regel, die der ganzen Datei ihre Form gibt:
//
//   **Der Betrag eines Hebels ist die Summe seiner Belege.**
//
// Er wird nicht danebengeschrieben, sondern aus den Differenzen gerechnet, auf
// die der Hebel verweist. Eine Zahl, die sich nicht aus ihren Belegen ergibt,
// ist bei der ersten Rückfrage weg — und mit ihr die Glaubwürdigkeit des ganzen
// Berichts.
//
// Die Texte kommen aus Vorlagen mit eingesetzten Parametern. Kein Freitext,
// keine KI — die Formulierung muss über hundert Vergleiche hinweg dieselbe
// sein, sonst liest sie niemand mehr als Kategorie, sondern als Einzelmeinung.
//
// Pure Funktionen, keine I/O.

import type { DifferenceRecord } from './all-differences'
import type { Severity } from './data-quality'

export type LeverRule =
  | 'second_tool_without_spec'
  | 'spec_removed'
  | 'base_deleted_hard_value'
  | 'zero_cost_positions'
  | 'stale_total_line'
  | 'award_placeholder_priced'
  | 'external_links_unverifiable'

export type LeverUnit = 'EUR' | 'EUR_per_piece'

export interface Lever {
  leverId: string
  rule: LeverRule
  textDe: string
  /** EUR-Wirkung — die Summe der Belege, nicht eine daneben gestellte Zahl. */
  amount: number | null
  unit: LeverUnit | null
  severity: Severity
  /** Belege im Differenzkatalog. */
  differenceIds: string[]
  /** Datenqualitäts-Befunde, die den Hebel tragen. */
  dqIds: string[]
}

interface RuleSpec {
  severity: Severity
  unit: LeverUnit | null
  /** `{count}` und `{amount}` werden ersetzt; sonst nichts. */
  template: string
}

/**
 * Die Regeln mit ihren Vorlagen.
 *
 * Als Tabelle und nicht als Zweige im Code: So lässt sich der Katalog lesen und
 * ergänzen, ohne die Erzeugungslogik anzufassen — und eine neue Regel ist eine
 * Zeile, kein Eingriff.
 */
export const LEVER_RULES: Readonly<Record<LeverRule, RuleSpec>> = {
  second_tool_without_spec: {
    severity: 'high',
    unit: 'EUR',
    template:
      'Zweitwerkzeuge über {amount} EUR ohne Angabe von Kavitäten und Standzeit ({count} Positionen) — Auslegungsnachweise anfordern und klären, ob Kapazitätsaufbau oder Ersatz.',
  },
  spec_removed: {
    severity: 'high',
    unit: null,
    template:
      'Bei {count} Werkzeugpositionen fehlen die Auslegungsangaben, die im Vergabestand noch standen — Wiederherstellung verlangen, bevor über die Beträge gesprochen wird.',
  },
  base_deleted_hard_value: {
    severity: 'high',
    unit: 'EUR_per_piece',
    template:
      'Bei {count} Positionen steht der Betrag als feste Zahl ohne Einzelpreis und Menge — Herleitung nachfordern, sonst ist der Preis nicht verhandelbar, nur hinnehmbar.',
  },
  zero_cost_positions: {
    severity: 'medium',
    unit: null,
    template:
      '{count} benannte Positionen tragen keinen Preis — je Position klären, ob sie aus dem Umfang gefallen ist oder ihr Preis anderswo steckt.',
  },
  stale_total_line: {
    severity: 'medium',
    unit: null,
    template:
      '{count} Infozeilen widersprechen der Summe ihrer Bestandteile — vor jeder Argumentation mit diesen Zwischensummen nachziehen lassen.',
  },
  award_placeholder_priced: {
    severity: 'medium',
    unit: 'EUR',
    template:
      '{count} Positionen waren im Vergabestand ohne Preis benannt und sind jetzt mit {amount} EUR bepreist — als nachgereichte Bepreisung führen, nicht als neuen Umfang.',
  },
  external_links_unverifiable: {
    severity: 'low',
    unit: null,
    template:
      '{count} Werte stammen aus nicht beiliegenden Arbeitsmappen — Dateien anfordern oder die Werte als geprüfte Konstanten übernehmen.',
  },
}

export interface LeverInput {
  rule: LeverRule
  /** Differenzen, die den Hebel belegen. Ihre Summe ergibt den Betrag. */
  differenceIds: string[]
  dqIds?: string[]
  /**
   * Anzahl der betroffenen Positionen, falls sie nicht der Zahl der Belege
   * entspricht — etwa wenn ein Befund mehrere Zeilen zusammenfasst.
   */
  count?: number
}

const round2 = (v: number) => Math.round(v * 100) / 100

/**
 * Hebel bilden.
 *
 * Der Betrag wird aus den benannten Differenzen gerechnet. Fehlt eine davon im
 * Katalog, entsteht kein Hebel mit einer geschätzten Zahl, sondern gar keiner —
 * ein Hebel mit einem nicht belegbaren Betrag ist schlimmer als keiner.
 */
export function buildLevers(inputs: readonly LeverInput[], records: readonly DifferenceRecord[]): Lever[] {
  const byId = new Map(records.map((r) => [r.differenceId, r] as const))
  const out: Lever[] = []

  for (const input of [...inputs].sort((a, b) => a.rule.localeCompare(b.rule))) {
    const spec = LEVER_RULES[input.rule]
    const belege = input.differenceIds.map((id) => byId.get(id))
    if (belege.some((b) => b === undefined)) continue

    const gefunden = belege as DifferenceRecord[]
    if (gefunden.length === 0) continue

    const passend = spec.unit === null ? [] : gefunden.filter((r) => r.unit === spec.unit)
    const amount = spec.unit === null ? null : round2(passend.reduce((s, r) => s + (r.delta ?? 0), 0))
    const count = input.count ?? gefunden.length

    out.push({
      leverId: `LEV-${String(out.length + 1).padStart(3, '0')}`,
      rule: input.rule,
      textDe: spec.template
        .replace('{count}', String(count))
        .replace('{amount}', amount === null ? '—' : formatEur(amount)),
      amount,
      unit: spec.unit,
      severity: spec.severity,
      differenceIds: [...input.differenceIds].sort(),
      dqIds: [...(input.dqIds ?? [])].sort(),
    })
  }

  return out
}

/** Beträge in der Schreibweise, die im Bericht steht. */
function formatEur(v: number): string {
  return new Intl.NumberFormat('de-DE', { maximumFractionDigits: 2 }).format(v)
}

export interface LeverProblem {
  leverId: string
  kind: 'amount_not_backed' | 'no_evidence' | 'unknown_difference' | 'template_placeholder_left'
  detail: string
}

/**
 * Hebel gegen ihre eigenen Versprechen prüfen.
 *
 * Der Betrag muss die Summe der Belege sein — nicht ungefähr, sondern auf den
 * Cent. Und in keinem Text darf eine nicht ersetzte Vorlagenstelle stehen
 * bleiben; ein Hebel, in dem `{amount}` als Text steht, ist in einer
 * Verhandlung eine Blamage.
 */
export function validateLevers(levers: readonly Lever[], records: readonly DifferenceRecord[]): LeverProblem[] {
  const byId = new Map(records.map((r) => [r.differenceId, r] as const))
  const problems: LeverProblem[] = []

  for (const l of levers) {
    if (l.differenceIds.length === 0) {
      problems.push({ leverId: l.leverId, kind: 'no_evidence', detail: 'Hebel ohne Beleg.' })
      continue
    }

    const unbekannt = l.differenceIds.filter((id) => !byId.has(id))
    for (const id of unbekannt) {
      problems.push({ leverId: l.leverId, kind: 'unknown_difference', detail: `Verweis auf ${id}.` })
    }

    if (l.amount !== null && l.unit !== null) {
      const soll = round2(
        l.differenceIds
          .map((id) => byId.get(id))
          .filter((r): r is DifferenceRecord => r !== undefined && r.unit === l.unit)
          .reduce((s, r) => s + (r.delta ?? 0), 0),
      )
      if (Math.abs(soll - l.amount) > 0.005) {
        problems.push({
          leverId: l.leverId,
          kind: 'amount_not_backed',
          detail: `Betrag ${l.amount}, Belege ergeben ${soll}.`,
        })
      }
    }

    if (/\{[a-z_]+\}/.test(l.textDe)) {
      problems.push({
        leverId: l.leverId,
        kind: 'template_placeholder_left',
        detail: 'Nicht ersetzte Stelle in der Textvorlage.',
      })
    }
  }

  return problems
}
