// BudgetPage (Platform rebuild) — window.BudgetPage
//
// Budget vs Actual planning page. Pulls budget targets from the `budgets`
// table (company_id, fiscal_year, period_type, line_item, budget_amount,
// period_label) and compares them against windowed actuals rolled up from
// plHistory (revenue / COGS / gross profit / OpEx / EBITDA / net income).
//
// Renders four variance KPI tiles, a P&L bridge with per-line variance bars and
// F/U flags, a ranked top-variances table with a materiality filter, and a CFO
// commentary panel whose every number is derived from the real computed
// variances — nothing is fabricated. When no budget rows exist, an upload card
// is shown that parses a client-side CSV and inserts the rows into Supabase.
//
// Props: { data, companyProfile, scopedCompanyId, globalPeriod, setPage }

(function () {
  const h = React.createElement;
  const { useState, useEffect, useMemo } = React;

  // Favorable direction per line: revenue/profit lines want actual ABOVE budget;
  // cost lines want actual BELOW budget.
  const FAVOR_HIGHER = "higher"; // actual > budget = favorable
  const FAVOR_LOWER = "lower";   // actual < budget = favorable

  // Normalise a line-item label into a comparable key for matching CSV rows to
  // the canonical P&L rows.
  function normKey(s) {
    return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
  }

  // Synonym buckets so a CSV "Sales" / "Total Revenue" both map to revenue, etc.
  const LINE_SYNONYMS = [
    { key: "revenue", words: ["revenue", "sales", "total revenue", "net revenue", "income", "turnover"] },
    { key: "cogs", words: ["cogs", "cost of goods sold", "cost of sales", "cost of revenue", "direct costs"] },
    { key: "gross_profit", words: ["gross profit", "gross margin", "gp"] },
    { key: "opex", words: ["opex", "operating expenses", "operating expense", "total opex", "overhead", "sg a", "sga"] },
    { key: "ebitda", words: ["ebitda", "operating income", "operating profit"] },
    { key: "net_income", words: ["net income", "net profit", "net earnings", "bottom line", "profit"] },
  ];
  function canonLineKey(label) {
    const nk = normKey(label);
    for (const b of LINE_SYNONYMS) { if (b.words.indexOf(nk) >= 0) return b.key; }
    // partial fallback
    for (const b of LINE_SYNONYMS) { for (const w of b.words) { if (nk.indexOf(w) >= 0) return b.key; } }
    return null;
  }

  // F/U pill.
  function FlagPill(props) {
    const fav = props.fav;
    const bg = fav ? "rgba(5,150,105,.12)" : "rgba(220,38,38,.12)";
    const col = fav ? "#059669" : "#DC2626";
    return h("span", { style: { display: "inline-block", minWidth: 18, textAlign: "center", padding: "2px 8px", borderRadius: 999, fontSize: 11, fontWeight: 800, color: col, background: bg, fontFamily: "'JetBrains Mono',monospace" } }, fav ? "F" : "U");
  }

  // Horizontal status bar: budget = full track, actual fills proportionally.
  // Green when favorable, red when unfavorable. ~100px wide.
  function StatusBar(props) {
    const budget = Math.abs(Number(props.budget) || 0);
    const actual = Math.abs(Number(props.actual) || 0);
    const fav = props.fav;
    const track = budget || actual || 1;
    const pct = Math.max(0, Math.min(1, actual / track));
    const fill = fav ? "#059669" : "#DC2626";
    return h("div", { title: "Actual " + (pct * 100).toFixed(0) + "% of budget", style: { width: 100, height: 12, borderRadius: 6, background: "rgba(13,32,64,.08)", overflow: "hidden", position: "relative" } },
      h("div", { style: { width: (pct * 100) + "%", height: "100%", background: fill, borderRadius: 6, transition: "width .5s ease" } }));
  }

  function Page(props) {
    const K = window.PerduraPageKit;
    if (!K) return h("div", { className: "pc-page" }, "Loading…");
    const { data, scopedCompanyId } = props;
    const F = window.PerduraFormat;
    const M = (v) => K.moneyStr(v, { compact: true });
    const moneyEl = (v) => (F && F.money) ? F.money(v, { compact: true }) : M(v);

    const plH = (data && (data.plHistory || data.pl)) || { labels: [], years: [], revenue: [], cogs: [], opex: [], gp: [], ebitda: [] };
    const anchor = K.anchorFromPlH(plH);
    const ps = K.usePeriodState("budget", "ltm");
    const range = K.resolvePeriod(ps.mode, anchor, ps.custom);
    const span = K.plIdxRange(plH, range);

    // ── windowed actual rollups from plHistory ────────────────────────────────
    const actRevenue = K.sumIdx(plH.revenue, span);
    const actCogs = K.sumIdx(plH.cogs, span);
    const actOpex = K.sumIdx(plH.opex, span);
    const actGp = (plH.gp && plH.gp.length) ? K.sumIdx(plH.gp, span) : (actRevenue - actCogs);
    const actEbitda = (plH.ebitda && plH.ebitda.length) ? K.sumIdx(plH.ebitda, span) : (actGp - actOpex);
    // Net income: prefer an explicit LTM figure when present, else approximate
    // with the EBITDA series (no D&A / interest / tax detail in plHistory here).
    const actNetIncome = (data && data.net_income_ltm != null && ps.mode === "ltm") ? Number(data.net_income_ltm) : actEbitda;

    // Fiscal year for the active window (anchor year).
    const fiscalYear = anchor.getFullYear();

    // GL transactions windowed to the active period — used to derive a real
    // actual for budget sub-lines (e.g. Marketing, Rent) that the canonical
    // plHistory rollups can't separate. Matched by canonical_category /
    // account_name, summed by absolute amount.
    const winTxns = useMemo(function () {
      return K.windowTxns((data && data.txns) || [], range);
    }, [data, +range.start, +range.end]);

    function matchActual(label) {
      const li = normKey(label);
      if (!li) return 0;
      let sum = 0;
      for (let i = 0; i < winTxns.length; i++) {
        const t = winTxns[i];
        const cat = normKey(t.canonical_category);
        const acct = normKey(t.account_name);
        const hit = (cat && (cat.indexOf(li) >= 0 || li.indexOf(cat) >= 0))
          || (acct && (acct.indexOf(li) >= 0 || li.indexOf(acct) >= 0));
        if (hit) sum += Math.abs(Number(t.amount) || 0);
      }
      return sum;
    }

    // ── budget fetch ──────────────────────────────────────────────────────────
    const [budgetRows, setBudgetRows] = useState(null); // null = loading, [] = none
    const [reloadTick, setReloadTick] = useState(0);

    useEffect(() => {
      let cancelled = false;
      const db = window.supabaseClient;
      if (!db || !scopedCompanyId) { setBudgetRows([]); return; }
      setBudgetRows(null);
      db.from("budget_targets").select("*").eq("company_id", scopedCompanyId)
        .then(({ data: d }) => { if (!cancelled) setBudgetRows(d || []); },
          () => { if (!cancelled) setBudgetRows([]); });
      return () => { cancelled = true; };
    }, [scopedCompanyId, reloadTick]);

    const loading = budgetRows === null;
    const hasBudget = !loading && budgetRows.length > 0;

    // Sum budget_amount by canonical line key across all matching rows.
    const budgetByKey = useMemo(function () {
      const m = {};
      (budgetRows || []).forEach(function (r) {
        const key = canonLineKey(r.line_item);
        const amt = Number(r.budget_amount) || 0;
        if (key) { m[key] = (m[key] || 0) + amt; }
        // Keep raw label rows that don't map to a canonical bucket so OpEx
        // sub-categories can still surface in the bridge / top-variances.
      });
      return m;
    }, [budgetRows]);

    // Distinct raw budget line items (for OpEx sub-categories & extra rows).
    const rawBudgetLines = useMemo(function () {
      const m = {};
      (budgetRows || []).forEach(function (r) {
        const label = String(r.line_item || "").trim();
        if (!label) return;
        const key = canonLineKey(label);
        const id = key || ("raw::" + normKey(label));
        const e = m[id] || (m[id] = { id: id, key: key, label: label, budget: 0 });
        e.budget += Number(r.budget_amount) || 0;
      });
      return Object.values(m);
    }, [budgetRows]);

    // ── canonical bridge rows (Budget vs Actual) ──────────────────────────────
    // Each: { label, budget, actual, favorDir, isMargin }
    const bridgeRows = useMemo(function () {
      if (!hasBudget) return [];
      const bk = budgetByKey;
      const rows = [];
      const push = function (label, key, actual, favorDir, isMargin) {
        const budget = bk[key];
        if (budget == null && actual == null) return;
        rows.push({ label: label, key: key, budget: budget == null ? null : budget, actual: actual == null ? null : actual, favorDir: favorDir, isMargin: !!isMargin });
      };
      push("Revenue", "revenue", actRevenue, FAVOR_HIGHER, false);
      push("COGS", "cogs", actCogs, FAVOR_LOWER, false);
      push("Gross Profit", "gross_profit", actGp, FAVOR_HIGHER, true);

      // OpEx sub-categories from raw budget lines that didn't map to a canonical
      // bucket — actuals for sub-lines aren't separable from plHistory, so show
      // budget only (actual omitted) to stay honest. The rolled-up OpEx line
      // carries the real actual.
      const subOpex = rawBudgetLines.filter(function (r) { return r.key == null; });
      subOpex.forEach(function (r) {
        // Derive a real actual from windowed GL txns. A 0 match means nothing
        // mapped to this line — stay honest and leave actual null rather than
        // fabricating a full-budget miss.
        const matched = matchActual(r.label);
        rows.push({ label: r.label, key: r.id, budget: r.budget, actual: matched > 0 ? matched : null, favorDir: FAVOR_LOWER, isMargin: false, subLine: true });
      });
      push("Operating Expenses", "opex", actOpex, FAVOR_LOWER, false);
      push("EBITDA", "ebitda", actEbitda, FAVOR_HIGHER, true);
      push("Net Income", "net_income", actNetIncome, FAVOR_HIGHER, true);
      return rows;
    }, [hasBudget, budgetByKey, rawBudgetLines, winTxns, actRevenue, actCogs, actGp, actOpex, actEbitda, actNetIncome]);

    // Variance computation helper.
    const computeVar = function (row) {
      const b = row.budget, a = row.actual;
      const hasBoth = b != null && a != null;
      const varAmt = hasBoth ? (a - b) : null;
      const varPct = (hasBoth && b !== 0) ? ((a - b) / Math.abs(b) * 100) : null;
      // Favorable: higher-is-better lines → actual > budget; cost lines → actual < budget.
      let fav = null;
      if (hasBoth) {
        fav = row.favorDir === FAVOR_HIGHER ? (a >= b) : (a <= b);
      }
      return { varAmt: varAmt, varPct: varPct, fav: fav, hasBoth: hasBoth };
    };

    // Materiality threshold: 0.5% of total revenue budget.
    const revBudget = budgetByKey.revenue || 0;
    const materiality = Math.abs(revBudget) * 0.005;

    // ── KPI tiles (Revenue / Gross Profit / EBITDA / Net Income) ───────────────
    const kpiDefs = [
      { label: "Total Revenue", key: "revenue", actual: actRevenue, favorDir: FAVOR_HIGHER },
      { label: "Gross Profit", key: "gross_profit", actual: actGp, favorDir: FAVOR_HIGHER },
      { label: "EBITDA", key: "ebitda", actual: actEbitda, favorDir: FAVOR_HIGHER },
      { label: "Net Income", key: "net_income", actual: actNetIncome, favorDir: FAVOR_HIGHER },
    ];
    const kpis = kpiDefs.map(function (d) {
      const budget = budgetByKey[d.key];
      const cv = computeVar({ budget: budget == null ? null : budget, actual: d.actual, favorDir: d.favorDir });
      let delta = null, deltaDir = "flat", sub;
      if (cv.hasBoth) {
        const sign = cv.varAmt >= 0 ? "+" : "−";
        delta = sign + M(Math.abs(cv.varAmt)) + (cv.varPct != null ? " · " + sign + Math.abs(cv.varPct).toFixed(1) + "%" : "");
        deltaDir = cv.fav ? "up" : "dn";
        sub = "Budget " + M(budget) + " · " + (cv.fav ? "Favorable" : "Unfavorable");
      } else {
        sub = hasBudget ? "No budget line for this metric" : "Awaiting budget";
      }
      return { label: d.label, value: M(d.actual), delta: delta, deltaDir: deltaDir, sub: sub, valueColor: "navy" };
    });

    // ── upload-card state ─────────────────────────────────────────────────────
    const [parsed, setParsed] = useState(null);   // [{ line_item, budget_amount }]
    const [parseErr, setParseErr] = useState(null);
    const [saving, setSaving] = useState(false);
    const [saveErr, setSaveErr] = useState(null);

    function downloadBudgetTemplate() {
      const csv = [
        "line_item,budget_amount,period",
        "Revenue,500000,FY" + fiscalYear,
        "Cost of Goods Sold,300000,FY" + fiscalYear,
        "Salaries & Wages,80000,FY" + fiscalYear,
        "Rent,24000,FY" + fiscalYear,
        "Marketing,15000,FY" + fiscalYear,
        "Software & Technology,12000,FY" + fiscalYear,
      ].join("\n");
      const a = document.createElement("a");
      a.href = "data:text/csv;charset=utf-8," + encodeURIComponent(csv);
      a.download = "budget_template.csv";
      a.click();
    }

    function handleFile(e) {
      setParseErr(null); setParsed(null); setSaveErr(null);
      const file = e.target.files && e.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = function (ev) {
        try {
          const text = String(ev.target.result || "");
          const lines = text.split(/\r?\n/).map(function (l) { return l.trim(); }).filter(Boolean);
          if (!lines.length) { setParseErr("File is empty."); return; }
          // Detect header → find line_item / budget_amount columns.
          const splitRow = function (l) { return l.split(",").map(function (c) { return c.replace(/^"|"$/g, "").trim(); }); };
          const head = splitRow(lines[0]).map(function (c) { return c.toLowerCase(); });
          let liCol = head.indexOf("line_item");
          if (liCol < 0) liCol = head.indexOf("line item");
          let baCol = head.indexOf("budget_amount");
          if (baCol < 0) baCol = head.indexOf("budget amount");
          if (baCol < 0) baCol = head.indexOf("budget");
          let perCol = head.indexOf("period");
          if (perCol < 0) perCol = head.indexOf("period_label");
          let start = 1;
          if (liCol < 0 || baCol < 0) { liCol = 0; baCol = 1; start = 0; } // no header → assume first two columns
          const out = [];
          for (let i = start; i < lines.length; i++) {
            const cells = splitRow(lines[i]);
            const label = cells[liCol];
            const amtRaw = (cells[baCol] || "").replace(/[$,]/g, "");
            const amt = Number(amtRaw);
            if (!label || !isFinite(amt)) continue;
            const period = (perCol >= 0 && cells[perCol]) ? cells[perCol] : ("FY" + fiscalYear);
            out.push({ line_item: label, budget_amount: amt, period: period });
          }
          if (!out.length) { setParseErr("No valid rows found. Expected columns: line_item, budget_amount."); return; }
          setParsed(out);
        } catch (err) { setParseErr("Could not parse CSV: " + (err && err.message ? err.message : "unknown error")); }
      };
      reader.onerror = function () { setParseErr("Could not read file."); };
      reader.readAsText(file);
    }

    function confirmUpload() {
      if (!parsed || !parsed.length) return;
      const db = window.supabaseClient;
      if (!db || !scopedCompanyId) { setSaveErr("Not connected — cannot save."); return; }
      setSaving(true); setSaveErr(null);
      const rows = parsed.map(function (r) {
        return { company_id: scopedCompanyId, fiscal_year: fiscalYear, line_item: r.line_item, budget_amount: r.budget_amount, period_label: r.period || ("FY" + fiscalYear), period_type: "annual" };
      });
      // Idempotent replace: clear this fiscal year's rows then insert. The table
      // has no unique constraint on (company_id, fiscal_year, line_item), so an
      // upsert/onConflict would error — delete-then-insert avoids duplicate
      // accumulation on re-upload while staying scoped to this FY.
      db.from("budget_targets").delete().eq("company_id", scopedCompanyId).eq("fiscal_year", fiscalYear)
        .then(function (delRes) {
          if (delRes && delRes.error) throw new Error(delRes.error.message);
          return db.from("budget_targets").insert(rows);
        })
        .then(function (res) {
          setSaving(false);
          if (res && res.error) { setSaveErr("Save failed: " + res.error.message); return; }
          setParsed(null);
          setReloadTick(function (t) { return t + 1; });
          window.logAuditEvent && window.logAuditEvent("data_upload", { category: "data", page: "budget", detail: "Budget uploaded: " + rows.length + " line items" });
        })
        .catch(function (err) {
          setSaving(false);
          setSaveErr("Save failed: " + (err && err.message ? err.message : "unknown error"));
        });
    }

    // ── render: upload card (no budget) ───────────────────────────────────────
    function renderUploadCard() {
      return h(K.Card, { title: "CONNECT YOUR BUDGET", sub: "Upload a CSV to start tracking budget vs actual" },
        h("div", { style: { padding: "4px 4px 8px" } },
          h("div", { style: { fontSize: 16, fontWeight: 800, color: "#0d2040", marginBottom: 6 } }, "No budget connected"),
          h("div", { style: { fontSize: 13, color: "#4a5680", lineHeight: 1.6, marginBottom: 14, maxWidth: 620 } },
            "Upload a CSV with columns ",
            h("code", { style: { background: "rgba(13,32,64,.06)", padding: "1px 6px", borderRadius: 4, fontFamily: "'JetBrains Mono',monospace", fontSize: 12 } }, "line_item, budget_amount"),
            ". Use line items like Revenue, COGS, Gross Profit, Operating Expenses, EBITDA and Net Income (sub-categories are allowed). Amounts may include $ and commas.",
            " Budget rows will be saved for fiscal year ",
            h("b", null, String(fiscalYear)), "."),
          h("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10 } },
            h("button", { onClick: downloadBudgetTemplate, style: { display: "inline-flex", alignItems: "center", gap: 8, cursor: "pointer", background: "#fff", color: "#1C4ED8", fontWeight: 700, fontSize: 13, padding: "9px 16px", borderRadius: 8, border: "1px solid #1C4ED8" } }, "⭳ Download sample CSV"),
            h("label", { style: { display: "inline-flex", alignItems: "center", gap: 8, cursor: "pointer", background: "#1C4ED8", color: "#fff", fontWeight: 700, fontSize: 13, padding: "9px 16px", borderRadius: 8 } },
              "⭱ Choose CSV file",
              h("input", { type: "file", accept: ".csv", onChange: handleFile, style: { display: "none" } }))),
          parseErr ? h("div", { style: { marginTop: 12, color: "#DC2626", fontSize: 12.5, fontWeight: 600 } }, "⚠ " + parseErr) : null,
          parsed ? h("div", { style: { marginTop: 16 } },
            h("div", { style: { fontSize: 12, fontWeight: 700, color: "#0d2040", marginBottom: 8 } }, "Preview — " + parsed.length + " line item" + (parsed.length === 1 ? "" : "s")),
            h("div", { style: { overflowX: "auto", border: "1px solid rgba(13,32,64,.10)", borderRadius: 8, maxHeight: 280 } },
              h("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 12.5 } },
                h("thead", null, h("tr", { style: { background: "#0d2040" } },
                  h("th", { style: { textAlign: "left", padding: "8px 12px", color: "#fff", fontSize: 11, fontWeight: 700 } }, "LINE ITEM"),
                  h("th", { style: { textAlign: "right", padding: "8px 12px", color: "#fff", fontSize: 11, fontWeight: 700 } }, "BUDGET AMOUNT"),
                  h("th", { style: { textAlign: "right", padding: "8px 12px", color: "#fff", fontSize: 11, fontWeight: 700 } }, "PERIOD"))),
                h("tbody", null, parsed.map(function (r, i) {
                  return h("tr", { key: i, style: { borderTop: "1px solid #f1f3f7" } },
                    h("td", { style: { textAlign: "left", padding: "6px 12px", fontWeight: 600, color: "#1a2540" } }, r.line_item),
                    h("td", { style: { textAlign: "right", padding: "6px 12px", fontFamily: "'JetBrains Mono',monospace" } }, moneyEl(r.budget_amount)),
                    h("td", { style: { textAlign: "right", padding: "6px 12px", color: "#6475a0", fontFamily: "'JetBrains Mono',monospace", fontSize: 11.5 } }, r.period || ("FY" + fiscalYear)));
                })))),
            saveErr ? h("div", { style: { marginTop: 10, color: "#DC2626", fontSize: 12.5, fontWeight: 600 } }, "⚠ " + saveErr) : null,
            h("button", { onClick: confirmUpload, disabled: saving, style: { marginTop: 14, background: saving ? "#94a3b8" : "#059669", color: "#fff", fontWeight: 700, fontSize: 13, padding: "9px 18px", borderRadius: 8, border: "none", cursor: saving ? "default" : "pointer" } }, saving ? "Saving…" : "✓ Confirm & save budget")
          ) : null));
    }

    // ── render: P&L bridge table ──────────────────────────────────────────────
    function renderBridge() {
      const th = function (txt, align) { return h("th", { style: { textAlign: align || "right", padding: "9px 12px", fontSize: 10, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.85)", whiteSpace: "nowrap" } }, txt); };
      const rowEls = bridgeRows.map(function (r, i) {
        const cv = computeVar(r);
        const marginBg = r.isMargin ? (r.key === "gross_profit" ? "rgba(5,150,105,.05)" : r.key === "ebitda" ? "rgba(217,119,6,.05)" : "rgba(124,58,237,.05)") : null;
        const marginCol = r.isMargin ? (r.key === "gross_profit" ? "#059669" : r.key === "ebitda" ? "#d97706" : "#7c3aed") : "#1a2540";
        return h("tr", { key: r.key || i, style: { background: marginBg, borderTop: r.isMargin ? "1px solid rgba(13,32,64,.10)" : "1px solid #f1f3f7" } },
          h("td", { style: { textAlign: "left", padding: r.subLine ? "6px 12px 6px 24px" : "8px 12px", fontWeight: r.isMargin ? 800 : (r.subLine ? 400 : 600), color: marginCol, fontSize: r.subLine ? 11.5 : 12.5, borderLeft: r.isMargin ? "3px solid " + marginCol : "none" } }, r.label),
          h("td", { style: cellR() }, r.budget == null ? "—" : moneyEl(r.budget)),
          h("td", { style: cellR() }, r.actual == null ? "—" : moneyEl(r.actual)),
          h("td", { style: Object.assign({}, cellR(), { color: cv.hasBoth ? (cv.fav ? "#059669" : "#DC2626") : "#94a3b8", fontWeight: 700 }) }, cv.varAmt == null ? "—" : (cv.varAmt >= 0 ? "+" : "−") + M(Math.abs(cv.varAmt))),
          h("td", { style: Object.assign({}, cellR(), { color: cv.hasBoth ? (cv.fav ? "#059669" : "#DC2626") : "#94a3b8" }) }, cv.varPct == null ? "—" : (cv.varPct >= 0 ? "+" : "−") + Math.abs(cv.varPct).toFixed(1) + "%"),
          h("td", { style: { textAlign: "center", padding: "8px 12px" } }, cv.hasBoth ? h(FlagPill, { fav: cv.fav }) : h("span", { style: { color: "#cbd5e1" } }, "—")),
          h("td", { style: { padding: "8px 12px" } }, cv.hasBoth ? h(StatusBar, { budget: r.budget, actual: r.actual, fav: cv.fav }) : null));
      });
      return h(K.Card, { title: "P&L BRIDGE — BUDGET VS ACTUAL", sub: "Line-by-line variance for " + range.label + " (FY" + fiscalYear + ")", padding: 0 },
        h("div", { style: { overflowX: "auto" } },
          h("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 12.5, minWidth: 720 } },
            h("thead", null, h("tr", { style: { background: "#0d2040" } },
              th("Line Item", "left"), th("Budget"), th("Actual"), th("Variance $"), th("Variance %"), th("F/U", "center"), th("Status", "left"))),
            h("tbody", null, rowEls))));
    }
    function cellR() { return { textAlign: "right", padding: "8px 12px", fontFamily: "'JetBrains Mono',monospace", whiteSpace: "nowrap", color: "#1a2540" }; }

    // ── top variances (materiality-filtered, ranked by |variance $|) ──────────
    const topVariances = useMemo(function () {
      const items = [];
      bridgeRows.forEach(function (r) {
        // Skip pure-margin subtotal rows from "top variances" to avoid
        // double-counting (Revenue/COGS/OpEx drive the same dollars). Keep the
        // operating lines + net income.
        const cv = computeVar(r);
        if (!cv.hasBoth) return;
        if (Math.abs(cv.varAmt) <= materiality) return;
        items.push({ label: r.label, budget: r.budget, actual: r.actual, varAmt: cv.varAmt, varPct: cv.varPct, fav: cv.fav });
      });
      items.sort(function (a, b) { return Math.abs(b.varAmt) - Math.abs(a.varAmt); });
      return items.slice(0, 10);
    }, [bridgeRows, materiality]);

    function renderTopVariances() {
      const th = function (txt, align) { return h("th", { style: { textAlign: align || "right", padding: "9px 12px", fontSize: 10, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.85)", whiteSpace: "nowrap" } }, txt); };
      const maxAbs = topVariances.reduce(function (m, v) { return Math.max(m, Math.abs(v.varAmt)); }, 0) || 1;
      const body = topVariances.length ? topVariances.map(function (v, i) {
        const pctOfBudget = (v.budget && v.budget !== 0) ? (v.varAmt / Math.abs(v.budget) * 100) : null;
        const barPct = Math.abs(v.varAmt) / maxAbs * 100;
        const col = v.fav ? "#059669" : "#DC2626";
        return h("tr", { key: i, style: { borderTop: "1px solid #f1f3f7" } },
          h("td", { style: { textAlign: "center", padding: "8px 12px", color: "#6475a0", fontWeight: 700, fontFamily: "'JetBrains Mono',monospace" } }, i + 1),
          h("td", { style: { textAlign: "left", padding: "8px 12px", fontWeight: 600, color: "#1a2540" } }, v.label),
          h("td", { style: cellR() }, v.budget == null ? "—" : moneyEl(v.budget)),
          h("td", { style: cellR() }, v.actual == null ? "—" : moneyEl(v.actual)),
          h("td", { style: Object.assign({}, cellR(), { color: col, fontWeight: 700 }) }, (v.varAmt >= 0 ? "+" : "−") + M(Math.abs(v.varAmt))),
          h("td", { style: Object.assign({}, cellR(), { color: col }) }, pctOfBudget == null ? "—" : (pctOfBudget >= 0 ? "+" : "−") + Math.abs(pctOfBudget).toFixed(1) + "%"),
          h("td", { style: { textAlign: "center", padding: "8px 12px" } }, h(FlagPill, { fav: v.fav })),
          h("td", { style: { padding: "8px 12px", width: 140 } },
            h("div", { style: { height: 12, borderRadius: 6, background: "rgba(13,32,64,.08)", overflow: "hidden" } },
              h("div", { style: { width: barPct + "%", height: "100%", background: col, borderRadius: 6 } }))));
      }) : [h("tr", { key: "none" }, h("td", { colSpan: 8, style: { padding: 16, fontSize: 13, color: "#6475a0" } }, "No variances exceed the materiality threshold (" + M(materiality) + ", 0.5% of revenue budget) for this period — actuals are tracking close to plan."))];
      return h(K.Card, { title: "TOP VARIANCES", sub: "Ranked by absolute dollar variance · materiality > " + M(materiality) + " (0.5% of revenue budget)", padding: 0 },
        h("div", { style: { overflowX: "auto" } },
          h("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 12.5, minWidth: 640 } },
            h("thead", null, h("tr", { style: { background: "#0d2040" } },
              th("#", "center"), th("Line Item", "left"), th("Budget"), th("Actual"), th("Variance $"), th("Variance %"), th("F/U", "center"), th("", "left"))),
            h("tbody", null, body))));
    }

    // ── CFO variance commentary — every number derived from computed variances ─
    function renderCommentary() {
      const material = [];
      bridgeRows.forEach(function (r) {
        const cv = computeVar(r);
        if (!cv.hasBoth) return;
        if (Math.abs(cv.varAmt) <= materiality) return;
        material.push({ label: r.label, varAmt: cv.varAmt, varPct: cv.varPct, fav: cv.fav });
      });
      material.sort(function (a, b) { return Math.abs(b.varAmt) - Math.abs(a.varAmt); });

      const ebitdaCv = computeVar({ budget: budgetByKey.ebitda == null ? null : budgetByKey.ebitda, actual: actEbitda, favorDir: FAVOR_HIGHER });

      const items = [];
      const favCount = material.filter(function (m) { return m.fav; }).length;
      const unfavCount = material.length - favCount;
      items.push({ icon: "▣", text: "<b>" + material.length + "</b> material variance" + (material.length === 1 ? "" : "s") + " this period (> " + M(materiality) + ", 0.5% of revenue budget): <b>" + favCount + "</b> favorable, <b>" + unfavCount + "</b> unfavorable." });

      material.slice(0, 3).forEach(function (m) {
        const dir = m.fav ? "favorable" : "unfavorable";
        const icon = m.fav ? "◆" : "▼";
        const pctTxt = m.varPct == null ? "" : " (" + (m.varPct >= 0 ? "+" : "−") + Math.abs(m.varPct).toFixed(1) + "%)";
        items.push({ icon: icon, text: "<b>" + m.label + "</b> " + (m.varAmt >= 0 ? "+" : "−") + M(Math.abs(m.varAmt)) + pctTxt + " vs budget — <b style='color:" + (m.fav ? "#059669" : "#DC2626") + "'>" + dir + "</b>." });
      });

      if (ebitdaCv.hasBoth) {
        const dir = ebitdaCv.fav ? "favorable" : "unfavorable";
        items.push({ icon: "➜", text: "<b>Net impact:</b> EBITDA is <b style='color:" + (ebitdaCv.fav ? "#059669" : "#DC2626") + "'>" + dir + "</b> by " + (ebitdaCv.varAmt >= 0 ? "+" : "−") + M(Math.abs(ebitdaCv.varAmt)) + " vs plan (actual " + M(actEbitda) + " vs budget " + M(budgetByKey.ebitda) + ")." });
      } else if (!material.length) {
        items.push({ icon: "➜", text: "Actuals are tracking close to plan — no line exceeds the materiality threshold this period." });
      }
      return h(K.Commentary, { title: "CFO variance commentary", items: items });
    }

    // ── shell ─────────────────────────────────────────────────────────────────
    const subtitle = "Compare actual performance to your annual budget · FY" + fiscalYear + " · " + range.label + (hasBudget ? "" : " · awaiting budget");

    return h(K.Shell, { hero: {
      eyebrow: "PLANNING", title: "Budget vs Actual", subtitle: subtitle,
      controls: h(K.PeriodControls, Object.assign({}, ps, { showCompare: false })),
    } },

      loading
        ? h(K.Card, { title: "Budget vs Actual" }, h("div", { style: { padding: 18, fontSize: 13, color: "#6475a0" } }, "Loading budget…"))
        : !hasBudget
          ? renderUploadCard()
          : h(React.Fragment, null,
            // KPI strip
            h("div", { className: "pa-kpi-strip pa-kpi-strip-4" }, kpis.map(function (k, i) { return h(K.Kpi, Object.assign({ key: i, animDelay: i * 0.05, onClick: () => window.__perduraSetPage && window.__perduraSetPage("income_statement") }, k)); })),
            // Section 1 — bridge
            renderBridge(),
            // Section 2 — top variances
            renderTopVariances(),
            // Section 4 — CFO commentary
            renderCommentary()));
  }

  window.BudgetPage = Page;
})();
