// Werkzeuge und Einmalzahlungen (Spezifikation Kap. 7.5, R-11; Detektoren D05/D08).
//
// Die Einmalzahlung ist die zweite Preisdimension eines Angebots und wird
// regelmäßig stiefmütterlich behandelt: eine Summe, ein Delta, fertig. Damit
// bleibt unbeantwortet, wofür die Differenz anfällt — und genau darin liegt der
// Unterschied zwischen „neuer Umfang wurde beauftragt" und „derselbe Umfang
// kostet mehr".
//
// Der schärfste Befund des Anlassfalls hängt hier: Zweitwerkzeuge in
// erheblicher Höhe bei gleichzeitig gelöschten Kavitäten- und
// Standzeitangaben. Jedes für sich wäre erklärbar; zusammen ist es eine Frage,
// die gestellt gehört.
//
// Pure Funktionen, keine I/O.

import { normalizeMaterialName } from './material-name'
import type { SbmRow } from './sbm-parser'

/**
 * Die Auslegungsangaben eines Werkzeugs.
 *
 * Die Kavitätenangabe ist bewusst Text und keine Zahl: im Formblatt steht dort
 * „4-fach", „1-fach (1K)" oder „Sonstige (siehe Bemerkung)". Für die Frage, um
 * die es hier geht — war eine Auslegung angegeben und ist sie verschwunden —
 * zählt ohnehin nur, ob überhaupt etwas dasteht.
 */
export interface ToolSpecification {
  /** Kavitätenangabe im Wortlaut des Formblatts. */
  cavityDesignation: string | null
  /** Standzeit in Zyklen. */
  lifetimeCycles: number | null
}

export interface MappableTool extends ToolSpecification {
  row: number
  /** Bauteil, für das das Werkzeug steht. */
  part: string
  /** Werkzeugart, soweit angegeben. */
  toolType: string | null
  /** Einmalbetrag in Angebotswährung. */
  oneTimeCost: number | null
  /**
   * Umlage je Stück. Vorrichtungen und Behälter werden so verrechnet statt über
   * eine Einmalzahlung — sie gehören deshalb nicht in die Einmalzahlungs-Brücke.
   */
  perPieceCost?: number | null
  /**
   * Summenzeile des Formblatts statt echter Werkzeugposition. Sie darf in keine
   * Zählung und in keine Brücke eingehen — sonst wird jeder Betrag doppelt
   * gezählt.
   */
  isTotalLine?: boolean
}

/**
 * Summenzeilen des Formblatts tragen einen Sammelbegriff statt einer
 * Bauteilbezeichnung. Sie am führenden Wort zu erkennen ist die Regel, die das
 * Formblatt selbst anbietet — eine Namensliste müsste für jede neue Vorlage
 * gepflegt werden und wäre bei der ersten fremden Datei still falsch.
 */
const TOTAL_LINE_PREFIX = /^(total|summe|sum|gesamt|zwischensumme)\b/i

/** Zeilennummer aus einem Zellbezug der Form `Blatt!AP94`. */
function rowFromCellRef(ref: string | undefined): number | null {
  const m = /(\d+)\s*$/.exec(ref ?? '')
  return m === null ? null : Number(m[1])
}

/**
 * Geparste SBM-Zeilen auf die Vergleichsform bringen.
 *
 * Zeilen ohne erkennbare Zeilennummer fallen heraus: ohne sie gibt es keinen
 * Zellnachweis, und ein Befund ohne Fundstelle ist in diesem Bericht wertlos.
 */
