/* RILA · Registered Index-Linked Annuity Calculator — app logic + UI.
   React 18 via Babel standalone (no build step), matching the sister sites.

   v1 supports the Point-to-Point crediting method only, single segment,
   user-entered rates (cap / participation / buffer / fee) — see CLAUDE.md
   for why, and for the exact formula this was calibrated against. */

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

/* ---------- Icons (inline SVG, stroke style — matches sister calculators) -- */

const Icon = {
  Vault: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <rect x="3" y="4" width="18" height="16" rx="2" /><circle cx="12" cy="12" r="3.4" /><path d="M12 8.6v.01M12 15.4v.01M8.6 12h.01M15.4 12h.01" />
    </svg>
  ),
  Shield: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6l7-3Z" /><path d="M9 12l2 2 4-4" />
    </svg>
  ),
  Cap: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M4 19V9M20 19V9M4 9l8-5 8 5-8 5-8-5Z" /><path d="M8 11.5V16c0 1.4 1.8 2.5 4 2.5s4-1.1 4-2.5v-4.5" />
    </svg>
  ),
  Trend: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M3 17l6-6 4 4 8-8" /><path d="M15 7h6v6" />
    </svg>
  ),
  Coins: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <ellipse cx="9" cy="7" rx="6" ry="3.2" /><path d="M3 7v6c0 1.8 2.7 3.2 6 3.2s6-1.4 6-3.2V7" /><path d="M13.2 10.2c3 .2 5.3 1.5 5.3 3.1 0 1.8-2.7 3.2-6 3.2-2 0-3.8-.5-4.9-1.3" /><path d="M9 13.2v3.6M20 13.3v3.6c0 1.8-2.7 3.2-6 3.2-1.1 0-2.2-.2-3.1-.5" />
    </svg>
  ),
  Calendar: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <rect x="3" y="5" width="18" height="16" rx="2" /><path d="M3 10h18M8 3v4M16 3v4" />
    </svg>
  ),
  Trophy: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M8 4h8v5a4 4 0 0 1-8 0V4Z" /><path d="M8 5H5a3 3 0 0 0 3 3M16 5h3a3 3 0 0 1-3 3" /><path d="M12 13v3M9 20h6M10 20v-2.2M14 20v-2.2" />
    </svg>
  ),
  Alert: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M12 3 2 20h20L12 3Z" /><path d="M12 10v4M12 17v.01" />
    </svg>
  ),
  Clock: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="12" cy="12" r="9" /><path d="M12 7v5l3.2 2" />
    </svg>
  ),
  Cursor: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M6 3l6 15 2.3-6.2L20.5 9.5 6 3Z" />
    </svg>
  ),
  Chart: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M4 20V10M11 20V4M18 20v-7" />
    </svg>
  ),
  Sun: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="12" cy="12" r="4.2" /><path d="M12 2.5v2.2M12 19.3v2.2M4.5 12H2.3M21.7 12h-2.2M5.6 5.6l1.6 1.6M16.8 16.8l1.6 1.6M18.4 5.6l-1.6 1.6M7.2 16.8l-1.6 1.6" />
    </svg>
  ),
  Moon: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5Z" />
    </svg>
  ),
};

/* ---------- Index metadata --------------------------------------------------- */

const INDEX_META = {
  SPX: { label: "S&P 500®", full: "S&P 500® Index", file: "data/spx.json" },
  NDX: { label: "Nasdaq-100®", full: "Nasdaq-100® Index", file: "data/ndx.json" },
  RTY: { label: "Russell 2000®", full: "Russell 2000® Index", file: "data/rty.json" },
  DJI: { label: "Dow Jones", full: "Dow Jones Industrial Average", file: "data/dji.json" },
};

const TERM_OPTIONS = [1, 3, 6];

/* ---------- Data loading ------------------------------------------------------ */

function useIndexData() {
  const [state, setState] = useState({ loading: true, error: null, data: {} });

  useEffect(() => {
    let cancelled = false;
    Promise.all(
      Object.entries(INDEX_META).map(([key, meta]) =>
        fetch(meta.file)
          .then((r) => {
            if (!r.ok) throw new Error(`Failed to load ${meta.file}`);
            return r.json();
          })
          .then((json) => [key, json])
      )
    )
      .then((entries) => {
        if (cancelled) return;
        const data = {};
        for (const [key, series] of entries) data[key] = series;
        setState({ loading: false, error: null, data });
      })
      .catch((err) => {
        if (cancelled) return;
        setState({ loading: false, error: err.message, data: {} });
      });
    return () => { cancelled = true; };
  }, []);

  return state;
}

/* ---------- Date / calculation utilities --------------------------------------

   Trading-day convention: every date lookup resolves to the nearest trading
   day ON OR BEFORE the target calendar date. This was verified against
   Athene's own published Amplify 3.0 example (SPX, 10% buffer, 6-yr,
   Point-to-Point, No Fee): with this convention our real Yahoo Finance data
   reproduces their published Worst/Best/Recent index changes (-18.4% /
   +240.5% / +125.9%) to within 0.05 percentage points. See CLAUDE.md. */

