// ───────────────────────────────────────────────────────────
// Campaign Economics — CPL breakEven by channel / theme / campaign
// Data: campaign-data.json, exported daily from the BigQuery DW by
// C:\CarBrain\scripts\dw_export.py (leads, OA, APC by lead-date cohort,
// spend by campaign, maturation curves, profit per sold unit).
//
// Projection per cohort (lead date × campaign × segment), Bornhuetter-Ferguson style:
//   OA_final  = OA_seen  + leads   × OA rate(channel, seg)  × (1 − share of OA already visible at this age)
//   APC_final = APC_seen + OA_final × OA→APC(channel, seg)  × (1 − share of APC already visible at this age)
// Rates come from mature cohorts (leads 35–125 days old), shrunk toward the
// all-channel segment rate when a channel has little volume.
//   Value per lead    = Σ APC_final × profit per unit ÷ leads
//   Break-even CPL    = value per lead
//   Target CPL        = value per lead ÷ efficiency ratio
// CPL meter: two maximums, target CPL (keeps the efficiency ratio) and break-even CPL. A lower CPL is always better.
//   Below target → room to raise CPL (budget / tCPA) up to the target, progressively.
// Min P+P share: the P+P share the current CPL needs to reach target (or break-even), keeping the rest of the mix.
//
// DMA layer: campaign-geo.json (same keys, last ~200 days) splits cohorts by the lead's DMA (CRM dma_name) and
// spend by the platform's location report (Google DMA report, Bing metro, Meta comScore market). Spend with no
// location is spread over the campaign's DMAs that day and flagged as estimated. Rates and curves stay national.
// Profit per unit "By DMA" = DMA average shrunk toward national: (n·DMA + K·national) ÷ (n + K), K = CE_SHRINK_UNITS.
// ───────────────────────────────────────────────────────────
// Data order (indices): profit per unit, low → high. Two color families: SP + Parts (violet), Priority + Premium (blue).
const CE_SEGS = ['SP', 'Parts', 'Priority', 'Premium', 'No offer'];
const CE_SEG_COLORS = { SP: '#A47FD8', Parts: '#6A3DB0', Priority: '#3FA2E6', Premium: '#1C4D9C', 'No offer': '#DDE2E8' };
const CE_SEG_INK = { SP: '#1F1240', Parts: '#fff', Priority: '#06223F', Premium: '#fff', 'No offer': '#4A5568' };
// Display order (bar, legend, tables): Premium → Priority → Parts → SP → No offer, so P+P starts at the left
// edge and reads against the Min P+P line. Indices into CE_SEGS stay unchanged for the math.
const CE_SEG_ORDER = [3, 2, 1, 0, 4];
const CE_SEG_GROUPS = [
  { key: 'high', label: 'P+P', segs: [3, 2], color: '#1C4D9C' },
  { key: 'low', label: 'SP + Parts', segs: [1, 0], color: '#6A3DB0' },
  { key: 'none', label: 'No offer', segs: [4], color: '#B5BEC9' },
];
const CE_STEP = 0.10;          // progressive CPL raise per step
const CE_PAID = ['PPC - CB', 'PPC - Pmax', 'Paid Social'];
const CE_REF_MIN_AGE = 35;
const CE_REF_MAX_AGE = 125;
const CE_SHRINK_LEADS = 400;   // pseudo-leads pulling a channel's OA rate toward the segment average
const CE_SHRINK_OA = 40;       // pseudo-OAs pulling a channel's OA→APC toward the segment average
const CE_DEFAULT_RATIO = 2.49;
const CE_SHRINK_UNITS = 20;    // pseudo-units pulling a DMA's profit per unit toward the national average
const CE_BREAKDOWN_ROWS = 25;  // rows shown in an open row's DMA breakdown before "Show all"

const ceDay = (s) => Date.UTC(+s.slice(0, 4), +s.slice(5, 7) - 1, +s.slice(8, 10)) / 864e5;
const ceIso = (d) => new Date(d * 864e5).toISOString().slice(0, 10);
const ceMoney = (n, d = 0) => (n == null || !isFinite(n) ? '—' : `${n < 0 ? '−' : ''}$${Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d })}`);
const ceNum = (n, d = 0) => (n == null || !isFinite(n) ? '—' : n.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }));
const cePct = (n, d = 1) => (n == null || !isFinite(n) ? '—' : `${(n * 100).toFixed(d)}%`);

// Comparison period. Auto: a month-to-date range (starts on the 1st, one month) compares with the same days
// of the previous month (Oct 1–15 → Sep 1–15, clamped to month end); anything else with the N days just before it.
function ceCompareRange(from, to, mode) {
  if (mode === 'off') return null;
  const [fy, fm, fd] = from.split('-').map(Number), [ty, tm, td] = to.split('-').map(Number);
  if (mode === 'month' || (mode === 'auto' && fd === 1 && fy === ty && fm === tm)) {
    const back = (y, m, d) => ceIso(Date.UTC(y, m - 2, Math.min(d, new Date(Date.UTC(y, m - 1, 0)).getUTCDate())) / 864e5);
    return { from: back(fy, fm, fd), to: back(ty, tm, td), kind: 'month' };
  }
  const n = ceDay(to) - ceDay(from) + 1;
  return { from: ceIso(ceDay(from) - n), to: ceIso(ceDay(from) - 1), kind: 'prev' };
}

// Change vs the comparison period: % for amounts, percentage points for shares. good = +1/−1 colors the direction, 0 = neutral.
function CeDelta({ cur, prev, good = 1, pt, title }) {
  if (cur == null || !isFinite(cur)) return null;
  if (prev == null || !isFinite(prev)) return <span className="ce-delta" title="No data in the comparison period">new</span>;
  const d = pt ? (cur - prev) * 100 : prev ? ((cur - prev) / Math.abs(prev)) * 100 : null;
  if (d == null) return null;
  const cls = Math.abs(d) < 0.5 || !good ? '' : (d > 0) === (good > 0) ? 'up' : 'down';
  return (
    <span className={`ce-delta ${cls}`} title={title}>
      {d > 0.05 ? '▲' : d < -0.05 ? '▼' : '■'} {Math.abs(d).toFixed(pt ? 1 : 0)}{pt ? ' pt' : '%'}
    </span>
  );
}

function useCampaignData() {
  const [state, setState] = React.useState({ data: null, error: null });
  React.useEffect(() => {
    fetch(`campaign-data.json?v=${Date.now()}`)
      .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(data => setState({ data, error: null }))
      .catch(e => setState({ data: null, error: e.message }));
  }, []);
  return state;
}

// DMA layer, loaded after the main file (same export run, so key indices match). Rows are reshaped to the main
// layout plus the DMA at the end: cohorts [date, key, seg, leads, oa, apc, dma] · spend [date, key, cost, clicks (0), dma, estimated]
function useCampaignGeo(data) {
  const [state, setState] = React.useState({ geo: null, error: null });
  React.useEffect(() => {
    if (!data) return;
    fetch(`campaign-geo.json?v=${Date.now()}`)
      .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(raw => {
        const d0 = ceDay(raw.d0), iso = [];
        const at = (i) => iso[i] || (iso[i] = ceIso(d0 + i));
        setState({
          geo: {
            ...raw,
            keys: data.keys,
            curves: data.curves,
            cohorts: raw.cohorts.map(([d, k, s, m, l, o, a]) => [at(d), k, s, l, o, a, m]),
            spend: raw.spend.map(([d, k, m, c, e]) => [at(d), k, c, 0, m, e || 0]),
          },
          error: null,
        });
      })
      .catch(e => setState({ geo: null, error: e.message }));
  }, [data]);
  return state;
}

