/* Folclore Latin Fund I — 14-year fund pacing & European waterfall model */

const { useState, useEffect, useMemo, useCallback, useRef } = React;

const FP_YEARS = 14;
const FP_START_CAL = 2027;
const FP_MAX_DEALS = 12;
const FP_EPS = 1e-6;

const FP_DEFAULT_PARAMS = {
  committed: 150,
  feeY1_5: 0.02,
  feeY6_10: 0.015,
  feeY11_14: 0.01,
  orgExpense: 1.5,
  annualOpEx: 0.3,
  carry: 0.2,
  preferred: 0.08,
};

const FP_BASE_DEALS = [
  { id: "deal-1", name: "Deal 1 (Core Winner)", entry: 1, exit: 6, initial: 18, followOnYear: 3, followOn: 4, moic: 3.5 },
  { id: "deal-2", name: "Deal 2 (Core Winner)", entry: 2, exit: 7, initial: 18, followOnYear: 4, followOn: 4, moic: 3.0 },
  { id: "deal-3", name: "Deal 3 (The 10x Scale-Up)", entry: 3, exit: 9, initial: 16, followOnYear: 5, followOn: 9, moic: 10.0 },
  { id: "deal-4", name: "Deal 4 (Core Winner)", entry: 3, exit: 8, initial: 16, followOnYear: 5, followOn: 4, moic: 3.27 },
  { id: "deal-5", name: "Deal 5 (Trend Flop — Salvage)", entry: 4, exit: 8, initial: 15, followOnYear: 5, followOn: 2.5, moic: 0.8 },
  { id: "deal-6", name: "Deal 6 (Trend Flop — Salvage)", entry: 5, exit: 10, initial: 15, followOnYear: 6, followOn: 2.5, moic: 0.8 },
];

const FP_PRESETS = [
  { id: "base-10x", name: "Base 10.0x", moic: 10, exit: 9 },
  { id: "extended-hold", name: "10.0x · Year 12", moic: 10, exit: 12 },
  { id: "moderate-6x", name: "Moderate 6.0x", moic: 6, exit: 9 },
  { id: "modest-4x", name: "Modest 4.0x", moic: 4, exit: 9 },
  { id: "flop-08x", name: "Flop 0.8x", moic: 0.8, exit: 9 },
];

const FP_VIEWS = [
  { id: "portfolio", label: "Portfolio" },
  { id: "j-curve", label: "Pacing & J-Curve" },
  { id: "sensitivity", label: "Sensitivity" },
  { id: "reading", label: "Reading" },
];

/* ---------- Math ---------- */
function fpCloneDeals(deals) {
  return deals.map((d) => ({ ...d }));
}

function fpCloneParams(params) {
  return { ...params };
}

function fpSolveIrr(cashFlows) {
  const npv = (r) => cashFlows.reduce((s, cf, t) => s + cf / Math.pow(1 + r, t), 0);
  let hasPos = false;
  let hasNeg = false;
  for (let i = 0; i < cashFlows.length; i++) {
    if (cashFlows[i] > FP_EPS) hasPos = true;
    if (cashFlows[i] < -FP_EPS) hasNeg = true;
  }
  if (!hasPos || !hasNeg) return 0;

  let lo = -0.99;
  let hi = 5;
  if (npv(lo) < 0) return lo;
  for (let i = 0; i < 90; i++) {
    const mid = (lo + hi) / 2;
    if (npv(mid) > 0) lo = mid;
    else hi = mid;
  }
  return (lo + hi) / 2;
}

function fpDealCost(deal) {
  return Number(deal.initial || 0) + Number(deal.followOn || 0);
}

function fpDealExit(deal) {
  return fpDealCost(deal) * Number(deal.moic || 0);
}

function fpDealMetrics(deal) {
  const entry = Math.max(1, Math.min(FP_YEARS, Number(deal.entry) || 1));
  const exit = Math.max(entry + 1, Math.min(FP_YEARS, Number(deal.exit) || entry + 1));
  const followOnYear = Math.max(entry, Math.min(exit, Number(deal.followOnYear) || entry));
  const hold = exit - entry;
  const cost = fpDealCost(deal);
  const grossExit = fpDealExit(deal);
  const n = exit - entry;
  const cfs = Array(n + 1).fill(0);
  cfs[0] = -Number(deal.initial || 0);
  const foOffset = followOnYear - entry;
  if (foOffset >= 0 && foOffset <= n) cfs[foOffset] -= Number(deal.followOn || 0);
  cfs[n] += grossExit;
  const irr = fpSolveIrr(cfs);
  return {
    deal: { ...deal, entry, exit, followOnYear },
    hold,
    cost,
    grossExit,
    irr,
    profit: grossExit - cost,
  };
}

function fpFeeRate(year, params) {
  if (year <= 5) return params.feeY1_5;
  if (year <= 10) return params.feeY6_10;
  return params.feeY11_14;
}