function addYears(dateStr, years) {
  const d = new Date(dateStr + "T00:00:00Z");
  d.setUTCFullYear(d.getUTCFullYear() + years);
  return d.toISOString().slice(0, 10);
}

function addMonths(dateStr, months) {
  const d = new Date(dateStr + "T00:00:00Z");
  d.setUTCMonth(d.getUTCMonth() + months);
  return d.toISOString().slice(0, 10);
}

function daysBetween(a, b) {
  const da = new Date(a + "T00:00:00Z");
  const db = new Date(b + "T00:00:00Z");
  return Math.round((db - da) / 86400000);
}

function formatDateDisplay(dateStr) {
  if (!dateStr) return "";
  const [y, m, d] = dateStr.split("-");
  return `${m}/${d}/${y}`;
}

// Binary search: last index with dates[i] <= targetDate.
function findCloseOnOrBefore(series, targetDate) {
  const { dates, closes } = series;
  let lo = 0, hi = dates.length - 1, ans = 0;
  while (lo <= hi) {
    const mid = (lo + hi) >> 1;
    if (dates[mid] <= targetDate) { ans = mid; lo = mid + 1; }
    else hi = mid - 1;
  }
  return { index: ans, date: dates[ans], close: closes[ans] };
}

function getEarliestStart(series) {
  return series.dates[0];
}

// The last start date for which a full termYears window fits inside the data.
function getLatestStart(series, termYears) {
  const lastDate = series.dates[series.dates.length - 1];
  return addYears(lastDate, -termYears);
}

function computeSegment({ series, startDate, termYears, bufferPct, capPct, participationPct, feePct, purchasePayment }) {
  const targetEnd = addYears(startDate, termYears);
  const startPoint = findCloseOnOrBefore(series, startDate);
  const endPoint = findCloseOnOrBefore(series, targetEnd);
  const indexChange = (endPoint.close - startPoint.close) / startPoint.close;

  const buffer = bufferPct / 100;
  const participation = participationPct / 100;
  const cap = capPct == null ? null : capPct / 100;

  let creditRate;
  if (indexChange >= 0) {
    const applied = cap == null ? indexChange : Math.min(indexChange, cap);
    creditRate = applied * participation;
  } else {
    const loss = -indexChange;
    creditRate = loss <= buffer ? 0 : -(loss - buffer);
  }

  let finalValue = purchasePayment * (1 + creditRate);
  let feeAmount = 0;
  if (feePct > 0) {
    const days = daysBetween(startPoint.date, endPoint.date);
    const dailyFactor = Math.pow(1 - feePct / 100, 1 / 365);
    const withoutFee = finalValue;
    finalValue *= Math.pow(dailyFactor, days);
    feeAmount = withoutFee - finalValue;
  }

  // Monthly snapshots for the chart: index cumulative % at each month across
  // the term — mirrors Athene's own detailed-report chart, which plots the
  // index at monthly resolution even though the axis only labels whole
  // years. Point-to-Point itself only credits once, at maturity, so the
  // RILA Return / Index Return bars and buffer bands are only meaningful at
  // the final month (see ChartPanel).
  const totalMonths = termYears * 12;
  const snapshots = [];
  for (let m = 0; m <= totalMonths; m++) {
    const snapTarget = m === totalMonths ? endPoint.date : addMonths(startDate, m);
    const snap = findCloseOnOrBefore(series, snapTarget);
    const cumPct = ((snap.close - startPoint.close) / startPoint.close) * 100;
    snapshots.push({ month: m, isYearMark: m > 0 && m % 12 === 0, yearNumber: m / 12, date: snap.date, indexPct: cumPct });
  }

  return { startPoint, endPoint, targetEnd, indexChange, creditRate, finalValue, feeAmount, snapshots };
}

function scanBestWorst(series, termYears) {
  const { dates, closes } = series;
  const n = dates.length;
  let j = 0;
  let best = null, worst = null;
  for (let i = 0; i < n; i++) {
    const targetEnd = addYears(dates[i], termYears);
    while (j + 1 < n && dates[j + 1] <= targetEnd) j++;
    // Only bail once we've truly run out of future data (j pinned at the
    // last available point and it still predates the target). dates[j]
    // being < targetEnd on its own is normal and expected — targetEnd is
    // an exact calendar date N years later, which is a weekend/holiday
    // more often than not, so j correctly lands on the nearest earlier
    // trading day rather than matching it exactly.
    if (j === n - 1 && dates[j] < targetEnd) break;
    const change = (closes[j] - closes[i]) / closes[i];
    if (!best || change > best.change) best = { startDate: dates[i], change };
    if (!worst || change < worst.change) worst = { startDate: dates[i], change };
  }
  return { best, worst };
}