// Channel × segment rates from mature cohorts, shrunk toward the all-channel segment rate.
function ceRates(data, asOfDay) {
  const acc = {};
  const add = (ch, s, l, o, a) => {
    const k = `${ch}|${s}`;
    const r = acc[k] || (acc[k] = { leads: 0, oa: 0, apc: 0 });
    r.leads += l; r.oa += o; r.apc += a;
  };
  for (const [d, k, s, l, o, a] of data.cohorts) {
    const age = asOfDay - ceDay(d);
    if (age < CE_REF_MIN_AGE || age > CE_REF_MAX_AGE) continue;
    add(data.keys[k].ch, s, l, o, a);
    add('*', s, l, o, a);
  }
  const rates = {};
  const get = (ch, s) => {
    const k = `${ch}|${s}`;
    if (rates[k]) return rates[k];
    const g = acc[`*|${s}`] || { leads: 0, oa: 0, apc: 0 };
    const gOa = g.leads ? g.oa / g.leads : 0;
    const gConv = g.oa ? g.apc / g.oa : 0;
    const c = acc[k] || { leads: 0, oa: 0, apc: 0 };
    return (rates[k] = {
      oaRate: (c.oa + CE_SHRINK_LEADS * gOa) / (c.leads + CE_SHRINK_LEADS),
      oa2apc: (c.apc + CE_SHRINK_OA * gConv) / (c.oa + CE_SHRINK_OA),
      leads: c.leads, oa: c.oa, apc: c.apc,
    });
  };
  return get;
}

function ceCurve(data, seg, kind, age) {
  const arr = (data.curves[seg] || {})[kind];
  if (!arr) return 1;
  if (age >= arr.length) return 1;
  return arr[Math.max(0, age)] ?? 1;
}

const ceEmptySeg = () => ({ leads: 0, oa: 0, apc: 0, oaProj: 0, apcProj: 0, profit: 0, revenue: 0 });

// data is the main file or the DMA layer (geo rows carry the DMA last). With the DMA layer:
//   dma = only that DMA · groupBy 'dma' = one row per DMA · splitDma = one row per group × DMA (row breakdown)
//   profitFor(seg, dma) = profit per unit for that DMA (By DMA mode); otherwise the national `profit`.
function ceAggregate(data, { from, to, groupBy, channels, profit, todayDay, rateFor, dma = null, splitDma = false, profitFor = null }) {
  const groups = new Map();
  const labelOf = (key, m) => {
    if (groupBy === 'dma') return data.dmas[m];
    if (groupBy === 'channel') return key.ch;
    if (groupBy === 'theme') return `${key.ch} · ${key.theme}`;
    return key.name || (key.cid ? `Campaign ${key.cid}` : `${key.ch} · ${key.theme}`);
  };
  const groupOf = (ki, m) => {
    const key = data.keys[ki];
    const label = labelOf(key, m);
    const base = groupBy === 'campaign' ? (key.cid || label) : groupBy === 'dma' ? `dma:${m}` : label;
    const id = splitDma ? `${base}|${m}` : base;
    let g = groups.get(id);
    if (!g) {
      g = groupBy === 'dma'
        ? { id, label, dma: m, spend: 0, estSpend: 0, clicks: 0, segs: CE_SEGS.map(ceEmptySeg) }
        : { id, base, label, dma: splitDma ? m : undefined, ch: key.ch, theme: key.theme, platform: key.platform, spend: 0, estSpend: 0, clicks: 0, segs: CE_SEGS.map(ceEmptySeg) };
      groups.set(id, g);
    }
    return g;
  };
  for (const [d, ki, s, l, o, a, m] of data.cohorts) {
    if (d < from || d > to) continue;
    if (dma != null && m !== dma) continue;
    const key = data.keys[ki];
    if (!channels.has(key.ch)) continue;
    const seg = CE_SEGS[s];
    const age = Math.max(0, todayDay - ceDay(d) - 1);
    const r = rateFor(key.ch, s);
    const oaProj = o + l * r.oaRate * (1 - ceCurve(data, seg, 'oa', age));
    const apcProj = s === 4 ? a : a + oaProj * r.oa2apc * (1 - ceCurve(data, seg, 'apc', age));
    const unit = profitFor ? profitFor(s, m) : (profit[seg] || {});
    const x = groupOf(ki, m).segs[s];
    x.leads += l; x.oa += o; x.apc += a; x.oaProj += oaProj; x.apcProj += apcProj;
    x.profit += apcProj * (unit.profit || 0);
    x.revenue += apcProj * (unit.revenue || 0);
  }
  for (const [d, ki, cost, clicks, m, est] of data.spend) {
    if (d < from || d > to) continue;
    if (dma != null && m !== dma) continue;
    if (!channels.has(data.keys[ki].ch)) continue;
    const g = groupOf(ki, m); g.spend += cost; g.clicks += clicks; g.estSpend += est || 0;
  }
  return [...groups.values()].map(ceFinish);
}

function ceFinish(g) {
  const t = g.segs.reduce((a, x) => {
    Object.keys(a).forEach(k => { a[k] += x[k]; });
    return a;
  }, ceEmptySeg());
  const pp = g.segs[2].leads + g.segs[3].leads;
  return {
    ...g, ...t,
    cpl: t.leads ? g.spend / t.leads : null,
    ppShare: t.leads ? pp / t.leads : null,
    premShare: t.leads ? g.segs[3].leads / t.leads : null,
    oaRateSeen: t.leads ? t.oa / t.leads : null,
    prProj: t.leads ? t.apcProj / t.leads : null,
    valuePerLead: t.leads ? t.profit / t.leads : null,
    revenuePerLead: t.leads ? t.revenue / t.leads : null,
    profitPerApc: t.apcProj ? t.profit / t.apcProj : null,
    costPerApc: t.apcProj ? g.spend / t.apcProj : null,
    profitPerDollar: g.spend ? t.profit / g.spend : null,
    maturity: t.apcProj ? t.apc / t.apcProj : null,
  };
}

function ceTotal(rows) {
  const g = { id: '__total', label: 'Total', spend: 0, estSpend: 0, clicks: 0, segs: CE_SEGS.map(ceEmptySeg) };
  rows.forEach(r => {
    g.spend += r.spend; g.clicks += r.clicks; g.estSpend += r.estSpend || 0;
    r.segs.forEach((x, i) => Object.keys(x).forEach(k => { g.segs[i][k] += x[k]; }));
  });
  return ceFinish(g);
}

// Value per lead of the P+P and SP+Parts groups; rows with no leads in a group borrow it from `fb`.
function ceGroupValues(row, fb) {
  const sum = (ix, k) => ix.reduce((a, i) => a + row.segs[i][k], 0);
  const lH = sum([2, 3], 'leads'), lL = sum([0, 1], 'leads'), lN = row.segs[4].leads;
  return {
    vH: lH ? sum([2, 3], 'profit') / lH : fb && fb.vH,
    vL: lL ? sum([0, 1], 'profit') / lL : fb && fb.vL,
    vN: lN ? row.segs[4].profit / lN : 0,
  };
}

// P+P share needed for profit per lead to reach `required`, swapping SP+Parts leads for P+P and keeping No offer.
//   profit/lead(x) = x·vH + (1 − n − x)·vL + n·vN  →  x = (required − (1 − n)·vL − n·vN) ÷ (vH − vL)
function cePpNeed(row, required, fb) {
  if (!row.leads || required == null) return null;
  const { vH, vL, vN } = ceGroupValues(row, fb);
  if (vH == null || vL == null || vH <= vL) return null;
  const n = row.segs[4].leads / row.leads;
  const x = (required - (1 - n) * vL - n * vN) / (vH - vL);
  const share = (row.segs[2].leads + row.segs[3].leads) / row.leads;
  return { min: Math.max(0, x), share, reachable: x <= 1 - n, ok: share >= x, noShare: n };
}

function cePpMin(row, ratio, fb) {
  if (row.cpl == null) return null;
  const target = cePpNeed(row, row.cpl * ratio, fb);
  return target && { target, breakEven: cePpNeed(row, row.cpl, fb) };
}

function CePpChip({ pp }) {
  if (!pp) return <span className="ce-muted">—</span>;
  const t = pp.target, b = pp.breakEven;
  const sub = b && (!b.reachable ? 'cannot break even at this CPL' : b.min > 0 ? `${cePct(b.min, 0)} to break even` : 'breaks even with any mix');
  if (!t.reachable) return <span className="ce-room"><span className="ce-chip bad">✗ Not reachable</span><span className="ce-sub">even 100% P+P misses target{sub ? ` · ${sub}` : ''}</span></span>;
  return (
    <span className="ce-room">
      <span className={`ce-chip ${t.ok ? 'ok' : 'bad'}`}>{t.ok ? '✓' : '✗'} min {cePct(t.min, 0)} · has {cePct(t.share, 0)}</span>
      <span className="ce-sub">{sub}</span>
    </span>
  );
}