export function sbmRowsToMappableTools(rows: readonly SbmRow[]): MappableTool[] {
  const tools: MappableTool[] = []
  for (const r of rows) {
    const row = rowFromCellRef(r.sourceCells.positionNumber ?? r.sourceCells.componentDesignation)
    if (row === null) continue
    const part = r.componentDesignation.trim()
    tools.push({
      row,
      part,
      toolType: r.toolFixtureType.trim() === '' ? null : r.toolFixtureType.trim(),
      cavityDesignation: r.cavityConfiguration.trim() === '' ? null : r.cavityConfiguration.trim(),
      lifetimeCycles: r.serviceLifeCycles,
      oneTimeCost: r.totalToolFixtureCostAw,
      perPieceCost: r.devicesFollowupMaintenancePerUnit,
      isTotalLine: TOTAL_LINE_PREFIX.test(part),
    })
  }
  return tools
}

export type ToolStatus = 'unchanged' | 'changed' | 'new' | 'removed'

/**
 * Wofür ein Betrag anfällt.
 *
 * `second_tool` ist die verhandlungsrelevanteste Klasse: für ein Bauteil, das
 * bereits ein Werkzeug hat, kommt ein weiteres hinzu. Das ist entweder
 * Kapazitätsaufbau oder Ersatz — und die Antwort steht nicht im Betrag, sondern
 * in der Auslegung daneben.
 */
export type ToolClassification = 'new_scope' | 'second_tool' | 'price_change' | 'fixture' | 'unchanged'

export interface ToolMapping {
  mappingId: string
  awardRow: number | null
  currentRow: number | null
  part: string
  status: ToolStatus
  classification: ToolClassification
  /** Stand die Auslegung im Vergabestand und fehlt sie jetzt? */
  specRemovedInCurrent: boolean
  /** Zeile hat die Position gewechselt, wirtschaftlich unverändert. */
  rowMoved: boolean
  evidenceDe: string
  oneTimeAward: number | null
  oneTimeCurrent: number | null
  delta: number | null
  /**
   * Stückumlage beider Stände.
   *
   * Vorrichtungen tragen keine Einmalzahlung, sondern eine Umlage je Stück.
   * Ohne diese Felder bliebe eine Position, deren Umlage sich ändert, während
   * die Einmalzahlung gleich bleibt, unsichtbar — im Anlassfall genau die
   * Vorrichtung, deren Kostenbasis gelöscht wurde.
   */
  perPieceAward: number | null
  perPieceCurrent: number | null
  perPieceDelta: number | null
}

export interface OneTimeBridgeStep {
  classification: ToolClassification
  label: string
  delta: number
  toolRows: number[]
}

export interface ToolingResult {
  mappings: ToolMapping[]
  counts: {
    toolsAward: number
    toolsCurrent: number
    byClassification: Record<ToolClassification, number>
    /** Positionen, deren Auslegungsnachweis im aktuellen Stand fehlt. */
    specRemoved: number
    /** Zweitwerkzeuge ohne Auslegungsnachweis — die eigentliche Frage. */
    secondToolsWithoutSpec: number
  }
  /** Einmalzahlungs-Brücke: welcher Betrag fällt wofür an. */
  oneTimeBridge: {
    start: number | null
    end: number | null
    delta: number | null
    steps: OneTimeBridgeStep[]
    residual: number | null
    /** `null`, solange keine Ausweissummen zum Abgleich vorliegen. */
    passed: boolean | null
  }
}

const EMPTY_CLASSIFICATION: Record<ToolClassification, number> = {
  new_scope: 0,
  second_tool: 0,
  price_change: 0,
  fixture: 0,
  unchanged: 0,
}

const CLASSIFICATION_LABEL: Record<ToolClassification, string> = {
  new_scope: 'Neue Werkzeuge für neuen Umfang',
  second_tool: 'Zweitwerkzeuge für bereits bewerkzeugte Bauteile',
  price_change: 'Preisänderung bestehender Werkzeuge',
  fixture: 'Vorrichtungen und Behälter (Stückumlage)',
  unchanged: 'Unverändert',
}

const TOLERANCE = 0.005

/** Klassen, die in die Einmalzahlungs-Brücke eingehen — in dieser Reihenfolge. */
const BRIDGE_CLASSES: readonly ToolClassification[] = ['new_scope', 'second_tool', 'price_change']