/* ---------- Formatting helpers -------------------------------------------- */

function formatCurrency(val) {
  const sign = val < 0 ? "-" : "";
  const abs = Math.abs(val);
  if (abs >= 1000000) return sign + "$" + (abs / 1000000).toFixed(2) + "M";
  return sign + "$" + Math.round(abs).toLocaleString();
}

function formatPercent(pct, digits = 1) {
  const sign = pct > 0 ? "+" : "";
  return sign + pct.toFixed(digits) + "%";
}

/* ---------- Theme hook ------------------------------------------------------ */

function useTheme() {
  const [theme, setTheme] = useState(() => document.documentElement.getAttribute("data-theme") || "dark");
  // Layout effect, not a plain effect: the chart (a child) reads CSS
  // variables in its own effect, and child passive effects run before this
  // parent's — a plain useEffect would redraw the chart with the previous
  // theme's colors.
  useLayoutEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]);
  return [theme, () => setTheme((t) => (t === "dark" ? "light" : "dark"))];
}

/* ---------- Header ----------------------------------------------------------- */

function SiteHeader({ theme, onToggleTheme }) {
  return (
    <header className="site-header">
      <div className="brand">
        <div className="brand-mark">R</div>
        <div className="brand-text">
          <div className="brand-title">
            RILA Calculator
            <span className="brand-pill">Point-to-Point</span>
          </div>
          <div className="brand-sub">Buffered index-linked annuity, historical illustration</div>
        </div>
      </div>
      <div className="header-actions">
        <button type="button" className="header-btn icon-only" onClick={onToggleTheme} aria-label="Toggle theme">
          {theme === "dark" ? <Icon.Sun /> : <Icon.Moon />}
        </button>
      </div>
    </header>
  );
}

/* ---------- Controls panel ---------------------------------------------------- */

function ControlsPanel({
  purchasePayment, setPurchasePayment,
  index, setIndex,
  termYears, setTermYears,
  bufferPct, setBufferPct,
  capEnabled, setCapEnabled,
  capPct, setCapPct,
  participationPct, setParticipationPct,
  feePct, setFeePct,
  dataRangeLabel,
}) {
  const paymentFill = ((purchasePayment - 10000) / (1000000 - 10000)) * 100;
  const bufferFill = bufferPct;
  const capFill = (capPct / 200) * 100;
  const participationFill = ((participationPct - 25) / (200 - 25)) * 100;
  const feeFill = (feePct / 3) * 100;

  return (
    <section className="glass-panel controls-panel">
      <div className="controls-grid">
        <div className="control-field control-span-2">
          <div className="control-label-row">
            <span className="control-label"><Icon.Vault />Purchase Payment</span>
            <span className="control-value">${purchasePayment.toLocaleString()}</span>
          </div>
          <input type="range" min="10000" max="1000000" step="5000" value={purchasePayment}
                 style={{ "--fill": paymentFill + "%" }}
                 onChange={(e) => setPurchasePayment(Number(e.target.value))} />
          <div className="range-scale"><span>$10k</span><span>$500k</span><span>$1M</span></div>
        </div>

        <div className="control-field">
          <div className="control-label-row">
            <span className="control-label"><Icon.Trend />Index</span>
          </div>
          <select className="control-select" value={index} onChange={(e) => setIndex(e.target.value)}>
            {Object.entries(INDEX_META).map(([key, meta]) => (
              <option key={key} value={key}>{meta.label}</option>
            ))}
          </select>
          <div className="control-note">{dataRangeLabel}</div>
        </div>

        <div className="control-field">
          <div className="control-label-row">
            <span className="control-label"><Icon.Calendar />Term Period</span>
          </div>
          <select className="control-select" value={termYears} onChange={(e) => setTermYears(Number(e.target.value))}>
            {TERM_OPTIONS.map((y) => (
              <option key={y} value={y}>{y}-Year</option>
            ))}
          </select>
          <div className="control-note">Segment matures after {termYears} year{termYears > 1 ? "s" : ""}</div>
        </div>

        <div className="control-field">
          <div className="control-label-row">
            <span className="control-label"><Icon.Shield />Buffer (Protection)</span>
            <span className="control-value">{bufferPct}%</span>
          </div>
          <input type="range" min="0" max="100" step="5" value={bufferPct}
                 style={{ "--fill": bufferFill + "%" }}
                 onChange={(e) => setBufferPct(Number(e.target.value))} />
          <div className="range-scale"><span>0%</span><span>50%</span><span>100%</span></div>
        </div>

        <div className="control-field">
          <div className="control-label-row">
            <span className="control-label"><Icon.Cap />Cap Rate</span>
            <label className="control-checkbox">
              <input type="checkbox" checked={!capEnabled} onChange={(e) => setCapEnabled(!e.target.checked)} />
              No Cap
            </label>
          </div>
          <input type="range" min="0" max="200" step="1" value={capPct} disabled={!capEnabled}
                 style={{ "--fill": capFill + "%" }}
                 onChange={(e) => setCapPct(Number(e.target.value))} />
          <div className="range-scale"><span>0%</span><span>100%</span><span>200%</span></div>
        </div>

        <div className="control-field">
          <div className="control-label-row">
            <span className="control-label"><Icon.Trend />Participation Rate</span>
            <span className="control-value">{participationPct}%</span>
          </div>
          <input type="range" min="25" max="200" step="1" value={participationPct}
                 style={{ "--fill": participationFill + "%" }}
                 onChange={(e) => setParticipationPct(Number(e.target.value))} />
          <div className="range-scale"><span>25%</span><span>100%</span><span>200%</span></div>
        </div>

        <div className="control-field">
          <div className="control-label-row">
            <span className="control-label"><Icon.Coins />Segment Fee</span>
            <span className="control-value">{feePct.toFixed(2)}%/yr</span>
          </div>
          <input type="range" min="0" max="3" step="0.05" value={feePct}
                 style={{ "--fill": feeFill + "%" }}
                 onChange={(e) => setFeePct(Number(e.target.value))} />
          <div className="range-scale"><span>0%</span><span>1.5%</span><span>3%</span></div>
        </div>
      </div>
    </section>
  );
}

