// G60 view-models (KAR-840 G60 stage G3): the analysis result → the shapes the
// existing charts/sections consume (buckets, bridge, movers) plus the
// G60-specific views (anomalies with CMEAN clear names, the three lever texts,
// the production index ALT=100). Pure + unit-tested.
//
// Deliberate improvement over V11 renderSec3: its Scrap/Cycle/#MA bars carried
// hard-coded placeholder values — here every index is the median over the real
// per-tab primary-step ratios.

import { BRIDGE_MIN_STEP, BRIDGE_MIN_RESIDUAL, BUCKET_LABEL_DE, type BridgeData, type BucketDatum } from '../summary-view'
import type { MoversInput } from '../movers'
import { G60_BUCKETS, type G60Analysis } from './analyze'

// ── Buckets & bridge (reuse the existing chart components) ───────────────────

export function buildG60Buckets(a: G60Analysis): BucketDatum[] {
  return G60_BUCKETS.map((key) => ({
    key,
    label: BUCKET_LABEL_DE[key],
    alt: a.prog.basis[key],
    neu: a.prog.repr[key],
    delta: a.prog.repr[key] - a.prog.basis[key],
  }))
}

/** V11 buildBridgeG60: bucket deltas + 'Other' residual per side. */
export function buildG60Bridge(a: G60Analysis): BridgeData | null {
  if (a.common.length === 0) return null

  const steps = G60_BUCKETS.map((key) => ({
    bucket: key as BucketDatum['key'],
    label: BUCKET_LABEL_DE[key],
    delta: a.prog.repr[key] - a.prog.basis[key],
  })).filter((s) => Math.abs(s.delta) > BRIDGE_MIN_STEP)

  const sumBuckets = (side: 'basis' | 'repr'): number => G60_BUCKETS.reduce((s, k) => s + a.prog[side][k], 0)
  const otherNeu = a.set_r - sumBuckets('repr')
  // Vor dem Einbuchen bestimmt, damit der ausgewiesene Rest die Wahrheit sagt
  // und nicht die nachträglich geschlossene Summe.
  const residual = a.dset - steps.reduce((s, x) => s + x.delta, 0)
  const residualWithinTolerance = Math.abs(residual) <= BRIDGE_MIN_RESIDUAL
  if (!residualWithinTolerance) {
    // G60 behält den Sammelposten vorerst: Die V2-Spezifikation (R-06) adressiert
    // den Preisblock des Standard-Vergleichs, dessen Additivität im Template
    // festgeschrieben ist. Für die G60-Kostenblöcke existiert diese Zusicherung
    // nicht, und es gibt keinen abgenommenen Golden Case dafür — der Umbau wäre
    // hier eine Vermutung. Der Rest wird jetzt aber wenigstens ausgewiesen.
    steps.push({ bucket: 'Other' as BucketDatum['key'], label: BUCKET_LABEL_DE.Other, delta: residual })
  }

  const parts = [
    ...G60_BUCKETS.map((key) => ({
      key: key as BucketDatum['key'] | 'Other',
      label: BUCKET_LABEL_DE[key],
      value: a.prog.repr[key],
    })),
    { key: 'Other' as const, label: BUCKET_LABEL_DE.Other, value: otherNeu },
  ]
  // Same honesty rule as the summary bridge: no stack that breaks below zero.
  const endComposition = parts.every((p) => p.value >= -BRIDGE_MIN_STEP) ? parts : []

  return { start: a.set_b, end: a.set_r, delta: a.dset, steps, endComposition, residual, residualWithinTolerance }
}

/** Tornado input: one row per common cost tab (V11 movers kind 'bauteil'). */
export function buildG60MoversInput(a: G60Analysis): MoversInput {
  return {
    rows: a.common.map((t) => {
      const s = a.salesByTab[t]
      return {
        stepLabel: s.part ? `${t} · ${s.part}` : t,
        delta: s.r - s.b,
        uncertain: false,
      }
    }),
  }
}

// ── Anomalies & drivers (V11 section 8, CMEAN clear names) ───────────────────

