// Tabellenvorlagen der Anzeigeebene (Spezifikation Kap. 22, Invariante U-08).
//
// Material, Fertigung und Werkzeuge zeigen dieselbe Tabelle mit denselben
// Spalten, denselben Filtern und derselben Sortierung. Drei eigene
// Implementierungen wären drei Gelegenheiten, es verschieden zu machen — und
// der Nutzer müsste dreimal lernen, wie er filtert.
//
// Zwei Entscheidungen prägen die Datei:
//
//   1. **Die Sortierung ist die Blattreihenfolge.** Nicht der Betrag. Wer eine
//      Position sucht, sucht sie dort, wo sie in der Datei steht; eine nach
//      Grösse sortierte Tabelle zwingt zum Suchen. Sortieren nach Betrag bleibt
//      möglich, ist aber nicht der Ausgangszustand.
//   2. **Filter verstecken, sie löschen nicht.** Jede gefilterte Ansicht nennt,
//      wie viele Zeilen sie ausblendet und welche Summe damit aus dem Bild
//      fällt. Eine Tabelle, die stillschweigend die Hälfte weglässt, ist die
//      bequemste Art, eine Verhandlung zu verlieren.
//
// Pure Funktionen, keine I/O.

import type { DifferenceCell, DifferenceRecord } from './all-differences'

export interface TableRow {
  differenceId: string
  /** Blattreihenfolge — die Sortiergrundlage. */
  anchorRow: number | null
  namesAward: string[]
  namesCurrent: string[]
  matchType: string
  valueAward: number | null
  valueCurrent: number | null
  delta: number | null
  unit: string
  bucket: string | null
  /** Kennungen der Datenqualitäts-Befunde an dieser Zeile. */
  findingIds: string[]
  /** SRC-002 (Loop 5): die Beleg-Kette des Satzes — Rolle → Blatt → Zelle →
   * Formel → Wertzustand, unverändert aus dem Katalog durchgereicht. Leer,
   * wenn der Satz keine Zellen trägt (die Traceability-Zählung
   * `difference_without_cells` bleibt die Summenaussage dazu). */
  cells: DifferenceCell[]
}

export interface TableFilter {
  matchTypes?: string[]
  buckets?: string[]
  /** Nur Zeilen mit einem Delta ungleich null. */
  changedOnly?: boolean
  /** Nur Zeilen ohne Kosten auf mindestens einer Seite. */
  zeroCostOnly?: boolean
  /** Nur Zeilen mit mindestens einem Datenqualitäts-Befund. */
  withFindingsOnly?: boolean
  /** Freitextsuche über Namen und Kennung. */
  search?: string
}

export type TableSort = 'sheet_order' | 'delta_desc' | 'delta_asc'

export interface TableSpec {
  rows: TableRow[]
  /** Was der Filter ausblendet — gehört sichtbar an die Tabelle. */
  hidden: {
    count: number
    deltaSum: number
  }
  totalRows: number
  sort: TableSort
  /** Summe der angezeigten Deltas — erst summiert, dann gerundet (U-03). */
  displayedDeltaSum: number
  noticeDe: string | null
}

const DELTA_TOLERANCE = 0.005

const round = (v: number, decimals: number) => {
  const f = 10 ** decimals
  return Math.round(v * f) / f
}

const matchesSearch = (row: TableRow, needle: string): boolean => {
  const n = needle.trim().toLowerCase()
  if (n === '') return true
  if (row.differenceId.toLowerCase().includes(n)) return true
  return [...row.namesAward, ...row.namesCurrent].some((x) => x.toLowerCase().includes(n))
}

function passes(row: TableRow, filter: TableFilter): boolean {
  if (filter.matchTypes !== undefined && filter.matchTypes.length > 0 && !filter.matchTypes.includes(row.matchType)) {
    return false
  }
  if (filter.buckets !== undefined && filter.buckets.length > 0 && (row.bucket === null || !filter.buckets.includes(row.bucket))) {
    return false
  }
  if (filter.changedOnly === true && Math.abs(row.delta ?? 0) <= DELTA_TOLERANCE) return false
  if (filter.zeroCostOnly === true && (row.valueAward ?? 0) !== 0 && (row.valueCurrent ?? 0) !== 0) return false
  if (filter.withFindingsOnly === true && row.findingIds.length === 0) return false
  if (filter.search !== undefined && !matchesSearch(row, filter.search)) return false
  return true
}