// Segment mix: segments in value order, grouped by color family, with the group share bracketed underneath.
function CeMix({ segs, leads, size = 'sm' }) {
  if (!leads) return <span className="ce-muted">—</span>;
  const minLabel = size === 'lg' ? 0.05 : 0.11;
  const groups = CE_SEG_GROUPS.map(g => ({ ...g, n: g.segs.reduce((a, i) => a + segs[i].leads, 0) })).filter(g => g.n > 0);
  return (
    <div className={`ce-mix ${size}`}>
      <div className="ce-mix-track">
        {groups.map(g => (
          <div key={g.key} className="ce-mix-group" style={{ flex: `${g.n} 1 0` }}>
            {g.segs.filter(i => segs[i].leads > 0).map(i => {
              const s = CE_SEGS[i], share = segs[i].leads / leads;
              return (
                <div key={s} className={`ce-mix-seg${i === 4 ? ' none' : ''}`} style={{ flex: `${segs[i].leads} 1 0`, background: CE_SEG_COLORS[s], color: CE_SEG_INK[s] }}
                  title={`${s}: ${cePct(share)} · ${ceNum(segs[i].leads)} leads`}>
                  {share >= minLabel && <span>{size === 'lg' && share >= 0.15 ? `${s} ` : ''}{cePct(share, 0)}</span>}
                </div>
              );
            })}
          </div>
        ))}
      </div>
      <div className="ce-mix-brackets">
        {groups.map(g => (
          <div key={g.key} style={{ flex: `${g.n} 1 0`, borderTopColor: g.color }}>
            {g.n / leads >= (size === 'lg' ? 0.08 : 0.2) && g.key !== 'none' && <span>{g.label} {cePct(g.n / leads, 0)}</span>}
          </div>
        ))}
      </div>
    </div>
  );
}

// Where the current CPL sits against target and break-even CPL.
function ceRoom(cpl, target, breakEven) {
  if (cpl == null || target == null || breakEven == null) return null;
  if (breakEven <= 0) return { state: 'bad', toTarget: -cpl, toBreakEven: -cpl, pct: -1 };
  const state = cpl < target ? 'ok' : cpl <= breakEven ? 'warn' : 'bad';
  return { state, toTarget: target - cpl, toBreakEven: breakEven - cpl, pct: cpl ? (target - cpl) / cpl : null };
}

function CeMeter({ cpl, target, breakEven, size = 'sm' }) {
  if (cpl == null || target == null || breakEven == null) return <span className="ce-muted">—</span>;
  if (breakEven <= 0) return <span className="ce-muted">No projected profit</span>;
  const max = Math.min(Math.max(breakEven * 1.3, cpl * 1.12), breakEven * 2.2);
  const at = (v) => `${Math.min(100, Math.max(0, (v / max) * 100))}%`;
  const off = cpl > max;
  return (
    <div className={`ce-meter ${size}`} title={`CPL ${ceMoney(cpl, 2)} · target ${ceMoney(target, 2)} · break-even ${ceMoney(breakEven, 2)}`}>
      <div className="ce-meter-track">
        <div className="z good" style={{ width: at(target) }}>{size === 'lg' && <span>Room to raise</span>}</div>
        <div className="z warn" style={{ width: `calc(${at(breakEven)} - ${at(target)})` }}>{size === 'lg' && <span>Profitable · below ratio</span>}</div>
        <div className="z bad" style={{ flex: 1 }}>{size === 'lg' && <span>Losing</span>}</div>
        <div className="ce-meter-tick" style={{ left: at(target) }} />
        <div className="ce-meter-tick" style={{ left: at(breakEven) }} />
        <div className={`ce-meter-mark${off ? ' off' : ''}`} style={{ left: at(cpl) }}>
          {size === 'lg' && <span>Now {ceMoney(cpl, 2)}{off ? ' ▸' : ''}</span>}
        </div>
      </div>
      <div className="ce-meter-scale">
        <span style={{ left: at(target) }}>{size === 'lg' ? 'Target ' : ''}{ceMoney(target, size === 'lg' ? 2 : 0)}</span>
        <span style={{ left: at(breakEven) }}>{size === 'lg' ? 'Break-even ' : ''}{ceMoney(breakEven, size === 'lg' ? 2 : 0)}</span>
      </div>
    </div>
  );
}

function CeRoomChip({ room }) {
  if (!room) return <span className="ce-muted">—</span>;
  if (room.state === 'ok') return (
    <span className="ce-room"><span className="ce-chip ok">▲ +{ceMoney(room.toTarget, 2)} (+{cePct(room.pct, 0)})</span>
      <span className="ce-sub">up to +{ceMoney(room.toBreakEven, 2)} at break-even</span></span>
  );
  if (room.state === 'warn') return (
    <span className="ce-room"><span className="ce-chip warn">● Hold · {ceMoney(-room.toTarget, 2)} over target</span>
      <span className="ce-sub">{ceMoney(room.toBreakEven, 2)} below break-even</span></span>
  );
  return <span className="ce-room"><span className="ce-chip bad">▼ Cut · {ceMoney(-room.toBreakEven, 2)} over break-even</span></span>;
}