/** V11 CMEAN: INPUT C-code → [clear name, unit, category]. */
const CMEAN: Record<string, [string, string, string]> = {
  C20: ['FX-Kurs USD→EUR', '%', 'fx'],
  C21: ['FX-Kurs CNY→EUR', '%', 'fx'],
  C22: ['Gemeinkosten Fertigung', '%', 'gk'],
  C23: ['Gemeinkosten Material', '%', 'gk'],
  C24: ['Gewinnaufschlag Fertigung', '%', 'gewinn'],
  C25: ['Gewinnaufschlag Material', '%', 'gewinn'],
  C26: ['Gemeinkosten Fertigung (sourced)', '%', 'gk'],
  C27: ['Gemeinkosten Material (sourced)', '%', 'gk'],
  C28: ['Gewinn Fertigung (sourced)', '%', 'gewinn'],
  C29: ['Gewinn Material (sourced)', '%', 'gewinn'],
  C30: ['Material-Gemeinkosten (MGK)', '%', 'gk'],
  C31: ['Schrott Durchschnitt', '%', 'sonst'],
  C32: ['Sekof Zinssatz', '%', 'sonst'],
  C33: ['Zoll', '%', 'sonst'],
  C34: ['RIK-Quartal', 't', 'sonst'],
  C35: ['Granulatpreis ABS', '€', 'roh'],
  C36: ['Granulatpreis ABS-PC', '€', 'roh'],
  C37: ['Granulatpreis PA', '€', 'roh'],
  C38: ['Granulatpreis PP', '€', 'roh'],
  C40: ['Lohnsatz Kragujevac (RS)', '€', 'lohn'],
  C41: ['Lohnsatz Namestovo (SK)', '€', 'lohn'],
  C42: ['Lohnsatz Neustadt (DE)', '€', 'lohn'],
  C43: ['Lohnsatz Papa (HU)', '€', 'lohn'],
  C44: ['Lohnsatz Zatec (CZ)', '€', 'lohn'],
  C58: ['Nahtband / Seam Tape', '€', 'roh'],
}

export const G60_CATEGORY_LABEL: Record<string, string> = {
  lohn: 'Lohn',
  gk: 'Gemeinkosten',
  gewinn: 'Gewinn',
  fx: 'FX',
  roh: 'Rohstoff',
  sonst: 'Sonstiges',
}

/** V11: |Δ%| from this threshold up renders bold ("heiß"). */
const ANOMALY_HOT_THRESHOLD = 0.2

export interface G60AnomalyRow {
  code: string
  name: string
  unit: string
  category: string
  basis: number
  repr: number
  chg: number | null
  hot: boolean
}

export function buildG60Anomalies(a: G60Analysis): G60AnomalyRow[] {
  return a.driverDiff
    .map((d) => {
      const meta = CMEAN[d.code]
      return {
        code: d.code,
        name: meta ? meta[0] : d.label,
        unit: meta ? meta[1] : '',
        category: meta ? meta[2] : 'sonst',
        basis: d.basis,
        repr: d.repr,
        chg: d.chg,
        hot: d.chg !== null && Math.abs(d.chg) >= ANOMALY_HOT_THRESHOLD,
      }
    })
    .sort((x, y) => Math.abs(y.chg ?? 0) - Math.abs(x.chg ?? 0))
}

// ── Levers (V11 section 7, G60 variant) ───────────────────────────────────────

export interface G60Lever {
  title: string
  detail: string
  ask: string
}

const pctFmt = new Intl.NumberFormat('de-DE', { style: 'percent', maximumFractionDigits: 0, signDisplay: 'exceptZero' })
const eurFmt = new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 })

export function buildG60Levers(a: G60Analysis): G60Lever[] {
  const byCode = Object.fromEntries(a.driverDiff.map((d) => [d.code, d]))
  const ov = byCode.C23 ?? byCode.C27
  const pf = byCode.C25 ?? byCode.C29
  const lo = byCode.C42
  // Same number the Produktionssicht shows — median, not first-tab (review #251).
  const ineff = medianIneff(a)

  return [
    {
      title: a.glob
        ? `1 · Maschinen-Stundensatz (MSS) ×${a.glob.toFixed(2)} (${pctFmt.format(a.glob - 1)})`
        : '1 · Maschinen-Stundensatz (MSS)',
      detail: 'Identischer Faktor über fast alle Teile.',
      ask: 'MSS-Kalkulation + Benchmark offenlegen.',
    },
    {
      title: `2 · Ineffizienz-Aufschlag ${Math.round(ineff * 100)} % auf Lohn (neu)`,
      detail: lo ? `Lohnsatz geändert (${eurFmt.format(lo.basis)} → ${eurFmt.format(lo.repr)} €/h).` : '',
      ask: 'Ineffizienz streichen/begründen.',
    },
    {
      title: '3 · Gemeinkosten/Gewinn Material',
      detail: [
        ov ? `Overhead MAT ${Math.round(ov.basis * 100)} → ${Math.round(ov.repr * 100)} %` : null,
        pf ? `Profit MAT ${Math.round(pf.basis * 100)} → ${Math.round(pf.repr * 100)} %` : null,
      ]
        .filter(Boolean)
        .join(', '),
      ask: 'Einkaufsvorteil durchreichen.',
    },
  ]
}

