/* ============================================================
   BATTERY DISPATCH MODEL — interactive React view
   A deterministic battery-storage dispatcher and economics
   model, ported from session_06_battery_dispatch.xlsx.

   Pipeline (mirrors the spreadsheet's tabs):
     Battery Config  → physical + financial assumptions
     Price Series    → 168-hour wholesale price profile
     Dispatch        → rank hours/day; charge cheapest D,
                       discharge most expensive D ("buy low /
                       sell high" heuristic)
     Revenue Stack   → arbitrage + capacity + ancillary
     Project Econ    → 15-year DCF → NPV / IRR / payback

   Self-contained: registers window.BatteryDispatchView and is
   routed from index.html like the other workspace views.
   ============================================================ */
const { useState: bUse, useMemo: bMemo } = React;

/* ---------- default 168-hour synthetic price series ----------
   Lifted verbatim from the spreadsheet's Price Series tab
   (one week, hourly, $/MWh — realistic intraday + weekly shape,
   including a few scarcity spikes). */
const BD_DEFAULT_PRICES = [
  42.84, 48.52, 56.42, 50.58, 55.63, 49.41, 186.98, 63.98, 74.06, 75.04,
  75.64, 52.72, 40.47, 41.80, 43.36, 38.66, 45.31, 52.44, 54.54, 69.31,
  75.46, 80.66, 79.78, 75.98, 54.07, 55.12, 52.53, 47.39, 43.17, 53.18,
  52.52, 234.69, 70.13, 78.50, 75.70, 66.31, 50.21, 40.37, 45.83, 45.19,
  44.16, 48.11, 53.85, 63.73, 65.50, 71.41, 72.14, 71.17, 48.47, 46.20,
  54.01, 47.57, 57.51, 50.71, 60.36, 60.76, 78.56, 78.29, 63.49, 65.10,
  46.35, 48.23, 40.03, 39.34, 44.18, 49.60, 61.34, 67.70, 77.16, 78.34,
  80.24, 77.99, 47.90, 47.44, 50.10, 47.15, 57.01, 47.16, 65.42, 60.11,
  65.86, 75.45, 76.02, 58.99, 47.87, 45.06, 43.28, 40.94, 45.59, 46.03,
  63.87, 66.75, 75.63, 70.83, 77.79, 76.72, 54.32, 54.06, 44.89, 43.00,
  50.01, 46.81, 55.40, 58.96, 77.62, 85.10, 67.37, 59.62, 45.32, 36.88,
  39.10, 44.21, 56.49, 59.29, 49.82, 68.74, 65.18, 72.26, 86.26, 83.81,
  48.89, 46.89, 43.78, 39.63, 45.51, 40.26, 47.53, 56.96, 56.26, 66.69,
  65.61, 46.52, 40.39, 28.01, 32.98, 30.11, 35.58, 42.71, 42.65, 59.44,
  59.33, 61.40, 63.86, 72.85, 172.67, 45.00, 35.17, 46.19, 41.47, 39.18,
  51.61, 50.64, 59.41, 61.89, 61.38, 50.65, 48.65, 27.21, 35.96, 29.00,
  48.29, 36.49, 45.50, 62.24, 66.90, 67.78, 61.32, 73.64,
];

/* ---------- default configuration (spreadsheet Battery Config) ---------- */
const BD_DEFAULTS = {
  powerMW: 100,        // power capacity (MW)
  energyMWh: 400,      // energy capacity (MWh) → duration = energy/power
  rte: 0.87,           // round-trip efficiency
  capexPerKwh: 300,    // $/kWh installed
  omPerKwYr: 8,        // annual O&M, $/kW-yr
  capPayment: 100,     // capacity payment, $/kW-yr
  ancPayment: 40,      // ancillary services, $/kW-yr
  life: 15,            // asset life, years
  degradation: 0.015,  // annual capacity loss, %/yr
  wacc: 0.07,          // discount rate / WACC
  cycleLimit: 365,     // max cycles/year
  spread: 1.0,         // price-spread multiplier (stress test)
};

/* ============================================================
   FINANCE HELPERS
   ============================================================ */