function computeFundPacing(deals, paramsInput) {
  const params = { ...FP_DEFAULT_PARAMS, ...(paramsInput || {}) };
  const metrics = (deals || []).map(fpDealMetrics);
  const schedule = [];

  for (let y = 1; y <= FP_YEARS; y++) {
    let initial = 0;
    let followOn = 0;
    let costOut = 0;
    let gross = 0;
    for (let i = 0; i < metrics.length; i++) {
      const m = metrics[i];
      const d = m.deal;
      if (d.entry === y) initial += Number(d.initial || 0);
      if (d.followOnYear === y) followOn += Number(d.followOn || 0);
      if (d.exit === y) {
        costOut += m.cost;
        gross += m.grossExit;
      }
    }
    schedule.push({
      year: y,
      calYear: FP_START_CAL + y - 1,
      initial,
      followOn,
      costOut,
      gross,
      active: 0,
      fee: 0,
      opex: 0,
      call: 0,
      gpCarry: 0,
      lpDist: 0,
      lpNet: 0,
      cumPic: 0,
      cumLpDist: 0,
      nav: 0,
      dpi: 0,
      rvpi: 0,
      tvpi: 0,
    });
  }

  let active = 0;
  for (let i = 0; i < schedule.length; i++) {
    const r = schedule[i];
    active += r.initial + r.followOn - r.costOut;
    if (active < FP_EPS) active = 0;
    r.active = active;
    const feeBase = r.year <= 5 ? params.committed : r.active;
    r.fee = feeBase * fpFeeRate(r.year, params);
    r.opex = r.year === 1 ? params.orgExpense + params.annualOpEx : params.annualOpEx;
    r.call = r.initial + r.followOn + r.fee + r.opex;
  }

  let prevLp = 0;
  let prevGp = 0;
  for (let n = 1; n <= FP_YEARS; n++) {
    let unret = 0;
    let pref = 0;
    let gp = 0;
    let lpProfit = 0;
    let lp = 0;
    let pic = 0;

    for (let i = 0; i < n; i++) {
      const r = schedule[i];
      unret += r.call;
      pic += r.call;
      pref += unret * params.preferred;

      let cash = r.gross;
      const payPref = Math.min(cash, Math.max(0, pref));
      pref -= payPref;
      cash -= payPref;
      lp += payPref;
      lpProfit += payPref;

      const payCap = Math.min(cash, Math.max(0, unret));
      unret -= payCap;
      cash -= payCap;
      lp += payCap;

      if (unret < FP_EPS) unret = 0;
      if (pref < FP_EPS) pref = 0;

      if (cash > FP_EPS && unret === 0 && pref === 0) {
        const need = Math.max(0, lpProfit * 0.25 - gp);
        const cu = Math.min(cash, need);
        cash -= cu;
        gp += cu;
        gp += cash * params.carry;
        lp += cash * (1 - params.carry);
        lpProfit += cash * (1 - params.carry);
      }
    }

    const short = unret + pref;
    let fairGp = gp;
    let fairLp = lp;
    if (short > FP_EPS && gp > 0) {
      const back = Math.min(gp, short);
      fairGp = gp - back;
      fairLp = lp + back;
    }

    const r = schedule[n - 1];
    r.gpCarry = fairGp - prevGp;
    r.lpDist = fairLp - prevLp;
    r.lpNet = -r.call + r.lpDist;
    r.cumPic = pic;
    r.cumLpDist = fairLp;
    r.nav = r.active;
    r.dpi = pic > FP_EPS ? fairLp / pic : 0;
    r.rvpi = pic > FP_EPS ? r.active / pic : 0;
    r.tvpi = pic > FP_EPS ? (fairLp + r.active) / pic : 0;
    prevGp = fairGp;
    prevLp = fairLp;
  }

  const last = schedule[schedule.length - 1];
  const totalInitial = metrics.reduce((s, m) => s + Number(m.deal.initial || 0), 0);
  const totalFollowOn = metrics.reduce((s, m) => s + Number(m.deal.followOn || 0), 0);
  const totalCost = metrics.reduce((s, m) => s + m.cost, 0);
  const grossExitProceeds = metrics.reduce((s, m) => s + m.grossExit, 0);
  const totalFees = schedule.reduce((s, r) => s + r.fee, 0);
  const totalOpex = schedule.reduce((s, r) => s + r.opex, 0);
  const totalGpCarry = schedule.reduce((s, r) => s + r.gpCarry, 0);
  const totalLpDist = last.cumLpDist;
  const totalCalled = last.cumPic;
  const grossCfs = schedule.map((r) => -r.initial - r.followOn + r.gross);
  const netCfs = schedule.map((r) => r.lpNet);
  const avgHold = metrics.length
    ? metrics.reduce((s, m) => s + m.hold, 0) / metrics.length
    : 0;
  const avgMoic = totalCost > FP_EPS ? grossExitProceeds / totalCost : 0;

  let cumNet = 0;
  let troughYear = 1;
  let troughValue = 0;
  let largestExitYear = 1;
  let largestExit = 0;
  schedule.forEach((r) => {
    cumNet += r.lpNet;
    if (cumNet < troughValue) {
      troughValue = cumNet;
      troughYear = r.year;
    }
    if (r.gross > largestExit) {
      largestExit = r.gross;
      largestExitYear = r.year;
    }
  });

  let dpiNearOneYear = null;
  for (let i = 0; i < schedule.length; i++) {
    if (schedule[i].dpi >= 0.95) {
      dpiNearOneYear = schedule[i].year;
      break;
    }
  }

  return {
    params,
    deals: metrics,
    schedule,
    totals: {
      totalInitial,
      totalFollowOn,
      totalCost,
      avgHold,
      avgMoic,
      grossExitProceeds,
      totalFees,
      totalOpex,
      totalFeesAndExpenses: totalFees + totalOpex,
      totalCalled,
      uncalled: params.committed - totalCalled,
      overCommitted: totalCalled > params.committed + FP_EPS,
      totalGpCarry,
      totalLpDist,
      totalLpNetProfit: totalLpDist - totalCalled,
      finalDpi: last.dpi,
      finalTvpiOnPic: last.tvpi,
      finalTvpiOnCommitted: params.committed > FP_EPS ? totalLpDist / params.committed : 0,
      netIrr: fpSolveIrr(netCfs),
      grossIrr: fpSolveIrr(grossCfs),
      portfolioGrossMoic: avgMoic,
      troughYear,
      troughValue,
      largestExitYear,
      largestExit,
      dpiNearOneYear,
    },
  };
}

function fpFreshDeal(existing) {
  const n = (existing || []).length + 1;
  const usedEntries = new Set((existing || []).map((d) => Number(d.entry)));
  let entry = Math.min(5, n);
  while (usedEntries.has(entry) && entry < 6) entry += 1;
  return {
    id: "deal-" + Date.now() + "-" + n,
    name: "Deal " + n,
    entry,
    exit: Math.min(FP_YEARS, entry + 5),
    initial: 10,
    followOnYear: Math.min(FP_YEARS, entry + 2),
    followOn: 2,
    moic: 2.5,
  };
}

function fpFm(x, d) {
  const n = Number(x);
  if (!Number.isFinite(n)) return "—";
  const digits = d == null ? 1 : d;
  const sign = n < -FP_EPS ? "−" : "";
  return sign + "$" + Math.abs(n).toFixed(digits) + "M";
}

function fpFx(x, d) {
  const n = Number(x);
  if (!Number.isFinite(n)) return "—";
  return n.toFixed(d == null ? 2 : d) + "×";
}

function fpFp(x, d) {
  const n = Number(x);
  if (!Number.isFinite(n)) return "—";
  return (n * 100).toFixed(d == null ? 1 : d) + "%";
}

function fpNegClass(n) {
  return n < -FP_EPS ? " fp-neg" : n > FP_EPS ? " fp-pos" : "";
}

/* ---------- Small UI pieces ---------- */
function FpKpi({ label, value, sub }) {
  return (
    <div className="fp-kpi">
      <div className="fp-kpi-label">{label}</div>
      <div className="fp-kpi-value dr-mono">{value}</div>
      {sub ? <div className="fp-kpi-sub">{sub}</div> : null}
    </div>
  );
}