/* ---------- Market scenario panel ---------------------------------------------- */

function ScenarioPanel({ scenario, setScenario, customDate, setCustomDate, minDate, maxDate }) {
  return (
    <section className="glass-panel scenario-panel">
      <div className="scenario-head">
        <div className="scenario-title"><Icon.Clock />Market Scenario</div>
      </div>
      <div className="mode-toggle scenario-toggle" role="tablist" aria-label="Market scenario">
        <button type="button" className={scenario === "recent" ? "is-active" : ""} onClick={() => setScenario("recent")}>
          <Icon.Clock />Most Recent
        </button>
        <button type="button" className={scenario === "best" ? "is-active" : ""} onClick={() => setScenario("best")}>
          <Icon.Trophy />Best
        </button>
        <button type="button" className={scenario === "worst" ? "is-active" : ""} onClick={() => setScenario("worst")}>
          <Icon.Alert />Worst
        </button>
        <button type="button" className={scenario === "custom" ? "is-active" : ""} onClick={() => setScenario("custom")}>
          <Icon.Cursor />Custom
        </button>
      </div>
      {scenario === "custom" && (
        <div className="custom-date-row">
          <label htmlFor="custom-start-date">Segment Start Date</label>
          <input
            id="custom-start-date"
            type="date"
            value={customDate}
            min={minDate}
            max={maxDate}
            onChange={(e) => setCustomDate(e.target.value)}
          />
          <span className="control-note">Must be between {formatDateDisplay(minDate)} and {formatDateDisplay(maxDate)}</span>
        </div>
      )}
    </section>
  );
}

/* ---------- Results panel ------------------------------------------------------ */

function ResultsPanel({ result, purchasePayment, feePct, indexLabel, termYears, bufferPct, capEnabled, capPct, participationPct }) {
  const totalPct = ((result.finalValue - purchasePayment) / purchasePayment) * 100;
  const isGain = result.finalValue >= purchasePayment;

  const summaryRows = [
    ["Purchase Payment", formatCurrency(purchasePayment)],
    ["Index", indexLabel],
    ["Time Period", `${formatDateDisplay(result.startPoint.date)} – ${formatDateDisplay(result.endPoint.date)} (${termYears}-Year)`],
    ["Buffer", bufferPct + "% Buffer"],
    ["Cap", capEnabled ? capPct + "%" : "Uncapped"],
    ["Participation", participationPct + "%"],
  ];

  return (
    <section className="results-grid">
      <div className={"glass-card stat-card is-highlight results-span-2"}>
        <span className="stat-card-label"><Icon.Vault />Total Contract Value</span>
        <span className="stat-card-value">{formatCurrency(result.finalValue)}</span>
        <span className="stat-card-foot" style={{ color: isGain ? "var(--accent-deep)" : "var(--danger)" }}>
          {formatPercent(totalPct, 2)} from {formatCurrency(purchasePayment)} purchase payment
        </span>
        <div className="segment-summary-list">
          {summaryRows.map(([label, value]) => (
            <div className="segment-summary-row" key={label}>
              <span className="segment-summary-label">{label}</span>
              <span className="segment-summary-value">{value}</span>
            </div>
          ))}
        </div>
      </div>

      <div className="glass-card stat-card">
        <span className="stat-card-label"><Icon.Trend />Index Price Return</span>
        <span className="stat-card-value" style={{ fontSize: "20px" }}>{formatPercent(result.indexChange * 100, 2)}</span>
        <span className="stat-card-foot">{result.startPoint.date} → {result.endPoint.date}</span>
      </div>

      <div className="glass-card stat-card">
        <span className="stat-card-label"><Icon.Shield />Segment Credit Rate</span>
        <span className="stat-card-value" style={{ fontSize: "20px" }}>{formatPercent(result.creditRate * 100, 2)}</span>
        <span className="stat-card-foot">After buffer / cap / participation</span>
      </div>

      {feePct > 0 && (
        <div className="glass-card stat-card results-span-2">
          <span className="stat-card-label"><Icon.Coins />Segment Fee Impact</span>
          <span className="stat-card-value" style={{ fontSize: "20px" }}>-{formatCurrency(result.feeAmount)}</span>
          <span className="stat-card-foot">{feePct.toFixed(2)}%/yr, deducted daily from Base Segment Value</span>
        </div>
      )}
    </section>
  );
}