function bdNpv(rate, cashflows) {
  return cashflows.reduce((s, cf, t) => s + cf / Math.pow(1 + rate, t), 0);
}
function bdIrr(cashflows) {
  const f = (r) => bdNpv(r, cashflows);
  let lo = -0.9, hi = 3.0;
  let flo = f(lo), fhi = f(hi);
  if (!isFinite(flo) || !isFinite(fhi) || flo * fhi > 0) return null;
  for (let k = 0; k < 200; k++) {
    const mid = (lo + hi) / 2, fm = f(mid);
    if (Math.abs(fm) < 1e-9) return mid;
    if (flo * fm < 0) { hi = mid; fhi = fm; } else { lo = mid; flo = fm; }
  }
  return (lo + hi) / 2;
}
function bdPayback(cashflows) {
  // cashflows[0] is the (negative) capex; returns fractional years or null
  let cum = cashflows[0];
  for (let t = 1; t < cashflows.length; t++) {
    const prev = cum;
    cum += cashflows[t];
    if (cum >= 0) return (t - 1) + (-prev) / cashflows[t];
  }
  return null;
}

/* ============================================================
   FORMATTING
   ============================================================ */
const bdMoney = (x) => "$" + Math.round(x).toLocaleString();
const bdM = (x) => (x < 0 ? "(" + Math.abs(x).toFixed(1) + ")" : x.toFixed(1));
const bdPct = (x, d = 1) => (x * 100).toFixed(d) + "%";

/* ============================================================
   CORE MODEL — pure function of config
   ============================================================ */
function bdComputeModel(cfg, basePrices) {
  const mean = basePrices.reduce((a, b) => a + b, 0) / basePrices.length;
  // apply spread multiplier around the mean (compress / widen the curve)
  const prices = basePrices.map((p) => Math.max(0, mean + (p - mean) * cfg.spread));

  const duration = cfg.powerMW > 0 ? cfg.energyMWh / cfg.powerMW : 0;
  const D = Math.max(1, Math.min(12, Math.round(duration))); // charge/discharge hrs per day

  // ----- hour-by-hour dispatch, day by day (24h blocks) -----
  const hours = prices.map((price, i) => ({
    i, hour: i + 1, day: Math.floor(i / 24), price,
    charge: 0, discharge: 0, net: 0, revenue: 0, rank: 0,
  }));
  const byDay = {};
  hours.forEach((h) => { (byDay[h.day] = byDay[h.day] || []).push(h); });

  Object.values(byDay).forEach((dayHours) => {
    const sorted = [...dayHours].sort((a, b) => a.price - b.price);
    sorted.forEach((h, idx) => { h.rank = idx + 1; }); // 1 = cheapest
    const n = sorted.length;
    const k = Math.min(D, Math.floor(n / 2));
    const chargeSet = sorted.slice(0, k);
    const dischargeSet = sorted.slice(n - k);
    chargeSet.forEach((h) => {
      h.charge = cfg.powerMW;
      h.net = -cfg.powerMW;
      // round-trip losses priced as a charging penalty: to deliver a unit
      // on discharge you must buy 1/RTE units when charging
      h.revenue = (-h.price * cfg.powerMW) / cfg.rte;
    });
    dischargeSet.forEach((h) => {
      h.discharge = cfg.powerMW;
      h.net = cfg.powerMW;
      h.revenue = h.price * cfg.powerMW;            // earn full price on discharge
    });
  });

  const weeklyCharged = hours.reduce((s, h) => s + h.charge, 0);          // MWh stored
  const weeklyDischarged = hours.reduce((s, h) => s + h.discharge, 0);    // MWh delivered
  const weeklyArb = hours.reduce((s, h) => s + h.revenue, 0);            // $
  const cyclesPerWeek = cfg.energyMWh > 0 ? weeklyDischarged / cfg.energyMWh : 0;

  // ----- annualize (52 weeks, capped by annual cycle limit) -----
  const cycleFactor = Math.min(1, cfg.cycleLimit / Math.max(cyclesPerWeek * 52, 1));
  const annualArb = weeklyArb * 52 * cycleFactor;          // $/yr nameplate
  const annualArbM = annualArb / 1e6;
  const arbPerKwYr = cfg.powerMW > 0 ? annualArb / (cfg.powerMW * 1000) : 0;

  // ----- revenue stack ($M / yr, nameplate) -----
  const capacityRevM = (cfg.capPayment * cfg.powerMW * 1000) / 1e6;
  const ancillaryRevM = (cfg.ancPayment * cfg.powerMW * 1000) / 1e6;

  // ----- capex / opex ($M) -----
  const totalCapexM = (cfg.capexPerKwh * cfg.energyMWh * 1000) / 1e6;
  const annualOmM = (cfg.omPerKwYr * cfg.powerMW * 1000) / 1e6;

  // ----- DCF -----
  const rows = [];
  const cashflows = [-totalCapexM];
  for (let t = 1; t <= cfg.life; t++) {
    const cf = Math.pow(1 - cfg.degradation, t);       // capacity factor (geometric)
    const arb = annualArbM * cf;                       // throughput degrades
    const total = arb + capacityRevM + ancillaryRevM;  // capacity/ancillary flat
    const fcf = total - annualOmM;
    cashflows.push(fcf);
    rows.push({ t, cf, capex: 0, arb, capacity: capacityRevM, ancillary: ancillaryRevM, total, om: annualOmM, fcf });
  }
  rows.unshift({ t: 0, cf: 1, capex: totalCapexM, arb: 0, capacity: 0, ancillary: 0, total: 0, om: 0, fcf: -totalCapexM });

  const npv = bdNpv(cfg.wacc, cashflows);
  const irr = bdIrr(cashflows);
  const payback = bdPayback(cashflows);

  const totalRevY1 = rows[1] ? rows[1].total : 0;
  const totalRevPerKwYr = cfg.powerMW > 0 ? (totalRevY1 * 1e6) / (cfg.powerMW * 1000) : 0;

  // price stats (effective series)
  const sortedP = [...prices].sort((a, b) => a - b);
  const std = Math.sqrt(prices.reduce((s, p) => s + (p - mean) * (p - mean), 0) / prices.length);

  return {
    prices, mean, duration, D, hours,
    weeklyCharged, weeklyDischarged, weeklyArb, cyclesPerWeek,
    annualArbM, arbPerKwYr, capacityRevM, ancillaryRevM,
    totalCapexM, annualOmM,
    rows, cashflows, npv, irr, payback,
    totalRevY1, totalRevPerKwYr,
    priceStats: {
      avg: mean, min: sortedP[0], max: sortedP[sortedP.length - 1], std,
    },
  };
}

