// G60 scenario recompute engine (KAR-840 stage G4) — 1:1 port of V11
// b3_core.js effParams/recomputeTab/scenarioModel/recomputeScenario/tabDetail.
//
// Delta anchoring (the V11 core idea): the engine recomputes each tab twice —
// once with no overrides ("base") and once with the scenario overrides — and
// applies only the DIFFERENCE on top of the cached (persisted) aggregates.
// With no overrides the scenario therefore reproduces the persisted numbers
// EXACTLY, even where our row model approximates the workbook's formulas
// (rem_pu holds the per-unit remainder constant).
//
// Pure and client-safe: runs in the browser for live editing.

import { G60_BUCKETS } from './analyze'
import { SECONDS_PER_HOUR, type G60ComponentRow, type G60Rates, type G60TabAggregate } from './parser'

// ── Overrides ─────────────────────────────────────────────────────────────────

/** Global scenario parameters (V11 override bundle, factors ×1 = unchanged). */
export interface G60ScenarioGlobal {
  cycleFactor?: number
  empFactor?: number
  /** Absolute employee count for every step (wins over empFactor). */
  empAbs?: number
  /** Absolute labour rate €/h (wins over labourFactor). */
  labourRate?: number
  labourFactor?: number
  /** Absolute machine rate €/h (wins over machFactor). */
  machRate?: number
  machFactor?: number
  matFactor?: number
  /** Absolute inefficiency surcharge (0 = removed). */
  ineffAbs?: number
  /** Absolute scrap-per-step rate. */
  scrapAbs?: number
}

/** Per-row overrides keyed by sheet row number (the editor's line edits). */
export interface G60RowOverride {
  AO?: number
  AQ?: number
  AR?: number
  AS?: number
  AT?: number
  BC?: number
  W?: number
}

export interface G60TabOverrides {
  global?: G60ScenarioGlobal
  rows?: Record<number, G60RowOverride>
}

export interface G60ScenarioOverrides {
  global?: G60ScenarioGlobal
  perTab?: Record<string, G60TabOverrides>
}

interface EffectiveParams {
  AO: number
  AQ: number
  AR: number
  AS: number
  AT: number
  BC: number
  W: number
}

/** Resolve effective params for one component row (row override > global > base). */
export function effParams(
  c: G60ComponentRow,
  ov: { global?: G60ScenarioGlobal; rows?: Record<number, G60RowOverride> },
): EffectiveParams {
  const g = ov.global ?? {}
  const pr = ov.rows?.[c.r] ?? {}
  return {
    AO: pr.AO ?? c.AO * (g.cycleFactor ?? 1),
    AQ: pr.AQ ?? g.empAbs ?? c.AQ * (g.empFactor ?? 1),
    AR: pr.AR ?? g.labourRate ?? c.AR * (g.labourFactor ?? 1),
    AS: pr.AS ?? g.ineffAbs ?? c.AS,
    AT: pr.AT ?? g.machRate ?? c.AT * (g.machFactor ?? 1),
    BC: pr.BC ?? g.scrapAbs ?? c.BC,
    W: pr.W ?? c.W * (g.matFactor ?? 1),
  }
}

// ── Tab recompute (V11 recomputeTab, QAF SUMIF semantics) ─────────────────────

export interface G60TabTotals {
  W: number
  Y: number
  Pers: number
  Mach: number
  MfgTot: number
  Scrap: number
  SGA: number
  Profit: number
  Sales: number
  TC: number
  HK: number
  AV: number
  Material: number
  Labor: number
  Manufacturing: number
  ScrapB: number
}

export function recomputeTab(
  rows: G60ComponentRow[],
  rates: G60Rates,
  ov: { global?: G60ScenarioGlobal; rows?: Record<number, G60RowOverride> },
): G60TabTotals {
  const out = { W: 0, Y: 0, Pers: 0, Mach: 0, MfgTot: 0, Scrap: 0, SGA: 0, Profit: 0, Sales: 0, TC: 0, HK: 0, AV: 0 }
  for (const c of rows) {
    const p = effParams(c, ov)
    const labPu = c.AP > 0 ? (p.AQ * p.AR * (p.AS + 1) * p.AO) / c.AP / SECONDS_PER_HOUR : 0
    const macPu = c.AP > 0 ? (p.AT * p.AO) / c.AP / SECONDS_PER_HOUR : 0
    const AX = labPu + macPu + c.rem_pu
    const BB = AX * c.BA * c.AZ
    const BH = labPu * c.BA * c.AZ
    const BI = macPu * c.BA * c.AZ
    const BD = BB * (1 / (1 - p.BC) - 1)
    const Y = c.E === 'prämie' ? 0 : p.W * (1 / (1 - c.X) - 1)
    const DG = p.W + Y + BD + BB + c.DB
    let sga: number
    let prof: number
    if (c.E === 'directed') {
      sga = AX * rates.ovFK_d + p.W * rates.ovMAT_d
      prof = AX * rates.pfFK_d + p.W * rates.pfMAT_d
    } else {
      sga = AX * rates.ovFK_s + p.W * rates.ovMAT_s
      prof = AX * rates.pfFK_s + p.W * rates.pfMAT_s
    }
    const DI = sga + prof
    const DJ = DG + DI + c.AD
    // QAF SUMIF criteria: material/price columns gate on L=EUR, mfg on AY=EUR.
    if (c.L === 'EUR') {
      out.W += p.W
      out.Y += Y
      out.SGA += sga
      out.Profit += prof
      out.TC += DG
      out.Sales += DJ
      out.HK += p.W + BB
    }
    if (c.AY === 'EUR') {
      out.Pers += BH
      out.Mach += BI
      out.MfgTot += BB
      out.Scrap += BD
      out.AV += BH + BI
    }
  }
  return {
    ...out,
    Material: out.W + out.Y,
    Labor: out.Pers,
    Manufacturing: out.MfgTot - out.Pers,
    ScrapB: out.Scrap,
  }
}