// ── Production index (V11 section 3 — generalized over real medians) ─────────

export interface G60ProdIndexRow {
  kpi: string
  /** ALT reference (100 for indices, ALT percent for Ineffizienz). */
  alt: number
  neu: number
  /** True when the pair is an ALT=100 index (vs. absolute percent values). */
  isIndex: boolean
  /** Median raw values (Kais feedback: units + before/after + Δ%). */
  altRaw: number | null
  neuRaw: number | null
  unit: string
  /** neu/alt − 1 over the raw medians; null when not computable. */
  deltaPct: number | null
}

/** Median NEU inefficiency over all tabs — single source for lever + prod view. */
export function medianIneff(a: G60Analysis): number {
  return median(a.prod.map((p) => p.inef_r).filter((v): v is number => v !== null)) ?? 0
}

function median(values: number[]): number | null {
  if (values.length === 0) return null
  const sorted = [...values].sort((a, b) => a - b)
  return sorted[Math.floor(sorted.length / 2)]
}

function medianRatio(pairs: Array<[number | null, number | null]>): number | null {
  return median(
    pairs
      .filter((p): p is [number, number] => p[0] !== null && p[1] !== null && p[0] !== 0)
      .map(([b, r]) => r / b),
  )
}

function medianOf(values: Array<number | null>): number | null {
  return median(values.filter((v): v is number => v !== null))
}

function rawDeltaPct(alt: number | null, neu: number | null): number | null {
  if (alt === null || neu === null || alt === 0) return null
  return neu / alt - 1
}

export function buildProdIndex(a: G60Analysis): G60ProdIndexRow[] {
  const rows: G60ProdIndexRow[] = []

  const mssAlt = medianOf(a.prod.map((p) => p.mr_b))
  const mssNeu = medianOf(a.prod.map((p) => p.mr_r))
  rows.push({
    kpi: 'Maschinensatz',
    alt: 100,
    neu: (a.glob ?? 1) * 100,
    isIndex: true,
    altRaw: mssAlt,
    neuRaw: mssNeu,
    unit: '€/h',
    deltaPct: a.glob !== null ? a.glob - 1 : rawDeltaPct(mssAlt, mssNeu),
  })

  // Chart needs numbers (missing → 0 bar); the table must show '—' instead of
  // a false exact zero, so the raw fields keep null for missing data.
  const ineffAltRaw = medianOf(a.prod.map((p) => p.inef_b))
  const ineffNeuRaw = medianOf(a.prod.map((p) => p.inef_r))
  rows.push({
    kpi: 'Ineffizienz',
    alt: (ineffAltRaw ?? 0) * 100,
    neu: medianIneff(a) * 100,
    isIndex: false,
    altRaw: ineffAltRaw === null ? null : ineffAltRaw * 100,
    neuRaw: ineffNeuRaw === null ? null : ineffNeuRaw * 100,
    unit: '%-Pkt.',
    deltaPct: rawDeltaPct(ineffAltRaw, ineffNeuRaw),
  })

  const defs: Array<
    [string, string, number, (p: G60Analysis['prod'][number]) => [number | null, number | null]]
  > = [
    ['Scrap/Step', '%', 100, (p) => [p.scr_b, p.scr_r]],
    ['Cycle time', 's', 1, (p) => [p.cyc_b, p.cyc_r]],
    ['# Mitarbeiter', 'MA', 1, (p) => [p.emp_b, p.emp_r]],
  ]
  for (const [kpi, unit, scale, pick] of defs) {
    const ratio = medianRatio(a.prod.map(pick))
    const altRaw = medianOf(a.prod.map((p) => pick(p)[0]))
    const neuRaw = medianOf(a.prod.map((p) => pick(p)[1]))
    rows.push({
      kpi,
      alt: 100,
      neu: (ratio ?? 1) * 100,
      isIndex: true,
      altRaw: altRaw === null ? null : altRaw * scale,
      neuRaw: neuRaw === null ? null : neuRaw * scale,
      unit,
      deltaPct: ratio === null ? null : ratio - 1,
    })
  }
  return rows
}