/* ============================================================
   CONTROL — labeled slider + numeric readout
   ============================================================ */
function BdControl({ label, value, onChange, min, max, step, unit, fmt, hint }) {
  const display = fmt ? fmt(value) : value;
  return (
    <div className="bd-ctrl">
      <div className="bd-ctrl-head">
        <span className="bd-ctrl-label">{label}</span>
        <span className="bd-ctrl-value">{display}{unit ? <span className="bd-ctrl-unit">{unit}</span> : null}</span>
      </div>
      <input
        type="range"
        className="bd-range"
        min={min} max={max} step={step} value={value}
        onChange={(e) => onChange(Number(e.target.value))}
      />
      {hint ? <div className="bd-ctrl-hint">{hint}</div> : null}
    </div>
  );
}

/* ============================================================
   PRICE + DISPATCH CHART (full week)
   ============================================================ */
function BdWeekChart({ model }) {
  const W = 980, H = 220, padB = 22, padT = 12;
  const prices = model.prices;
  const maxP = Math.max(...prices) * 1.05;
  const n = prices.length;
  const x = (i) => (i / (n - 1)) * W;
  const y = (p) => padT + (1 - p / maxP) * (H - padT - padB);

  const linePath = prices.map((p, i) => (i === 0 ? "M" : "L") + x(i).toFixed(1) + "," + y(p).toFixed(1)).join(" ");

  return (
    <div className="bd-chart-wrap">
      <svg className="bd-week-chart" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none">
        {/* day gridlines (every 24h) */}
        {[1, 2, 3, 4, 5, 6].map((d) => (
          <line key={d} className="bd-daygrid" x1={x(d * 24)} x2={x(d * 24)} y1={padT} y2={H - padB} />
        ))}
        {/* mean line */}
        <line className="bd-mean" x1="0" x2={W} y1={y(model.mean)} y2={y(model.mean)} />
        {/* charge / discharge markers */}
        {model.hours.map((h) => {
          if (h.charge > 0) return <circle key={"c" + h.i} className="bd-dot-charge" cx={x(h.i)} cy={y(h.price)} r="3" />;
          if (h.discharge > 0) return <circle key={"d" + h.i} className="bd-dot-discharge" cx={x(h.i)} cy={y(h.price)} r="3" />;
          return null;
        })}
        {/* price line */}
        <path className="bd-price-line" d={linePath} />
      </svg>
      <div className="bd-chart-axis">
        {[0, 1, 2, 3, 4, 5, 6].map((d) => (
          <span key={d} style={{ left: `${(d * 24 / (n - 1)) * 100}%` }}>Day {d + 1}</span>
        ))}
      </div>
      <div className="bd-legend">
        <span><i className="bd-key bd-key-charge" /> Charge hour (buy)</span>
        <span><i className="bd-key bd-key-discharge" /> Discharge hour (sell)</span>
        <span><i className="bd-key bd-key-mean" /> Weekly average ${model.mean.toFixed(0)}/MWh</span>
      </div>
    </div>
  );
}