const nameKey = (part: string) => normalizeMaterialName(part).normalized

/**
 * Zählsuffix eines Zweitwerkzeugs abstreifen.
 *
 * Ein zweites Werkzeug für dasselbe Bauteil wird im Formblatt durchnummeriert:
 * aus „Halter (KF1)" wird „Halter (KF1) 2". Nur Ziffern und römisch II/III
 * gelten als Zählung — Buchstabensuffixe wie „Platte-A"/„Platte-B" bezeichnen
 * Varianten, nicht Wiederholungen, und dürfen nicht zusammenfallen.
 *
 * `null`, wenn kein Suffix da ist.
 */
export function stripSequenceSuffix(part: string): string | null {
  // Vor der Zählung muss ein Trenner stehen. Ohne diese Bedingung würde jede
  // Typbezeichnung, die auf eine Ziffer endet („Cartridge M12"), als
  // durchnummeriert gelesen.
  const m = /^(.*?)[\s\-_#]+\(?(?:1[0-9]|[2-9]|II|III|IV)\)?\s*$/.exec(part.trim())
  if (m === null) return null
  const base = m[1].trim()
  return base === '' ? null : base
}

/** Trägt die Position einen Auslegungsnachweis? */
function hasSpecification(t: ToolSpecification): boolean {
  const cav = t.cavityDesignation?.trim() ?? ''
  return cav !== '' || (t.lifetimeCycles ?? 0) > 0
}

/**
 * Vorrichtung oder Behälter statt Werkzeug?
 *
 * Entschieden wird an der Verrechnung, nicht am Namen: Wer keine Einmalzahlung
 * trägt, sondern eine Stückumlage, gehört nicht in die Einmalzahlungs-Brücke.
 * Eine Namensliste wäre hier die schlechtere Regel — sie müsste für jedes neue
 * Formblatt gepflegt werden.
 */
function isFixture(t: MappableTool): boolean {
  return (t.oneTimeCost ?? 0) === 0 && (t.perPieceCost ?? 0) > 0
}

/**
 * Veränderung der Stückumlage.
 *
 * `null`, solange auf keiner Seite eine Umlage steht — dann gibt es nichts zu
 * vergleichen. Steht sie nur auf einer Seite, ist der Zugang beziehungsweise
 * Wegfall die Aussage.
 */
function perPieceDeltaOf(award: number | null, current: number | null): number | null {
  if (award === null && current === null) return null
  return (current ?? 0) - (award ?? 0)
}

interface Pairing {
  award: MappableTool | null
  current: MappableTool | null
  /** Über Zählsuffix zugeordnet — also ein zusätzliches Werkzeug. */
  viaSuffix: boolean
  /** Basisposition, an die das Zweitwerkzeug andockt. */
  suffixBase: MappableTool | null
}

/**
 * Werkzeuge zuordnen, klassifizieren und die Einmalzahlung aufbrücken.
 *
 * Die Zuordnung läuft in zwei Durchgängen, und die Reihenfolge ist tragend:
 *
 *  1. Namensgleiche Positionen paaren. Das fängt auch verschobene Zeilen — eine
 *     Position, die nur ans Listenende gewandert ist, ist wirtschaftlich
 *     unverändert und kein Brückenschritt.
 *  2. Was danach übrig ist, gegen den abgestreiften Zählsuffix prüfen. Findet
 *     sich die Basisposition, ist es ein Zweitwerkzeug.
 *
 * Andersherum ginge es schief: „Halter 2" würde die Basisposition wegschnappen,
 * das verschobene Original bliebe unzugeordnet stehen und wäre plötzlich das
 * Zweitwerkzeug. Die Brückensumme stimmte dann zwar weiterhin, aber genau die
 * Aussage, um die es geht, wäre vertauscht.
 *
 * `awardTotal`/`currentTotal` sind die ausgewiesenen Einmalzahlungen aus der
 * Zusammenfassung. Ohne sie bleibt die Brücke ungeprüft — das steht dann als
 * „nicht geprüft" da und nicht als bestanden.
 */
export function analyseTooling(
  award: MappableTool[],
  current: MappableTool[],
  awardTotal: number | null = null,
  currentTotal: number | null = null,
): ToolingResult {
  const byRow = (a: MappableTool, b: MappableTool) => a.row - b.row
  const realAward = award.filter((t) => !t.isTotalLine).sort(byRow)
  const realCurrent = current.filter((t) => !t.isTotalLine).sort(byRow)

  const awardByName = new Map<string, MappableTool[]>()
  for (const t of realAward) {
    const k = nameKey(t.part)
    if (k === '') continue
    awardByName.set(k, [...(awardByName.get(k) ?? []), t])
  }

  const usedAward = new Set<number>()
  const pairings: Pairing[] = []
  const unmatchedCurrent: MappableTool[] = []

  // Durchgang 1 — namensgleiche Positionen.
  for (const c of realCurrent) {
    const k = nameKey(c.part)
    const partner = (awardByName.get(k) ?? []).find((a) => !usedAward.has(a.row))
    if (partner === undefined) {
      unmatchedCurrent.push(c)
      continue
    }
    usedAward.add(partner.row)
    pairings.push({ award: partner, current: c, viaSuffix: false, suffixBase: null })
  }

  // Durchgang 2 — Zählsuffix abstreifen und die Basisposition suchen.
  for (const c of unmatchedCurrent) {
    const stripped = stripSequenceSuffix(c.part)
    const baseCandidates = stripped === null ? [] : (awardByName.get(nameKey(stripped)) ?? [])
    // Ohne Suffix zählt, ob der Name im Vergabestand überhaupt vorkam: dann
    // steht hier eine zweite Position gleichen Namens.
    const sameNameExisted = (awardByName.get(nameKey(c.part)) ?? []).length > 0
    const base = baseCandidates[0] ?? (sameNameExisted ? (awardByName.get(nameKey(c.part)) ?? [])[0] : null)
    pairings.push({ award: null, current: c, viaSuffix: base !== null, suffixBase: base ?? null })
  }

  const mappings: ToolMapping[] = []
  let id = 0
  const nextId = () => `TOOL-${String(++id).padStart(3, '0')}`

  for (const p of pairings.sort((a, b) => (a.current?.row ?? 0) - (b.current?.row ?? 0))) {
    const c = p.current!
    const a = p.award

    if (a === null) {
      const base = p.suffixBase
      // Ein Zweitwerkzeug ist es nur, wenn die Basisposition im aktuellen Stand
      // noch steht. Ist sie verschwunden, wurde nichts verdoppelt, sondern
      // ersetzt — dann als Wegfall plus Zugang zu zeigen ist die ehrlichere
      // Darstellung.
      const second = base !== null && usedAward.has(base.row)
      const specRemoved = second && !hasSpecification(c) && hasSpecification(base!)
      const classification: ToolClassification = isFixture(c) ? 'fixture' : second ? 'second_tool' : 'new_scope'

      mappings.push({
        mappingId: nextId(),
        awardRow: null,
        currentRow: c.row,
        part: c.part,
        status: 'new',
        classification,
        specRemovedInCurrent: specRemoved,
        rowMoved: false,
        evidenceDe: second
          ? `Für „${base!.part}" bestand im Vergabestand bereits ein Werkzeug (Zeile ${base!.row}); dies ist ein zusätzliches.` +
            (specRemoved ? ' Kavitäten und Standzeit sind hier nicht angegeben, beim ursprünglichen Werkzeug schon.' : '')
          : `„${c.part}" hatte im Vergabestand kein Werkzeug.`,
        oneTimeAward: null,
        oneTimeCurrent: c.oneTimeCost,
        delta: c.oneTimeCost ?? 0,
        perPieceAward: null,
        perPieceCurrent: c.perPieceCost ?? null,
        perPieceDelta: c.perPieceCost ?? null,
      })
      continue
    }

    const delta = (c.oneTimeCost ?? 0) - (a.oneTimeCost ?? 0)
    const changed = Math.abs(delta) > TOLERANCE
    const specRemoved = hasSpecification(a) && !hasSpecification(c)
    const rowMoved = a.row !== c.row
    const classification: ToolClassification = isFixture(c) ? 'fixture' : changed ? 'price_change' : 'unchanged'

    mappings.push({
      mappingId: nextId(),
      awardRow: a.row,
      currentRow: c.row,
      part: c.part,
      status: changed ? 'changed' : 'unchanged',
      classification,
      specRemovedInCurrent: specRemoved,
      rowMoved,
      evidenceDe:
        (changed
          ? `Werkzeug für „${c.part}" ist im Einmalbetrag verändert.`
          : `Werkzeug für „${c.part}" ist im Einmalbetrag unverändert.`) +
        (rowMoved ? ` Zeile ${a.row} steht jetzt an Position ${c.row}.` : '') +
        (specRemoved ? ' Die Auslegungsangaben sind im aktuellen Stand nicht mehr gefüllt.' : ''),
      oneTimeAward: a.oneTimeCost,
      oneTimeCurrent: c.oneTimeCost,
      delta,
      perPieceAward: a.perPieceCost ?? null,
      perPieceCurrent: c.perPieceCost ?? null,
      perPieceDelta: perPieceDeltaOf(a.perPieceCost ?? null, c.perPieceCost ?? null),
    })
  }

  for (const a of realAward) {
    if (usedAward.has(a.row)) continue
    mappings.push({
      mappingId: nextId(),
      awardRow: a.row,
      currentRow: null,
      part: a.part,
      status: 'removed',
      classification: isFixture(a) ? 'fixture' : 'price_change',
      specRemovedInCurrent: false,
      rowMoved: false,
      evidenceDe: `Werkzeug für „${a.part}" ist im aktuellen Stand nicht mehr enthalten.`,
      oneTimeAward: a.oneTimeCost,
      oneTimeCurrent: null,
      delta: -(a.oneTimeCost ?? 0),
      perPieceAward: a.perPieceCost ?? null,
      perPieceCurrent: null,
      perPieceDelta: a.perPieceCost === null || a.perPieceCost === undefined ? null : -a.perPieceCost,
    })
  }

  const byClassification = { ...EMPTY_CLASSIFICATION }
  for (const m of mappings) byClassification[m.classification] += 1

  const steps: OneTimeBridgeStep[] = BRIDGE_CLASSES.map((classification) => {
    const group = mappings.filter((m) => m.classification === classification)
    return {
      classification,
      label: CLASSIFICATION_LABEL[classification],
      delta: group.reduce((s, m) => s + (m.delta ?? 0), 0),
      toolRows: group.map((m) => m.currentRow ?? m.awardRow ?? -1),
    }
  }).filter((s) => Math.abs(s.delta) > TOLERANCE)

  const bridgeDelta = awardTotal !== null && currentTotal !== null ? currentTotal - awardTotal : null
  const stepSum = steps.reduce((s, x) => s + x.delta, 0)
  const residual = bridgeDelta === null ? null : bridgeDelta - stepSum

  return {
    mappings,
    counts: {
      toolsAward: realAward.length,
      toolsCurrent: realCurrent.length,
      byClassification,
      specRemoved: mappings.filter((m) => m.specRemovedInCurrent).length,
      secondToolsWithoutSpec: mappings.filter((m) => m.classification === 'second_tool' && m.specRemovedInCurrent)
        .length,
    },
    oneTimeBridge: {
      start: awardTotal,
      end: currentTotal,
      delta: bridgeDelta,
      steps,
      residual,
      passed: residual === null ? null : Math.abs(residual) <= TOLERANCE,
    },
  }
}
