// G60 pair analysis (KAR-840 G60 stage 1) — 1:1 port of V11 b3_core.js
// analyze(): common cost tabs, six-bucket aggregation per side, global
// machine lever (median at15 ratio) with the PART-SPECIFIC ±5 % flag,
// the 11 per-tab metric rows, the production table (primary steps), the
// annual impact from the repricing volumes and the INPUT rate-card diff.
// Pure + unit-tested.

import {
  primaryStep,
  type G60InputCard,
  type G60MetricKey,
  type G60ParseResult,
  type G60TabAggregate,
} from './parser'
import { parseLocaleNumber } from '../normalizer'

export const G60_BUCKETS = ['Material', 'Labor', 'Manufacturing', 'ScrapB', 'SGA', 'Profit'] as const
export type G60Bucket = (typeof G60_BUCKETS)[number]

export interface G60SideAggregate extends Record<G60Bucket, number> {
  Sales: number
  /** Bucket share of Sales in percent (V11 _pct). */
  pct: Record<G60Bucket, number>
}

export interface G60MetricRow {
  reiter: string
  part: string
  /** KAR-913/P4.3: the row-41 aggregate metric key this row was built from —
   * additive, lets a consumer (the G60 export builder) look up the matching
   * `G60TabAggregate.sourceCells[key]` provenance cell without re-deriving
   * headline → key from METRIC_DEFS (kept module-local). Does not affect any
   * existing consumer (qaf-g60-metrics-table.tsx renders by headline). */
  key: G60MetricKey
  headline: string
  bereich: string
  basis: number | null
  repr: number | null
  delta: number | null
  deltaPct: number | null
  flag: '' | 'PART-SPECIFIC'
}

export interface G60ProdRow {
  t: string
  part: string
  cyc_b: number | null
  cyc_r: number | null
  emp_b: number | null
  emp_r: number | null
  scr_b: number | null
  scr_r: number | null
  mr_b: number | null
  mr_r: number | null
  inef_b: number | null
  inef_r: number | null
}

export interface G60DriverDiff {
  code: string
  label: string
  basis: number
  repr: number
  /** repr/basis − 1; null when basis is 0. */
  chg: number | null
}

export interface G60Analysis {
  common: string[]
  prog: { basis: G60SideAggregate; repr: G60SideAggregate }
  /** Global machine lever — V11 median of repr/basis at15 ratios. */
  glob: number | null
  partSpecific: string[]
  rows: G60MetricRow[]
  prod: G60ProdRow[]
  driverDiff: G60DriverDiff[]
  years: number[]
  volume: number[]
  annual: number[]
  set_b: number
  set_r: number
  dset: number
  salesByTab: Record<string, { b: number; r: number; part: string }>
}

// V11 metricDefs: key, headline, bereich.
const METRIC_DEFS: Array<[G60MetricKey, string, string]> = [
  ['W', 'Material total', 'Material'],
  ['Y', 'Raw material surcharge', 'Material'],
  ['MfgTot', 'Manufacturing total', 'Manufacturing'],
  ['Pers', 'Personnel (Lohn)', 'Manufacturing'],
  ['Mach', 'Machine', 'Manufacturing'],
  ['AV', 'FEK', 'Manufacturing'],
  ['Scrap', 'Scrap (Mfg)', 'Manufacturing'],
  ['HK', 'HK (Total prod. costs)', 'HK'],
  ['Sur', 'Surcharges', 'Surcharges'],
  ['Sales', 'Quotation Price', 'Price'],
  ['TC', 'Total Costs', 'Total Costs'],
]

/** Metric headlines that carry the PART-SPECIFIC flag (V11). */
const FLAGGED_HEADLINES = new Set([
  'Machine',
  'Manufacturing total',
  'FEK',
  'HK (Total prod. costs)',
  'Quotation Price',
  'Total Costs',
])

/** V11 threshold: a tab whose at15 ratio deviates >5 % from the median. */
const PART_SPECIFIC_TOLERANCE = 0.05

/** V11: rate-card values differing by less than this are considered equal. */
const DRIVER_EPSILON = 1e-9

function sideAggregate(tabs: Record<string, G60TabAggregate>, common: string[]): G60SideAggregate {
  const agg = (key: G60Bucket | 'Sales'): number => common.reduce((s, t) => s + (tabs[t][key] || 0), 0)
  const sales = agg('Sales')
  const out = { Sales: sales, pct: {} as Record<G60Bucket, number> } as G60SideAggregate
  for (const bucket of G60_BUCKETS) {
    out[bucket] = agg(bucket)
    out.pct[bucket] = sales ? (out[bucket] / sales) * 100 : 0
  }
  return out
}