/* ============================================================
   STAT TILE
   ============================================================ */
function BdStat({ label, value, sub, tone }) {
  return (
    <div className={"bd-stat " + (tone ? "bd-stat--" + tone : "")}>
      <div className="bd-stat-label">{label}</div>
      <div className="bd-stat-value">{value}</div>
      {sub ? <div className="bd-stat-sub">{sub}</div> : null}
    </div>
  );
}

/* ============================================================
   MAIN VIEW
   ============================================================ */
function BatteryDispatchView() {
  const [cfg, setCfg] = bUse(BD_DEFAULTS);
  const set = (k) => (v) => setCfg((c) => ({ ...c, [k]: v }));
  const reset = () => setCfg(BD_DEFAULTS);

  const model = bMemo(() => bdComputeModel(cfg, BD_DEFAULT_PRICES), [cfg]);

  const irrStr = model.irr == null ? "n/a" : bdPct(model.irr);
  const paybackStr = model.payback == null ? ">life" : model.payback.toFixed(1) + " yr";
  const npvTone = model.npv >= 0 ? "good" : "bad";

  return (
    <div className="bd">
      {/* top bar */}
      <div className="bd-bar">
        <div className="bd-bar-left">
          <span className="bd-bar-brand"><span className="bd-bar-dot" />Battery Dispatch</span>
          <span className="bd-bar-crumb"><span className="bd-bar-sep">·</span><span>Storage arbitrage &amp; economics</span></span>
        </div>
        <button className="bd-reset" onClick={reset}>↺ Reset to base case</button>
      </div>

      {/* hero */}
      <header className="bd-hero">
        <div>
          <div className="bd-eyebrow">Session 06 · deterministic dispatcher</div>
          <h1 className="bd-title">Charge cheap, discharge dear.</h1>
          <p className="bd-lede">
            A simple, deterministic battery-storage model. It takes a 168-hour wholesale
            price profile and a battery configuration, ranks each day's hours, charges in
            the cheapest and discharges in the priciest, then stacks arbitrage with
            capacity and ancillary revenue and runs a {cfg.life}-year DCF.
          </p>
        </div>
        <div className="bd-hero-stats">
          <BdStat label="NPV @ WACC" value={"$" + bdM(model.npv) + "M"} tone={npvTone} sub={"discounted at " + bdPct(cfg.wacc, 0)} />
          <BdStat label="Project IRR" value={irrStr} sub={"payback " + paybackStr} />
          <BdStat label="Total revenue" value={"$" + bdM(model.totalRevY1) + "M/yr"} sub={"$" + model.totalRevPerKwYr.toFixed(0) + "/kW-yr (Y1)"} />
        </div>
      </header>

      <div className="bd-grid">
        {/* LEFT: configuration */}
        <aside className="bd-config">
          <div className="bd-section-tag">Battery configuration</div>

          <BdControl label="Power capacity" value={cfg.powerMW} onChange={set("powerMW")}
            min={10} max={500} step={5} unit=" MW" />
          <BdControl label="Energy capacity" value={cfg.energyMWh} onChange={set("energyMWh")}
            min={40} max={2000} step={20} unit=" MWh"
            hint={"Duration: " + model.duration.toFixed(1) + " h → " + model.D + "h charge / " + model.D + "h discharge per cycle"} />
          <BdControl label="Round-trip efficiency" value={cfg.rte} onChange={set("rte")}
            min={0.70} max={1.0} step={0.01} fmt={(v) => bdPct(v, 0)}
            hint="Applied as a charge-cost penalty: energy bought ÷ RTE." />

          <div className="bd-section-tag">Capex &amp; opex</div>
          <BdControl label="Capex" value={cfg.capexPerKwh} onChange={set("capexPerKwh")}
            min={100} max={600} step={10} fmt={bdMoney} unit="/kWh"
            hint={"Total capex: $" + model.totalCapexM.toFixed(0) + "M"} />
          <BdControl label="Annual O&M" value={cfg.omPerKwYr} onChange={set("omPerKwYr")}
            min={0} max={30} step={1} fmt={bdMoney} unit="/kW-yr"
            hint={"Total O&M: $" + model.annualOmM.toFixed(1) + "M/yr"} />

          <div className="bd-section-tag">Revenue stack</div>
          <BdControl label="Capacity payment" value={cfg.capPayment} onChange={set("capPayment")}
            min={0} max={200} step={5} fmt={bdMoney} unit="/kW-yr"
            hint={"$" + model.capacityRevM.toFixed(1) + "M/yr"} />
          <BdControl label="Ancillary services" value={cfg.ancPayment} onChange={set("ancPayment")}
            min={0} max={100} step={5} fmt={bdMoney} unit="/kW-yr"
            hint={"$" + model.ancillaryRevM.toFixed(1) + "M/yr"} />

          <div className="bd-section-tag">Financing &amp; degradation</div>
          <BdControl label="Discount rate (WACC)" value={cfg.wacc} onChange={set("wacc")}
            min={0.02} max={0.15} step={0.005} fmt={(v) => bdPct(v)} />
          <BdControl label="Annual degradation" value={cfg.degradation} onChange={set("degradation")}
            min={0} max={0.04} step={0.0025} fmt={(v) => bdPct(v, 2)} unit="/yr" />
          <BdControl label="Asset life" value={cfg.life} onChange={set("life")}
            min={5} max={25} step={1} unit=" yr" />

          <div className="bd-section-tag">Market stress test</div>
          <BdControl label="Price spread" value={cfg.spread} onChange={set("spread")}
            min={0.2} max={2.0} step={0.1} fmt={(v) => v.toFixed(1) + "×"}
            hint="Compress (<1) or widen (>1) prices around the weekly mean." />
        </aside>

        {/* RIGHT: results */}
        <div className="bd-results">
          {/* dispatch */}
          <section className="bd-panel">
            <div className="bd-panel-head">
              <h2>Dispatch — one week, 168 hours</h2>
              <span className="bd-panel-meta">Rank each day low→high · charge bottom {model.D} · discharge top {model.D}</span>
            </div>
            <BdWeekChart model={model} />
            <div className="bd-stat-row">
              <BdStat label="Energy charged" value={Math.round(model.weeklyCharged).toLocaleString() + " MWh"} sub="per week (stored)" />
              <BdStat label="Energy discharged" value={Math.round(model.weeklyDischarged).toLocaleString() + " MWh"} sub="per week (sold)" />
              <BdStat label="Cycles / week" value={model.cyclesPerWeek.toFixed(1)} sub={"~" + Math.round(model.cyclesPerWeek * 52) + "/yr"} />
              <BdStat label="Weekly arbitrage" value={bdMoney(model.weeklyArb)} sub="buy ÷ RTE, sell full" />
            </div>
          </section>

          {/* revenue stack */}
          <section className="bd-panel">
            <div className="bd-panel-head">
              <h2>Revenue stack</h2>
              <span className="bd-panel-meta">Year 1, nameplate · $/kW-yr basis</span>
            </div>
            <BdRevenueStack model={model} cfg={cfg} />
          </section>

          {/* economics */}
          <section className="bd-panel">
            <div className="bd-panel-head">
              <h2>Project economics — {cfg.life}-year DCF</h2>
              <span className="bd-panel-meta">$M · discounted at {bdPct(cfg.wacc, 0)}</span>
            </div>
            <div className="bd-stat-row bd-stat-row--econ">
              <BdStat label="NPV @ WACC" value={"$" + bdM(model.npv) + "M"} tone={npvTone} />
              <BdStat label="IRR" value={irrStr} tone={model.irr != null && model.irr >= cfg.wacc ? "good" : "bad"} />
              <BdStat label="Payback" value={paybackStr} />
              <BdStat label="Total capex" value={"$" + model.totalCapexM.toFixed(0) + "M"} />
            </div>
            <BdDcfTable model={model} />
          </section>

          {/* stress prompts */}
          <section className="bd-panel">
            <div className="bd-panel-head"><h2>Stress-test prompts</h2></div>
            <ul className="bd-prompts">
              <li><b>Halve the capacity payment</b> → watch the IRR collapse. Capacity is load-bearing for many BESS projects today.</li>
              <li><b>Move to 8-hour duration</b> (double MWh, keep MW) → capex roughly doubles but arbitrage revenue grows only modestly.</li>
              <li><b>Compress the price spread</b> (slider &lt; 1×) → arbitrage collapses; the saturation analog as storage floods a market.</li>
              <li><b>Capex $250 vs $400/kWh</b> → see how sensitive the IRR is to installed cost.</li>
            </ul>
            <div className="bd-caveat">
              <b>Modeling caveats.</b> Single cycle/day with perfect price foresight (overstates
              real revenue ~10–20%). Ancillary revenue is an add-on, not co-optimized. Degradation
              is geometric capacity loss. A production dispatcher would use linear programming with
              explicit cycle and degradation constraints — this is intentionally simpler, for intuition.
            </div>
          </section>
        </div>
      </div>
    </div>
  );
}

