// Persistierte Material-Zuordnungen in Differenzsätze überführen
// (Loop 3 / Block 2d; Vorbefund docs/qaf-v2/loop-3-vorbefund.md).
//
// Der dritte datenbankfähige Produzent des Katalogs — Input ist das
// DB-Zeilen-Subset von `qaf_material_diff` (Muster wie der MFG-Produzent):
// hier wird nichts neu gerechnet, die Zeilen tragen, was der Vergleich
// damals entschied (persistierte Wirkungsart, Reproduzierbarkeits-Prinzip).
//
// Drei Entscheidungen:
//
//   1. **kind aus matchType × Wirkungsart**, nicht aus einem allein:
//      `renamed`/`merged`/`split` haben native Katalog-Pendants; gematchte
//      Positionen unterscheiden sich nach Wirkung — `price_change` →
//      `changed`, `scope_activated` (war benannt, erstmals bepreist) →
//      `newly_filled`, auf null gefallen → `cleared` (die Spiegel-Vokabeln
//      existieren genau dafür). Der ersatzlose und der bepreiste Wegfall
//      teilen `removed`; die Wirkungsart trägt die Unterscheidung
//      deterministisch in die Beschreibung.
//   2. **Unveränderte Positionen erzeugen keinen Satz** — auch die
//      Nullkosten-Positionen (zero_cost): deren Befund läuft bereits im
//      Datenqualitäts-Kanal (Detektor D02), ein Katalog-Satz wäre die
//      zweite Quelle derselben Aussage. Unbekannte Kombinationen werden
//      benannt zurückgegeben, nie still übersprungen.
//   3. **Keine Zelle ohne vollständige Herkunft (Weg 4):** die Zeilen führen
//      Blattzeilen und Namen, aber weder Zelladresse noch Formel noch
//      Wertzustand — es entsteht keine Zelle; `validateTraceability` meldet
//      die Sätze ohne Fundstelle beim Verbraucher.
//
// Pure Funktionen, keine I/O.

import type { DifferenceKind, DifferenceRecord, RawDifference } from './all-differences'
import { sortKeyTuple } from './differences-from-summary'

/** Subset der `qaf_material_diff`-Spalten, das dieser Produzent liest. */
export interface MaterialDiffRowData {
  mapping_id: string
  award_rows: number[]
  current_rows: number[]
  award_names: string[]
  current_names: string[]
  match_type: string
  cost_award: number | null
  cost_current: number | null
  delta: number | null
  effect: string | null
  zero_cost: boolean | null
}

export interface MaterialTableName {
  award: string[]
  current: string[]
  matchType: string
}

export interface MaterialDifferences {
  differences: RawDifference[]
  /** Vier-Schlüssel-Tupel → Tabellen-Namen (echte Namen je Stand). */
  nameByTuple: Map<string, MaterialTableName>
  /** Kombinationen ohne Ziel im Katalog — benannt statt still übersprungen. */
  skippedDe: string[]
}

/**
 * Ziel im Katalog je (matchType, effect) — deterministisch, denn die
 * Beschreibung ist der vierte Sortierschlüssel der ID-Vergabe.
 * `null` = bewusst kein Satz (kein Unterschied); `undefined` = Kombination
 * ohne Ziel (der Aufrufer benennt sie, nichts verschwindet still).
 */
function übersetze(
  matchType: string,
  effect: string | null,
): { kind: DifferenceKind; verbDe: string } | null | undefined {
  switch (matchType) {
    case 'added':
      return effect === 'unchanged' ? null : { kind: 'added', verbDe: 'neu im Umfang' }
    case 'removed':
      return effect === 'scope_removed'
        ? { kind: 'removed', verbDe: 'ersatzlos entfallen' }
        : { kind: 'removed', verbDe: 'entfallen' }
    case 'renamed':
      return { kind: 'renamed', verbDe: 'umbenannt' }
    case 'merged':
      return { kind: 'merged', verbDe: 'in Sammelposition aufgegangen' }
    case 'split':
      return { kind: 'split', verbDe: 'aus Sammelposition aufgeteilt' }
    case 'exact':
    case 'duplicate_context':
      switch (effect) {
        case 'price_change':
          return { kind: 'changed', verbDe: 'Preis verändert' }
        case 'scope_activated':
          return { kind: 'newly_filled', verbDe: 'erstmals bepreist' }
        case 'price_removed':
          return { kind: 'cleared', verbDe: 'auf null gefallen' }
        case 'unchanged':
          return null
        default:
          return undefined
      }
    default:
      return undefined
  }
}

export function differencesFromMaterial(rows: readonly MaterialDiffRowData[]): MaterialDifferences {
  const differences: RawDifference[] = []
  const nameByTuple = new Map<string, MaterialTableName>()
  const skippedDe: string[] = []

  for (const r of rows) {
    const ziel = übersetze(r.match_type, r.effect)
    if (ziel === undefined) {
      skippedDe.push(
        `Material-Zuordnung ${r.mapping_id}: Kombination match_type=„${r.match_type}"/effect=„${r.effect}" hat kein Ziel im Katalog — kein Satz erzeugt.`,
      )
      continue
    }
    if (ziel === null) continue // kein Unterschied, kein Satz (zero_cost läuft im DQ-Kanal)

    const primärName = r.current_names[0] ?? r.award_names[0] ?? r.mapping_id

    const raw: RawDifference = {
      area: 'MAT',
      kind: ziel.kind,
      // Der Material-DB-Pfad führt keinen Formelvergleich.
      formulaFindingKind: null,
      descriptionDe: `${primärName}: ${ziel.verbDe}`,
      valueAward: r.cost_award,
      valueCurrent: r.cost_current,
      delta: r.delta,
      deltaNullReason: null,
      // Alle Positionskosten sind Angebotswährung („[AW]", mat_calc_material_cost).
      unit: 'AW',
      cells: [], // Entscheidung 3 im Dateikopf.
      relatedDq: [],
      anchorRow: r.current_rows[0] ?? r.award_rows[0] ?? null,
    }
    differences.push(raw)
    nameByTuple.set(sortKeyTuple(raw), {
      award: r.award_names,
      current: r.current_names,
      matchType: r.match_type,
    })
  }

  return { differences, nameByTuple, skippedDe }
}

/** Tabellen-Namen je Kennung — Zuordnung über die Sortierschlüssel. */
export function materialTableNamesByDifferenceId(
  records: readonly DifferenceRecord[],
  nameByTuple: ReadonlyMap<string, MaterialTableName>,
): Map<string, { award: string[]; current: string[]; matchType: string; bucket?: string | null }> {
  const out = new Map<string, { award: string[]; current: string[]; matchType: string; bucket?: string | null }>()
  for (const r of records) {
    const n = nameByTuple.get(sortKeyTuple(r))
    if (n === undefined) continue
    out.set(r.differenceId, { award: n.award, current: n.current, matchType: n.matchType })
  }
  return out
}