// ── Scenario model + delta-anchored recompute (V11 scenarioModel) ─────────────

export interface G60ScenarioTab {
  part: string
  rows: G60ComponentRow[]
  cached: G60TabAggregate
  /** Cached Sales − Σ cached buckets: logistics/duty remainder, held constant. */
  logistics: number
}

export interface G60ScenarioModel {
  order: string[]
  rates: G60Rates
  tabs: Record<string, G60ScenarioTab>
}

export function buildScenarioModel(
  parsed: { rates: G60Rates; tabs: Record<string, G60TabAggregate> },
  fullTabs: Record<string, G60ComponentRow[]>,
): G60ScenarioModel {
  // Only tabs that BOTH sides know: a stale row filtered out by the
  // rehydrate shape-guard must not reach the model (undefined cached → crash).
  const order = Object.keys(parsed.tabs).filter((t) => fullTabs[t] && parsed.tabs[t])
  const tabs: Record<string, G60ScenarioTab> = {}
  for (const t of order) {
    const cached = parsed.tabs[t]
    // PR #328 review fix (finding [3]/[5]/task f): SGA/Profit can be `null`
    // on a rateDegradation-affected G60TabAggregate (see that field's doc
    // comment). `?? 0` here is a defensive/type-satisfying fallback ONLY —
    // qaf-g60-detail.tsx (the sole caller of the scenario editor) disables
    // the whole "Was-wäre-wenn"-Sektion whenever `rateDegradation` is set,
    // specifically so this engine never has to reconcile a live recompute
    // (which always produces a real number, see recomputeTab below) against
    // a persisted `null` — a real null reaching here in practice would mean
    // that gate was bypassed, not that 0 is a trustworthy value.
    const logistics = cached.Sales - G60_BUCKETS.reduce((s, b) => s + (cached[b] ?? 0), 0)
    tabs[t] = { part: cached.part, rows: fullTabs[t], cached, logistics }
  }
  return { order, rates: parsed.rates, tabs }
}

export interface G60ScenarioTabResult {
  part: string
  Material: number
  Labor: number
  Manufacturing: number
  ScrapB: number
  SGA: number
  Profit: number
  Sales: number
}

export interface G60ScenarioResult {
  prog: G60ScenarioTabResult & { _pct: Record<string, number> }
  byTab: Record<string, G60ScenarioTabResult>
}

function mergedTabOverrides(
  ov: G60ScenarioOverrides,
  t: string,
): { global: G60ScenarioGlobal; rows: Record<number, G60RowOverride> } {
  const pt = ov.perTab?.[t] ?? {}
  return { global: { ...(ov.global ?? {}), ...(pt.global ?? {}) }, rows: pt.rows ?? {} }
}

/**
 * Baseline result assembled directly from the persisted aggregates — O(tabs).
 * Identical to recomputeScenario(model, {}) by the delta-anchoring invariant
 * (unit-tested); avoids a full formula pass just to reproduce cached numbers.
 */
export function scenarioBaseline(model: G60ScenarioModel): G60ScenarioResult {
  const prog: Record<string, number> = { Sales: 0 }
  for (const b of G60_BUCKETS) prog[b] = 0
  const byTab: Record<string, G60ScenarioTabResult> = {}
  for (const t of model.order) {
    const T = model.tabs[t]
    const vals = {} as Record<(typeof G60_BUCKETS)[number], number>
    // `?? 0` — see buildScenarioModel's doc comment above (rateDegradation
    // gate lives in the UI, this is a defensive fallback only).
    for (const b of G60_BUCKETS) vals[b] = T.cached[b] ?? 0
    byTab[t] = { part: T.part, ...vals, Sales: T.cached.Sales }
    prog.Sales += T.cached.Sales
    for (const b of G60_BUCKETS) prog[b] += T.cached[b] ?? 0
  }
  const _pct: Record<string, number> = {}
  for (const b of G60_BUCKETS) _pct[b] = prog.Sales ? (prog[b] / prog.Sales) * 100 : 0
  return { prog: { part: '', ...(prog as unknown as Omit<G60ScenarioTabResult, 'part'>), _pct }, byTab }
}