function FpScenarioBar({
  activeScenarioId,
  onSelectPreset,
  onReset,
  focusDeal,
  onChangeFocusMoic,
  onChangeFocusExit,
}) {
  if (!focusDeal) return null;
  return (
    <div className="fp-scenario">
      <div className="fp-scenario-presets">
        <span className="fp-scenario-label">Scenarios</span>
        {FP_PRESETS.map((sc) => (
          <button
            key={sc.id}
            type="button"
            className={"fp-chip" + (activeScenarioId === sc.id ? " active" : "")}
            onClick={() => onSelectPreset(sc)}
          >
            {sc.name}
          </button>
        ))}
        {activeScenarioId === "custom" ? <span className="fp-chip ghost">Custom</span> : null}
      </div>
      <div className="fp-scenario-focus">
        <label className="fp-focus-control">
          <span>Focus MOIC</span>
          <input
            type="range"
            min="0.8"
            max="15"
            step="0.1"
            value={focusDeal.moic}
            onChange={(e) => onChangeFocusMoic(parseFloat(e.target.value))}
          />
          <span className="dr-mono">{fpFx(focusDeal.moic, 1)}</span>
        </label>
        <label className="fp-focus-control">
          <span>Exit year</span>
          <select
            value={focusDeal.exit}
            onChange={(e) => onChangeFocusExit(parseInt(e.target.value, 10))}
            aria-label="Focus deal exit year"
          >
            {Array.from({ length: FP_YEARS - 1 }, (_, i) => i + 2).map((y) => (
              <option key={y} value={y}>
                Year {y} ({FP_START_CAL + y - 1})
              </option>
            ))}
          </select>
        </label>
        <button type="button" className="dr-scenario-tab" onClick={onReset}>
          Reset
        </button>
      </div>
    </div>
  );
}