// Raise CPL in CE_STEP steps until the target; each step re-read after the data refreshes.
function CeRaisePlan({ row, target, breakEven }) {
  const room = ceRoom(row.cpl, target, breakEven);
  if (!room) return null;
  if (room.state !== 'ok') return (
    <div className="ce-plan-note">
      {room.state === 'warn'
        ? `CPL is between target and break-even: still profitable, but under the efficiency ratio. Hold bids/budget and work on mix or conversion before scaling.`
        : `CPL is above break-even: every lead loses ${ceMoney(-room.toBreakEven, 2)} on projected profit. Lower tCPA/budget or fix the mix.`}
    </div>
  );
  const steps = [];
  for (let c = row.cpl * (1 + CE_STEP); c < target && steps.length < 6; c *= 1 + CE_STEP) steps.push(c);
  steps.push(target);
  return (
    <table className="wow ce-plan">
      <thead><tr><th>Step</th><th>CPL</th><th>vs today</th><th>Net profit / lead</th><th>Profit / $1</th><th>Spend at today's leads</th></tr></thead>
      <tbody>
        <tr className="ce-plan-now"><td className="metric">Today</td><td>{ceMoney(row.cpl, 2)}</td><td>—</td><td>{ceMoney(breakEven - row.cpl, 2)}</td><td>{ceMoney(breakEven / row.cpl, 2)}</td><td>{ceMoney(row.spend)}</td></tr>
        {steps.map((c, i) => (
          <tr key={i}>
            <td className="metric">{i === steps.length - 1 ? 'Target' : `Step ${i + 1}`}</td>
            <td>{ceMoney(c, 2)}</td><td>+{cePct(c / row.cpl - 1, 0)}</td>
            <td>{ceMoney(breakEven - c, 2)}</td><td>{ceMoney(breakEven / c, 2)}</td><td>{ceMoney(c * row.leads)}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function CeRange({ row, ratio, title, fb }) {
  const target = row.valuePerLead == null ? null : row.valuePerLead / ratio;
  const room = ceRoom(row.cpl, target, row.valuePerLead);
  const pp = cePpMin(row, ratio, fb);
  return (
    <div className="ce-range">
      <div className="ce-range-col">
        <div className="ce-range-label">{title || 'Segment mix'} <span className="ce-muted">· ordered by profit per unit, high → low</span></div>
        <CeMix segs={row.segs} leads={row.leads} size="lg" />
        <div className="ce-range-foot"><CePpChip pp={pp} /></div>
      </div>
      <div className="ce-range-col">
        <div className="ce-range-label">CPL vs target · break-even <span className="ce-muted">· target = break-even ÷ {ratio.toFixed(2)}</span></div>
        <CeMeter cpl={row.cpl} target={target} breakEven={row.valuePerLead} size="lg" />
        <div className="ce-range-foot"><CeRoomChip room={room} /></div>
      </div>
    </div>
  );
}

function CeDetail({ row, ratio, profit, rateFor, fb, breakdown }) {
  const target = row.valuePerLead == null ? null : row.valuePerLead / ratio;
  return (
    <div className="ce-detail">
      <CeRange row={row} ratio={ratio} fb={fb} />
      <table className="wow ce-seg-table">
        <thead>
          <tr><th>Segment</th><th>Leads</th><th>Mix</th><th>OA seen</th><th>OA → APC used</th><th>APC seen</th><th>APC proj.</th><th>PR proj.</th><th>Revenue / unit</th><th>Profit / unit</th><th>Proj. revenue</th><th>Proj. profit</th><th>Profit / lead</th></tr>
        </thead>
        <tbody>
          {CE_SEG_ORDER.map(i => {
            const s = CE_SEGS[i], x = row.segs[i];
            if (!x.leads) return null;
            const r = row.ch && s !== 'No offer' ? rateFor(row.ch, i) : null;
            return (
              <tr key={s}>
                <td className="metric"><i className="ce-dot" style={{ background: CE_SEG_COLORS[s] }} />{s}</td>
                <td>{ceNum(x.leads)}</td>
                <td>{cePct(x.leads / row.leads)}</td>
                <td>{ceNum(x.oa)}</td>
                <td>{r ? cePct(r.oa2apc) : '—'}</td>
                <td>{ceNum(x.apc)}</td>
                <td>{ceNum(x.apcProj, 1)}</td>
                <td>{cePct(x.apcProj / x.leads, 2)}</td>
                <td>{ceMoney(x.apcProj ? x.revenue / x.apcProj : (profit[s] || {}).revenue || 0)}</td>
                <td>{ceMoney(x.apcProj ? x.profit / x.apcProj : (profit[s] || {}).profit || 0)}</td>
                <td>{ceMoney(x.revenue)}</td>
                <td>{ceMoney(x.profit)}</td>
                <td>{ceMoney(x.profit / x.leads, 2)}</td>
              </tr>
            );
          })}
          <tr className="ce-total">
            <td className="metric">Total</td><td>{ceNum(row.leads)}</td><td>100%</td><td>{ceNum(row.oa)}</td><td>—</td><td>{ceNum(row.apc)}</td>
            <td>{ceNum(row.apcProj, 1)}</td><td>{cePct(row.prProj, 2)}</td>
            <td>{ceMoney(row.apcProj ? row.revenue / row.apcProj : null)}</td><td>{ceMoney(row.profitPerApc)}</td>
            <td>{ceMoney(row.revenue)}</td><td>{ceMoney(row.profit)}</td><td>{ceMoney(row.valuePerLead, 2)}</td>
          </tr>
        </tbody>
      </table>
      {row.spend > 0 && <>
        <div className="ce-range-label">Raise plan <span className="ce-muted">· +{Math.round(CE_STEP * 100)}% CPL per step (budget, tCPA or bids) until the target CPL</span></div>
        <CeRaisePlan row={row} target={target} breakEven={row.valuePerLead} />
      </>}
      <div className="ce-explain">
        Projection = Σ segment leads × PR proj. (channel × segment) × revenue or profit per unit.
        Profit per lead {ceMoney(row.valuePerLead, 2)} = projected profit {ceMoney(row.profit)} ÷ {ceNum(row.leads)} leads; revenue per lead {ceMoney(row.revenuePerLead, 2)}.
        {row.spend > 0
          ? <> Break-even CPL {ceMoney(row.valuePerLead, 2)} · Target CPL (at {ratio.toFixed(2)}×) {ceMoney(target, 2)}.
            The raise plan assumes extra leads keep today's mix and PR; take one step, let a few days of cohorts come in, and check the meter again before the next one.</>
          : ' No paid spend in this period, so there is no CPL or raise plan; read it by volume, mix and value per lead.'}
        {row.maturity != null && ` ${cePct(row.maturity, 0)} of projected APCs have already happened; the rest is projected from this period's own OAs.`}
        {!row.ch && ' OA → APC rates are shown per row when grouped by channel, theme or campaign.'}
      </div>
      {breakdown}
    </div>
  );
}

const CE_COLUMNS = [
  { key: 'label', label: 'Name', sort: r => r.label },
  { key: 'spend', label: 'Spend', sort: r => r.spend, fmt: r => ceMoney(r.spend), cmp: { v: r => r.spend, good: 0 } },
  { key: 'leads', label: 'Leads', sort: r => r.leads, fmt: r => ceNum(r.leads), cmp: { v: r => r.leads } },
  { key: 'cpl', label: 'CPL', sort: r => r.cpl ?? -1, fmt: r => ceMoney(r.cpl, 2), cmp: { v: r => r.cpl, good: -1 } },
  { key: 'mix', label: 'Segment mix', sort: r => r.ppShare ?? -1 },
  { key: 'ppShare', label: 'P+P', sort: r => r.ppShare ?? -1, fmt: r => cePct(r.ppShare), cmp: { v: r => r.ppShare, pt: true } },
  { key: 'ppMin', label: 'Min P+P for target', sort: (r, ratio, fb) => { const p = cePpMin(r, ratio, fb); return p ? (p.target.reachable ? p.target.share - p.target.min : -1) : -2; } },
  { key: 'oaRateSeen', label: 'OA rate', sort: r => r.oaRateSeen ?? -1, fmt: r => cePct(r.oaRateSeen), cmp: { v: r => r.oaRateSeen, pt: true } },
  { key: 'apc', label: 'APC seen', sort: r => r.apc, fmt: r => ceNum(r.apc) },
  { key: 'apcProj', label: 'APC proj.', sort: r => r.apcProj, fmt: r => ceNum(r.apcProj), cmp: { v: r => r.apcProj } },
  { key: 'prProj', label: 'PR proj.', sort: r => r.prProj ?? -1, fmt: r => cePct(r.prProj, 2), cmp: { v: r => r.prProj, pt: true } },
  { key: 'costPerApc', label: 'Cost / APC', sort: r => r.costPerApc ?? -1, fmt: r => ceMoney(r.costPerApc), cmp: { v: r => r.costPerApc, good: -1 } },
  { key: 'revenue', label: 'Proj. revenue', sort: r => r.revenue, fmt: r => ceMoney(r.revenue), cmp: { v: r => r.revenue } },
  { key: 'profit', label: 'Proj. profit', sort: r => r.profit, fmt: r => ceMoney(r.profit), cmp: { v: r => r.profit } },
  { key: 'valuePerLead', label: 'Profit / lead', sort: r => r.valuePerLead ?? -1, fmt: r => ceMoney(r.valuePerLead, 2), cmp: { v: r => r.valuePerLead } },
  { key: 'meter', label: 'CPL vs target · break-even', sort: (r, ratio) => (r.cpl == null || !r.valuePerLead ? 1e9 : r.cpl / (r.valuePerLead / ratio)) },
  { key: 'room', label: 'Room to raise CPL', sort: (r, ratio) => (r.cpl == null || r.valuePerLead == null ? -1e9 : (r.valuePerLead / ratio - r.cpl) / r.cpl) },
  { key: 'profitPerDollar', label: 'Profit / $1', sort: r => r.profitPerDollar ?? -1, fmt: r => (r.profitPerDollar == null ? '—' : `${ceMoney(r.profitPerDollar, 2)}`), cmp: { v: r => r.profitPerDollar } },
  { key: 'chgProfitPerDollar', label: 'Δ Profit / $1', cmpOnly: true,
    sort: (r, ratio, fb, prev) => { const p = prev && prev.get(r.id); return r.profitPerDollar == null || !p || p.profitPerDollar == null ? -1e9 : r.profitPerDollar - p.profitPerDollar; },
    fmt: (r, prev) => { const p = prev && prev.get(r.id); return r.profitPerDollar == null || !p || p.profitPerDollar == null ? '—' : `${r.profitPerDollar >= p.profitPerDollar ? '+' : '−'}${ceMoney(Math.abs(r.profitPerDollar - p.profitPerDollar), 2)}`; } },
  { key: 'maturity', label: 'Seen', sort: r => r.maturity ?? -1, fmt: r => cePct(r.maturity, 0) },
];

// Breakdown inside an open row: its DMAs (campaign / theme / channel rows) or its campaigns (DMA rows).
// Same economics as the main table; rows with few leads are faded (too few to read).
// Rows without spend (SEO, direct, referral…) switch to a volume view from CRM data: leads, share, OA, APC, profit.
function CeBreakdown({ items, prev, by, nameOf, ratio, minCell, setMinCell, onPick, picked, covNote, cmp }) {
  const organic = !items.some(r => r.spend > 0);
  const [sort, setSort] = React.useState({ key: organic ? 'leads' : 'spend', dir: -1 });
  const [all, setAll] = React.useState(false);
  const target = (r) => (r.valuePerLead == null ? null : r.valuePerLead / ratio);
  const totalLeads = items.reduce((a, r) => a + r.leads, 0);
  const delta = (r, v, opts = {}) => prev && <CeDelta cur={v(r)} prev={prev.get(r.id) ? v(prev.get(r.id)) : null} good={opts.good ?? 1} pt={opts.pt} />;
  const common = {
    leads: { key: 'leads', label: 'Leads', sort: r => r.leads, cell: r => <>{ceNum(r.leads)}{delta(r, x => x.leads)}</> },
    mix: { key: 'mix', label: 'Segment mix', sort: r => r.ppShare ?? -1, cell: r => <CeMix segs={r.segs} leads={r.leads} /> },
    ppShare: { key: 'ppShare', label: 'P+P', sort: r => r.ppShare ?? -1, cell: r => <>{cePct(r.ppShare)}{delta(r, x => x.ppShare, { pt: true })}</> },
    prProj: { key: 'prProj', label: 'PR proj.', sort: r => r.prProj ?? -1, cell: r => <>{cePct(r.prProj, 2)}{delta(r, x => x.prProj, { pt: true })}</> },
    valuePerLead: { key: 'valuePerLead', label: 'Profit / lead', sort: r => r.valuePerLead ?? -1, cell: r => <>{ceMoney(r.valuePerLead, 2)}{delta(r, x => x.valuePerLead)}</> },
  };
  const cols = [
    { key: 'label', label: by === 'dma' ? 'DMA' : 'Campaign', sort: r => nameOf(r) },
    ...(organic ? [
      common.leads,
      { key: 'share', label: 'Share of leads', sort: r => r.leads, cell: r => cePct(totalLeads ? r.leads / totalLeads : null) },
      common.mix, common.ppShare,
      { key: 'oa', label: 'OA seen', sort: r => r.oa, cell: r => ceNum(r.oa) },
      { key: 'oaRateSeen', label: 'OA rate', sort: r => r.oaRateSeen ?? -1, cell: r => <>{cePct(r.oaRateSeen)}{delta(r, x => x.oaRateSeen, { pt: true })}</> },
      { key: 'apc', label: 'APC seen', sort: r => r.apc, cell: r => ceNum(r.apc) },
      { key: 'apcProj', label: 'APC proj.', sort: r => r.apcProj, cell: r => <>{ceNum(r.apcProj, 1)}{delta(r, x => x.apcProj)}</> },
      common.prProj, common.valuePerLead,
      { key: 'profit', label: 'Proj. profit', sort: r => r.profit, cell: r => <>{ceMoney(r.profit)}{delta(r, x => x.profit)}</> },
    ] : [
      { key: 'spend', label: 'Spend', sort: r => r.spend,
        cell: r => <>{ceMoney(r.spend)}{r.estSpend > 0.05 * r.spend && <span className="ce-sub">~{cePct(r.estSpend / r.spend, 0)} estimated</span>}</> },
      common.leads,
      { key: 'cpl', label: 'CPL', sort: r => r.cpl ?? -1, cell: r => <>{ceMoney(r.cpl, 2)}{delta(r, x => x.cpl, { good: -1 })}</> },
      common.mix, common.ppShare, common.prProj, common.valuePerLead,
      { key: 'meter', label: 'CPL vs target · break-even', sort: r => (r.cpl == null || !r.valuePerLead ? 1e9 : r.cpl / target(r)),
        cell: r => <CeMeter cpl={r.cpl} target={target(r)} breakEven={r.valuePerLead} /> },
      { key: 'room', label: 'Room to raise CPL', sort: r => (r.cpl == null || r.valuePerLead == null ? -1e9 : (target(r) - r.cpl) / r.cpl),
        cell: r => <CeRoomChip room={ceRoom(r.cpl, target(r), r.valuePerLead)} /> },
      { key: 'profitPerDollar', label: 'Profit / $1', sort: r => r.profitPerDollar ?? -1, cell: r => <>{ceMoney(r.profitPerDollar, 2)}{delta(r, x => x.profitPerDollar)}</> },
    ]),
  ];
  const col = cols.find(c => c.key === sort.key);
  const list = items.filter(r => r.leads || r.spend).sort((a, b) => {
    const x = col.sort(a), y = col.sort(b);
    return (x > y ? 1 : x < y ? -1 : 0) * sort.dir;
  });
  const shown = all ? list : list.slice(0, CE_BREAKDOWN_ROWS);
  return (
    <div className="ce-breakdown">
      <div className="ce-range-label">
        {by === 'dma' ? 'By DMA' : 'Campaigns in this DMA'} <span className="ce-muted">· {list.length} {by === 'dma' ? 'DMAs' : 'campaigns'}{organic ? ' · no spend: lead DMA from the CRM' : ''} · click a column to sort{by === 'dma' ? ' · click a DMA to filter the page' : ''}</span>
      </div>
      <div className="ce-scroll">
        <table className="wow ce-table ce-bd-table">
          <thead>
            <tr>
              {cols.map(c => (
                <th key={c.key} className={sort.key === c.key ? 'sorted' : ''}
                  onClick={() => setSort(s => ({ key: c.key, dir: s.key === c.key ? -s.dir : (c.key === 'label' ? 1 : -1) }))}>
                  {c.label}{sort.key === c.key ? (sort.dir < 0 ? ' ↓' : ' ↑') : ''}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {shown.map(r => (
              <tr key={r.id} className={r.leads < minCell ? 'ce-thin' : ''}>
                <td className="metric ce-name">
                  {by === 'dma'
                    ? <a className={`ce-link${picked === r.dma ? ' sel' : ''}`} onClick={() => onPick(r.dma)} title="Filter the page to this DMA">{nameOf(r)}</a>
                    : nameOf(r)}
                  {by !== 'dma' && r.ch && <span className="ce-sub">{r.ch} · {r.theme}</span>}
                </td>
                {cols.slice(1).map(c => <td key={c.key}>{c.cell(r)}</td>)}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <div className="ce-row ce-bd-foot">
        {list.length > CE_BREAKDOWN_ROWS && (
          <button className="dl-btn" onClick={() => setAll(a => !a)}>{all ? `Top ${CE_BREAKDOWN_ROWS} only` : `Show all ${list.length}`}</button>
        )}
        <span className="ce-inline">Faded under <input type="number" className="number-input ce-small" value={minCell} min={0}
          onChange={e => setMinCell(Math.max(0, +e.target.value || 0))} /> leads (too few to read)</span>
        {prev && <span className="ce-muted">▲▼ vs {cmp.from} to {cmp.to}</span>}
        {covNote && <span className="ce-muted">Spend without location spread over the campaign's DMAs (estimated): {covNote}</span>}
      </div>
    </div>
  );
}

function CampaignEconomicsTab() {
  const { data, error } = useCampaignData();
  const { geo, error: geoError } = useCampaignGeo(data);
  const todayDay = Math.floor(Date.now() / 864e5);

  const defaults = React.useMemo(() => {
    if (!data) return null;
    const ls = data.lastSpendDate || {};
    const lastFull = [ls['Google Ads'], ls['Facebook']].filter(Boolean).sort()[0] || data.asOf;
    const end = ceDay(lastFull < data.asOf ? lastFull : data.asOf);
    return { from: ceIso(end - 27), to: ceIso(end) };
  }, [data]);

  const [from, setFrom] = React.useState(null);
  const [to, setTo] = React.useState(null);
  const [groupBy, setGroupBy] = React.useState('campaign');
  const [channels, setChannels] = React.useState(new Set(CE_PAID));
  const [ratio, setRatio] = React.useState(CE_DEFAULT_RATIO);
  const [profitEdits, setProfitEdits] = React.useState({});
  const [sort, setSort] = React.useState({ key: 'spend', dir: -1 });
  const [open, setOpen] = React.useState(null);
  const [minLeads, setMinLeads] = React.useState(30);
  const [cmpMode, setCmpMode] = React.useState('auto');
  const [dma, setDma] = React.useState(null);            // DMA index in geo.dmas, null = all
  const [profitMode, setProfitMode] = React.useState('national');
  const [thinLeads, setThinLeads] = React.useState(20);

  const f = from || (defaults && defaults.from);
  const t = to || (defaults && defaults.to);
  const cmp = f && t ? ceCompareRange(f, t, cmpMode) : null;

  const allChannels = React.useMemo(() => {
    if (!data) return [];
    const n = {};
    data.cohorts.forEach(([, k, , l]) => { const ch = data.keys[k].ch; n[ch] = (n[ch] || 0) + l; });
    return Object.keys(n).sort((a, b) => (CE_PAID.includes(b) - CE_PAID.includes(a)) || n[b] - n[a]);
  }, [data]);

  const profit = React.useMemo(() => {
    if (!data) return {};
    const p = {};
    CE_SEGS.forEach(s => {
      const base = data.profit[s] || { profit: 0, revenue: 0 };
      p[s] = { ...base, profit: profitEdits[s] != null ? profitEdits[s] : base.profit };
    });
    return p;
  }, [data, profitEdits]);

  const rateFor = React.useMemo(() => (data ? ceRates(data, todayDay - 1) : null), [data, todayDay]);

  // By DMA: each DMA's own profit per unit (last 180 days), shrunk toward national. Manual edits to the national
  // values scale the DMA values by the same factor.
  const profitFor = React.useMemo(() => {
    if (!geo || profitMode !== 'dma') return null;
    const cache = new Map();
    return (s, m) => {
      const k = s * 10000 + m;
      if (cache.has(k)) return cache.get(k);
      const seg = CE_SEGS[s], nat = profit[seg] || { profit: 0, revenue: 0 }, base = (data.profit[seg] || {}).profit;
      const p = (geo.profit[m] || {})[s], n = p ? p[0] : 0;
      const scale = base ? nat.profit / base : 1;
      const v = {
        n,
        profit: (n * (p ? p[1] * scale : 0) + CE_SHRINK_UNITS * (nat.profit || 0)) / (n + CE_SHRINK_UNITS),
        revenue: (n * (p ? p[2] : 0) + CE_SHRINK_UNITS * (nat.revenue || 0)) / (n + CE_SHRINK_UNITS),
      };
      cache.set(k, v);
      return v;
    };
  }, [geo, profitMode, profit, data]);

  // The DMA layer is the source whenever a DMA is picked, rows are DMAs or profit is per DMA.
  const useGeo = dma != null || groupBy === 'dma' || profitMode === 'dma';
  const src = useGeo ? geo : data;
  const agg = { groupBy, channels, profit, todayDay, rateFor, dma, profitFor };

  const rows = React.useMemo(() => {
    if (!src || !f || !t) return [];
    return ceAggregate(src, { ...agg, from: f, to: t });
  }, [src, f, t, groupBy, channels, profit, todayDay, rateFor, dma, profitFor]);

  const cmpFrom = cmp && cmp.from, cmpTo = cmp && cmp.to;
  const prevRows = React.useMemo(() => {
    if (!src || !cmpFrom) return null;
    return ceAggregate(src, { ...agg, from: cmpFrom, to: cmpTo });
  }, [src, cmpFrom, cmpTo, groupBy, channels, profit, todayDay, rateFor, dma, profitFor]);
  const prev = React.useMemo(() => (prevRows ? new Map(prevRows.map(r => [r.id, r])) : null), [prevRows]);

  // Row breakdown: each campaign / theme / channel split by DMA (campaigns split by DMA when rows are DMAs).
  // Ignores the DMA filter, so an open row always lists all its DMAs.
  const splitGroup = groupBy === 'dma' ? 'campaign' : groupBy;
  const split = React.useMemo(() => {
    if (!geo || !f || !t) return null;
    const o = { ...agg, groupBy: splitGroup, dma: null, splitDma: true };
    return {
      cells: ceAggregate(geo, { ...o, from: f, to: t }),
      prev: cmpFrom ? new Map(ceAggregate(geo, { ...o, from: cmpFrom, to: cmpTo }).map(r => [r.id, r])) : null,
      dmas: ceAggregate(geo, { ...agg, groupBy: 'dma', dma: null, from: f, to: t }),
    };
  }, [geo, f, t, cmpFrom, cmpTo, splitGroup, channels, profit, todayDay, rateFor, profitFor]);

  // DMA picker: sorted by leads in the period
  const dmaOptions = React.useMemo(() => {
    if (!split) return [];
    return split.dmas.filter(c => c.leads || c.spend).sort((a, b) => b.leads - a.leads);
  }, [split]);

  if (error) return <div className="error-state">Couldn't load campaign-data.json: {error}</div>;
  if (!data || !defaults) return <div className="loading-state">Loading DW data…</div>;
  if (useGeo && !geo) return geoError
    ? <div className="error-state">Couldn't load the DMA layer (campaign-geo.json): {geoError}. <button className="dl-btn" onClick={() => { setDma(null); setProfitMode('national'); setGroupBy('campaign'); }}>Back to national</button></div>
    : <div className="loading-state">Loading DMA layer…</div>;

  const total = ceTotal(rows);
  const prevTotal = prevRows ? ceTotal(prevRows) : null;
  const fb = ceGroupValues(total);
  const columns = CE_COLUMNS.filter(c => !c.cmpOnly || prev);
  const col = columns.find(c => c.key === sort.key) || columns[1];
  const shown = rows
    .filter(r => r.leads >= minLeads || r.spend >= 500)
    .sort((a, b) => {
      const x = col.sort(a, ratio, fb, prev), y = col.sort(b, ratio, fb, prev);
      return (x > y ? 1 : x < y ? -1 : 0) * sort.dir;
    });
  const hidden = rows.length - shown.length;
  const days = ceDay(t) - ceDay(f) + 1;
  const ls = data.lastSpendDate || {};
  const staleSpend = Object.entries(ls).filter(([, d]) => d < t);
  const dmaName = dma != null && geo ? geo.dmas[dma] : null;
  const geoCut = useGeo && geo && [f, cmp && cmp.from].some(d => d && d < geo.d0);
  const cov = geo ? geo.coverage || {} : {};
  const covNote = Object.entries(cov).filter(([, c]) => c.total)
    .map(([p, c]) => `${p === 'Facebook' ? 'Meta' : p} ${cePct(c.estimated / c.total, 0)}${c.unmapped > 0.005 * c.total ? ` (+${cePct(c.unmapped / c.total, 0)} outside US)` : ''}`).join(' · ');

  const toggleChannel = (ch) => setChannels(prev => {
    const n = new Set(prev); n.has(ch) ? n.delete(ch) : n.add(ch); return n;
  });
  const setPreset = (n) => { const end = ceDay(defaults.to); setFrom(ceIso(end - n + 1)); setTo(ceIso(end)); };
  const setMonth = (offset) => {
    const end = new Date(ceDay(defaults.to) * 864e5);
    const y = end.getUTCFullYear(), m = end.getUTCMonth() + offset;
    const first = Date.UTC(y, m, 1) / 864e5;
    const last = Math.min(Date.UTC(y, m + 1, 0) / 864e5, ceDay(defaults.to));
    setFrom(ceIso(first)); setTo(ceIso(last));
  };

  const csvNum = (v, d) => (v == null || !isFinite(v) ? '' : v.toFixed(d));
  const csvRows = [
    ['Name', 'Channel', 'Theme', 'Spend', 'Leads', 'CPL', ...CE_SEGS.map(s => `${s} leads`), 'P+P %', 'OA seen', 'APC seen', 'APC projected', 'PR projected', 'Cost per APC', 'Break-even CPL', `Target CPL (${ratio}x)`, 'Room to target', 'Status', 'Min P+P for target %', 'Min P+P for break-even %', 'Projected revenue', 'Projected profit', 'Profit per $1',
      ...(prev ? [`Compare ${cmp.from} to ${cmp.to}: Spend`, 'Leads', 'CPL', 'P+P %', 'PR projected', 'Projected profit', 'Profit per $1'] : [])],
    ...shown.map(r => {
      const room = ceRoom(r.cpl, r.valuePerLead == null ? null : r.valuePerLead / ratio, r.valuePerLead);
      const pp = cePpMin(r, ratio, fb);
      const ppCsv = (x) => (!x ? '' : !x.reachable ? 'not reachable' : (x.min * 100).toFixed(1));
      const p = prev && prev.get(r.id);
      return [r.label, r.ch || '', groupBy === 'channel' ? '' : r.theme || '', r.spend.toFixed(2), r.leads, r.cpl == null ? '' : r.cpl.toFixed(2),
        ...r.segs.map(x => x.leads), r.ppShare == null ? '' : (r.ppShare * 100).toFixed(1), r.oa, r.apc, r.apcProj.toFixed(1),
        r.prProj == null ? '' : (r.prProj * 100).toFixed(2), r.costPerApc == null ? '' : r.costPerApc.toFixed(0),
        r.valuePerLead == null ? '' : r.valuePerLead.toFixed(2), r.valuePerLead == null ? '' : (r.valuePerLead / ratio).toFixed(2),
        room ? room.toTarget.toFixed(2) : '', room ? { ok: 'Room to raise', warn: 'Hold', bad: 'Cut' }[room.state] : '', ppCsv(pp && pp.target), ppCsv(pp && pp.breakEven),
        r.revenue.toFixed(0), r.profit.toFixed(0), r.profitPerDollar == null ? '' : r.profitPerDollar.toFixed(2),
        ...(prev ? (p ? [csvNum(p.spend, 2), p.leads, csvNum(p.cpl, 2), csvNum(p.ppShare * 100, 1), csvNum(p.prProj * 100, 2), csvNum(p.profit, 0), csvNum(p.profitPerDollar, 2)]
          : ['', '', '', '', '', '', '']) : [])];
    }),
  ];

  // p = the same row in the comparison period (undefined when comparison is off)
  const cell = (c, r, p) => {
    if (c.key === 'label') return (
      <td className="metric ce-name">
        <span className="ce-caret">{open === r.id ? '▾' : '▸'}</span>{r.label}
        {groupBy !== 'channel' && r.ch && <span className="ce-sub">{groupBy === 'campaign' ? `${r.ch} · ${r.theme}` : ''}</span>}
      </td>
    );
    if (c.key === 'spend' && r.estSpend > 0.05 * r.spend) return (
      <td>{c.fmt(r)}
        {prev && <CeDelta cur={r.spend} prev={p ? p.spend : null} good={0} title={p ? `${cmp.from} to ${cmp.to}: ${c.fmt(p)}` : undefined} />}
        <span className="ce-sub" title="Spend the platform reports without a location, spread over this campaign's DMAs that day">~{cePct(r.estSpend / r.spend, 0)} estimated</span>
      </td>
    );
    const target = r.valuePerLead == null ? null : r.valuePerLead / ratio;
    if (c.key === 'mix') return <td><CeMix segs={r.segs} leads={r.leads} /></td>;
    if (c.key === 'ppMin') return <td><CePpChip pp={cePpMin(r, ratio, fb)} /></td>;
    if (c.key === 'meter') return <td><CeMeter cpl={r.cpl} target={target} breakEven={r.valuePerLead} /></td>;
    if (c.key === 'room') return <td><CeRoomChip room={ceRoom(r.cpl, target, r.valuePerLead)} /></td>;
    if (c.key === 'chgProfitPerDollar') return <td>{c.fmt(r, p ? new Map([[r.id, p]]) : null)}</td>;
    return (
      <td>{c.fmt(r)}
        {prev && c.cmp && <CeDelta cur={c.cmp.v(r)} prev={p ? c.cmp.v(p) : null} good={c.cmp.good ?? 1} pt={c.cmp.pt}
          title={p ? `${cmp.from} to ${cmp.to}: ${c.fmt(p)}` : undefined} />}
      </td>
    );
  };
  // DMAs of an open row, or campaigns of an open DMA row
  const breakdownOf = (r) => {
    if (!split) return <div className="ce-muted">{geoError ? `DMA layer not available: ${geoError}` : 'Loading DMA breakdown…'}</div>;
    const byDma = groupBy !== 'dma';
    const items = split.cells.filter(c => (byDma ? c.base === r.id : c.dma === r.dma));
    return (
      <CeBreakdown key={r.id} items={items} prev={split.prev} by={byDma ? 'dma' : 'campaign'} ratio={ratio}
        nameOf={byDma ? (c => geo.dmas[c.dma]) : (c => c.label)} minCell={thinLeads} setMinCell={setThinLeads}
        onPick={(m) => setDma(dma === m ? null : m)} picked={dma} covNote={covNote} cmp={cmp} />
    );
  };
  const tileDelta = (v, opts = {}) => prevTotal && <CeDelta cur={v(total)} prev={v(prevTotal)} good={opts.good ?? 1} pt={opts.pt} />;

  return (
    <>
      <div className="section-label"><span className="dot" />CAMPAIGN ECONOMICS<span className="range"> — {f} to {t} ({days} days){cmp ? ` vs ${cmp.from} to ${cmp.to}` : ''} · lead-date cohorts{dmaName ? ` · DMA ${dmaName}` : ''}{profitMode === 'dma' ? ' · profit per unit by DMA' : ''}</span></div>

      <div className="ce-controls">
        <div className="ce-control">
          <label>Period</label>
          <div className="ce-row">
            <input type="date" className="number-input ce-date" value={f} max={t} onChange={e => setFrom(e.target.value)} />
            <span>→</span>
            <input type="date" className="number-input ce-date" value={t} min={f} onChange={e => setTo(e.target.value)} />
          </div>
          <div className="ce-row ce-presets">
            {[7, 14, 28, 90].map(n => <button key={n} className="dl-btn" onClick={() => setPreset(n)}>Last {n}d</button>)}
            <button className="dl-btn" onClick={() => setMonth(0)}>This month</button>
            <button className="dl-btn" onClick={() => setMonth(-1)}>Last month</button>
          </div>
          <label>Compare with</label>
          <div className="ce-row ce-seg-toggle">
            {[['auto', 'Auto'], ['prev', `Previous ${days}d`], ['month', 'Same days, prior month'], ['off', 'Off']].map(([k, l]) => (
              <button key={k} className={`tab-btn${cmpMode === k ? ' active' : ''}`} onClick={() => setCmpMode(k)}>{l}</button>
            ))}
          </div>
          {cmp && (
            <span className="ce-muted">
              {cmp.from} → {cmp.to}{cmpMode === 'auto' ? ` (${cmp.kind === 'month' ? 'month to date → same days of prior month' : 'previous period'})` : ''}
              {prevTotal && prevTotal.maturity != null && total.maturity != null && ` · APCs seen ${cePct(total.maturity, 0)} now vs ${cePct(prevTotal.maturity, 0)} then`}
            </span>
          )}
        </div>
        <div className="ce-control">
          <label>Group by</label>
          <div className="ce-row ce-seg-toggle">
            {[['channel', 'Channel'], ['theme', 'Theme'], ['campaign', 'Campaign'], ['dma', 'DMA']].map(([k, l]) => (
              <button key={k} className={`tab-btn${groupBy === k ? ' active' : ''}`} onClick={() => { setGroupBy(k); setOpen(null); }}>{l}</button>
            ))}
          </div>
          <label className="ce-inline">Hide rows under <input type="number" className="number-input ce-small" value={minLeads} min={0} onChange={e => setMinLeads(Math.max(0, +e.target.value || 0))} /> leads</label>
          <label>Location (DMA)</label>
          <div className="ce-row">
            <select className="ce-select" value={dma == null ? '' : dma} disabled={!geo} onChange={e => { setDma(e.target.value === '' ? null : +e.target.value); setOpen(null); }}>
              <option value="">{geo ? 'All DMAs (national)' : geoError ? 'DMA layer not available' : 'Loading DMAs…'}</option>
              {dmaOptions.map(c => <option key={c.dma} value={c.dma}>{c.label} · {ceNum(c.leads)} leads</option>)}
            </select>
            {dma != null && <button className="dl-btn" onClick={() => setDma(null)}>Clear</button>}
          </div>
        </div>
        <div className="ce-control">
          <label>Efficiency ratio <span className="ce-muted">(profit ÷ spend to stay healthy)</span></label>
          <div className="ce-row">
            <input type="number" step="0.05" min="1" className="number-input ce-small" value={ratio} onChange={e => setRatio(Math.max(1, +e.target.value || 1))} />
            <span className="ce-muted">Target CPL = break-even ÷ {ratio.toFixed(2)} → keep {cePct(1 - 1 / ratio, 0)} of profit</span>
          </div>
          <label>Profit per unit <span className="ce-muted">(DW, last 180 days — editable · SP = Service Provider fee)</span></label>
          <div className="ce-row ce-seg-toggle">
            {[['national', 'National'], ['dma', 'By DMA']].map(([k, l]) => (
              <button key={k} className={`tab-btn${profitMode === k ? ' active' : ''}`} disabled={k === 'dma' && !geo} onClick={() => setProfitMode(k)}>{l}</button>
            ))}
            <span className="ce-muted">{profitMode === 'dma'
              ? `Each lead valued at its DMA's average, pulled toward national (${CE_SHRINK_UNITS} units). Values below are the national base.`
              : 'Same value per unit in every DMA.'}</span>
          </div>
          <div className="ce-row ce-profits">
            {CE_SEG_ORDER.slice(0, 4).map(i => CE_SEGS[i]).map(s => (
              <span key={s} className="ce-profit-input">
                <i className="ce-dot" style={{ background: CE_SEG_COLORS[s] }} />{s}
                <input type="number" className="number-input ce-small" value={Math.round(profit[s].profit)}
                  onChange={e => setProfitEdits(p => ({ ...p, [s]: +e.target.value || 0 }))} />
              </span>
            ))}
            {Object.keys(profitEdits).length > 0 && <button className="dl-btn" onClick={() => setProfitEdits({})}>Reset</button>}
          </div>
        </div>
      </div>

      <div className="ce-row ce-channels">
        {allChannels.map(ch => (
          <button key={ch} className={`ce-pill${channels.has(ch) ? ' on' : ''}`} onClick={() => toggleChannel(ch)}>{ch}</button>
        ))}
        <button className="dl-btn" onClick={() => setChannels(new Set(CE_PAID))}>Paid only</button>
        <button className="dl-btn" onClick={() => setChannels(new Set(allChannels))}>All</button>
      </div>

      {geoCut && (
        <div className="alert-box ce-alert">
          <b>DMA layer starts {geo.d0}:</b> days before that are left out of {f < geo.d0 ? 'this period' : 'the comparison period'} while a DMA, DMA rows or profit by DMA are on.
        </div>
      )}
      {staleSpend.length > 0 && (
        <div className="alert-box ce-alert">
          <b>Spend not complete for the whole period:</b> {staleSpend.map(([p, d]) => `${p} through ${d}`).join(' · ')}. CPL and profit per $1 for those platforms are understated after that date.
        </div>
      )}

      <div className="tiles ce-tiles">
        <Tile label="Spend" value={<>{fmtMoneyK(total.spend)} {tileDelta(r => r.spend, { good: 0 })}</>} sub={`${ceMoney(total.spend / days)} / day`} />
        <Tile label="Leads · CPL" value={<>{ceNum(total.leads)} {tileDelta(r => r.leads)}</>}
          sub={<>CPL {ceMoney(total.cpl, 2)} {tileDelta(r => r.cpl, { good: -1 })} · P+P {cePct(total.ppShare)} {tileDelta(r => r.ppShare, { pt: true })}</>} />
        <Tile label="APC projected" value={<>{ceNum(total.apcProj)} {tileDelta(r => r.apcProj)}</>} sub={<>{ceNum(total.apc)} seen · {ceMoney(total.costPerApc)} per APC {tileDelta(r => r.costPerApc, { good: -1 })}</>} />
        <Tile label="Target · break-even CPL" value={`${ceMoney(total.valuePerLead / ratio, 0)} – ${ceMoney(total.valuePerLead, 0)}`}
          sub={<>Target at {ratio.toFixed(2)}× · break-even {tileDelta(r => r.valuePerLead)}</>} />
        <Tile label="Projected profit · revenue" value={<>{fmtMoneyK(total.profit)} {tileDelta(r => r.profit)}</>}
          sub={<>Revenue {fmtMoneyK(total.revenue)} · {ceMoney(total.profitPerDollar, 2)} per $1 {tileDelta(r => r.profitPerDollar)}</>} />
      </div>

      <div className="panel ce-panel">
        <div className="panel-head">All selected rows · mix and CPL range</div>
        <div className="panel-body"><CeRange row={total} ratio={ratio} title="Segment mix" fb={fb} /></div>
      </div>

      <div className="panel ce-panel">
        <div className="panel-head" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          <span>{shown.length} {{ channel: 'channels', theme: 'themes', campaign: 'campaigns', dma: 'DMAs' }[groupBy]}{dmaName ? ` in ${dmaName}` : ''}{hidden > 0 ? ` (${hidden} small rows hidden)` : ''} · click a row for the segment breakdown</span>
          <DownloadButton label="CSV" rows={csvRows} filename={`carbrain_campaign_economics_${f}_${t}${dmaName ? `_${dmaName.replace(/[^A-Za-z0-9]+/g, '-')}` : ''}${groupBy === 'dma' ? '_by-dma' : ''}.csv`} />
        </div>
        <div className="ce-scroll">
          <table className="wow ce-table">
            <thead>
              <tr>
                {columns.map(c => (
                  <th key={c.key} onClick={() => setSort(s => ({ key: c.key, dir: s.key === c.key ? -s.dir : (c.key === 'label' ? 1 : -1) }))}
                    className={sort.key === c.key ? 'sorted' : ''}>
                    {c.label}{sort.key === c.key ? (sort.dir < 0 ? ' ↓' : ' ↑') : ''}
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {shown.map(r => (
                <React.Fragment key={r.id}>
                  <tr className={`ce-row-click${open === r.id ? ' open' : ''}`} onClick={() => setOpen(open === r.id ? null : r.id)}>
                    {columns.map(c => <React.Fragment key={c.key}>{cell(c, r, prev && prev.get(r.id))}</React.Fragment>)}
                  </tr>
                  {open === r.id && (
                    <tr className="ce-detail-row"><td colSpan={columns.length}><CeDetail row={r} ratio={ratio} profit={profit} rateFor={rateFor} fb={fb} breakdown={breakdownOf(r)} /></td></tr>
                  )}
                </React.Fragment>
              ))}
              <tr className="ce-total">
                {columns.map(c => <React.Fragment key={c.key}>{c.key === 'label' ? <td className="metric">Total (all rows)</td> : cell(c, total, prevTotal)}</React.Fragment>)}
              </tr>
            </tbody>
          </table>
        </div>
        <div className="mix-legend ce-legend">
          {CE_SEG_ORDER.map(i => CE_SEGS[i]).map(s => <span key={s}><i style={{ background: CE_SEG_COLORS[s] }} />{s}{data.profit[s] ? ` (${ceMoney(profit[s].profit)}/unit)` : ''}</span>)}
          <span className="ce-legend-sep" />
          <span><i className="z good" />Below target: room to raise</span>
          <span><i className="z warn" />Target → break-even: hold</span>
          <span><i className="z bad" />Above break-even: losing</span>
          <span><i className="legend-maintain" />Current CPL</span>
          {prev && <span className="ce-muted">▲▼ change vs {cmp.from} to {cmp.to} (green = better · pt = percentage points)</span>}
        </div>
      </div>

      <div className="note ce-method">
        <b>How the projection works.</b> Each day's leads are tracked as a cohort by creation date. APCs already seen are kept; what is still to come is projected from the cohort's own Offer Accepted:
        OA still to come = leads × the channel's OA rate for that segment × the share of OAs not yet visible at that age; APC still to come = projected OA × the channel's OA → APC rate × the share of APCs not yet visible.
        Rates come from leads {CE_REF_MIN_AGE}–{CE_REF_MAX_AGE} days old. Channels with little volume lean toward the all-channel rate for the segment.
        By day 3, about 80% of a cohort's OAs are visible against about 30% of its APCs, so a campaign can be read within the first week.
        <b> Seen</b> = share of projected APCs that already happened. Profit per unit = average for units sold in the last 180 days (DW); SP vehicles are handed to a Service Provider, so their value is the fee it pays (no auction sale).
        <b> By DMA.</b> Leads use the lead's DMA in the CRM. Spend by DMA comes from each platform's location report (Google Ads DMA report, Microsoft Ads metro area, Meta comScore market), which places the person who saw the ad; spend with no location is spread over the same campaign's DMAs that day and marked as estimated.
        OA and APC rates and curves stay national. With profit per unit "By DMA", each DMA's average per segment is pulled toward the national value by {CE_SHRINK_UNITS} units, so DMAs with few sales stay close to national.
        Data: BigQuery DW, generated {data.generated.replace('T', ' ')}{geo ? ` · DMA layer from ${geo.d0}, generated ${geo.generated.replace('T', ' ')}` : ''}.
      </div>
    </>
  );
}