/* ---------- Chart panel --------------------------------------------------------- */

// Mirrors Athene's own Amplify 3.0 detailed-report chart: a dashed monthly
// index-performance line across the whole term, with a pair of bars (index
// return vs. "RILA Return" — deliberately not naming any specific insurer's
// product) plus Downside Protection / Risk of Loss bands appearing only at
// the final month, since Point-to-Point only credits once, at maturity.
// Chart colors come from CSS tokens (--rila-index / --rila-risk in
// styles.css, --accent in the shared theme) so they follow light/dark.
function hexToRgba(hex, alpha) {
  const h = (hex || "").replace("#", "");
  if (h.length !== 6) return hex;
  const n = parseInt(h, 16);
  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
}
const RISK_BAND_DEPTH = 20; // illustrative extra depth (pp) shown below the buffer — loss beyond the buffer is not actually capped at this

// Draws the two final-period bars (Index Return, RILA Return) *and* the
// Downside Protection / Risk of Loss bands directly on the canvas, all in
// one plugin, rather than as Chart.js bar datasets. Two real bugs pushed
// this here, both verified live, not hypothetical:
//   1. Floating-bar ([start,end]) data combined with a shared `stack`
//      collapses to zero height in Chart.js 4.4.4.
//   2. `barThickness` (needed so the bars are wide enough to read — see
//      ChartPanel) breaks Chart.js's automatic side-by-side grouping offset
//      for multiple bar datasets sharing one category: the offset between
//      grouped bars is computed from the category-percentage width *before*
//      barThickness overrides it, so two barThickness-sized bars ended up
//      drawn almost exactly on top of each other instead of side by side.
// Drawing everything directly sidesteps both and gives the exact pixel
// control needed to match the reference chart's proportions (bars flush
// left/right of each other, bands spanning their combined width).
function makeFinalPeriodPlugin({
  lastIdx, barThickness, gap,
  indexValue, rilaValue, indexFill, indexStroke, rilaFill,
  bufferBottom, riskBottom, protectionColor, riskFill,
}) {
  return {
    id: "finalPeriod",
    afterDatasetsDraw(chart) {
      const xScale = chart.scales.x;
      const yScale = chart.scales.y;
      const tickX = xScale.getPixelForValue(lastIdx);
      const leftX = tickX - gap / 2 - barThickness;
      const rightX = tickX + gap / 2;
      const yZero = yScale.getPixelForValue(0);
      const { ctx } = chart;

      function drawBar(x, value, fillStyle, strokeStyle) {
        const yVal = yScale.getPixelForValue(value);
        const top = Math.min(yZero, yVal);
        const height = Math.max(1, Math.abs(yVal - yZero));
        ctx.save();
        ctx.fillStyle = fillStyle;
        ctx.fillRect(x, top, barThickness, height);
        if (strokeStyle) {
          ctx.strokeStyle = strokeStyle;
          ctx.lineWidth = 1;
          ctx.strokeRect(x + 0.5, top + 0.5, barThickness - 1, height - 1);
        }
        ctx.restore();
        return { centerX: x + barThickness / 2, top };
      }

      const spanLeft = leftX;
      const spanRight = rightX + barThickness;
      const yBuffer = yScale.getPixelForValue(bufferBottom);
      const yRisk = yScale.getPixelForValue(riskBottom);
      ctx.save();
      ctx.fillStyle = protectionColor;
      ctx.fillRect(spanLeft, yZero, spanRight - spanLeft, yBuffer - yZero);
      ctx.fillStyle = riskFill;
      ctx.fillRect(spanLeft, yBuffer, spanRight - spanLeft, yRisk - yBuffer);
      ctx.restore();

      const left = drawBar(leftX, indexValue, indexFill, indexStroke);
      const right = drawBar(rightX, rilaValue, rilaFill, null);

      ctx.save();
      ctx.textAlign = "center";
      ctx.font = "700 16px Outfit, sans-serif";
      const fmt = (v) => (v >= 0 ? "+" : "") + v.toFixed(1) + "%";
      ctx.fillStyle = indexStroke;
      ctx.fillText(fmt(indexValue), left.centerX, left.top - 8);
      ctx.fillStyle = rilaFill;
      ctx.fillText(fmt(rilaValue), right.centerX, right.top - 8);
      ctx.restore();
    },
  };
}