/** Delta-anchored: cached + (overridden − base); no overrides → exactly cached. */
export function recomputeScenario(model: G60ScenarioModel, ov: G60ScenarioOverrides): G60ScenarioResult {
  const prog: Record<string, number> = { Sales: 0 }
  for (const b of G60_BUCKETS) prog[b] = 0
  const byTab: Record<string, G60ScenarioTabResult> = {}

  for (const t of model.order) {
    const T = model.tabs[t]
    const tov = mergedTabOverrides(ov, t)
    const base = recomputeTab(T.rows, model.rates, {})
    const cur = recomputeTab(T.rows, model.rates, tov)
    const vals = {} as Record<(typeof G60_BUCKETS)[number], number>
    // `?? 0` — see buildScenarioModel's doc comment (rateDegradation gate
    // lives in the UI, this is a defensive fallback only).
    for (const b of G60_BUCKETS) vals[b] = (T.cached[b] ?? 0) + (cur[b] - base[b])
    const sales = G60_BUCKETS.reduce((s, b) => s + vals[b], 0) + T.logistics
    byTab[t] = { part: T.part, ...vals, Sales: sales }
    prog.Sales += sales
    for (const b of G60_BUCKETS) prog[b] += vals[b]
  }

  const _pct: Record<string, number> = {}
  for (const b of G60_BUCKETS) _pct[b] = prog.Sales ? (prog[b] / prog.Sales) * 100 : 0
  return { prog: { part: '', ...(prog as unknown as Omit<G60ScenarioTabResult, 'part'>), _pct }, byTab }
}

// ── Per-row detail for the process editor (V11 tabDetail) ─────────────────────

export interface G60DetailRow {
  r: number
  proc: string
  desig: string
  isProcess: boolean
  cycle: number
  ppc: number
  emp: number
  labourRate: number
  ineff: number
  mss: number
  scrapStep: number
  material: number
  labor: number
  machine: number
  mfg: number
  scrap: number
  orig: { cycle: number; emp: number; labourRate: number; mss: number; scrapStep: number; material: number }
}

export interface G60TabDetail {
  part: string
  rows: G60DetailRow[]
  agg: Record<(typeof G60_BUCKETS)[number], number>
  sales: number
}

export function tabDetail(model: G60ScenarioModel, t: string, ov: G60ScenarioOverrides): G60TabDetail {
  const T = model.tabs[t]
  const tov = mergedTabOverrides(ov, t)

  const rows: G60DetailRow[] = T.rows.map((c) => {
    const p = effParams(c, tov)
    const labPu = c.AP > 0 ? (p.AQ * p.AR * (p.AS + 1) * p.AO) / c.AP / SECONDS_PER_HOUR : 0
    const macPu = c.AP > 0 ? (p.AT * p.AO) / c.AP / SECONDS_PER_HOUR : 0
    const AX = labPu + macPu + c.rem_pu
    const BB = AX * c.BA * c.AZ
    const BH = labPu * c.BA * c.AZ
    const BI = macPu * c.BA * c.AZ
    const BD = BB * (1 / (1 - p.BC) - 1)
    return {
      r: c.r,
      proc: c.proc,
      desig: c.desig,
      isProcess: Boolean(c.AO > 0 || c.proc),
      cycle: p.AO,
      ppc: c.AP,
      emp: p.AQ,
      labourRate: p.AR,
      ineff: p.AS,
      mss: p.AT,
      scrapStep: p.BC,
      material: p.W,
      labor: BH,
      machine: BI,
      mfg: BB,
      scrap: BD,
      orig: { cycle: c.AO, emp: c.AQ, labourRate: c.AR, mss: c.AT, scrapStep: c.BC, material: c.W },
    }
  })

  const agg = recomputeTab(T.rows, model.rates, tov)
  const bas = recomputeTab(T.rows, model.rates, {})
  const vals = {} as Record<(typeof G60_BUCKETS)[number], number>
  // `?? 0` — see buildScenarioModel's doc comment (rateDegradation gate
  // lives in the UI, this is a defensive fallback only).
  for (const b of G60_BUCKETS) vals[b] = (T.cached[b] ?? 0) + (agg[b] - bas[b])
  const sales = G60_BUCKETS.reduce((s, b) => s + vals[b], 0) + T.logistics

  return { part: T.part, rows, agg: vals, sales }
}