/**
 * Tabelle aufbauen.
 *
 * Die Ausgangssortierung ist die Blattreihenfolge; Zeilen ohne Zeilenbezug
 * hängen hinten an, damit sie nicht zwischen den anderen auftauchen. Bei
 * gleicher Zeile entscheidet die Kennung — sonst wandert die Reihenfolge
 * zwischen zwei Läufen.
 */
export function buildTable(
  rows: readonly TableRow[],
  filter: TableFilter = {},
  sort: TableSort = 'sheet_order',
  decimals = 2,
): TableSpec {
  const sichtbar = rows.filter((r) => passes(r, filter))
  const versteckt = rows.filter((r) => !passes(r, filter))

  const sortiert = [...sichtbar].sort((a, b) => {
    if (sort === 'sheet_order') {
      return (
        (a.anchorRow ?? Number.MAX_SAFE_INTEGER) - (b.anchorRow ?? Number.MAX_SAFE_INTEGER) ||
        a.differenceId.localeCompare(b.differenceId)
      )
    }
    const richtung = sort === 'delta_desc' ? -1 : 1
    return richtung * ((a.delta ?? 0) - (b.delta ?? 0)) || a.differenceId.localeCompare(b.differenceId)
  })

  const versteckteSumme = round(
    versteckt.reduce((s, r) => s + (r.delta ?? 0), 0),
    decimals,
  )

  return {
    rows: sortiert,
    hidden: { count: versteckt.length, deltaSum: versteckteSumme },
    totalRows: rows.length,
    sort,
    displayedDeltaSum: round(
      sortiert.reduce((s, r) => s + (r.delta ?? 0), 0),
      decimals,
    ),
    noticeDe:
      versteckt.length === 0
        ? null
        : `${versteckt.length} von ${rows.length} Zeilen ausgeblendet (${versteckteSumme >= 0 ? '+' : ''}${versteckteSumme}).`,
  }
}

/**
 * Differenzsätze in Tabellenzeilen überführen.
 *
 * Die Namen stehen im Katalog nur in der Beschreibung; für die Tabelle werden
 * sie gesondert übergeben, damit die Beschreibung ein Satz bleiben kann und die
 * Spalte ein Name.
 */
export function rowsFromRecords(
  records: readonly DifferenceRecord[],
  names: ReadonlyMap<string, { award: string[]; current: string[]; matchType: string; bucket?: string | null }>,
): TableRow[] {
  return records.map((r) => {
    const n = names.get(r.differenceId)
    return {
      differenceId: r.differenceId,
      anchorRow: r.anchorRow,
      namesAward: n?.award ?? [],
      namesCurrent: n?.current ?? [],
      matchType: n?.matchType ?? r.kind,
      valueAward: typeof r.valueAward === 'number' ? r.valueAward : null,
      valueCurrent: typeof r.valueCurrent === 'number' ? r.valueCurrent : null,
      delta: r.delta,
      unit: r.unit,
      bucket: n?.bucket ?? null,
      findingIds: [...r.relatedDq].sort(),
      cells: [...r.cells],
    }
  })
}

export interface TableProblem {
  kind: 'hidden_without_notice' | 'sum_mismatch'
  detail: string
}

/**
 * Die Tabelle gegen ihre Zusagen prüfen.
 *
 * Angezeigte plus ausgeblendete Summe muss die Gesamtsumme ergeben. Sonst
 * verschwindet beim Filtern ein Betrag, den niemand vermisst — und genau das
 * ist der Fehler, den ein Filter am leichtesten macht.
 */
export function validateTable(spec: TableSpec, allRows: readonly TableRow[], decimals = 2): TableProblem[] {
  const problems: TableProblem[] = []

  if (spec.hidden.count > 0 && spec.noticeDe === null) {
    problems.push({ kind: 'hidden_without_notice', detail: 'Zeilen ausgeblendet, ohne es zu sagen.' })
  }

  const gesamt = round(
    allRows.reduce((s, r) => s + (r.delta ?? 0), 0),
    decimals,
  )
  const summe = round(spec.displayedDeltaSum + spec.hidden.deltaSum, decimals)
  if (Math.abs(gesamt - summe) > 0.005) {
    problems.push({
      kind: 'sum_mismatch',
      detail: `Angezeigt ${spec.displayedDeltaSum} plus ausgeblendet ${spec.hidden.deltaSum} ergibt ${summe}, gesamt sind es ${gesamt}.`,
    })
  }

  return problems
}