function ChartPanel({ result, indexLabel, termYears, bufferPct, purchasePayment, theme }) {
  const canvasRef = useRef(null);
  const wrapRef = useRef(null);
  const chartRef = useRef(null);
  // Chart.js's own responsive:true (ResizeObserver-driven) never completes
  // a real draw, and neither does driving our own ResizeObserver — verified
  // live, both leave the canvas correctly *sized* but with genuinely zero
  // pixels painted. ResizeObserver callbacks are tied to the render/paint
  // pipeline, which doesn't run in an undisplayed/non-compositing preview
  // pane. A synchronous clientWidth read at the moment of mount is *also*
  // unreliable (reads 0 — layout hasn't run yet for just-inserted DOM).
  // What works: defer past the current tick with setTimeout so layout has
  // actually run at least once, then read synchronously (a plain layout
  // query, unlike ResizeObserver, doesn't need a compositor).
  const [containerWidth, setContainerWidth] = useState(0);

  useEffect(() => {
    const measure = () => { if (wrapRef.current) setContainerWidth(wrapRef.current.clientWidth); };
    const t = setTimeout(measure, 400);
    window.addEventListener("resize", measure);
    return () => { clearTimeout(t); window.removeEventListener("resize", measure); };
  }, []);

  const yearRow = useMemo(() => {
    const rows = [];
    for (let y = 1; y <= termYears; y++) {
      rows.push({ year: y, value: y === termYears ? result.finalValue : purchasePayment });
    }
    return rows;
  }, [termYears, purchasePayment, result.finalValue]);

  useEffect(() => {
    if (!containerWidth) return; // wait for the ResizeObserver's first real measurement

    const styles = getComputedStyle(document.documentElement);
    const ink = styles.getPropertyValue("--ink").trim() || "#333333";
    const inkMuted = styles.getPropertyValue("--ink-muted").trim() || "rgba(51,51,51,0.55)";
    const accent = styles.getPropertyValue("--accent").trim() || "#C4A055";
    const accentDimBd = styles.getPropertyValue("--accent-dim-bd").trim() || "rgba(196,160,85,0.25)";
    const indexColor = styles.getPropertyValue("--rila-index").trim() || "#8AA4D0";
    const riskColor = styles.getPropertyValue("--rila-risk").trim() || "#E07070";
    const gridColor = "rgba(148,148,148,0.14)";

    const snapshots = result.snapshots;
    const lastIdx = snapshots.length - 1;
    const bufferBottom = -bufferPct;
    const riskBottom = Math.max(-(bufferPct + RISK_BAND_DEPTH), -100);

    const chartHeight = 400;
    // The two final-period bars sit inside a single month-wide category
    // slot (one of termYears*12+1 categories) — at any %-of-category width
    // they'd be a few px, unreadably thin. barThickness (used below) sets
    // an absolute pixel width instead, sized off the *year*-cell width
    // (matching the value-row above) so the two bars combined read as
    // wider than one value-row cell, per the reference chart's
    // proportions. Capped so a 1-year term (one giant "cell") doesn't
    // produce absurdly fat bars.
    const yearCellWidth = containerWidth / termYears;
    const barThickness = Math.max(50, Math.min(130, yearCellWidth * 0.65));

    // Chart.js has no `options.width`/`options.height` — non-responsive
    // sizing comes from the canvas element's own width/height attributes
    // (plain HTML canvas behavior). Without setting these explicitly, the
    // canvas silently falls back to the browser default of 300x150, which
    // is exactly the bug this was: `options: {width, height}` looked
    // plausible but isn't a real Chart.js option, so it was silently
    // ignored — verified by finding canvasW/H stuck at 300x150 regardless
    // of containerWidth. Set the attributes to the plain logical (CSS)
    // size and leave devicePixelRatio scaling entirely to Chart.js's own
    // `options.devicePixelRatio` below — pre-multiplying by dpr here too
    // double-applies it (Chart.js scales whatever backing-store size it
    // finds), which measured out as literally dpr² on a dpr=2.5 display.
    const dpr = window.devicePixelRatio || 1;
    canvasRef.current.width = containerWidth;
    canvasRef.current.height = chartHeight;
    canvasRef.current.style.width = containerWidth + "px";
    canvasRef.current.style.height = chartHeight + "px";

    const ctx = canvasRef.current.getContext("2d");

    if (chartRef.current) chartRef.current.destroy();
    chartRef.current = new Chart(ctx, {
      data: {
        labels: snapshots.map((s) => (s.isYearMark ? "Year " + s.yearNumber : "")),
        datasets: [
          {
            type: "line",
            label: indexLabel + " Performance",
            data: snapshots.map((s) => s.indexPct),
            borderColor: indexColor,
            borderDash: [6, 4],
            backgroundColor: "transparent",
            fill: false,
            tension: 0.25,
            borderWidth: 2,
            pointRadius: 0,
          },
        ],
      },
      plugins: [
        makeFinalPeriodPlugin({
          lastIdx,
          barThickness,
          gap: 10,
          indexValue: result.indexChange * 100,
          rilaValue: result.creditRate * 100,
          indexFill: window.pattern ? window.pattern.draw("diagonal-right-left", hexToRgba(indexColor, 0.6)) : hexToRgba(indexColor, 0.5),
          indexStroke: indexColor,
          rilaFill: accent,
          bufferBottom,
          riskBottom,
          protectionColor: styles.getPropertyValue("--accent-dim-bg").trim() || "rgba(196,160,85,0.09)",
          riskFill: window.pattern ? window.pattern.draw("disc", hexToRgba(riskColor, 0.65)) : hexToRgba(riskColor, 0.5),
        }),
      ],
      options: {
        responsive: false,
        devicePixelRatio: dpr,
        // Default bar/line animation is requestAnimationFrame-driven, which
        // never fires in a non-compositing preview pane — verified live,
        // this was the last piece: sized correctly, still zero pixels
        // painted, until animation was turned off entirely.
        animation: false,
        interaction: { mode: "index", intersect: false },
        plugins: {
          legend: { display: false },
          tooltip: {
            backgroundColor: styles.getPropertyValue("--surface-2").trim() || "#FFFFFF",
            titleColor: ink,
            bodyColor: ink,
            titleFont: { family: "Outfit", size: 12, weight: "600" },
            bodyFont: { family: "JetBrains Mono", size: 11 },
            padding: 10,
            borderColor: accentDimBd,
            borderWidth: 1,
            callbacks: {
              label: (c) => c.dataset.label + ": " + (c.raw >= 0 ? "+" : "") + c.raw.toFixed(1) + "%",
            },
          },
        },
        scales: {
          x: {
            // Only draw gridlines/labels at whole-year marks, not at all
            // termYears*12+1 monthly categories — with every month gridded,
            // the axis was an illegible wall of thin lines. The underlying
            // category scale still spans every month (bar/line positions
            // are unaffected), this only trims which ticks actually render.
            afterBuildTicks: (axis) => {
              axis.ticks = axis.ticks.filter((t) => snapshots[t.value] && snapshots[t.value].isYearMark);
            },
            grid: { color: gridColor, display: true },
            ticks: { color: inkMuted, font: { family: "Outfit", size: 14, weight: "600" } },
          },
          y: {
            grid: { color: gridColor },
            ticks: { color: inkMuted, font: { family: "JetBrains Mono", size: 13 }, callback: (v) => v + "%" },
          },
        },
      },
    });

    return () => { if (chartRef.current) chartRef.current.destroy(); };
  }, [result, indexLabel, bufferPct, containerWidth, theme]);

  return (
    <div className="glass-panel chart-panel">
      <div className="chart-head">
        <div>
          <div className="chart-title"><Icon.Chart />{indexLabel} Return vs. RILA Return</div>
          <div className="chart-sub">Monthly index path across the {termYears}-year term — Point-to-Point only credits once, at maturity</div>
        </div>
      </div>
      <div className="chart-value-row">
        {yearRow.map((r) => (
          <div className="chart-value-cell" key={r.year}>{formatCurrency(r.value)}</div>
        ))}
      </div>
      <div className="chart-canvas-wrap" ref={wrapRef}><canvas ref={canvasRef}></canvas></div>
      <div className="chart-legend-custom">
        <span><i className="legend-swatch legend-swatch-protection" />Downside Protection</span>
        <span><i className="legend-swatch legend-swatch-risk" />Risk Of Loss</span>
        <span><i className="legend-swatch legend-swatch-line" />{indexLabel} Performance</span>
        <span><i className="legend-swatch legend-swatch-index" />{indexLabel} Return</span>
        <span><i className="legend-swatch legend-swatch-rila" />RILA Return</span>
      </div>
    </div>
  );
}