function FpDealTable({
  deals,
  totals,
  focusId,
  editingId,
  onEdit,
  onUpdate,
  onAdd,
  onRemove,
  onSetFocus,
}) {
  return (
    <div className="dr-panel fp-panel">
      <div className="fp-panel-head">
        <div>
          <h3 className="dr-panel-title">Deal control</h3>
          <p className="dr-muted" style={{ margin: "4px 0 0" }}>
            Change entry, exit, checks, or multiples. The 14-year schedule and waterfall recalculate.
          </p>
        </div>
        <div className="fp-panel-actions">
          <button
            type="button"
            className="dr-scenario-tab"
            onClick={onAdd}
            disabled={deals.length >= FP_MAX_DEALS}
          >
            Add deal
          </button>
        </div>
      </div>

      {totals.overCommitted ? (
        <p className="fp-warn">
          Capital called ({fpFm(totals.totalCalled)}) exceeds committed capital (
          {fpFm(totals.paramsCommitted != null ? totals.paramsCommitted : 150, 0)}
          ). Reduce a cheque or the fee base before relying on the figures.
        </p>
      ) : null}

      <div className="fp-table-wrap">
        <table className="fp-table">
          <thead>
            <tr>
              <th>Deal</th>
              <th className="num">Entry</th>
              <th className="num">Exit</th>
              <th className="num">Hold</th>
              <th className="num">Initial</th>
              <th className="num">FO year</th>
              <th className="num">Follow-on</th>
              <th className="num">Cost</th>
              <th className="num">MOIC</th>
              <th className="num">Exit $</th>
              <th className="num">Gross IRR</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {deals.map((dm) => {
              const d = dm.deal;
              const editing = editingId === d.id;
              const focused = focusId === d.id;
              const salvage = dm.irr < 0 || d.moic < 1;
              return (
                <tr key={d.id} className={(focused ? "focus " : "") + (salvage ? "salvage" : "")}>
                  <td>
                    {editing ? (
                      <input
                        className="fp-input"
                        value={d.name}
                        onChange={(e) => onUpdate(d.id, { name: e.target.value })}
                      />
                    ) : (
                      <button type="button" className="fp-deal-name" onClick={() => onSetFocus(d.id)}>
                        <span>{d.name}</span>
                        {focused ? <span className="fp-tag">Focus</span> : null}
                        {salvage ? <span className="fp-tag salvage">Salvage</span> : null}
                      </button>
                    )}
                  </td>
                  <td className="num">
                    {editing ? (
                      <input
                        className="fp-input num"
                        type="number"
                        min="1"
                        max="13"
                        value={d.entry}
                        onChange={(e) => onUpdate(d.id, { entry: parseInt(e.target.value, 10) || 1 })}
                      />
                    ) : (
                      d.entry
                    )}
                  </td>
                  <td className="num">
                    {editing ? (
                      <input
                        className="fp-input num"
                        type="number"
                        min={d.entry + 1}
                        max={FP_YEARS}
                        value={d.exit}
                        onChange={(e) => onUpdate(d.id, { exit: parseInt(e.target.value, 10) || d.entry + 1 })}
                      />
                    ) : (
                      d.exit
                    )}
                  </td>
                  <td className="num muted">{dm.hold}</td>
                  <td className="num">
                    {editing ? (
                      <input
                        className="fp-input num"
                        type="number"
                        step="0.5"
                        min="0"
                        value={d.initial}
                        onChange={(e) => onUpdate(d.id, { initial: parseFloat(e.target.value) || 0 })}
                      />
                    ) : (
                      fpFm(d.initial)
                    )}
                  </td>
                  <td className="num">
                    {editing ? (
                      <input
                        className="fp-input num"
                        type="number"
                        min={d.entry}
                        max={d.exit}
                        value={d.followOnYear}
                        onChange={(e) => onUpdate(d.id, { followOnYear: parseInt(e.target.value, 10) || d.entry })}
                      />
                    ) : (
                      d.followOnYear
                    )}
                  </td>
                  <td className="num">
                    {editing ? (
                      <input
                        className="fp-input num"
                        type="number"
                        step="0.5"
                        min="0"
                        value={d.followOn}
                        onChange={(e) => onUpdate(d.id, { followOn: parseFloat(e.target.value) || 0 })}
                      />
                    ) : (
                      fpFm(d.followOn)
                    )}
                  </td>
                  <td className="num muted">{fpFm(dm.cost)}</td>
                  <td className="num">
                    {editing ? (
                      <input
                        className="fp-input num"
                        type="number"
                        step="0.1"
                        min="0"
                        value={d.moic}
                        onChange={(e) => onUpdate(d.id, { moic: parseFloat(e.target.value) || 0 })}
                      />
                    ) : (
                      fpFx(d.moic)
                    )}
                  </td>
                  <td className="num muted">{fpFm(dm.grossExit)}</td>
                  <td className={"num" + fpNegClass(dm.irr)}>{fpFp(dm.irr)}</td>
                  <td className="fp-row-actions">
                    <button type="button" className="fp-icon-btn" onClick={() => onEdit(editing ? null : d.id)}>
                      {editing ? "Done" : "Edit"}
                    </button>
                    {deals.length > 1 ? (
                      <button type="button" className="fp-icon-btn danger" onClick={() => onRemove(d.id)}>
                        Remove
                      </button>
                    ) : null}
                  </td>
                </tr>
              );
            })}
            <tr className="fp-total-row">
              <td>Portfolio</td>
              <td></td>
              <td></td>
              <td className="num">{totals.avgHold.toFixed(1)}</td>
              <td className="num">{fpFm(totals.totalInitial)}</td>
              <td></td>
              <td className="num">{fpFm(totals.totalFollowOn)}</td>
              <td className="num">{fpFm(totals.totalCost)}</td>
              <td className="num">{fpFx(totals.avgMoic)}</td>
              <td className="num">{fpFm(totals.grossExitProceeds)}</td>
              <td className="num">{fpFp(totals.grossIrr)}</td>
              <td></td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}

function FpScheduleGrid({ model }) {
  const years = model.schedule;
  const t = model.totals;
  const cell = (v, opts) => {
    const o = opts || {};
    return (
      <td className={"num" + (o.bold ? " bold" : "") + fpNegClass(o.signed ? v : 0)}>
        {o.money
          ? fpFm(o.signed ? v : Math.abs(v), o.digits)
          : o.mult
            ? fpFx(v)
            : v.toFixed(o.digits == null ? 2 : o.digits)}
      </td>
    );
  };

  const rows = [
    {
      label: "Initial deal checks",
      get: (r) => -r.initial,
      total: -t.totalInitial,
      money: true,
      signed: true,
    },
    {
      label: "Follow-on / license",
      get: (r) => -r.followOn,
      total: -t.totalFollowOn,
      money: true,
      signed: true,
    },
    {
      label: "Active invested cost",
      get: (r) => r.active,
      total: null,
      money: true,
      muted: true,
    },
    {
      label: "Management fees",
      get: (r) => -r.fee,
      total: -t.totalFees,
      money: true,
      signed: true,
      digits: 2,
    },
    {
      label: "Org & fund expenses",
      get: (r) => -r.opex,
      total: -t.totalOpex,
      money: true,
      signed: true,
      digits: 2,
    },
    {
      label: "LP capital called",
      get: (r) => -r.call,
      total: -t.totalCalled,
      money: true,
      signed: true,
      bold: true,
      digits: 2,
    },
    {
      label: "Gross exit proceeds",
      get: (r) => r.gross,
      total: t.grossExitProceeds,
      money: true,
      signed: true,
      bold: true,
    },
    {
      label: "Cumulative paid-in",
      get: (r) => r.cumPic,
      total: t.totalCalled,
      money: true,
      muted: true,
      digits: 2,
    },
  ];

  const perf = [
    {
      label: "GP carry",
      get: (r) => -r.gpCarry,
      total: -t.totalGpCarry,
      money: true,
      signed: true,
      digits: 2,
    },
    {
      label: "LP net distributions",
      get: (r) => r.lpDist,
      total: t.totalLpDist,
      money: true,
      signed: true,
      bold: true,
      digits: 2,
    },
    {
      label: "LP net cash flow",
      get: (r) => r.lpNet,
      total: t.totalLpNetProfit,
      money: true,
      signed: true,
      digits: 2,
    },
    {
      label: "Remaining NAV (1.0× cost)",
      get: (r) => r.nav,
      total: 0,
      money: true,
      muted: true,
    },
    {
      label: "DPI",
      get: (r) => r.dpi,
      total: t.finalDpi,
      mult: true,
      bold: true,
    },
    {
      label: "RVPI",
      get: (r) => r.rvpi,
      total: 0,
      mult: true,
    },
    {
      label: "TVPI",
      get: (r) => r.tvpi,
      total: t.finalTvpiOnPic,
      mult: true,
      bold: true,
    },
  ];

  const render = (list, title) => (
    <div className="dr-panel fp-panel" style={{ marginTop: 16 }}>
      <h3 className="dr-panel-title">{title}</h3>
      <div className="fp-table-wrap">
        <table className="fp-table fp-schedule">
          <thead>
            <tr>
              <th>Line item ($M)</th>
              {years.map((r) => (
                <th key={r.year} className="num">
                  Y{r.year}
                  <span className="fp-yr-sub">{r.calYear}</span>
                </th>
              ))}
              <th className="num">Total</th>
            </tr>
          </thead>
          <tbody>
            {list.map((row) => (
              <tr key={row.label} className={row.bold ? "bold-row" : row.muted ? "muted-row" : ""}>
                <td>{row.label}</td>
                {years.map((r) => (
                  <React.Fragment key={r.year}>
                    {cell(row.get(r), row)}
                  </React.Fragment>
                ))}
                <td className={"num" + (row.bold ? " bold" : "") + fpNegClass(row.signed && row.total != null ? row.total : 0)}>
                  {row.total == null
                    ? "—"
                    : row.money
                      ? fpFm(row.signed ? row.total : Math.abs(row.total), row.digits)
                      : row.mult
                        ? fpFx(row.total)
                        : row.total.toFixed(row.digits == null ? 2 : row.digits)}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );

  return (
    <div>
      {render(rows, "Fourteen-year cash flow")}
      {render(perf, "Waterfall & LP metrics")}
    </div>
  );
}

/* ---------- Charts ---------- */
function FpCharts({ model, focusId }) {
  const [chart, setChart] = useState("jcurve");
  const { schedule, deals, totals } = model;
  const width = 800;
  const height = 300;
  const pad = { top: 24, right: 24, bottom: 36, left: 56 };

  let cum = 0;
  const jPoints = schedule.map((s) => {
    cum += s.lpNet;
    return { year: s.year, cum, annual: s.lpNet, gross: s.gross };
  });
  const minCum = Math.min(...jPoints.map((p) => p.cum), -10);
  const maxCum = Math.max(...jPoints.map((p) => p.cum), 10);
  const cumRange = maxCum - minCum || 1;
  const getX = (year) => pad.left + ((year - 1) / (FP_YEARS - 1)) * (width - pad.left - pad.right);
  const getYCum = (val) => height - pad.bottom - ((val - minCum) / cumRange) * (height - pad.top - pad.bottom);
  const zeroY = getYCum(0);
  const pathD = jPoints.reduce((acc, p, i) => {
    const x = getX(p.year);
    const y = getYCum(p.cum);
    return i === 0 ? "M " + x + " " + y : acc + " L " + x + " " + y;
  }, "");
  const areaD = pathD + " L " + getX(FP_YEARS) + " " + zeroY + " L " + getX(1) + " " + zeroY + " Z";

  const maxBar = Math.max(
    ...schedule.map((s) => Math.max(s.call, s.gross, s.lpDist)),
    1
  );
  const barMid = pad.top + (height - pad.top - pad.bottom) / 2;

  const maxMult = Math.max(...schedule.map((s) => Math.max(s.dpi, s.tvpi)), 1);
  const getYMult = (val) => height - pad.bottom - (val / maxMult) * (height - pad.top - pad.bottom);

  const maxProfit = Math.max(...deals.map((d) => Math.abs(d.profit)), 1);
  const barW = Math.min(48, ((width - pad.left - pad.right) / Math.max(deals.length, 1)) * 0.6);

  const tabs = [
    { id: "jcurve", label: "J-curve" },
    { id: "pacing", label: "Annual pacing" },
    { id: "multiples", label: "DPI & TVPI" },
    { id: "deals", label: "Deal profit" },
  ];

  return (
    <div className="dr-panel fp-panel">
      <div className="fp-panel-head">
        <div>
          <h3 className="dr-panel-title">Pacing & J-curve</h3>
          <p className="dr-muted" style={{ margin: "4px 0 0" }}>
            Live from the schedule above. Trough and largest exit update with the deals.
          </p>
        </div>
        <div className="fp-chart-tabs">
          {tabs.map((t) => (
            <button
              key={t.id}
              type="button"
              className={"fp-chip" + (chart === t.id ? " active" : "")}
              onClick={() => setChart(t.id)}
            >
              {t.label}
            </button>
          ))}
        </div>
      </div>

      {chart === "jcurve" ? (
        <div>
          <p className="fp-chart-note">
            Cumulative LP net cash flow. Trough in year {totals.troughYear}:{" "}
            <strong className="dr-mono">{fpFm(totals.troughValue)}</strong>
            . Largest exit in year {totals.largestExitYear}:{" "}
            <strong className="dr-mono">{fpFm(totals.largestExit)}</strong>
            . Terminal profit: <strong className="dr-mono">{fpFm(totals.totalLpNetProfit)}</strong>.
          </p>
          <div className="fp-chart-frame">
            <svg viewBox={"0 0 " + width + " " + height} className="fp-svg">
              {[-200, -150, -100, -50, 0, 50, 100, 150, 200, 250, 300].map((v) => {
                if (v < minCum - 1 || v > maxCum + 1) return null;
                const y = getYCum(v);
                return (
                  <g key={v}>
                    <line
                      x1={pad.left}
                      y1={y}
                      x2={width - pad.right}
                      y2={y}
                      className={v === 0 ? "fp-axis" : "fp-grid"}
                    />
                    <text x={pad.left - 8} y={y + 3} textAnchor="end" className="fp-axis-label">
                      {v >= 0 ? "$" + v + "M" : "−$" + Math.abs(v) + "M"}
                    </text>
                  </g>
                );
              })}
              <path d={areaD} className="fp-j-area" />
              <path d={pathD} className="fp-j-line" />
              {jPoints.map((p) => {
                const x = getX(p.year);
                const y = getYCum(p.cum);
                const trough = p.year === totals.troughYear;
                const big = p.year === totals.largestExitYear && totals.largestExit > FP_EPS;
                return (
                  <g key={p.year}>
                    <circle cx={x} cy={y} r={trough || big ? 5 : 3.5} className={p.cum >= 0 ? "fp-dot-pos" : "fp-dot-neg"} />
                    {trough ? (
                      <text x={x} y={y + 18} textAnchor="middle" className="fp-callout neg">
                        Trough {fpFm(p.cum)}
                      </text>
                    ) : null}
                    {big ? (
                      <text x={x} y={y - 12} textAnchor="middle" className="fp-callout pos">
                        Exit {fpFm(p.gross)}
                      </text>
                    ) : null}
                    <text x={x} y={height - 10} textAnchor="middle" className="fp-axis-label">
                      Y{p.year}
                    </text>
                  </g>
                );
              })}
            </svg>
          </div>
        </div>
      ) : null}

      {chart === "pacing" ? (
        <div>
          <p className="fp-chart-note">
            Capital called below zero. Gross exits and LP distributions above.
          </p>
          <div className="fp-chart-frame">
            <svg viewBox={"0 0 " + width + " " + height} className="fp-svg">
              <line x1={pad.left} y1={barMid} x2={width - pad.right} y2={barMid} className="fp-axis" />
              {schedule.map((s) => {
                const x = getX(s.year);
                const callH = (s.call / maxBar) * ((height - pad.top - pad.bottom) / 2);
                const exitH = (s.gross / maxBar) * ((height - pad.top - pad.bottom) / 2);
                const distH = (s.lpDist / maxBar) * ((height - pad.top - pad.bottom) / 2);
                return (
                  <g key={s.year}>
                    <rect x={x - 10} y={barMid} width={8} height={callH} className="fp-bar-call" />
                    <rect x={x - 1} y={barMid - exitH} width={8} height={exitH} className="fp-bar-exit" />
                    <rect x={x + 8} y={barMid - distH} width={8} height={Math.max(0, distH)} className="fp-bar-dist" />
                    <text x={x} y={height - 10} textAnchor="middle" className="fp-axis-label">
                      Y{s.year}
                    </text>
                  </g>
                );
              })}
            </svg>
          </div>
          <div className="fp-legend">
            <span><i className="fp-swatch call" /> Capital called</span>
            <span><i className="fp-swatch exit" /> Gross exits</span>
            <span><i className="fp-swatch dist" /> LP distributions</span>
          </div>
        </div>
      ) : null}

      {chart === "multiples" ? (
        <div>
          <p className="fp-chart-note">DPI and TVPI across the fund life (NAV held at remaining cost).</p>
          <div className="fp-chart-frame">
            <svg viewBox={"0 0 " + width + " " + height} className="fp-svg">
              {[0, 0.5, 1, 1.5, 2, 2.5, 3].map((v) => {
                if (v > maxMult + 0.05) return null;
                const y = getYMult(v);
                return (
                  <g key={v}>
                    <line x1={pad.left} y1={y} x2={width - pad.right} y2={y} className="fp-grid" />
                    <text x={pad.left - 8} y={y + 3} textAnchor="end" className="fp-axis-label">
                      {v.toFixed(1)}×
                    </text>
                  </g>
                );
              })}
              <path
                d={schedule.reduce((acc, s, i) => {
                  const x = getX(s.year);
                  const y = getYMult(s.tvpi);
                  return i === 0 ? "M " + x + " " + y : acc + " L " + x + " " + y;
                }, "")}
                className="fp-j-line"
              />
              <path
                d={schedule.reduce((acc, s, i) => {
                  const x = getX(s.year);
                  const y = getYMult(s.dpi);
                  return i === 0 ? "M " + x + " " + y : acc + " L " + x + " " + y;
                }, "")}
                className="fp-dpi-line"
              />
              {schedule.map((s) => (
                <text key={s.year} x={getX(s.year)} y={height - 10} textAnchor="middle" className="fp-axis-label">
                  Y{s.year}
                </text>
              ))}
            </svg>
          </div>
          <div className="fp-legend">
            <span><i className="fp-swatch exit" /> TVPI</span>
            <span><i className="fp-swatch dist" /> DPI</span>
          </div>
        </div>
      ) : null}

      {chart === "deals" ? (
        <div>
          <p className="fp-chart-note">Gross profit by deal (exit proceeds minus invested cost).</p>
          <div className="fp-chart-frame">
            <svg viewBox={"0 0 " + width + " " + height} className="fp-svg">
              <line x1={pad.left} y1={barMid} x2={width - pad.right} y2={barMid} className="fp-axis" />
              {deals.map((dm, i) => {
                const slot = (width - pad.left - pad.right) / Math.max(deals.length, 1);
                const x = pad.left + slot * i + slot / 2;
                const h = (Math.abs(dm.profit) / maxProfit) * ((height - pad.top - pad.bottom) / 2 - 8);
                const focused = dm.deal.id === focusId;
                const y = dm.profit >= 0 ? barMid - h : barMid;
                return (
                  <g key={dm.deal.id}>
                    <rect
                      x={x - barW / 2}
                      y={y}
                      width={barW}
                      height={h}
                      className={dm.profit >= 0 ? (focused ? "fp-bar-exit" : "fp-bar-dist") : "fp-bar-call"}
                    />
                    <text x={x} y={height - 10} textAnchor="middle" className="fp-axis-label">
                      {i + 1}
                    </text>
                    <text x={x} y={dm.profit >= 0 ? y - 6 : y + h + 12} textAnchor="middle" className="fp-axis-label">
                      {fpFm(dm.profit, 0)}
                    </text>
                  </g>
                );
              })}
            </svg>
          </div>
        </div>
      ) : null}
    </div>
  );
}

function FpSensitivity({ deals, params, focusId, activeMoic, activeExit, onSelect }) {
  const moics = [0.8, 2, 4, 6, 8, 10, 12, 14];
  const exits = [7, 8, 9, 10, 11, 12];
  const matrix = useMemo(() => {
    return moics.map((moic) => ({
      moic,
      cols: exits.map((yr) => {
        const test = deals.map((d) =>
          d.id === focusId ? { ...d, moic, exit: yr } : { ...d }
        );
        const res = computeFundPacing(test, params);
        return { yr, tvpi: res.totals.finalTvpiOnPic, irr: res.totals.netIrr };
      }),
    }));
  }, [deals, params, focusId]);

  return (
    <div className="dr-panel fp-panel">
      <h3 className="dr-panel-title">Focus deal sensitivity</h3>
      <p className="dr-muted" style={{ margin: "0 0 16px" }}>
        Net TVPI on paid-in as the focus deal’s MOIC and exit year change. Click a cell to apply it.
      </p>
      <div className="fp-table-wrap">
        <table className="fp-table fp-sens">
          <thead>
            <tr>
              <th>MOIC \\ Exit</th>
              {exits.map((y) => (
                <th key={y} className="num">
                  Y{y}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {matrix.map((row) => (
              <tr key={row.moic}>
                <td className="dr-mono">{fpFx(row.moic, 1)}</td>
                {row.cols.map((c) => {
                  const on =
                    Math.abs(row.moic - activeMoic) < 0.05 && c.yr === activeExit;
                  return (
                    <td key={c.yr} className="num">
                      <button
                        type="button"
                        className={"fp-sens-cell" + (on ? " on" : "")}
                        onClick={() => onSelect(row.moic, c.yr)}
                      >
                        <span className="dr-mono">{fpFx(c.tvpi)}</span>
                        <span className="fp-sens-sub">{fpFp(c.irr)}</span>
                      </button>
                    </td>
                  );
                })}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function FpReading({ model, focusId, deals, params }) {
  const focus = model.deals.find((d) => d.deal.id === focusId) || model.deals[0];
  const t = model.totals;
  if (!focus) return null;

  const focusProfit = focus.profit;
  const totalProfit = model.deals.reduce((s, d) => s + d.profit, 0);
  const share = totalProfit > FP_EPS ? focusProfit / totalProfit : 0;
  const others = model.deals.filter((d) => d.deal.id !== focus.deal.id);

  const laterExit = Math.min(FP_YEARS, focus.deal.exit + 3);
  const heldLonger = computeFundPacing(
    deals.map((d) =>
      d.id === focus.deal.id ? { ...d, exit: laterExit, moic: focus.deal.moic } : { ...d }
    ),
    params
  );

  const dpiYear = t.dpiNearOneYear;
  const dpiRow = dpiYear ? model.schedule[dpiYear - 1] : null;

  return (
    <div className="dr-panel fp-panel">
      <h3 className="dr-panel-title">Reading the picture</h3>
      <div className="fp-reading-grid">
        <article className="fp-reading-card">
          <span className="fp-reading-n">01</span>
          <h4>Focus deal share of gross profit</h4>
          <p>
            {focus.deal.name} invests {fpFm(focus.cost)} and returns {fpFm(focus.grossExit)} (
            {fpFx(focus.deal.moic)}). That is {fpFm(focusProfit)} of gross profit
            {totalProfit > FP_EPS ? " — " + fpFp(share) + " of the portfolio’s gross profit" : ""}.
          </p>
          <p className="fp-reading-note">
            {others.length
              ? "Other deals run at " +
                others.map((d) => fpFx(d.deal.moic, 2)).join(", ") +
                "."
              : "This is the only deal in the book."}
          </p>
        </article>

        <article className="fp-reading-card">
          <span className="fp-reading-n">02</span>
          <h4>IRR versus MOIC on a longer hold</h4>
          <p>
            Holding the same {fpFx(focus.deal.moic)} to year {laterExit} leaves net TVPI near{" "}
            {fpFx(heldLonger.totals.finalTvpiOnPic)} (now {fpFx(t.finalTvpiOnPic)}), while net IRR moves from{" "}
            {fpFp(t.netIrr)} to {fpFp(heldLonger.totals.netIrr)}.
          </p>
          <p className="fp-reading-note">
            Cash amount is unchanged. The extra years dilute the time-weighted return.
          </p>
        </article>

        <article className="fp-reading-card">
          <span className="fp-reading-n">03</span>
          <h4>DPI near capital returned</h4>
          <p>
            {dpiRow
              ? "By year " +
                dpiYear +
                ", LPs have received " +
                fpFm(dpiRow.cumLpDist) +
                " against " +
                fpFm(dpiRow.cumPic) +
                " called (" +
                fpFx(dpiRow.dpi) +
                " DPI)."
              : "Distributions have not yet returned most of paid-in capital in this case."}
          </p>
          <p className="fp-reading-note">
            Early exits that return cash while a compounder is still marked at cost are what LPs look for when Fund II is raised.
          </p>
        </article>

        <article className="fp-reading-card">
          <span className="fp-reading-n">04</span>
          <h4>Uncalled buffer</h4>
          <p>
            Paid-in is {fpFm(t.totalCalled)} of {fpFm(model.params.committed, 0)} committed
            {t.uncalled >= 0
              ? " — " + fpFm(t.uncalled) + " still uncalled"
              : " — " + fpFm(-t.uncalled) + " over the commitment"}.
            Fees and expenses total {fpFm(t.totalFeesAndExpenses)}; invested cost is {fpFm(t.totalCost)}.
          </p>
          <p className="fp-reading-note">
            Management fees step from 2% on committed (years 1–5) to a percentage of remaining invested cost thereafter.
          </p>
        </article>
      </div>
    </div>
  );
}

function FpTermsPanel({ open, params, onClose, onSave }) {
  const [draft, setDraft] = useState(() => fpCloneParams(params));
  useEffect(() => {
    if (open) setDraft(fpCloneParams(params));
  }, [open, params]);
  if (!open) return null;
  const set = (key, val) => setDraft((p) => ({ ...p, [key]: val }));
  return (
    <div className="fp-modal-backdrop" onClick={onClose}>
      <div className="fp-modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="fp-panel-head">
          <h3 className="dr-panel-title">Fund terms</h3>
          <button type="button" className="dr-scenario-tab" onClick={onClose}>
            Close
          </button>
        </div>
        <div className="fp-terms-grid">
          {[
            ["committed", "Committed capital ($M)", 50, 400, 1],
            ["feeY1_5", "Fee years 1–5", 0.01, 0.03, 0.0025],
            ["feeY6_10", "Fee years 6–10", 0.005, 0.025, 0.0025],
            ["feeY11_14", "Fee years 11–14", 0.005, 0.02, 0.0025],
            ["orgExpense", "Org expense year 1 ($M)", 0, 5, 0.1],
            ["annualOpEx", "Annual fund expenses ($M)", 0, 2, 0.05],
            ["preferred", "Preferred return", 0, 0.12, 0.005],
            ["carry", "Carried interest", 0.1, 0.3, 0.025],
          ].map(([key, label, min, max, step]) => (
            <label key={key} className="fp-term-field">
              <span>{label}</span>
              <input
                type="number"
                min={min}
                max={max}
                step={step}
                value={
                  key.indexOf("fee") === 0 || key === "preferred" || key === "carry"
                    ? +(draft[key] * 100).toFixed(2)
                    : draft[key]
                }
                onChange={(e) => {
                  const raw = parseFloat(e.target.value);
                  if (!Number.isFinite(raw)) return;
                  if (key.indexOf("fee") === 0 || key === "preferred" || key === "carry") {
                    set(key, raw / 100);
                  } else set(key, raw);
                }}
              />
              {(key.indexOf("fee") === 0 || key === "preferred" || key === "carry") && (
                <em>% entered</em>
              )}
            </label>
          ))}
        </div>
        <p className="dr-muted" style={{ marginTop: 12 }}>
          European whole-of-fund waterfall with full GP catch-up after the preferred return.
        </p>
        <div className="fp-panel-actions" style={{ marginTop: 16 }}>
          <button type="button" className="dr-scenario-tab" onClick={onClose}>
            Cancel
          </button>
          <button
            type="button"
            className="dr-scenario-tab active"
            onClick={() => {
              onSave(draft);
              onClose();
            }}
          >
            Apply
          </button>
        </div>
      </div>
    </div>
  );
}

/* ---------- Main view ---------- */
function FundPacingView({ t, navigate, view, onViewChange }) {
  const copy = (t && t.dataroom && t.dataroom.fundPacing) || {};
  const [deals, setDeals] = useState(() => fpCloneDeals(FP_BASE_DEALS));
  const [params, setParams] = useState(() => fpCloneParams(FP_DEFAULT_PARAMS));
  const [focusId, setFocusId] = useState("deal-3");
  const [editingId, setEditingId] = useState(null);
  const [scenarioId, setScenarioId] = useState("base-10x");
  const [termsOpen, setTermsOpen] = useState(false);
  const activeView = FP_VIEWS.some((v) => v.id === view) ? view : "portfolio";

  const model = useMemo(() => computeFundPacing(deals, params), [deals, params]);
  const focusDeal = (model.deals.find((d) => d.deal.id === focusId) || model.deals[0] || {}).deal;

  useEffect(() => {
    if (!deals.some((d) => d.id === focusId) && deals[0]) setFocusId(deals[0].id);
  }, [deals, focusId]);

  const markCustom = useCallback(() => setScenarioId("custom"), []);

  const updateDeal = useCallback((id, patch) => {
    setDeals((prev) =>
      prev.map((d) => {
        if (d.id !== id) return d;
        const next = { ...d, ...patch };
        if (patch.entry != null && next.followOnYear < next.entry) next.followOnYear = next.entry;
        if (patch.exit != null && next.exit <= next.entry) next.exit = next.entry + 1;
        if (next.followOnYear > next.exit) next.followOnYear = next.exit;
        return next;
      })
    );
    markCustom();
  }, [markCustom]);

  const addDeal = useCallback(() => {
    setDeals((prev) => {
      if (prev.length >= FP_MAX_DEALS) return prev;
      return [...prev, fpFreshDeal(prev)];
    });
    markCustom();
  }, [markCustom]);

  const removeDeal = useCallback((id) => {
    setDeals((prev) => {
      if (prev.length <= 1) return prev;
      const next = prev.filter((d) => d.id !== id);
      if (id === focusId && next[0]) setFocusId(next[0].id);
      return next;
    });
    markCustom();
  }, [focusId, markCustom]);

  const resetAll = useCallback(() => {
    setDeals(fpCloneDeals(FP_BASE_DEALS));
    setParams(fpCloneParams(FP_DEFAULT_PARAMS));
    setFocusId("deal-3");
    setEditingId(null);
    setScenarioId("base-10x");
  }, []);

  const applyPreset = useCallback((sc) => {
    setDeals((prev) => {
      const hasFocus = prev.some((d) => d.id === focusId);
      const targetId = hasFocus ? focusId : (prev[2] && prev[2].id) || (prev[0] && prev[0].id);
      return prev.map((d) =>
        d.id === targetId ? { ...d, moic: sc.moic, exit: sc.exit } : d
      );
    });
    setScenarioId(sc.id);
  }, [focusId]);

  const changeFocusMoic = useCallback((moic) => {
    if (!focusDeal) return;
    updateDeal(focusDeal.id, { moic });
  }, [focusDeal, updateDeal]);

  const changeFocusExit = useCallback((exit) => {
    if (!focusDeal) return;
    updateDeal(focusDeal.id, { exit });
  }, [focusDeal, updateDeal]);

  const setView = useCallback((id) => {
    if (onViewChange) onViewChange(id);
  }, [onViewChange]);

  const totalsForTable = {
    ...model.totals,
    paramsCommitted: params.committed,
    committed: params.committed,
    grossIrr: model.totals.grossIrr,
  };

  const y7 = model.schedule[6];

  return (
    <div className="fp">
      <div className="fp-hero">
        <div>
          <span className="fp-eyebrow">{copy.eyebrow || "Fund I · Fund model"}</span>
          <h2 className="dr-title">{copy.title || "Fund model"}</h2>
          <p className="dr-body">
            {copy.intro ||
              "Illustrative 14-year portfolio construction and pacing. Change the deals, follow-on reserves, and exits — fees, capital calls, and the European waterfall recalculate."}
          </p>
          <p className="dr-hypo-note">
            {(t && t.dataroom && t.dataroom.hypotheticalNote) ||
              "Hypothetical · for discussion · not a forecast"}
          </p>
        </div>
        <div className="fp-hero-actions">
          <button type="button" className="dr-scenario-tab" onClick={() => setTermsOpen(true)}>
            Fund terms
          </button>
          <button type="button" className="dr-scenario-tab" onClick={resetAll}>
            Reset base case
          </button>
        </div>
      </div>

      <FpScenarioBar
        activeScenarioId={scenarioId}
        onSelectPreset={applyPreset}
        onReset={resetAll}
        focusDeal={focusDeal}
        onChangeFocusMoic={changeFocusMoic}
        onChangeFocusExit={changeFocusExit}
      />

      <div className="fp-kpi-grid">
        <FpKpi
          label="Net TVPI"
          value={fpFx(model.totals.finalTvpiOnCommitted)}
          sub={fpFx(model.totals.finalTvpiOnPic) + " on paid-in"}
        />
        <FpKpi
          label="Net LP IRR"
          value={fpFp(model.totals.netIrr)}
          sub="European waterfall · full catch-up"
        />
        <FpKpi
          label="Gross portfolio"
          value={fpFx(model.totals.portfolioGrossMoic)}
          sub={fpFp(model.totals.grossIrr) + " gross IRR"}
        />
        <FpKpi
          label="LP distributed"
          value={fpFm(model.totals.totalLpDist)}
          sub={fpFm(model.totals.totalLpNetProfit) + " net profit"}
        />
        <FpKpi
          label="GP carry"
          value={fpFm(model.totals.totalGpCarry)}
          sub={fpFm(model.totals.totalCalled) + " paid-in"}
        />
        <FpKpi
          label={y7 ? "DPI · Year " + y7.year : "DPI"}
          value={y7 ? fpFx(y7.dpi) : fpFx(model.totals.finalDpi)}
          sub={
            model.totals.dpiNearOneYear
              ? "Near 1.0× by year " + model.totals.dpiNearOneYear
              : "Terminal " + fpFx(model.totals.finalDpi)
          }
        />
      </div>

      <div className="fp-view-tabs" role="tablist" aria-label="Fund model views">
        {FP_VIEWS.map((v) => (
          <button
            key={v.id}
            type="button"
            role="tab"
            aria-selected={activeView === v.id}
            className={"fp-view-tab" + (activeView === v.id ? " active" : "")}
            onClick={() => setView(v.id)}
          >
            {v.label}
          </button>
        ))}
      </div>

      {activeView === "portfolio" ? (
        <div className="fp-view">
          <FpDealTable
            deals={model.deals}
            totals={totalsForTable}
            focusId={focusDeal ? focusDeal.id : null}
            editingId={editingId}
            onEdit={setEditingId}
            onUpdate={updateDeal}
            onAdd={addDeal}
            onRemove={removeDeal}
            onSetFocus={setFocusId}
          />
          <FpScheduleGrid model={model} />
        </div>
      ) : null}

      {activeView === "j-curve" ? (
        <div className="fp-view">
          <FpCharts model={model} focusId={focusDeal && focusDeal.id} />
          <FpScheduleGrid model={model} />
        </div>
      ) : null}

      {activeView === "sensitivity" ? (
        <div className="fp-view">
          <FpSensitivity
            deals={deals}
            params={params}
            focusId={focusDeal && focusDeal.id}
            activeMoic={focusDeal ? focusDeal.moic : 10}
            activeExit={focusDeal ? focusDeal.exit : 9}
            onSelect={(moic, exit) => {
              if (!focusDeal) return;
              updateDeal(focusDeal.id, { moic, exit });
            }}
          />
          <FpDealTable
            deals={model.deals}
            totals={totalsForTable}
            focusId={focusDeal ? focusDeal.id : null}
            editingId={editingId}
            onEdit={setEditingId}
            onUpdate={updateDeal}
            onAdd={addDeal}
            onRemove={removeDeal}
            onSetFocus={setFocusId}
          />
        </div>
      ) : null}

      {activeView === "reading" ? (
        <div className="fp-view">
          <FpReading model={model} focusId={focusDeal && focusDeal.id} deals={deals} params={params} />
          <FpSensitivity
            deals={deals}
            params={params}
            focusId={focusDeal && focusDeal.id}
            activeMoic={focusDeal ? focusDeal.moic : 10}
            activeExit={focusDeal ? focusDeal.exit : 9}
            onSelect={(moic, exit) => {
              if (!focusDeal) return;
              updateDeal(focusDeal.id, { moic, exit });
            }}
          />
        </div>
      ) : null}

      <FpTermsPanel
        open={termsOpen}
        params={params}
        onClose={() => setTermsOpen(false)}
        onSave={(next) => {
          setParams(fpCloneParams(next));
          markCustom();
        }}
      />
    </div>
  );
}

Object.assign(window, {
  FundPacingView,
  computeFundPacing,
  FP_BASE_DEALS,
  FP_DEFAULT_PARAMS,
  FP_PRESETS,
  FP_VIEWS,
  FP_YEARS,
});