/* ---------- revenue stack bar ---------- */
function BdRevenueStack({ model }) {
  const arb = model.rows[1] ? model.rows[1].arb : 0;
  const parts = [
    { label: "Arbitrage", val: arb, cls: "arb" },
    { label: "Capacity", val: model.capacityRevM, cls: "cap" },
    { label: "Ancillary", val: model.ancillaryRevM, cls: "anc" },
  ];
  const total = parts.reduce((s, p) => s + p.val, 0);
  return (
    <div className="bd-revstack">
      <div className="bd-revbar">
        {parts.map((p) => (
          <div key={p.label} className={"bd-revseg bd-revseg--" + p.cls}
            style={{ width: total > 0 ? (p.val / total) * 100 + "%" : "0%" }}
            title={p.label + " $" + p.val.toFixed(1) + "M"} />
        ))}
      </div>
      <div className="bd-revlegend">
        {parts.map((p) => (
          <div key={p.label} className="bd-revitem">
            <span className={"bd-key bd-key-" + p.cls} />
            <span className="bd-revitem-label">{p.label}</span>
            <span className="bd-revitem-val">${p.val.toFixed(1)}M</span>
            <span className="bd-revitem-pct">{total > 0 ? Math.round((p.val / total) * 100) : 0}%</span>
          </div>
        ))}
        <div className="bd-revitem bd-revitem--total">
          <span className="bd-key bd-key-total" />
          <span className="bd-revitem-label">Total</span>
          <span className="bd-revitem-val">${total.toFixed(1)}M</span>
          <span className="bd-revitem-pct">100%</span>
        </div>
      </div>
    </div>
  );
}