/* ---------- Footer -------------------------------------------------------------- */

function SiteFooter() {
  return (
    <footer className="site-footer">
      <div className="footer-inner">
        <div className="footer-brand">RILA Calculator · Zhijie Tan</div>
        <p className="footer-disclaimer">
          Educational illustration only, not affiliated with or endorsed by any insurance carrier. Index levels are real daily closing prices for the
          underlying index only (price return, no dividends reinvested) — S&P 500 and Nasdaq-100 and Russell 2000 and Dow Jones Industrial Average from
          public market data, not a licensed annuity product's actual crediting history. Cap rate, participation rate, buffer, and segment fee are
          entered by you and do not reflect any specific insurer's current or historical rates. Segment fee is modeled as a daily-compounding annual
          deduction, an approximation of how insurers describe "deducted daily." Past index performance does not predict future results. Consult a
          licensed financial professional and the actual product prospectus before making any annuity purchase decision.
        </p>
      </div>
    </footer>
  );
}

/* ---------- App ------------------------------------------------------------------ */

function App() {
  const [theme, toggleTheme] = useTheme();
  const { loading, error, data } = useIndexData();

  const [purchasePayment, setPurchasePayment] = useState(100000);
  const [index, setIndex] = useState("SPX");
  const [termYears, setTermYears] = useState(6);
  const [bufferPct, setBufferPct] = useState(10);
  const [capEnabled, setCapEnabled] = useState(false); // false = "No Cap"
  const [capPct, setCapPct] = useState(10);
  const [participationPct, setParticipationPct] = useState(100);
  const [feePct, setFeePct] = useState(0);
  const [scenario, setScenario] = useState("recent");
  const [customDate, setCustomDate] = useState("2015-01-02");

  const series = data[index];

  const dataRangeLabel = series
    ? `Real daily data: ${formatDateDisplay(series.dates[0])} – ${formatDateDisplay(series.dates[series.dates.length - 1])}`
    : "";

  const minDate = series ? getEarliestStart(series) : "1981-01-01";
  const maxDate = series ? getLatestStart(series, termYears) : "2020-01-01";

  // Keep the custom date valid whenever index/term changes underneath it.
  useEffect(() => {
    if (!series) return;
    if (customDate < minDate) setCustomDate(minDate);
    else if (customDate > maxDate) setCustomDate(maxDate);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [index, termYears, series]);

  const bestWorst = useMemo(() => (series ? scanBestWorst(series, termYears) : null), [series, termYears]);

  const startDate = useMemo(() => {
    if (!series) return null;
    if (scenario === "recent") return getLatestStart(series, termYears);
    if (scenario === "best") return bestWorst?.best?.startDate ?? getLatestStart(series, termYears);
    if (scenario === "worst") return bestWorst?.worst?.startDate ?? getLatestStart(series, termYears);
    return customDate;
  }, [series, scenario, termYears, bestWorst, customDate]);

  const result = useMemo(() => {
    if (!series || !startDate) return null;
    return computeSegment({
      series, startDate, termYears, bufferPct,
      capPct: capEnabled ? capPct : null,
      participationPct, feePct, purchasePayment,
    });
  }, [series, startDate, termYears, bufferPct, capEnabled, capPct, participationPct, feePct, purchasePayment]);

  return (
    <div className="app-shell">
      <div className="bg-halo" />
      <SiteHeader theme={theme} onToggleTheme={toggleTheme} />

      <main className="page">
        <div className="hero-block">
          <span className="eyebrow">RILA · Buffered Index-Linked Annuity</span>
          <h1 className="hero-title">See how a buffered segment would have <em>actually</em> performed.</h1>
          <p className="hero-sub">
            A Registered Index-Linked Annuity (RILA) credits interest based on an index's move, inside a Cap and Participation Rate on the upside,
            with a Buffer absorbing the first slice of any loss. This tool runs that math against real historical closing prices for four major
            indexes — pick any segment start date, or jump straight to the best, worst, or most recent historical outcome.
          </p>
        </div>

        {loading && <div className="glass-panel loading-panel">Loading historical index data…</div>}
        {error && <div className="glass-panel loading-panel">Couldn't load index data: {error}</div>}

        {!loading && !error && series && (
          <React.Fragment>
            <ControlsPanel
              purchasePayment={purchasePayment} setPurchasePayment={setPurchasePayment}
              index={index} setIndex={setIndex}
              termYears={termYears} setTermYears={setTermYears}
              bufferPct={bufferPct} setBufferPct={setBufferPct}
              capEnabled={capEnabled} setCapEnabled={setCapEnabled}
              capPct={capPct} setCapPct={setCapPct}
              participationPct={participationPct} setParticipationPct={setParticipationPct}
              feePct={feePct} setFeePct={setFeePct}
              dataRangeLabel={dataRangeLabel}
            />

            <ScenarioPanel
              scenario={scenario} setScenario={setScenario}
              customDate={customDate} setCustomDate={setCustomDate}
              minDate={minDate} maxDate={maxDate}
            />

            {result && (
              <React.Fragment>
                <ResultsPanel
                  result={result} purchasePayment={purchasePayment} feePct={feePct}
                  indexLabel={INDEX_META[index].label} termYears={termYears}
                  bufferPct={bufferPct} capEnabled={capEnabled} capPct={capPct}
                  participationPct={participationPct}
                />
                <ChartPanel result={result} indexLabel={INDEX_META[index].label} termYears={termYears} bufferPct={bufferPct} purchasePayment={purchasePayment} theme={theme} />
              </React.Fragment>
            )}
          </React.Fragment>
        )}
      </main>

      <SiteFooter />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