function diffCard(cardB: G60InputCard, cardR: G60InputCard): G60DriverDiff[] {
  const out: G60DriverDiff[] = []
  for (const code of Object.keys(cardB)) {
    const a = cardB[code]
    const b = cardR[code]
    if (!b) continue
    const fa = parseLocaleNumber(a.value)
    const fb = parseLocaleNumber(b.value)
    if (fa !== null && fb !== null && Math.abs(fa - fb) > DRIVER_EPSILON) {
      out.push({ code, label: a.label, basis: fa, repr: fb, chg: fa !== 0 ? fb / fa - 1 : null })
    }
  }
  return out
}

/**
 * Compare two parsed G60 files (basis = ALT, repr = NEU). aliasMap resolves
 * renamed repricing tabs to their basis names (V11/KAR-824 matching cascade).
 */
export function analyzeG60Pair(
  basis: G60ParseResult,
  repr: G60ParseResult,
  aliasMap?: Record<string, string>,
): G60Analysis {
  const B = basis.tabs
  const R: Record<string, G60TabAggregate> = {}
  for (const [name, tab] of Object.entries(repr.tabs)) {
    // V11 falsy fallback (empty-string alias → original name). On alias
    // collisions the FIRST tab wins (deterministic; V11 silently let the
    // last win) — resolving collisions is the matching-cascade caller's job.
    const key = aliasMap?.[name] || name
    if (!(key in R)) R[key] = tab
  }
  const common = Object.keys(B).filter((t) => t in R)

  // Global machine lever: V11 median = sorted ratios[floor(n/2)].
  const machRatios = common
    .filter((t) => B[t].at15 && R[t].at15)
    .map((t) => (R[t].at15 as number) / (B[t].at15 as number))
    .sort((a, b) => a - b)
  const glob = machRatios.length ? machRatios[Math.floor(machRatios.length / 2)] : null

  const flag = (t: string): '' | 'PART-SPECIFIC' => {
    if (B[t].at15 && R[t].at15 && glob) {
      const ratio = (R[t].at15 as number) / (B[t].at15 as number)
      if (Math.abs(ratio - glob) / glob > PART_SPECIFIC_TOLERANCE) return 'PART-SPECIFIC'
    }
    return ''
  }

  const rows: G60MetricRow[] = []
  for (const t of common) {
    const partFlag = flag(t)
    for (const [key, headline, bereich] of METRIC_DEFS) {
      const b = B[t][key] ?? null
      const r = R[t][key] ?? null
      if (b === null && r === null) continue
      const delta = b !== null && r !== null ? r - b : null
      rows.push({
        reiter: t,
        part: B[t].part || R[t].part,
        key,
        headline,
        bereich,
        basis: b,
        repr: r,
        delta,
        deltaPct: delta !== null && b ? delta / b : null,
        flag: FLAGGED_HEADLINES.has(headline) ? partFlag : '',
      })
    }
  }

  const prod: G60ProdRow[] = []
  for (const t of common) {
    const pb = primaryStep(B[t])
    const pr = primaryStep(R[t])
    if (pb && pr) {
      prod.push({
        t,
        part: B[t].part,
        cyc_b: pb.cycle,
        cyc_r: pr.cycle,
        emp_b: pb.emp,
        emp_r: pr.emp,
        scr_b: pb.scrapstep,
        scr_r: pr.scrapstep,
        mr_b: pb.machrate,
        mr_r: pr.machrate,
        inef_b: pb.ineff,
        inef_r: pr.ineff,
      })
    }
  }

  const prog = { basis: sideAggregate(B, common), repr: sideAggregate(R, common) }
  const set_b = prog.basis.Sales
  const set_r = prog.repr.Sales
  const dset = set_r - set_b

  // Annual impact uses the REPRICING volumes (V11).
  const years = repr.volumes.years
  const volume = repr.volumes.vol.slice(0, years.length)
  const annual = volume.map((v) => dset * v)

  const salesByTab: G60Analysis['salesByTab'] = {}
  for (const t of common) salesByTab[t] = { b: B[t].Sales, r: R[t].Sales, part: B[t].part }

  return {
    common,
    prog,
    glob,
    partSpecific: common.filter((t) => flag(t) !== ''),
    rows,
    prod,
    driverDiff: diffCard(basis.card, repr.card),
    years,
    volume,
    annual,
    set_b,
    set_r,
    dset,
    salesByTab,
  }
}
