/* ============================================================
   VKTR TCO — Stream Map (v1.7.5)
   Click-through audit trail: Macro-Phase -> Meso-Step -> Micro-Formula
   -> arbitrary-depth sub-formula drill-down. Any formula variable can
   carry its own nested `node` (same shape as a meso itself), so a
   term shown as "B = F + G" can be clicked into F's own derivation,
   then G's, recursively, to whatever depth the underlying engine
   actually supports. This is the platform's audit source of truth.
   Maps 1:1 onto Screens 2-6, plus a dedicated Depot Design Engine
   phase for the separately-codebased bay/dispenser/transformer sizing
   tool (depot_floorplan/js/compute.js) -- read live off s.depotMetrics,
   the exact object that tool posts back, not re-derived math.
   ============================================================ */

function buildStreamMapPayload(s, R, sizing, capex, vA, vB, L) {
  const rows = R.rows;
  const totalA = rows.reduce((sum, r) => sum + r.a, 0);
  const totalB = rows.reduce((sum, r) => sum + r.b, 0);
  const evIsA = vA.powertrain === "EV";
  const evVeh = evIsA ? vA : (vB.powertrain === "EV" ? vB : null);
  // v1.5: Depot Design (Screen 4 -> Depot Design tab) is the live infra
  // CAPEX/OPEX source once it has computed a result — see infraForVeh() in
  // data.jsx. Branches the infra phase's mesos below accordingly.
  const depotActive = !!s.depotBom;
  const trackProfile = s.trackProfile?.enabled ? window.computeTrackProfile(s.trackProfile) : null;
  const dm = s.depotMetrics || null;

  const money = (a, b) => ({ a: window.fmt.rpShort(a), b: window.fmt.rpShort(b) });
  const evOnly = (val) => evVeh
    ? (evIsA ? { a: window.fmt.rpShort(val), b: "—" } : { a: "—", b: window.fmt.rpShort(val) })
    : { a: "—", b: "—" };
  const evOnlyText = (txt) => evVeh
    ? (evIsA ? { a: txt, b: "—" } : { a: "—", b: txt })
    : { a: "—", b: "—" };

  const fleetSize = s.fleetSize ?? 0;
  const horizon = s.horizon ?? 0;
  const inflation = s.inflation ?? 5;
  const tenor = s.tenor ?? 0;
  const interest = s.interest ?? 0;
  const downPayment = s.downPayment ?? 0;

  const paybackText = R.paybackNote === "no_premium"
    ? L("no upfront premium for B", "tanpa premium awal untuk B")
    : R.paybackNote === "beyond_horizon"
    ? L(`> ${horizon} yr (beyond horizon)`, `> ${horizon} thn (di luar horizon)`)
    : `${R.payback.toLocaleString("en-US", { maximumFractionDigits: 2 })} ${L("yr", "thn")}`;

  const npvText = R.npv != null ? window.fmt.rpShort(R.npv) : "—";
  const irrText = R.irr != null ? `${R.irr.toLocaleString("en-US", { maximumFractionDigits: 1 })}%` : "—";

  /* ----------------------------------------------------------
     Recursive sub-formula node builders (v1.7.5). A "node" has the
     same shape as a meso: { icon, name, formula, vars, source,
     updated }. Any var may carry `node` to become clickable, drilling
     to arbitrary depth. Built bottom-up so deeper nodes can be
     referenced by their parents below.
     ---------------------------------------------------------- */

  // Operation → Energy → TerrainMult → RegenCredit (EV-only, route-profile mode)
  const regenCreditNode = trackProfile ? {
    icon: "🔋",
    name: L("Regenerative Braking Credit", "Kredit Pengereman Regeneratif"),
    formula: <><i>RegenCredit</i> = (1 &minus; <i>LinearityIndex</i>) &times; <i>OscillationCreditRate</i> &times; (<i>ContourMult</i> &minus; 1)</>,
    vars: [
      { sym: "LinearityIndex", desc: L(`Route elevation-profile linearity index (currently ${trackProfile.linearityIndex.toFixed(2)}) — how smooth vs. oscillating the route's grade changes are; lower = more up/down oscillation = more regen opportunity`, `Indeks linearitas profil elevasi rute (saat ini ${trackProfile.linearityIndex.toFixed(2)}) — seberapa halus vs. berosilasi perubahan kontur rute; lebih rendah = lebih banyak osilasi naik/turun = lebih banyak peluang regen`) },
      { sym: "OscillationCreditRate", desc: L(`Calibration constant converting oscillation into a credit rate (currently ${trackProfile.oscillationCreditRate.toFixed(2)})`, `Konstanta kalibrasi yang mengonversi osilasi menjadi tingkat kredit (saat ini ${trackProfile.oscillationCreditRate.toFixed(2)})`) },
      { sym: "ContourMult", desc: L("Base contour multiplier — same term as the parent formula's ContourMult", "Multiplier kontur dasar — suku sama dengan ContourMult pada formula induk") },
    ],
    source: "Track Profile engine", updated: "2026-07-01",
  } : null;

  const terrainMultNode = trackProfile ? {
    icon: "🗺️",
    name: L("Terrain Multiplier (route-profile-adjusted)", "Multiplier Medan (disesuaikan profil rute)"),
    formula: <><i>TerrainMult<sub>ICE</sub></i> = <i>ContourMult</i>, &nbsp; <i>TerrainMult<sub>EV</sub></i> = <i>ContourMult</i> &minus; <i>RegenCredit</i></>,
    vars: [
      { sym: "ContourMult", desc: L(`Base contour multiplier from Track Profile — ${trackProfile.contour} (${trackProfile.terrainMultiplier.toFixed(3)}×), gain/km ${trackProfile.gainPerKm.toFixed(1)} m/km`, `Multiplier kontur dasar dari Profil Rute — ${trackProfile.contour} (${trackProfile.terrainMultiplier.toFixed(3)}×), kenaikan/km ${trackProfile.gainPerKm.toFixed(1)} m/km`) },
      { sym: "RegenCredit", desc: L(`EV-only credit, currently ${trackProfile.regenCredit.toFixed(4)} → TerrainMult_ICE ${trackProfile.iceMultiplier.toFixed(3)}×, TerrainMult_EV ${trackProfile.evMultiplier.toFixed(3)}× — applies to energy only, not maintenance`, `Kredit khusus EV, saat ini ${trackProfile.regenCredit.toFixed(4)} → Multiplier_ICE ${trackProfile.iceMultiplier.toFixed(3)}×, Multiplier_EV ${trackProfile.evMultiplier.toFixed(3)}× — hanya berlaku pada energi, bukan perawatan`), node: regenCreditNode },
    ],
    source: "§2.4 / Track Profile", updated: "2026-07-01",
  } : {
    icon: "🗺️",
    name: L("Terrain Multiplier (route grade lookup)", "Multiplier Medan (tabel kontur rute)"),
    formula: <><i>TerrainMult</i> = <i>Lookup</i>(<i>RouteGrade</i>)</>,
    vars: [
      { sym: "Flat", desc: `${window.TERRAIN_MULTIPLIER.Flat.toFixed(2)}×` },
      { sym: "Rolling", desc: `${window.TERRAIN_MULTIPLIER.Rolling.toFixed(2)}×` },
      { sym: "Hilly", desc: `${window.TERRAIN_MULTIPLIER.Hilly.toFixed(2)}×` },
      { sym: "Mixed", desc: `${window.TERRAIN_MULTIPLIER.Mixed.toFixed(2)}×` },
    ],
    source: "§2.4", updated: "2026-06-22",
  };

  // Financials → Residual → SOH(Horizon)
  const sohNode = {
    icon: "🔋",
    name: L("Battery State-of-Health Curve", "Kurva State-of-Health Baterai"),
    formula: <><i>SOH</i>(<i>Horizon</i>) = max( 0.5, 1 &minus; <i>DegradationRate</i> &times; <i>Horizon</i> )</>,
    vars: [
      { sym: "DegradationRate", desc: L(`Annual battery degradation rate (currently ${window.fmt.num(s.batteryDegradationPctPerYear ?? 2.5)}%/yr — same field as the Battery Degradation card on Screen 2)`, `Tingkat degradasi baterai tahunan (saat ini ${window.fmt.num(s.batteryDegradationPctPerYear ?? 2.5)}%/thn — field sama dengan kartu Degradasi Baterai di Layar 2)`) },
      { sym: "Horizon", desc: L(`Analysis horizon (currently ${horizon} yr)`, `Horizon analisis (saat ini ${horizon} thn)`) },
      { sym: "floor", desc: L("SOH is floored at 50% — the curve does not model degradation below half of original capacity", "SOH dibatasi minimum 50% — kurva tidak memodelkan degradasi di bawah setengah kapasitas awal") },
    ],
    source: "§2.5", updated: "2026-06-30",
  };

  // Infrastructure → Sizing → P_req,adj → P_req → DailyFleetEnergy
  const dailyFleetEnergyNode = sizing ? {
    icon: "🔋",
    name: L("Daily Fleet Energy Demand", "Kebutuhan Energi Armada Harian"),
    formula: <><i>DailyFleetEnergy</i> = <i>FleetSize</i> &times; <i>DailyMileage</i> &times; <i>EC<sub>actual</sub></i></>,
    vars: [
      { sym: "FleetSize", desc: L(`Fleet size (currently ${sizing.fleetSize})`, `Ukuran armada (saat ini ${sizing.fleetSize})`) },
      { sym: "DailyMileage", desc: L(`Daily distance per vehicle, km (currently ${window.fmt.num(sizing.dailyMileage)})`, `Jarak harian per kendaraan, km (saat ini ${window.fmt.num(sizing.dailyMileage)})`) },
      { sym: "EC_actual", desc: L(`Payload-blended energy consumption, kWh/km (currently ${sizing.ecActual.toFixed(3)}) = EC_empty + (EC_full − EC_empty) × Payload%`, `Konsumsi energi campuran payload, kWh/km (saat ini ${sizing.ecActual.toFixed(3)}) = EC_kosong + (EC_penuh − EC_kosong) × Payload%`) },
    ],
    source: "§3.2", updated: "2026-06-15",
  } : null;
  const pReqNode = sizing ? {
    icon: "⚡",
    name: L("Required Charging Power (unadjusted)", "Daya Pengisian Dibutuhkan (belum disesuaikan)"),
    formula: <><i>P<sub>req</sub></i> = <i>DailyFleetEnergy</i> / (<i>ChargingWindow</i> &times; <i>ChargingEfficiency</i>)</>,
    vars: [
      { sym: "DailyFleetEnergy", desc: L("See derivation", "Lihat penjabaran"), node: dailyFleetEnergyNode },
      { sym: "ChargingWindow", desc: L(`Available charging window, hours (currently ${sizing.chargingWindowHours.toFixed(1)})`, `Jendela waktu pengisian tersedia, jam (saat ini ${sizing.chargingWindowHours.toFixed(1)})`) },
      { sym: "ChargingEfficiency", desc: L(`Charger-to-battery efficiency (currently ${(sizing.chargingEfficiency * 100).toFixed(0)}%)`, `Efisiensi charger-ke-baterai (saat ini ${(sizing.chargingEfficiency * 100).toFixed(0)}%)`) },
    ],
    source: "§3.3", updated: "2026-06-15",
  } : null;
  const pReqAdjNode = sizing ? {
    icon: "⚡",
    name: L("Adjusted Required Power", "Daya Dibutuhkan Disesuaikan"),
    formula: <><i>P<sub>req,adj</sub></i> = ( <i>P<sub>req</sub></i> / <i>UtilizationFactor</i> ) &times; <i>InfraMultiplier</i></>,
    vars: [
      { sym: "P_req", desc: L("See derivation", "Lihat penjabaran"), node: pReqNode },
      { sym: "UtilizationFactor", desc: L(`Ecosystem-profile utilization factor (currently ${sizing.utilizationFactor.toFixed(2)})`, `Faktor utilisasi profil ekosistem (saat ini ${sizing.utilizationFactor.toFixed(2)})`) },
      { sym: "InfraMultiplier", desc: L(`Ecosystem × project-type infra sizing multiplier (currently ${sizing.infraMultiplier.toFixed(2)})`, `Multiplier ukuran infrastruktur ekosistem × tipe proyek (saat ini ${sizing.infraMultiplier.toFixed(2)})`) },
    ],
    source: "§3.4", updated: "2026-06-15",
  } : null;

  // Infrastructure → EVCS CAPEX breakdown (A–E) → per-category line items
  const capexCategoryNode = (catObj, name) => catObj ? {
    icon: "🧾",
    name,
    formula: <>{catObj.items.map((it, i) => <React.Fragment key={i}>{i > 0 && " + "}<i>{it.label}</i></React.Fragment>)}</>,
    vars: catObj.items.map(it => ({
      sym: it.label,
      desc: L(`${window.fmt.rpShort(it.value)} — source: ${it.source}${it.note ? ` (${it.note})` : ""}`, `${window.fmt.rpShort(it.value)} — sumber: ${it.source}${it.note ? ` (${it.note})` : ""}`),
    })),
    source: "§4", updated: "2026-06-15",
  } : null;

  // Infrastructure → Depot Audit → per-category line items (Depot Design BOM)
  const depotCategoryNode = (catData, name) => catData ? {
    icon: "🏗️",
    name,
    formula: <>{Object.keys(catData.items || {}).map((k, i) => <React.Fragment key={i}>{i > 0 && " + "}<i>{k}</i></React.Fragment>)}</>,
    vars: Object.entries(catData.items || {}).map(([k, v]) => ({ sym: k, desc: window.fmt.rpShort(v) })),
    source: "Depot Design (Screen 4 → Depot Design)", updated: "2026-06-21",
  } : null;

  const procPhase = {
    id: "proc",
    name: L("Vehicle & Procurement", "Kendaraan & Pengadaan"),
    screenLabel: L("Screen 2 · Vehicle Selection", "Layar 2 · Pemilihan Kendaraan"),
    icon: "🚗",
    desc: L("Vehicle pricing & fleet sizing", "Harga kendaraan & ukuran armada"),
    mesos: [
      {
        id: "capex",
        icon: "🏷️",
        name: L("Unit Purchase Price (CAPEX)", "Harga Beli Unit (CAPEX)"),
        ...money(rows[0].a, rows[0].b),
        formula: <><i>CAPEX</i> = <i>Price</i> &times; <i>FleetSize</i></>,
        vars: [
          { sym: "Price", desc: L("Vehicle on-the-road unit price (per Screen 2 selection)", "Harga unit kendaraan on-the-road (sesuai pilihan Layar 2)") },
          { sym: "FleetSize", desc: L(`Number of units in the fleet (currently ${fleetSize})`, `Jumlah unit dalam armada (saat ini ${fleetSize})`) },
        ],
        source: "§2.1", updated: "2026-06-15",
      },
    ],
  };

  const energyMeso = {
    id: "energy",
    icon: "⚡",
    name: L("Energy / Fuel Consumption Stream", "Aliran Konsumsi Energi / BBM"),
    ...money(rows[2].a + rows[3].a, rows[2].b + rows[3].b),
    formula: trackProfile
      ? <><i>EnergyCost<sub>y</sub></i> = <i>AnnualKm</i> &times; <i>FleetSize</i> &times; <i>EC</i> &times; <i>TerrainMult<sub>ICE|EV</sub></i> &times; <i>Rate</i> &times; (1+<i>Infl</i>)<sup>y-1</sup></>
      : <><i>EnergyCost<sub>y</sub></i> = <i>AnnualKm</i> &times; <i>FleetSize</i> &times; <i>EC</i> &times; <i>TerrainMult</i> &times; <i>Rate</i> &times; (1+<i>Infl</i>)<sup>y-1</sup></>,
    vars: [
      { sym: "EC", desc: L("Energy consumption per vehicle — kWh/km for EV, L/100km for ICE (Screen 2 spec)", "Konsumsi energi per kendaraan — kWh/km untuk EV, L/100km untuk ICE (spesifikasi Layar 2)") },
      { sym: "TerrainMult", desc: trackProfile
          ? L("Route-profile-adjusted contour multiplier — see derivation", "Multiplier kontur disesuaikan profil rute — lihat penjabaran")
          : L("Route grade multiplier — Flat 1.00 / Rolling 1.10 / Hilly 1.20 / Mixed 1.05 — see derivation", "Multiplier kontur rute — Datar 1.00 / Berbukit Ringan 1.10 / Berbukit 1.20 / Campuran 1.05 — lihat penjabaran"),
        node: terrainMultNode },
      { sym: "Rate", desc: L("Electricity tariff (Rp/kWh) or fuel price (Rp/L) from Screen 3", "Tarif listrik (Rp/kWh) atau harga BBM (Rp/L) dari Layar 3") },
      { sym: "Infl", desc: L(`Annual cost inflation (currently ${inflation}%)`, `Inflasi biaya tahunan (saat ini ${inflation}%)`) },
    ],
    source: "§2.4", updated: "2026-07-09",
  };

  const opsPhase = {
    id: "ops",
    name: L("Operation", "Operasional"),
    screenLabel: L("Screen 3 · Operation", "Layar 3 · Operasional"),
    icon: "⚙️",
    desc: L("Daily energy & maintenance streams", "Aliran biaya energi & perawatan harian"),
    mesos: [
      energyMeso,
      {
        id: "maint",
        icon: "🔧",
        name: L("Maintenance Cost Stream", "Aliran Biaya Perawatan"),
        ...money(rows[4].a, rows[4].b),
        formula: <><i>MaintCost<sub>y</sub></i> = <i>MaintRate</i>(<i>veh</i>, <i>y</i>, <i>AnnualKm</i>, <i>Terrain</i>) &times; <i>FleetSize</i> &times; (1+<i>Infl</i>)<sup>y-1</sup></>,
        vars: [
          { sym: "MaintRate", desc: L("Per-vehicle maintenance schedule, optionally itemized via the maintenance breakdown toggle", "Skedul perawatan per kendaraan, dapat dirinci via toggle rincian perawatan") },
          { sym: "AnnualKm", desc: L(`Annual distance per unit (currently ${window.fmt.num(Math.round(window.resolveAnnualKm(s)))} km/yr)`, `Jarak tahunan per unit (saat ini ${window.fmt.num(Math.round(window.resolveAnnualKm(s)))} km/thn)`) },
          { sym: "Infl", desc: L(`Annual cost inflation (currently ${inflation}%)`, `Inflasi biaya tahunan (saat ini ${inflation}%)`) },
        ],
        source: "§5 / §6", updated: "2026-06-15",
      },
      {
        id: "co2",
        icon: "🌿",
        name: L("CO2 / Emissions Stream", "Aliran CO2 / Emisi"),
        a: `${window.fmt.num(Math.round(R.co2A))} t`, b: `${window.fmt.num(Math.round(R.co2B))} t`,
        formula: s.includeEmbodiedCo2
          ? <><i>Co2<sub>op</sub></i> = <i>AnnualKm</i> &times; <i>FleetSize</i> &times; <i>EC</i> &times; <i>TerrainMult</i> &times; <i>EF</i> / 1000 &times; <i>Horizon</i><br />
              <i>Co2<sub>embodied</sub></i> = <i>BatteryKwh</i> &times; <i>FleetSize</i> &times; <i>MfgFactor</i> / 1000 &nbsp;(EV only, one-time)<br />
              <i>Co2<sub>lifecycle</sub></i> = <i>Co2<sub>op</sub></i> + <i>Co2<sub>embodied</sub></i></>
          : <><i>Co2<sub>op</sub></i> = <i>AnnualKm</i> &times; <i>FleetSize</i> &times; <i>EC</i> &times; <i>TerrainMult</i> &times; <i>EF</i> / 1000 &times; <i>Horizon</i></>,
        vars: [
          { sym: "EC", desc: L("Same energy consumption used for cost above (respects energyOverride / LF×LMR) — not the catalog's raw spec", "Konsumsi energi sama seperti biaya di atas (menghormati energyOverride / LF×LMR) — bukan spesifikasi mentah katalog") },
          { sym: "EF", desc: L(`Emission factor, well-to-wheel — ${window.fmt.num(window.CO2.diesel_kg_per_liter)} kgCO2/L (diesel: combustion + upstream extraction/refining/transport) or ${window.fmt.num(window.CO2.grid_kg_per_kwh)} kgCO2/kWh (grid, Indonesia national average, 2023)`, `Faktor emisi, well-to-wheel — ${window.fmt.num(window.CO2.diesel_kg_per_liter)} kgCO2/L (solar: pembakaran + ekstraksi/pengilangan/transportasi hulu) atau ${window.fmt.num(window.CO2.grid_kg_per_kwh)} kgCO2/kWh (jaringan, rata-rata nasional Indonesia, 2023)`) },
          { sym: "v1.8", desc: L("Both factors moved to a consistent well-to-wheel boundary — previously the EV side counted its upstream (grid generation) while the ICE side counted only tailpipe combustion, biasing every comparison against EVs regardless of calibration accuracy.", "Kedua faktor dipindahkan ke batas well-to-wheel yang konsisten — sebelumnya sisi EV menghitung hulu (pembangkitan listrik) sementara sisi ICE hanya menghitung pembakaran knalpot, membuat setiap perbandingan bias melawan EV terlepas dari akurasi kalibrasi.") },
          ...(s.includeEmbodiedCo2 ? [
            { sym: "MfgFactor", desc: L(`${window.fmt.num(s.evBatteryMfgCo2PerKwh ?? 74)} kgCO2/kWh, NMC811 cradle-to-gate median (peer-reviewed battery LCA studies) — pending Indonesia-specific battery supply chain data. ICE embodied/glider manufacturing CO2 is not modeled.`, `${window.fmt.num(s.evBatteryMfgCo2PerKwh ?? 74)} kgCO2/kWh, median cradle-to-gate NMC811 (studi LCA baterai peer-review) — menunggu data rantai pasok baterai spesifik Indonesia. CO2 manufaktur/glider ICE tidak dimodelkan.`) },
          ] : []),
        ],
        source: "§7.5 / §2.11", updated: "2026-07-01",
      },
    ],
  };

  const sizingMeso = {
    id: "sizing",
    icon: "📐",
    name: L("Charging Infrastructure Sizing", "Penentuan Ukuran Infrastruktur Pengisian"),
    ...(sizing ? evOnlyText(`${window.fmt.num(sizing.chargerCount)} × ${sizing.chargerRatingKw}kW · ${window.fmt.num(Math.round(sizing.transformerKva))} kVA`) : { a: "—", b: "—" }),
    formula: <><i>ChargerCount</i> = ceil( ceil(<i>P<sub>req,adj</sub></i> / <i>P<sub>charger</sub></i>) &times; <i>Redundancy</i> )</>,
    vars: [
      { sym: "P_req,adj", desc: L("Required charging power after utilization & site multipliers — see derivation", "Daya pengisian dibutuhkan setelah multiplier utilisasi & lokasi — lihat penjabaran"), node: pReqAdjNode },
      { sym: "P_charger", desc: L(`Rated power per charger${sizing ? ` (currently ${sizing.chargerRatingKw} kW)` : ""}`, `Daya per charger${sizing ? ` (saat ini ${sizing.chargerRatingKw} kW)` : ""}`) },
      { sym: "Redundancy", desc: L("Ecosystem × project-type N+1 redundancy multiplier (Screen 4)", "Multiplier redundansi N+1 ekosistem × tipe proyek (Layar 4)") },
      ...(depotActive ? [{ sym: "⚠", desc: L("TCO's own independent estimate — Depot Design (below) is the live CAPEX/OPEX source for this comparison.", "Estimasi independen TCO sendiri — Desain Depot (di bawah) adalah sumber CAPEX/OPEX aktif untuk perbandingan ini.") }] : []),
    ],
    source: "§3.4–3.6", updated: "2026-06-15",
    empty: !sizing,
  };

  const capexbdMeso = depotActive ? {
    id: "capexbd",
    icon: "🧾",
    name: L("Infrastructure CAPEX — Depot Design (wholesale)", "CAPEX Infrastruktur — Desain Depot (substitusi penuh)"),
    ...evOnly(s.depotBom.tcoCapex),
    formula: <><i>InfraCAPEX</i> = <i>DepotBOM.tcoCapex</i> &nbsp;(<i>not</i> A+B+C+D+E)</>,
    vars: [
      { sym: "DepotBOM.tcoCapex", desc: L("Sum of Depot Design's own BOM categories currently toggled into TCO scope (see Depot Audit below) — replaces the Sizing Engine total wholesale, not merged field-by-field", "Jumlah kategori BOM Desain Depot yang diaktifkan ke lingkup TCO (lihat Audit Depot di bawah) — mengganti total Mesin Perhitungan secara penuh, bukan digabung per item") },
    ],
    source: "Depot Design (Screen 4 → Depot Design)", updated: "2026-06-21",
  } : {
    id: "capexbd",
    icon: "🧾",
    name: L("EVCS CAPEX Breakdown — A+B only (v1.7.7)", "Rincian CAPEX SPKLU — A+B saja (v1.7.7)"),
    ...(capex ? evOnly(capex.tcoCapex) : { a: "—", b: "—" }),
    formula: <><i>InfraCAPEX</i> = A<sub>charger</sub> + B<sub>electrical</sub> &nbsp;(<i>not</i> C+D+E — Helio Sinar Energi's)</>,
    vars: [
      { sym: "A_charger", desc: L("Charger equipment: charger units + dispensers + guns — see line items", "Peralatan charger: unit charger + dispenser + gun — lihat rincian"), node: capex ? capexCategoryNode(capex.A, L("Charger Equipment (A)", "Peralatan Charger (A)")) : null },
      { sym: "B_electrical", desc: L("Electrical infrastructure: transformer + panels — see line items", "Infrastruktur kelistrikan: trafo + panel — lihat rincian"), node: capex ? capexCategoryNode(capex.B, L("Electrical Infrastructure (B)", "Infrastruktur Kelistrikan (B)")) : null },
      { sym: "C_civil (excluded)", desc: L("Civil works — managed and expended by Helio Sinar Energi, not counted in VKTR's TCO", "Pekerjaan sipil — dikelola dan dibiayai Helio Sinar Energi, tidak dihitung dalam TCO VKTR"), node: capex ? capexCategoryNode(capex.C, L("Civil Works (C) — reference only", "Pekerjaan Sipil (C) — referensi saja")) : null },
      { sym: "D_utility (excluded)", desc: L("Utility upgrade — Helio's, not counted in VKTR's TCO", "Upgrade utilitas — milik Helio, tidak dihitung dalam TCO VKTR"), node: capex ? capexCategoryNode(capex.D, L("Utility Upgrade (D) — reference only", "Upgrade Utilitas (D) — referensi saja")) : null },
      { sym: "E_software (excluded)", desc: L("Software/licensing — Helio's, not counted in VKTR's TCO", "Perangkat lunak/lisensi — milik Helio, tidak dihitung dalam TCO VKTR"), node: capex ? capexCategoryNode(capex.E, L("Software (E) — reference only", "Perangkat Lunak (E) — referensi saja")) : null },
    ],
    source: "§4 (confirmed Helio Sinar Energi scope, v1.7.7)", updated: "2026-07-14",
    empty: !capex,
  };

  const infraopexMeso = depotActive ? {
    id: "infraopex",
    icon: "🔁",
    name: L("Infrastructure OPEX Stream — Depot Design (itemized)", "Aliran OPEX Infrastruktur — Desain Depot (terperinci)"),
    ...money(rows[6].a, rows[6].b),
    formula: <><i>InfraOPEX<sub>y</sub></i> = <i>DepotBOM.tcoOpex</i> &times; (1+<i>Infl</i>)<sup>y-1</sup></>,
    vars: [
      { sym: "DepotBOM.tcoOpex", desc: L("Year-1 itemized OPEX from Depot Design's own categories (civil/charger maintenance %, OCPP fees, security staff, insurance, permit renewal) — replaces the flat 5%-of-CAPEX assumption", "OPEX tahun-1 terperinci dari kategori Desain Depot sendiri (perawatan sipil/charger %, biaya OCPP, staf keamanan, asuransi, perpanjangan izin) — mengganti asumsi datar 5% dari CAPEX") },
      { sym: "Infl", desc: L(`Annual cost inflation (currently ${inflation}%)`, `Inflasi biaya tahunan (saat ini ${inflation}%)`) },
    ],
    source: "Depot Design (Screen 4 → Depot Design)", updated: "2026-06-21",
  } : {
    id: "infraopex",
    icon: "🔁",
    name: L("Infrastructure OPEX Stream", "Aliran OPEX Infrastruktur"),
    ...money(rows[6].a, rows[6].b),
    formula: <><i>InfraOPEX<sub>y</sub></i> = <i>InfraCAPEX</i> &times; <i>OpexRate</i> &times; (1+<i>Infl</i>)<sup>y-1</sup></>,
    vars: [
      { sym: "InfraCAPEX", desc: L("Total EVCS CAPEX from the breakdown above", "Total CAPEX SPKLU dari rincian di atas") },
      { sym: "OpexRate", desc: L("Annual EVCS OPEX rate, fixed at 5% of CAPEX", "Tarif OPEX SPKLU tahunan, ditetapkan 5% dari CAPEX") },
      { sym: "Infl", desc: L(`Annual cost inflation (currently ${inflation}%)`, `Inflasi biaya tahunan (saat ini ${inflation}%)`) },
    ],
    source: "§2.2", updated: "2026-06-15",
  };

  const infraPhase = {
    id: "infra",
    name: L("Infrastructure", "Infrastruktur"),
    screenLabel: L("Screen 4 · Infrastructure", "Layar 4 · Infrastruktur"),
    icon: "🔌",
    desc: L("Charging infra sizing & CAPEX/OPEX", "Ukuran infrastruktur pengisian & CAPEX/OPEX"),
    mesos: [
      sizingMeso,
      capexbdMeso,
      infraopexMeso,
      ...(depotActive ? [{
        id: "depotAudit",
        icon: "🏗️",
        name: L("Depot Audit — selected design & category subtotals", "Audit Depot — desain terpilih & subtotal kategori"),
        ...evOnlyText(
          `${s.depotMetrics?.planSummary?.planName || (s.depotMetrics?.planSummary?.source === "studio" ? L("Studio Mode", "Mode Studio") : "—")} · ` +
          `${window.fmt.num(s.depotMetrics?.planSummary?.bayCount ?? 0)} ${L("bays", "petak")} · ` +
          `${window.fmt.num(s.depotMetrics?.planSummary?.dispCount ?? 0)} ${L("dispensers", "dispenser")} · ` +
          `${window.fmt.num(Math.round(s.depotMetrics?.planSummary?.trafoKVA ?? 0))} kVA`
        ),
        formula: <>{DEPOT_CATEGORY_ORDER.filter(k => !s.depotBomInclude || s.depotBomInclude[k]).map(k => {
          const cat = s.depotBom.byCategory[k];
          const lbl = DEPOT_CATEGORY_LABELS[k];
          return cat ? `${lbl ? L(lbl.en, lbl.id) : k}: ${window.fmt.rpShort(cat.capex)}` : null;
        }).filter(Boolean).join("  +  ")}</>,
        vars: [
          { sym: "Plan", desc: L("Which procedural plan (or Studio Mode design) is marked \"Select for TCO\" inside the Depot Design tool", "Rencana prosedural mana (atau desain Mode Studio) yang ditandai \"Select for TCO\" di alat Desain Depot") },
          ...DEPOT_CATEGORY_ORDER.filter(k => !s.depotBomInclude || s.depotBomInclude[k]).map(k => {
            const cat = s.depotBom.byCategory[k];
            if (!cat) return null;
            const lbl = DEPOT_CATEGORY_LABELS[k];
            const lblText = lbl ? L(lbl.en, lbl.id) : k;
            return {
              sym: lblText,
              desc: L(`${window.fmt.rpShort(cat.capex)} CAPEX, ${window.fmt.rpShort(cat.opex)} OPEX/yr — see line items`, `${window.fmt.rpShort(cat.capex)} CAPEX, ${window.fmt.rpShort(cat.opex)} OPEX/thn — lihat rincian`),
              node: depotCategoryNode(cat, lblText),
            };
          }).filter(Boolean),
        ],
        source: "Depot Design (Screen 4 → Depot Design)", updated: "2026-06-21",
      }] : []),
    ],
  };

  // ---- Depot Design Engine (v1.7.5) — dedicated phase for the separately
  // codebased bay/dispenser/transformer sizing tool. Reads live off
  // s.depotMetrics (the exact object depot_floorplan posts back), never
  // re-derives its own copy of the math — this phase IS that engine's
  // audit trail, sourced directly from depot_floorplan/js/compute.js.
  const depotEmptyNote = L(
    "No Depot Design has been computed yet — open Screen 4 → Depot Design, generate a plan (or a Studio Mode design), and this phase will populate live.",
    "Belum ada Desain Depot yang dihitung — buka Layar 4 → Desain Depot, buat sebuah rencana (atau desain Mode Studio), dan fase ini akan terisi otomatis."
  );
  const geomMaxBaysNode = dm ? {
    icon: "📏",
    name: L("Geometric Bay Capacity", "Kapasitas Petak Geometris"),
    formula: <><i>GeomMaxBays</i> = <i>RowCount</i> &times; <i>BaysPerRow</i>{dm.pc && dm.pc.driveThrough ? null : <> &times; 2</>}</>,
    vars: [
      { sym: "RowCount", desc: L(`Rows that fit the site depth for the selected plan (currently ${dm.rowCount})`, `Baris yang muat pada kedalaman lokasi untuk rencana terpilih (saat ini ${dm.rowCount})`) },
      { sym: "BaysPerRow", desc: L(`Bays per row given site width and vehicle pitch (currently ${dm.bpRow})`, `Petak per baris berdasarkan lebar lokasi dan jarak antar kendaraan (saat ini ${dm.bpRow})`) },
    ],
    source: "depot_floorplan/js/compute.js", updated: "2026-07-09",
  } : null;
  const depotEnginePhase = {
    id: "depotEngine",
    name: L("Depot Design Engine", "Mesin Desain Depot"),
    screenLabel: L("Screen 4 · Depot Design (embedded tool)", "Layar 4 · Desain Depot (alat tertanam)"),
    icon: "🅿️",
    desc: L("Bay, dispenser & transformer sizing — independent geometry-driven engine", "Ukuran petak, dispenser & trafo — mesin berbasis geometri independen"),
    mesos: [
      {
        id: "depotBays",
        icon: "🅿️",
        name: L("Charging Bay Sizing", "Penentuan Ukuran Petak Pengisian"),
        a: dm ? `${window.fmt.num(dm.bayCount)} ${L("bays", "petak")} (${L("of", "dari")} ${window.fmt.num(dm.geomMaxBays)} ${L("max", "maks")})` : "—",
        b: "", single: true,
        formula: <><i>PeakConcurrent</i> = ceil( <i>Fleet</i> / <i>ShiftCount</i> ) &nbsp;&rarr;&nbsp; <i>NeededBays</i> = <i>PeakConcurrent</i> + 1 &nbsp;&rarr;&nbsp; <i>BayCount</i> = min( <i>GeomMaxBays</i>, <i>NeededBays</i> )</>,
        vars: dm ? [
          { sym: "Fleet", desc: L(`Fleet size configured in Depot Design (currently ${dm.fl})`, `Ukuran armada dikonfigurasi di Desain Depot (saat ini ${dm.fl})`) },
          { sym: "ShiftCount", desc: L("Number of disjoint vehicle groups sharing bays sequentially (Expert Mode Shift Count input; default 1) — NeededBays below already reflects it", "Jumlah kelompok kendaraan terpisah yang berbagi petak secara berurutan (input Jumlah Shift Mode Ahli; default 1) — NeededBays di bawah sudah mencerminkannya") },
          { sym: "NeededBays", desc: L(`= PeakConcurrent + 1, a flat +1 spare-bay reserve for early/late arrivals & maintenance swaps (currently ${dm.neededBays})`, `= PeakConcurrent + 1, cadangan +1 petak tetap untuk kedatangan awal/akhir & pergantian perawatan (saat ini ${dm.neededBays})`) },
          { sym: "GeomMaxBays", desc: L(`Maximum bays the site geometry can physically fit (currently ${dm.geomMaxBays}) — see derivation`, `Petak maksimum yang bisa dimuat geometri lokasi secara fisik (saat ini ${dm.geomMaxBays}) — lihat penjabaran`), node: geomMaxBaysNode },
        ] : [],
        source: "depot_floorplan/js/compute.js", updated: "2026-07-09",
        empty: !dm,
      },
      {
        id: "depotDispensers",
        icon: "🔌",
        name: L("Dispenser Sizing", "Penentuan Ukuran Dispenser"),
        a: dm ? `${window.fmt.num(dm.dispCount)} ${L("dispensers", "dispenser")} · ${window.fmt.num(dm.nozzles)} ${L("nozzles", "nozzle")}` : "—",
        b: "", single: true,
        formula: <><i>DispCount</i> = <i>Group</i>(<i>BayCount</i>, <i>RowLayout</i>, <i>NozzlesPerVehicle</i>)</>,
        vars: dm ? [
          { sym: "BayCount", desc: L(`Charging bays to serve (currently ${dm.bayCount})`, `Petak pengisian yang dilayani (saat ini ${dm.bayCount})`) },
          { sym: "RowLayout", desc: L("Dispensers are grouped per adjacent bay-pair/chain according to the selected plan's geometry (drive-through plans chain differently from nose-to-nose plans) — see computeDispCount / computeDispCountChain in compute.js for the exact grouping", "Dispenser dikelompokkan per pasangan/rantai petak berdekatan sesuai geometri rencana terpilih (rencana drive-through dirantai berbeda dari rencana nose-to-nose) — lihat computeDispCount / computeDispCountChain di compute.js untuk pengelompokan pastinya") },
          { sym: "NozzlesPerVehicle", desc: L(`Nozzles required per vehicle, capped by DISPENSER_NOZZLE_CAP (currently ${dm.nozzlesPerVehicle})`, `Nozzle dibutuhkan per kendaraan, dibatasi DISPENSER_NOZZLE_CAP (saat ini ${dm.nozzlesPerVehicle})`) },
        ] : [],
        source: "depot_floorplan/js/compute.js", updated: "2026-07-09",
        empty: !dm,
      },
      {
        id: "depotTrafo",
        icon: "⚡",
        name: L("Transformer Sizing", "Penentuan Ukuran Trafo"),
        a: dm ? `${window.fmt.num(Math.round(dm.trafoKVA))} kVA` : "—",
        b: "", single: true,
        formula: <><i>TrafoKVA</i> = ceil( <i>ChgBays</i> &times; <i>ChgKW</i> &times; <i>DivF</i> &times; <i>GrowthMargin</i> / 100 ) &times; 100</>,
        vars: dm ? [
          { sym: "ChgBays", desc: L(`Bays actually reserved for simultaneous charging, capped by the charging-fraction setting (currently ${dm.chgBays})`, `Petak yang dialokasikan untuk pengisian simultan, dibatasi setelan fraksi pengisian (saat ini ${dm.chgBays})`) },
          { sym: "ChgKW", desc: L(`Per-charger power rating, plan-adjusted (currently ${dm.chgKW} kW)`, `Rating daya per charger, disesuaikan rencana (saat ini ${dm.chgKW} kW)`) },
          { sym: "DivF", desc: L(`Diversity factor — not every bay draws peak power simultaneously (currently ${dm.divF})`, `Faktor diversitas — tidak semua petak menarik daya puncak bersamaan (saat ini ${dm.divF})`) },
          { sym: "GrowthMargin", desc: L(`Headroom for future fleet growth (currently ${dm.growthMargin})`, `Ruang untuk pertumbuhan armada di masa depan (saat ini ${dm.growthMargin})`) },
        ] : [],
        source: "depot_floorplan/js/compute.js", updated: "2026-07-09",
        empty: !dm,
      },
      {
        id: "depotReadiness",
        icon: "🚦",
        name: L("Site Readiness Check", "Pemeriksaan Kesiapan Lokasi"),
        a: dm && dm.readiness && dm.readiness !== "unknown" ? `${dm.readiness.toUpperCase()}${dm.increasePct != null ? ` (${dm.increasePct >= 0 ? "+" : ""}${dm.increasePct.toFixed(0)}%)` : ""}` : "—",
        b: "", single: true,
        formula: <><i>UpgradeRequired</i> = <i>TrafoKVA</i> &gt; <i>SiteAvailableKva</i>, &nbsp; <i>IncreasePct</i> = (<i>TrafoKVA</i> &minus; <i>SiteAvailableKva</i>) / <i>SiteAvailableKva</i> &times; 100</>,
        vars: dm ? [
          { sym: "TrafoKVA", desc: L(`Required transformer size (currently ${window.fmt.num(Math.round(dm.trafoKVA))} kVA)`, `Ukuran trafo dibutuhkan (saat ini ${window.fmt.num(Math.round(dm.trafoKVA))} kVA)`) },
          { sym: "SiteAvailableKva", desc: L(`User-entered existing site capacity (currently ${dm.siteAvailableKva ? window.fmt.num(dm.siteAvailableKva) + " kVA" : "not set"})`, `Kapasitas lokasi eksisting yang dimasukkan pengguna (saat ini ${dm.siteAvailableKva ? window.fmt.num(dm.siteAvailableKva) + " kVA" : "belum diisi"})`) },
          { sym: "readiness rule", desc: L("green: no upgrade needed · yellow: upgrade needed, ≤30% increase · red: upgrade needed, >30% increase", "hijau: tidak perlu upgrade · kuning: perlu upgrade, kenaikan ≤30% · merah: perlu upgrade, kenaikan >30%") },
        ] : [],
        source: "depot_floorplan/js/compute.js", updated: "2026-07-09",
        empty: !dm,
      },
    ],
  };
  if (!dm) {
    depotEnginePhase.mesos.forEach(m => { m.emptyNote = depotEmptyNote; });
  }

  // v1.7.6: Lead Time -- informational only, entirely separate from R/computeTCO()
  // (see lead_time.jsx). Guarded the same way as Depot Design Engine above --
  // computeLeadTime() can still return a null total for an unresolved vehicle,
  // so this checks the actual result rather than assuming vA/vB being non-null
  // is enough (the exact class of bug the Depot Design Engine crash taught us
  // to guard against explicitly).
  const leadTimeA = window.computeLeadTime(s.vehA, s.fleetSize, s.payloadBuildA);
  const leadTimeB = window.computeLeadTime(s.vehB, s.fleetSize, s.payloadBuildB);
  const ltReady = leadTimeA && leadTimeB && leadTimeA.total != null && leadTimeB.total != null;
  const ltEmptyNote = L(
    "Select both vehicles on Screen 2 and set a fleet size on Screen 3 to compute lead time.",
    "Pilih kedua kendaraan di Layar 2 dan atur jumlah armada di Layar 3 untuk menghitung lead time."
  );
  const leadTimePhase = {
    id: "leadTime",
    name: L("Lead Time", "Lead Time"),
    screenLabel: L("Screen 2 (Payload Build) + Screen 3 (Fleet Size) → Screen 6 Results", "Layar 2 (Payload Build) + Layar 3 (Jumlah Armada) → Hasil Layar 6"),
    icon: "🚚",
    desc: L("Order-to-delivery estimate — informational only, does not affect TCO", "Estimasi order-hingga-terkirim — murni informatif, tidak memengaruhi TCO"),
    mesos: [
      {
        id: "leadTimeTotal",
        icon: "🚚",
        name: L("Total Lead Time", "Total Lead Time"),
        a: ltReady ? `${window.fmt.num(Math.round(leadTimeA.total))} ${L("days", "hari")}` : "—",
        b: ltReady ? `${window.fmt.num(Math.round(leadTimeB.total))} ${L("days", "hari")}` : "—",
        formula: <><i>Total</i> = &Sigma;(<i>Phase</i>) + <i>PayloadBuild</i> (if applied)</>,
        vars: ltReady ? [
          { sym: "Fixed phases", desc: L("Internal Process, Principal Production, Shipping, Customs, Final PDI, Vehicle Registration — fixed per vehicle, not affected by unit count", "Proses Internal, Produksi Prinsipal, Pengiriman, Bea Cukai, PDI Akhir, Registrasi Kendaraan — tetap per kendaraan, tidak dipengaruhi jumlah unit") },
          { sym: "Scaling phases", desc: L(`Chassis Assembly / Body Builder / Delivery — +0.5 days/unit beyond 30 units (fleet size: ${s.fleetSize || 0}); a phase with 0 baseline never scales`, `Perakitan Sasis / Karoseri / Pengiriman — +0.5 hari/unit di atas 30 unit (jumlah armada: ${s.fleetSize || 0}); fase dengan dasar 0 tidak pernah berskala`) },
          { sym: "PayloadBuild", desc: L(`Optional attachment build (LDT/HDT cabin-chassis only) — 60 days base, +0.2 days/unit beyond 30 units. A: ${leadTimeA.estimated ? "n/a (estimated)" : (leadTimeA.payloadBuildApplied ? "on" : "off")}, B: ${leadTimeB.estimated ? "n/a (estimated)" : (leadTimeB.payloadBuildApplied ? "on" : "off")}`, `Pembangunan attachment opsional (khusus kabin sasis LDT/HDT) — 60 hari dasar, +0.2 hari/unit di atas 30 unit. A: ${leadTimeA.estimated ? "n/a (estimasi)" : (leadTimeA.payloadBuildApplied ? "aktif" : "nonaktif")}, B: ${leadTimeB.estimated ? "n/a (estimasi)" : (leadTimeB.payloadBuildApplied ? "aktif" : "nonaktif")}`) },
          { sym: "Conservative assumption", desc: L("Assumes the full order is delivered together — real batches often ship progressively and may be faster.", "Mengasumsikan seluruh pesanan dikirim bersamaan — batch nyata sering dikirim bertahap dan bisa lebih cepat.") },
          { sym: "Data source", desc: L(`Vehicle A: ${leadTimeA.estimated ? "modeled estimate (import type × segment × powertrain), no real PMO data" : "real PMO data"}. Vehicle B: ${leadTimeB.estimated ? "modeled estimate, no real PMO data" : "real PMO data"}.`, `Kendaraan A: ${leadTimeA.estimated ? "estimasi model (tipe impor × segmen × jenis penggerak), tanpa data PMO nyata" : "data PMO nyata"}. Kendaraan B: ${leadTimeB.estimated ? "estimasi model, tanpa data PMO nyata" : "data PMO nyata"}.`) },
        ] : [],
        source: "PMO Data.xlsx (2026-07-03 Chaca meeting) / data/lead_time.xlsx", updated: "2026-07-11",
        empty: !ltReady,
      },
    ],
  };
  if (!ltReady) {
    leadTimePhase.mesos.forEach(m => { m.emptyNote = ltEmptyNote; });
  }

  const financingMeso = {
    id: "financing",
    icon: "🏦",
    name: L("Financing / Lease Cost Stream", "Aliran Biaya Pembiayaan / Sewa"),
    ...money(rows[1].a, rows[1].b),
    formula: <>
      {(s.paymentA === "loan" || s.paymentB === "loan") && (
        s.loanInterestMethod === "amortizing"
          ? <div><i>FinCost<sub>loan</sub></i> = &Sigma;<sub>m=1..Tenor&times;12</sub> <i>Balance<sub>m-1</sub></i> &times; <i>Interest%</i>/12 &nbsp;(<i>declining-balance annuity</i>)</div>
          : <div><i>FinCost<sub>loan</sub></i> = <i>Principal</i> &times; <i>Interest%</i> &times; <i>Tenor</i> &nbsp;(<i>flat-rate</i>)</div>
      )}
    </>,
    vars: [
      ...(s.paymentA === "loan" || s.paymentB === "loan" ? (s.loanInterestMethod === "amortizing" ? [
        { sym: "Balance_m-1", desc: L("Outstanding loan balance at the start of month m — declines each month as principal is repaid", "Saldo pinjaman pada awal bulan m — menurun setiap bulan seiring pokok dibayar") },
        { sym: "Interest%", desc: L(`Annual nominal interest rate (currently ${interest}%)`, `Suku bunga nominal tahunan (saat ini ${interest}%)`) },
        { sym: "v1.6", desc: L("Amortizing mode (window.amortizingInterestByYear) — charges less total interest than flat for the same nominal rate, since it's computed off the declining balance.", "Mode amortizing (window.amortizingInterestByYear) — total bunga lebih rendah dari flat pada suku bunga nominal yang sama, karena dihitung dari saldo menurun.") },
      ] : [
        { sym: "Principal", desc: L(`CAPEX × (1 − Down Payment%) (currently DP ${downPayment}%)`, `CAPEX × (1 − Uang Muka%) (saat ini DP ${downPayment}%)`) },
        { sym: "Interest%", desc: L(`Annual flat interest rate (currently ${interest}%)`, `Suku bunga flat tahunan (saat ini ${interest}%)`) },
        { sym: "Tenor", desc: L(`Loan tenor in years (currently ${tenor})`, `Tenor pinjaman dalam tahun (saat ini ${tenor})`) },
      ]) : []),
    ],
    source: "§2.3", updated: "2026-07-14",
    empty: s.paymentA !== "loan" && s.paymentB !== "loan",
  };

  const residualMeso = {
    id: "residual",
    icon: "♻️",
    name: L("Residual Value Stream", "Aliran Nilai Sisa"),
    ...money(rows[7].a, rows[7].b),
    formula: ((s.residualValueMethod ?? "soh") === "soh")
      ? <><i>Residual<sub>EV</sub></i> = <i>Price</i> &times; <i>FleetSize</i> &times; [ <i>BattWt%</i> &times; <i>SOH</i>(<i>Horizon</i>) + (1−<i>BattWt%</i>) &times; (1−<i>NonBattDepr%</i>)<sup>Horizon</sup> ]</>
      : <><i>Residual</i> = <i>Price</i> &times; <i>FleetSize</i> &times; <i>ResidualFraction</i>(<i>Horizon</i>)</>,
    vars: ((s.residualValueMethod ?? "soh") === "soh") ? [
      { sym: "SOH(Horizon)", desc: L("Battery State-of-Health — see derivation", "State-of-Health Baterai — lihat penjabaran"), node: sohNode },
      { sym: "BattWt%", desc: L("Battery share of EV purchase price (batteryWeightPctOfPrice)", "Porsi baterai dari harga beli EV (batteryWeightPctOfPrice)") },
      { sym: "v1.6", desc: L("EV only — ties residual value to the SAME degradation rate the user already configures, instead of a fixed lookup curve disconnected from it. ICE still uses the DJKN PMK 223/2021 schedule below.", "Hanya EV — mengaitkan nilai sisa dengan tingkat degradasi yang sama dengan yang sudah dikonfigurasi pengguna, bukan tabel tetap yang tidak terkait. ICE tetap memakai skedul DJKN PMK 223/2021 di bawah.") },
    ] : [
      { sym: "ResidualFraction(Horizon)", desc: L("Depreciation schedule lookup (DJKN PMK 223/2021 for ICE; fixed empirical curve for EV) — residual value fraction remaining at end of horizon", "Tabel depresiasi (DJKN PMK 223/2021 untuk ICE; kurva empiris tetap untuk EV) — fraksi nilai sisa pada akhir horizon") },
      { sym: "Horizon", desc: L(`Analysis horizon (currently ${horizon} years)`, `Horizon analisis (saat ini ${horizon} tahun)`) },
    ],
    source: "§2.5", updated: "2026-06-30",
  };

  const finPhase = {
    id: "fin",
    name: L("Financials", "Keuangan"),
    screenLabel: L("Screen 5 · Financials", "Layar 5 · Keuangan"),
    icon: "💰",
    desc: L("Financing cost & residual value", "Biaya pembiayaan & nilai sisa"),
    mesos: [
      financingMeso,
      residualMeso,
      {
        id: "expenseBuckets",
        icon: "🧾",
        name: L("Expense Bucket Model (v1.7.7)", "Model Kelompok Biaya (v1.7.7)"),
        a: "", b: "",
        formula: <><i>CustomerTotalPayment</i> = <i>CustomerDirect</i> + <i>VktrBorneRaw</i> &times; (1+<i>Markup%</i>)</>,
        vars: [
          { sym: "v1.7.7", desc: L("Replaces the old Commercial Scheme Ladder and lease payment method entirely — UNIT/FMC/INFRA/WARRANTY/ENERGY/INSURANCE, each independently assignable to Customer or VKTR, with a per-bucket markup on the VKTR-borne side. See Screen 5 → Expense Bucket Toggle and CALCULATION_ENGINE.md.", "Menggantikan Tangga Skema Komersial dan metode pembayaran sewa lama sepenuhnya — UNIT/FMC/INFRA/WARRANTY/ENERGY/INSURANCE, masing-masing dapat ditetapkan secara independen ke Pelanggan atau VKTR, dengan markup per kelompok pada sisi yang ditanggung VKTR. Lihat Layar 5 → Toggle Kelompok Biaya dan CALCULATION_ENGINE.md.") },
        ],
        source: "§2.10", updated: "2026-07-14",
        empty: false,
      },
    ],
  };

  const synthPhase = {
    id: "synth",
    name: L("Total Cost of Ownership", "Total Biaya Kepemilikan"),
    screenLabel: L("Screen 6 · Report", "Layar 6 · Laporan"),
    icon: "🎯",
    mesos: [
      {
        id: "totaltco",
        icon: "🎯",
        name: L("Total TCO Stream Convergence", "Konvergensi Aliran Total TCO"),
        ...money(totalA, totalB),
        formula: <><i>TCO</i> = <i>CAPEX</i> + <i>FinCost</i> + <i>EnergyCost</i> + <i>AdBlueCost</i> + <i>MaintCost</i> + <i>InfraCAPEX</i> + <i>InfraOPEX</i> − <i>Residual</i></>,
        vars: [
          { sym: "CAPEX", desc: L("Streamed in from Vehicle & Procurement — see derivation", "Dialirkan dari Kendaraan & Pengadaan — lihat penjabaran"), node: procPhase.mesos[0] },
          { sym: "FinCost", desc: L("Streamed in from Financials — see derivation", "Dialirkan dari Keuangan — lihat penjabaran"), node: financingMeso },
          { sym: "EnergyCost", desc: L("Streamed in from Operation — see derivation", "Dialirkan dari Operasional — lihat penjabaran"), node: energyMeso },
          { sym: "MaintCost", desc: L("Streamed in from Operation — see derivation", "Dialirkan dari Operasional — lihat penjabaran"), node: opsPhase.mesos[1] },
          { sym: "InfraCAPEX", desc: L("Streamed in from Infrastructure — see derivation", "Dialirkan dari Infrastruktur — lihat penjabaran"), node: capexbdMeso },
          { sym: "InfraOPEX", desc: L("Streamed in from Infrastructure — see derivation", "Dialirkan dari Infrastruktur — lihat penjabaran"), node: infraopexMeso },
          { sym: "Residual", desc: L("Streamed in from Financials (subtracted) — see derivation", "Dialirkan dari Keuangan (dikurangkan) — lihat penjabaran"), node: residualMeso },
        ],
        source: "§2.6", updated: "2026-06-15",
      },
      {
        id: "payback",
        icon: "⏱",
        name: L("Payback Period (B vs A)", "Periode Balik Modal (B vs A)"),
        a: paybackText, b: "",
        formula: <><i>Payback</i> = <i>y</i> − 1 + (<i>UpfrontDiff</i> − <i>Cum<sub>y-1</sub></i>) / <i>YearSavings<sub>y</sub></i></>,
        vars: [
          { sym: "UpfrontDiff", desc: L("(CAPEX_B + InfraCAPEX_B) − (CAPEX_A + InfraCAPEX_A)", "(CAPEX_B + InfraCAPEX_B) − (CAPEX_A + InfraCAPEX_A)") },
          { sym: "YearSavings_y", desc: L("Year-y OPEX + financing savings of A vs B", "Penghematan OPEX + pembiayaan A vs B pada tahun-y") },
        ],
        source: "§7", updated: "2026-06-15",
        single: true,
      },
      {
        id: "npv",
        icon: "📈",
        name: L("NPV / IRR of Incremental Investment", "NPV / IRR Investasi Tambahan"),
        a: `NPV ${npvText}`, b: `IRR ${irrText}`,
        formula: <><i>NPV</i> = −<i>CapexPremium</i> + &Sigma;<sub>y=1..H</sub> <i>YearSavings<sub>y</sub></i> / (1+<i>WACC</i>)<sup>y</sup></>,
        vars: [
          { sym: "CapexPremium", desc: L("Upfront CAPEX + Infra CAPEX difference between A and B", "Selisih CAPEX + CAPEX Infra antara A dan B") },
          { sym: "WACC", desc: L(`Discount rate used for NPV (currently ${s.wacc}%)`, `Tingkat diskonto untuk NPV (saat ini ${s.wacc}%)`) },
          { sym: "IRR", desc: L("Rate at which NPV = 0, found via binary search", "Tingkat di mana NPV = 0, dicari via pencarian biner") },
        ],
        source: "§7", updated: "2026-06-15",
        single: true,
      },
    ],
  };

  return [procPhase, opsPhase, infraPhase, depotEnginePhase, leadTimePhase, finPhase, synthPhase];
}

function StreamMap({ s, set, R, sizing, capex, vA, vB, lang }) {
  const L = (en, id) => tr(lang, en, id);
  const [view, setView] = React.useState({ level: "macro", phase: null, meso: null, path: [] });
  const expertOn = !!s.screen6_streamExpert;
  const setExpert = (v) => set("screen6_streamExpert", v);

  const phases = React.useMemo(
    () => buildStreamMapPayload(s, R, sizing, capex, vA, vB, L),
    [s, R, sizing, capex, vA, vB, lang]
  );

  const goMacro = () => setView({ level: "macro", phase: null, meso: null, path: [] });
  const goPhase = (id) => setView({ level: "meso", phase: id, meso: null, path: [] });
  const goMeso  = (pid, mid) => setView({ level: "micro", phase: pid, meso: mid, path: [] });
  const goVar   = (sym) => setView(v => ({ ...v, path: [...v.path, sym] }));
  const goCrumbDepth = (depth) => setView(v => ({ ...v, path: v.path.slice(0, depth) }));

  const phase = phases.find(p => p.id === view.phase) || null;
  const meso  = phase ? phase.mesos.find(m => m.id === view.meso) : null;

  // Resolve the current node (meso itself, or a nested sub-formula var,
  // to arbitrary depth) plus the breadcrumb chain of {sym, name} for it.
  function resolveNode(root, path) {
    let node = root;
    const chain = [];
    for (const sym of path) {
      const v = (node.vars || []).find(vv => vv.sym === sym);
      if (!v || !v.node) break;
      node = v.node;
      chain.push({ sym, name: v.node.name || sym });
    }
    return { node, chain };
  }
  const { node, chain } = (view.level === "micro" && meso) ? resolveNode(meso, view.path) : { node: meso, chain: [] };

  const nonSynthPhases = phases.slice(0, -1);
  const synthPhase = phases[phases.length - 1];

  return (
    <div className="stream-map">
      <div className="sm-breadcrumb">
        <button className="sm-crumb-btn" onClick={goMacro} disabled={view.level === "macro"}>
          {L("Stream Map", "Peta Aliran")}
        </button>
        {phase && (
          <>
            <span className="sm-sep">/</span>
            <button className="sm-crumb-btn" onClick={() => goPhase(phase.id)} disabled={view.level === "meso"}>
              {phase.name}
            </button>
          </>
        )}
        {meso && (
          <>
            <span className="sm-sep">/</span>
            <button className="sm-crumb-btn" onClick={() => goMeso(phase.id, meso.id)} disabled={view.level === "micro" && chain.length === 0}>
              {meso.name}
            </button>
          </>
        )}
        {chain.map((c, i) => (
          <React.Fragment key={i}>
            <span className="sm-sep">/</span>
            {i === chain.length - 1
              ? <span className="sm-crumb-current">{c.name}</span>
              : <button className="sm-crumb-btn" onClick={() => goCrumbDepth(i + 1)}>{c.name}</button>}
          </React.Fragment>
        ))}
        <div className="sm-spacer" />
        <ExpertToggle isOn={expertOn} onToggle={setExpert} />
      </div>

      {view.level === "macro" && (
        <div className="sm-macro">
          <div className="sm-grid">
            {nonSynthPhases.map((p, i) => (
              <button key={p.id} className="sm-phase-card" onClick={() => goPhase(p.id)}>
                <div className="sm-phase-icon">{p.icon}</div>
                <div className="sm-phase-num">{i + 1}</div>
                <div className="sm-phase-name">{p.name}</div>
                <div className="sm-phase-desc">{p.desc}</div>
                <div className="sm-phase-screen">{p.screenLabel}</div>
                <div className="sm-phase-count">{p.mesos.length} {L("formula streams", "aliran formula")}</div>
              </button>
            ))}
          </div>
          <div className="sm-synth-row">
            <button className="sm-phase-card sm-synth" onClick={() => goPhase(synthPhase.id)}>
              <div className="sm-phase-icon">{synthPhase.icon}</div>
              <div className="sm-phase-num">{nonSynthPhases.length + 1}</div>
              <div className="sm-phase-name">{synthPhase.name}</div>
              <div className="sm-phase-screen">{synthPhase.screenLabel}</div>
              <div className="sm-phase-count">{synthPhase.mesos.length} {L("formula streams", "aliran formula")}</div>
            </button>
          </div>
        </div>
      )}

      {view.level === "meso" && phase && (
        <div className="sm-meso">
          <div className="sm-phase-head">
            <div className="sm-phase-icon-lg">{phase.icon}</div>
            <div>
              <div className="sm-phase-screen">{phase.screenLabel}</div>
              <h4>{phase.name}</h4>
            </div>
          </div>
          <div className="sm-meso-grid">
            {phase.mesos.map(m => (
              <button key={m.id} className="sm-meso-card" onClick={() => goMeso(phase.id, m.id)}>
                <div className="sm-meso-icon">{m.icon}</div>
                <div className="sm-meso-name">{m.name}</div>
                <div className="sm-meso-preview">
                  {m.single
                    ? <>{m.a}{m.b ? <><br />{m.b}</> : null}</>
                    : <>A: {m.a} · B: {m.b}</>}
                </div>
              </button>
            ))}
          </div>
        </div>
      )}

      {view.level === "micro" && phase && meso && (
        <div className="sm-micro">
          <div className="sm-micro-head">
            <div className="sm-meso-icon-lg">{node.icon || meso.icon}</div>
            <h4>{node.name || meso.name}</h4>
          </div>
          <div className="sm-formula-box">{node.formula}</div>
          {chain.length === 0 ? (
            meso.empty ? (
              <div className="sm-expert-note">
                {meso.emptyNote
                  ? meso.emptyNote
                  : <Tr en="No live values for this stream — depends on infrastructure inputs not applicable to the current vehicle pair." id="Tidak ada nilai langsung untuk aliran ini — bergantung pada input infrastruktur yang tidak berlaku untuk pasangan kendaraan saat ini." />}
              </div>
            ) : meso.single ? (
              <div className="sm-kpi-row sm-kpi-single">
                <div className="sm-kpi-card">
                  <div className="sm-kpi-label">{meso.a}</div>
                  {meso.b && <div className="sm-kpi-sub">{meso.b}</div>}
                </div>
              </div>
            ) : (
              <div className="sm-kpi-row">
                <div className="sm-kpi-card">
                  <div className="sm-kpi-tag a">
                    <Badge kind={vA.powertrain === "EV" ? "ev" : "ice"}>{vA.powertrain}</Badge>
                    {vA.name}
                  </div>
                  <div className="sm-kpi-val">{meso.a}</div>
                </div>
                <div className="sm-kpi-card">
                  <div className="sm-kpi-tag b">
                    <Badge kind={vB.powertrain === "EV" ? "ev" : "ice"}>{vB.powertrain}</Badge>
                    {vB.name}
                  </div>
                  <div className="sm-kpi-val">{meso.b}</div>
                </div>
              </div>
            )
          ) : node.empty ? (
            <div className="sm-expert-note">
              <Tr en="No further breakdown available for this term." id="Tidak ada rincian lebih lanjut untuk suku ini." />
            </div>
          ) : null}
          {expertOn ? (
            <div className="sm-expert-block">
              <div className="sm-vartable-title">{L("Variables", "Variabel")}</div>
              <table className="sm-vartable">
                <tbody>
                  {(node.vars || []).map((v, i) => v.node ? (
                    <tr key={i} className="sm-var-row sm-var-clickable" onClick={() => goVar(v.sym)} tabIndex={0} role="button">
                      <td className="sm-var-sym">{v.sym} <span className="sm-drill-hint">▸</span></td>
                      <td className="sm-var-desc">{v.desc}</td>
                    </tr>
                  ) : (
                    <tr key={i} className="sm-var-row">
                      <td className="sm-var-sym">{v.sym}</td>
                      <td className="sm-var-desc">{v.desc}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
              <div className="sm-meta">{L("Source", "Sumber")}: {node.source || meso.source} · {L("Last updated", "Terakhir diperbarui")}: {node.updated || meso.updated}</div>
            </div>
          ) : (
            <div className="sm-expert-note">
              <Tr en="Enable Expert Mode to see the variable matrix and audit metadata for this formula." id="Aktifkan Mode Ahli untuk melihat matriks variabel dan metadata audit formula ini." />
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { StreamMap, buildStreamMapPayload });