/* ---------- DCF table ---------- */
function BdDcfTable({ model }) {
  const cols = model.rows;
  const fmtRow = (key, fmt) => cols.map((r) => fmt(r[key]));
  return (
    <div className="bd-table-scroll">
      <table className="bd-table">
        <thead>
          <tr>
            <th className="bd-th-label">Year</th>
            {cols.map((r) => <th key={r.t}>{r.t}</th>)}
          </tr>
        </thead>
        <tbody>
          <tr>
            <td className="bd-th-label">Capacity factor</td>
            {fmtRow("cf", (v) => bdPct(v, 0)).map((v, i) => <td key={i}>{v}</td>)}
          </tr>
          <tr>
            <td className="bd-th-label">Arbitrage</td>
            {cols.map((r, i) => <td key={i}>{r.arb ? r.arb.toFixed(1) : "–"}</td>)}
          </tr>
          <tr>
            <td className="bd-th-label">Capacity</td>
            {cols.map((r, i) => <td key={i}>{r.capacity ? r.capacity.toFixed(1) : "–"}</td>)}
          </tr>
          <tr>
            <td className="bd-th-label">Ancillary</td>
            {cols.map((r, i) => <td key={i}>{r.ancillary ? r.ancillary.toFixed(1) : "–"}</td>)}
          </tr>
          <tr className="bd-tr-em">
            <td className="bd-th-label">Total revenue</td>
            {cols.map((r, i) => <td key={i}>{r.total ? r.total.toFixed(1) : "–"}</td>)}
          </tr>
          <tr>
            <td className="bd-th-label">O&amp;M</td>
            {cols.map((r, i) => <td key={i}>{r.om ? "(" + r.om.toFixed(1) + ")" : "–"}</td>)}
          </tr>
          <tr>
            <td className="bd-th-label">Capex</td>
            {cols.map((r, i) => <td key={i}>{r.capex ? "(" + r.capex.toFixed(0) + ")" : "–"}</td>)}
          </tr>
          <tr className="bd-tr-fcf">
            <td className="bd-th-label">Free cash flow</td>
            {cols.map((r, i) => <td key={i} className={r.fcf < 0 ? "bd-neg" : ""}>{bdM(r.fcf)}</td>)}
          </tr>
        </tbody>
      </table>
    </div>
  );
}

window.BatteryDispatchView = BatteryDispatchView;
