// OpExIntelligencePage (Stage 11) — window.OpExIntelligencePage
// Master design: Pattern-A KPI tiles, Pattern-B multi-series chart (expense bars
// + prior-year dashed + %-of-revenue secondary line), Pattern-C 3-column grid,
// Pattern-D ranked lists (red-scale for vendors), Pattern-E exception cards.

(function () {
  const h = React.createElement;
  const OPEX_SECS = ["opex", "da", "other_op_exp"];
  const num = (v) => Number(v) || 0;
  const RED_SCALE = ["#b91c1c", "#dc2626", "#ef4444", "#f87171", "#fca5a5", "#fecaca", "#fee2e2"];

  function Page(props) {
    const K = window.PerduraPageKit;
    if (!K) return h("div", { className: "pc-page" }, "Loading…");
    const { data, companyProfile, setPage } = props;
    // E2 — drill-through: which GL account's transactions are expanded (or null).
    const [drillAcct, setDrillAcct] = K.useState(null);
    // Two-level category → account expand state for the hierarchical matrix.
    const [expandedCats, setExpandedCats] = K.useState({});
    const toggleCat = (cat) => setExpandedCats((prev) => Object.assign({}, prev, { [cat]: !prev[cat] }));
    const tax = window.PerduraTaxonomy;
    const plH = (data && (data.plHistory || data.pl)) || { labels: [], opex: [], revenue: [] };
    const T = K.ttm(plH);
    const txns = (data && data.txns) || [];
    // Period now comes from the GLOBAL selector (single source of truth shared
    // with every other page) instead of a page-local period hook. We adapt
    // globalPeriod.resolved into the {start,end,label} shape the PageKit window
    // helpers expect, building LOCAL dates from startYM/endYM to match how
    // resolvePeriod builds them (parsing the ISO startDate/endDate strings would
    // drift a day across timezones). Falls back to LTM only if no global period.
    const anchor = K.anchorFromPlH(plH);
    const resolved = (props.globalPeriod && props.globalPeriod.resolved) || null;
    const ymStart = (ym) => new Date(ym.year, ym.month1 - 1, 1);
    const ymEnd = (ym) => new Date(ym.year, ym.month1, 0);
    const range = resolved
      ? { start: ymStart(resolved.startYM), end: ymEnd(resolved.endYM), label: resolved.label }
      : K.resolvePeriod("ltm", anchor);
    const cmpRange = resolved
      ? (resolved.compare && !resolved.compare.budget
          ? { start: ymStart(resolved.compare.startYM), end: ymEnd(resolved.compare.endYM), label: resolved.compare.label }
          : null)
      : K.comparePeriod(range, "prior_year");
    const cmpIsBudget = !!(resolved && resolved.compare && resolved.compare.budget);
    // Month keys ("YYYY-MM") spanned by the selected period — drives KPI counts.
    const periodMonthKeys = resolved
      ? (resolved.months || []).map((mo) => mo.year + "-" + String(mo.month1).padStart(2, "0"))
      : [];
    const cats = K.catBreakdownWindow(txns, companyProfile, OPEX_SECS, range, cmpRange);
    const totalCur = cats.reduce((s, r) => s + r.cur, 0) || (T ? T.cur.opex : 0);
    const totalPrior = cats.reduce((s, r) => s + r.prior, 0) || (T && T.prior ? T.prior.opex : 0);
    const pctChg = (cur, prior) => (Math.abs(prior) > Math.abs(cur) * 0.05) ? (cur - prior) / Math.abs(prior) * 100 : null;
    const chg = pctChg(totalCur, totalPrior);
    const revenue = T ? T.cur.revenue : 0;
    const opexRatio = revenue ? totalCur / revenue * 100 : null;
    const M = (v) => K.moneyStr(v, { compact: true });

    // Recurring cadence (# distinct months each category posted spend in trailing window).
    const monthsActive = {};
    let maxD = 0;
    for (const t of txns) { if (t.posted_date) { const d = +new Date(t.posted_date); if (d > maxD) maxD = d; } }
    if (!maxD) maxD = Date.now();
    const cut = maxD - 365 * 86400000;
    for (const t of txns) {
      const cat = t.canonical_category; if (!cat) continue;
      let sec = null; try { sec = tax && tax.sectionForCategory ? tax.sectionForCategory(cat, companyProfile) : null; } catch (e) { sec = null; }
      if (OPEX_SECS.indexOf(sec) < 0) continue;
      const d = t.posted_date ? +new Date(t.posted_date) : 0;
      if (d > cut && d <= maxD) { (monthsActive[cat] || (monthsActive[cat] = new Set())).add(t.posted_date.slice(0, 7)); }
    }
    const monthsOf = (name) => (monthsActive[name] ? monthsActive[name].size : 0);
    const recurring = cats.filter((r) => monthsOf(r.name) >= 10).sort((a, b) => b.cur - a.cur);

    // ── Per-account expense breakdown (GL account_name level) ───────────────
    // Finer than canonical category: groups trailing-12-month expense GL by
    // account_name. Expense membership uses the taxonomy section (OPEX_SECS) —
    // the same gate as the recurring loop above — so COGS / non-opex stay out
    // and we never hardcode bucket names in the live path. Months key off
    // posted_date (txns carry posted_date + amount, not date/net_amount).
    const expenseAccounts = {};
    for (const t of txns) {
      const cat = t.canonical_category; if (!cat) continue;
      let sec = null; try { sec = tax && tax.sectionForCategory ? tax.sectionForCategory(cat, companyProfile) : null; } catch (e) { sec = null; }
      if (OPEX_SECS.indexOf(sec) < 0) continue;
      const d = t.posted_date ? +new Date(t.posted_date) : 0;
      if (!(d > cut && d <= maxD)) continue;
      const acct = t.account_name || t.account_code || "Other";
      if (!expenseAccounts[acct]) expenseAccounts[acct] = { account: acct, category: cat, total: 0, months: {} };
      const amt = Math.abs(num(t.amount));
      const mon = t.posted_date.slice(0, 7);
      expenseAccounts[acct].total += amt;
      expenseAccounts[acct].months[mon] = (expenseAccounts[acct].months[mon] || 0) + amt;
    }
    const accountRows = Object.values(expenseAccounts).filter((r) => r.total > 50).sort((a, b) => b.total - a.total);
    const acctGrand = accountRows.reduce((s, r) => s + r.total, 0);

    // Donut: top 8 accounts + "Other".
    const acctDonut = (() => {
      const top = accountRows.slice(0, 8).map((r, i) => ({ label: r.account, value: r.total, color: K.RANK_COLORS[i % K.RANK_COLORS.length] }));
      const restTotal = accountRows.slice(8).reduce((s, r) => s + r.total, 0);
      if (restTotal > 0) top.push({ label: "Other", value: restTotal, color: "#94a3b8" });
      return top;
    })();

    // vs-prior for the top-lines table: current 3-mo avg vs prior 3-mo avg.
    const acctVsPrior = (r) => {
      const ms = Object.keys(r.months).sort();
      const cur = ms.slice(-3), pri = ms.slice(-6, -3);
      if (!cur.length || !pri.length) return null;
      const curAvg = cur.reduce((s, m) => s + r.months[m], 0) / cur.length;
      const priAvg = pri.reduce((s, m) => s + r.months[m], 0) / pri.length;
      if (!(priAvg > 0)) return null;
      return (curAvg - priAvg) / priAvg * 100;
    };

    // Account-level exceptions: last month > 150% of trailing 6-month average.
    const acctExceptions = [];
    for (const acct of Object.keys(expenseAccounts)) {
      const ms = Object.keys(expenseAccounts[acct].months).sort();
      const recent = ms.slice(-1).reduce((s, m) => s + expenseAccounts[acct].months[m], 0);
      const trailing = ms.slice(-7, -1);
      const trailingAvg = trailing.length ? trailing.reduce((s, m) => s + expenseAccounts[acct].months[m], 0) / trailing.length : 0;
      if (trailingAvg > 0 && recent > trailingAvg * 1.5 && recent > 500) {
        acctExceptions.push({ account: acct, recent, trailingAvg, delta: recent - trailingAvg, pct: (recent / trailingAvg - 1) * 100 });
      }
    }
    acctExceptions.sort((a, b) => b.delta - a.delta);
    const acctExceptionCards = acctExceptions.slice(0, 4).map((e, i) => {
      const sev = e.delta > 300000 ? "High" : e.delta > 100000 ? "Watch" : "Info";
      return { num: i + 1, title: e.account + " spike", impact: "+" + M(e.delta), severity: sev, action: "Review " + e.account,
        detail: M(e.recent) + " this month vs " + M(e.trailingAvg) + " 6-mo avg — up " + e.pct.toFixed(0) + "%." };
    });

    // Recurring by account: appears in ≥10 of last 12 months → annualized run-rate.
    const recurringAccts = accountRows
      .filter((r) => Object.keys(r.months).length >= 10)
      .map((r) => { const mc = Object.keys(r.months).length; return { account: r.account, category: r.category, monthsCount: mc, annualized: (r.total / mc) * 12 }; })
      .sort((a, b) => b.annualized - a.annualized);

    // ── E2: monthly breakdown matrix — top 15 accounts × the selected period ──
    // Columns follow the period selector (range): one column per calendar month
    // in the window that carries expense spend. Rows/cells rebuild from
    // range-windowed GL (not the fixed trailing-12m set), so 1M / 3M / 6M / YTD /
    // 12M / 13M all change the matrix — not just the KPI strip.
    const rsMs = +range.start, reMs = +range.end;
    const rangeMonths = (function () {
      const out = []; let y = range.start.getFullYear(), mo = range.start.getMonth();
      while (y < range.end.getFullYear() || (y === range.end.getFullYear() && mo <= range.end.getMonth())) {
        out.push(y + "-" + String(mo + 1).padStart(2, "0")); mo++; if (mo > 11) { mo = 0; y++; }
      }
      return out;
    })();
    const matrixAccts = {};
    for (const t of txns) {
      const cat = t.canonical_category; if (!cat) continue;
      let sec = null; try { sec = tax && tax.sectionForCategory ? tax.sectionForCategory(cat, companyProfile) : null; } catch (e) { sec = null; }
      if (OPEX_SECS.indexOf(sec) < 0) continue;
      const d = t.posted_date ? +new Date(t.posted_date) : 0; if (!(d >= rsMs && d <= reMs)) continue;
      const acct = t.account_name || t.account_code || "Other";
      const mon = t.posted_date.slice(0, 7);
      const r = matrixAccts[acct] || (matrixAccts[acct] = { account: acct, category: cat, total: 0, months: {} });
      const amt = Math.abs(num(t.amount));
      r.total += amt; r.months[mon] = (r.months[mon] || 0) + amt;
    }
    const matrixAcctRows = Object.values(matrixAccts).filter((r) => r.total > 50).sort((a, b) => b.total - a.total);
    const matrixGrand = matrixAcctRows.reduce((s, r) => s + r.total, 0);

    // FIX 2 — two-level hierarchy: canonical_category → its GL accounts. Every
    // account already carries .category / .months from the matrix build.
    const expenseHierarchy = {};
    matrixAcctRows.forEach((acct) => {
      const cat = acct.category || "Operating Expenses";
      const e = expenseHierarchy[cat] || (expenseHierarchy[cat] = { category: cat, total: 0, months: {}, accounts: [] });
      e.total += acct.total;
      e.accounts.push(acct);
      Object.keys(acct.months || {}).forEach((m) => { e.months[m] = (e.months[m] || 0) + acct.months[m]; });
    });
    const categoryRows = Object.values(expenseHierarchy).filter((c) => c.total > 0).sort((a, b) => b.total - a.total);
    const catGrand = categoryRows.reduce((s, c) => s + c.total, 0);
    const monthsWithData = new Set(matrixAcctRows.flatMap((r) => Object.keys(r.months)));
    const matrixMonths = rangeMonths.filter((mo) => monthsWithData.has(mo));
    const matrixRows = matrixAcctRows.slice(0, 15);
    let maxCell = 0;
    for (const r of matrixRows) for (const mo of matrixMonths) { const v = r.months[mo] || 0; if (v > maxCell) maxCell = v; }
    const cellShade = (v) => { if (!v || !maxCell) return "transparent"; const t = Math.min(1, v / maxCell); return "rgba(217,79,71," + (0.06 + t * 0.34).toFixed(3) + ")"; };
    const moLabel = (ym) => { const [y, m] = ym.split("-"); return ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][(+m) - 1] + " '" + y.slice(-2); };

    // E2 drill-through: trailing-window GL transactions for the selected account.
    const drillTxns = drillAcct
      ? txns.filter((t) => (t.account_name || t.account_code || "Other") === drillAcct && t.posted_date)
        .filter((t) => { const d = +new Date(t.posted_date); return d > cut && d <= maxD; })
        .sort((a, b) => (a.posted_date < b.posted_date ? 1 : -1))
      : [];

    // Vendor concentration from AP subledger.
    const bills = (data && data.dimensions && data.dimensions.apBills) || [];
    const vendorSpend = {};
    for (const b of bills) { const nm = b.vendor_name || "—"; const amt = Math.abs(num(b.amount)); if (amt) vendorSpend[nm] = (vendorSpend[nm] || 0) + amt; }
    const vendors = Object.keys(vendorSpend).map((k) => ({ name: k, spend: vendorSpend[k] })).sort((a, b) => b.spend - a.spend);
    const vendorTotal = vendors.reduce((s, v) => s + v.spend, 0);
    const topVendorPct = vendorTotal ? vendors[0].spend / vendorTotal * 100 : null;

    // FIX 2 — vendor spend by month (last 6 months of AP bills, by issue_date).
    // Heat-mapped so spend spikes per vendor/month pop visually. Source is the AP
    // subledger (real bill dates + amounts), not GL memo guesses.
    const vendorMonthSet = new Set(rangeMonths.filter((mo) => bills.some((b) => (b.issue_date || "").slice(0, 7) === mo)));
    const vendorMonths = rangeMonths.filter((mo) => vendorMonthSet.has(mo));
    const vendorMonthly = {};
    for (const b of bills) {
      const amt = Math.abs(num(b.amount)); if (!amt) continue;
      const mon = (b.issue_date || "").slice(0, 7);
      if (!vendorMonthSet.has(mon)) continue; // only the selected window
      const nm = b.vendor_name || "—";
      const v = vendorMonthly[nm] || (vendorMonthly[nm] = { name: nm, total: 0, months: {} });
      v.total += amt; v.months[mon] = (v.months[mon] || 0) + amt;
    }
    const topVendorRows = Object.values(vendorMonthly).sort((a, b) => b.total - a.total).slice(0, 10);
    const vendorWinTotal = Object.values(vendorMonthly).reduce((s, v) => s + v.total, 0);
    let maxVendorCell = 0;
    for (const v of topVendorRows) for (const mo of vendorMonths) { const x = v.months[mo] || 0; if (x > maxVendorCell) maxVendorCell = x; }
    const vCellShade = (x) => { if (!x || !maxVendorCell) return "transparent"; const t = Math.min(1, x / maxVendorCell); return "rgba(217,79,71," + (0.06 + t * 0.34).toFixed(3) + ")"; };
    const vMoLabel = (ym) => { const [y, m] = ym.split("-"); return ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][(+m) - 1] + " '" + y.slice(-2); };

    // Monthly series for the chart.
    const opex = (plH.opex || []).map(Number), rev = (plH.revenue || []).map(Number);
    const NO = opex.length;
    const m12 = (plH.labels || []).slice(Math.max(0, NO - 12));
    const opex12 = opex.slice(Math.max(0, NO - 12));
    const opexPrior12 = NO >= 24 ? opex.slice(NO - 24, NO - 12) : null;
    const opexPct12 = opex12.map((v, i) => { const idx = NO - 12 + i; const r = rev[idx]; return r ? v / r * 100 : null; });
    const labels = (plH.labels || []).map((l, i) => l + " " + String(plH.years ? plH.years[i] : "").slice(-2));

    // Recurring vendors — real AP bill payees (gl txns carry no payee) that
    // billed in ≥3 distinct months of the selected period. When no AP subledger
    // is connected we fall back to recurring GL accounts so the tile is never
    // empty-by-omission and never fabricates a vendor name.
    const recurVendorMonths = {};
    for (const b of bills) {
      const mon = (b.issue_date || "").slice(0, 7);
      if (periodMonthKeys.length && periodMonthKeys.indexOf(mon) < 0) continue;
      const nm = b.vendor_name || "—";
      (recurVendorMonths[nm] || (recurVendorMonths[nm] = new Set())).add(mon);
    }
    const recurringVendorCount = Object.values(recurVendorMonths).filter((s) => s.size >= 3).length;
    const hasAP = bills.length > 0;
    const drillTop = accountRows[0]
      ? () => window.openAccountDetail && window.openAccountDetail({ name: accountRows[0].account, category: accountRows[0].category, page: "expenses" })
      : null;
    const drillRecur = recurringAccts[0]
      ? () => window.openAccountDetail && window.openAccountDetail({ name: recurringAccts[0].account, category: recurringAccts[0].category, page: "expenses" })
      : drillTop;
    const expandAllCats = () => { const all = {}; categoryRows.forEach((c) => { all[c.category] = true; }); setExpandedCats(all); };

    // KPI tiles (Pattern A) — expense semantics: cost up = red. Every tile is
    // clickable: it opens the relevant GL account in the detail panel, or (for
    // Categories) expands the category breakdown table in place.
    const kpis = [
      { label: "Total Expense", value: M(totalCur), valueColor: "red", sub: chg == null ? "no prior period" : (chg >= 0 ? "▲ " : "▼ ") + Math.abs(chg).toFixed(1) + "% vs prior", subColor: chg == null ? K.MUTE : chg <= 0 ? "#18a867" : "#d94f47", onClick: drillTop },
      { label: "Categories", value: String(cats.length), valueColor: "navy", sub: opexRatio == null ? "expense categories in period" : opexRatio.toFixed(0) + "% of revenue", onClick: expandAllCats },
      hasAP
        ? { label: "Recurring Vendors", value: String(recurringVendorCount), valueColor: "amber", sub: recurringVendorCount ? "billed in 3+ months" : "no vendor recurs 3+ months", onClick: drillRecur }
        : { label: "Recurring Charges", value: String(recurringAccts.length), valueColor: "amber", sub: recurringAccts.length ? "GL accounts ≥10 of 12 months" : "no recurring cadence yet", onClick: drillRecur },
      { label: "Vendor Concentration", value: topVendorPct == null ? "—" : topVendorPct.toFixed(0) + "%", valueColor: "teal", sub: topVendorPct == null ? "connect AP subledger" : "top vendor of " + M(vendorTotal), onClick: drillTop },
    ];

    const swatch = (color, label, dashed) => h("span", { style: { display: "inline-flex", alignItems: "center", gap: 5, fontSize: 10, color: "#475569", fontWeight: 600 } },
      h("span", { style: { width: 14, height: dashed ? 0 : 8, borderRadius: 2, background: dashed ? "transparent" : color, borderTop: dashed ? "2px dashed " + color : "none", display: "inline-block" } }), label);
    const legend = h("div", { style: { display: "flex", gap: 12, flexWrap: "wrap" } },
      swatch("#d94f47", "Expense"), swatch("#94a3b8", "Prior year", true), swatch("#18a867", "% of revenue"));

    const chartSeries = [
      { type: "bar", color: "#d94f47", data: opex12 },
      opexPrior12 ? { type: "dashed-line", color: "#94a3b8", data: opexPrior12 } : null,
      { type: "line", color: "#18a867", data: opexPct12, secondary: true },
    ].filter(Boolean);

    // Recurring table.
    const recurColor = (v) => v > 300000 ? "#d94f47" : v >= 100000 ? "#d97706" : "#18a867";
    const recurDot = (v) => v > 300000 ? "pa-dot-red" : v >= 100000 ? "pa-dot-amber" : "pa-dot-green";
    const recurTable = h(K.Card, { title: "Recurring charges", sub: "By GL account · ≥10 of last 12 months · annualized run-rate", padding: 0 },
      recurringAccts.length ? h("table", { className: "pa-table" },
        h("thead", null, h("tr", null, h("th", null, "Account"), h("th", { className: "num" }, "Annualized"), h("th", { className: "num" }, "Months"), h("th", null, "Tier"))),
        h("tbody", null, recurringAccts.slice(0, 8).map((r, i) => h("tr", { key: i, style: { borderLeft: "3px solid " + recurColor(r.annualized) } },
          h("td", { style: { fontWeight: 600 } }, r.account),
          h("td", { className: "num" }, M(r.annualized)),
          h("td", { className: "num muted" }, r.monthsCount + "/12"),
          h("td", null, h("span", { className: "pa-dot " + recurDot(r.annualized) }), r.annualized > 300000 ? "Major" : r.annualized >= 100000 ? "Material" : "Minor")))))
        : h("div", { className: "pa-card-body", style: { color: "#6475a0", fontSize: 13 } }, "No monthly recurring cadence detected at the account level."));

    // ── G3: deeper, plain-English CFO commentary ────────────────────────────
    // Names specific GL accounts, separates one-time charges from the recurring
    // base, calls out the biggest spike, and always ends with a dollar-
    // quantified place to save. One-time = posted in ≤2 months but material.
    const oneTimeAccts = accountRows.filter((r) => Object.keys(r.months).length <= 2 && r.total > 1000).sort((a, b) => b.total - a.total);
    const topSpike = acctExceptions[0];
    const vendorConc = topVendorPct != null ? topVendorPct / 100 : 0;
    const topVendor = vendors[0];
    const saveText = (vendorConc > 0.4 && topVendor)
      ? "Your top vendor <b>" + topVendor.name + "</b> is <b>" + (vendorConc * 100).toFixed(0) + "%</b> of payable spend (" + M(topVendor.spend) + "). Get competing quotes or negotiate a volume discount — even an 8% reduction frees roughly <b>" + M(topVendor.spend * 0.08) + "/yr</b>."
      : recurringAccts.length
        ? "Audit the recurring lines — <b>" + recurringAccts[0].account + "</b> runs about <b>" + M(recurringAccts[0].annualized) + "/yr</b>. Cancelling unused software/subscriptions typically recovers 5–10% of recurring spend (≈ <b>" + M(recurringAccts.reduce((s, r) => s + r.annualized, 0) * 0.07) + "/yr</b> here)."
        : "Review your largest recurring expense lines for unused services — commonly 5–10% of software/subscription spend is redundant.";
    // FIX 4 — standardised CFO Intelligence panel (shared design across pages).
    const Panel = K.CFOCommentaryPanel;
    const commentary = h(Panel, {
      title: "Operating Expenses",
      insights: [
        { type: "neutral", text: "OpEx is <b>" + M(totalCur) + "</b> TTM" + (opexRatio != null ? " (<b>" + opexRatio.toFixed(0) + "%</b> of revenue)" : "") + (chg == null ? "." : ", " + (chg >= 0 ? "up" : "down") + " <b>" + Math.abs(chg).toFixed(1) + "%</b> vs the prior year."), detail: accountRows.length ? "Largest line: <b>" + accountRows[0].account + "</b> at " + M(accountRows[0].total) + "." : null },
        recurringAccts.length ? { type: "neutral", text: "<b>" + recurringAccts.length + "</b> expense line" + (recurringAccts.length > 1 ? "s" : "") + " run on a steady monthly cadence — your committed base.", detail: "The biggest: " + recurringAccts.slice(0, 3).map((r) => r.account).join(", ") + "." } : null,
        topSpike ? { type: "warning", text: "<b>" + topSpike.account + "</b> spiked <b>" + topSpike.pct.toFixed(0) + "%</b> above its trailing average this period.", detail: M(topSpike.recent) + " this period vs " + M(topSpike.trailingAvg) + " 6-mo average — investigate before next close." } : null,
        oneTimeAccts.length ? { type: "neutral", text: "<b>" + oneTimeAccts[0].account + "</b> (" + M(oneTimeAccts[0].total) + ") appears one-time.", detail: "Active only " + Object.keys(oneTimeAccts[0].months).length + " month(s) — exclude from run-rate." } : null,
      ].filter(Boolean),
      savings: saveText,
    });

    const opexTakeaway = "Operating expense is <b>" + M(totalCur) + "</b> for " + range.label.toLowerCase() +
      (opexRatio != null ? " — <b>" + opexRatio.toFixed(0) + "%</b> of revenue" : "") +
      (chg == null ? "." : ", " + (chg >= 0 ? "up" : "down") + " <b>" + Math.abs(chg).toFixed(1) + "%</b> vs " + (cmpRange ? cmpRange.label.toLowerCase() : "prior") + ".") +
      (cats.length ? " Largest category is <b>" + cats[0].name + "</b> at " + (totalCur ? (cats[0].cur / totalCur * 100).toFixed(0) : "0") + "% of spend." : "");

    // Budget vs actual — honest: only render the comparison if a budget exists.
    const hasBudget = cmpIsBudget && (data && (data.budget || (data.dimensions && data.dimensions.budget)));
    const budgetCard = hasBudget
      ? h(K.Card, { title: "Budget vs actual", sub: "By category · " + range.label }, h("div", { style: { fontSize: 12.5, color: "#6475a0", padding: 8 } }, "Budget comparison rendering from connected budget data."))
      : h("div", { style: { background: "rgba(13,32,64,.03)", border: "1px dashed rgba(13,32,64,.15)", borderRadius: 10, padding: 28, textAlign: "center", margin: "0 0 24px 0" } },
        h("div", { style: { fontSize: 13, fontWeight: 700, color: "#0d2040", marginBottom: 6 } }, "Budget vs Actual"),
        h("div", { style: { fontSize: 11.5, color: "#6475a0", marginBottom: 16 } }, "Compare actual spend to your annual budget with variance flags and CFO commentary."),
        h("button", { onClick: () => typeof setPage === "function" && setPage("budget"),
          style: { padding: "8px 20px", background: "#0d2040", color: "white", border: "none", borderRadius: 7, fontSize: 12, fontWeight: 700, cursor: "pointer" } }, "→ Open Budget vs Actual"));

    return h(K.Shell, { hero: { eyebrow: "COST INTELLIGENCE", title: "Operating Expenses", subtitle: range.label + (cmpRange ? " · vs " + cmpRange.label.toLowerCase() : "") + " · category breakdown · vendor concentration", controls: null } },

      // Row 1 — KPI tiles (each clickable → relevant drill; K.Kpi handles onClick)
      h("div", { className: "pa-kpi-strip pa-kpi-strip-4" }, kpis.map((k, i) => h(K.Kpi, Object.assign({ key: i, animDelay: i * 0.05 }, k)))),

      // Row 2 — expenditure monthly trend (Pattern B) + Key Takeaway
      h(K.Card, { title: "EXPENDITURE — MONTHLY TREND", sub: "Expense bars · prior-year dashed · % of revenue (secondary)", right: legend },
        h(K.MultiSeriesBarChart, { months: m12, series: chartSeries, height: 180 }),
        h(K.KeyTakeaway, { text: opexTakeaway })),

      // Row 2b — budget vs actual (honest)
      budgetCard,

      // Row 3 — mix donut (by GL account) · account-level exceptions
      h("div", { className: "pa-grid-2" },
        h(K.Card, { title: "Expense mix", sub: "Top GL accounts by trailing-12-month spend" },
          acctDonut.length ? h(K.Donut, { items: acctDonut }) : h("div", { style: { color: K.MUTE, fontSize: 12 } }, "No classified expenses yet.")),
        h(K.Card, { title: "Expense exceptions", sub: "Accounts >150% of their 6-month average" },
          acctExceptionCards.length ? acctExceptionCards.map((e, i) => h(K.ExceptionCard, Object.assign({ key: i }, e)))
            : h("div", { style: { fontSize: 12.5, color: "#6475a0" } }, "No account is spiking materially above its trailing 6-month average."))),

      // Row 3a — FIX 2: expenses by category → expand to GL accounts (TB hierarchy)
      h(K.Card, { title: "Expenses by category", sub: "Grouped by canonical category · click a category to expand its GL accounts · " + range.label, padding: 0,
        right: h("div", { style: { display: "flex", gap: 8 } },
          h("button", { onClick: () => { const all = {}; categoryRows.forEach((c) => { all[c.category] = true; }); setExpandedCats(all); }, style: { padding: "5px 12px", background: "#0d2040", color: "#fff", border: "none", borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: "pointer" } }, "▼ Expand All"),
          h("button", { onClick: () => setExpandedCats({}), style: { padding: "5px 12px", background: "rgba(13,32,64,.07)", color: "#0d2040", border: "1px solid rgba(13,32,64,.15)", borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: "pointer" } }, "▲ Collapse All")) },
        categoryRows.length ? h("div", { style: { overflowX: "auto" } },
          h("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 11.5, minWidth: 720 } },
            h("thead", null, h("tr", { style: { background: "#0d2040" } },
              h("th", { style: { textAlign: "left", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.85)", position: "sticky", left: 0, background: "#0d2040", whiteSpace: "nowrap" } }, "Category / Account"),
              matrixMonths.map((mo) => h("th", { key: mo, style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.85)", whiteSpace: "nowrap" } }, moLabel(mo))),
              h("th", { style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "#b8921e", whiteSpace: "nowrap" } }, "Total"),
              h("th", { style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.6)", whiteSpace: "nowrap" } }, "% of Total"))),
            h("tbody", null, categoryRows.flatMap((cat) => {
              const isExpanded = !!expandedCats[cat.category];
              const catRow = h("tr", { key: "cat-" + cat.category, onClick: () => toggleCat(cat.category), style: { background: "rgba(13,32,64,.04)", cursor: "pointer", borderTop: "1px solid rgba(13,32,64,.08)" } },
                h("td", { style: { padding: "9px 12px", fontWeight: 700, color: "#0d2040", fontSize: 12, position: "sticky", left: 0, background: "#eef1f6", whiteSpace: "nowrap" } },
                  h("span", { style: { marginRight: 8, fontSize: 10, color: "#6475a0" } }, isExpanded ? "▼" : "▶"),
                  cat.category,
                  h("span", { style: { marginLeft: 8, fontSize: 10, color: "#6475a0", fontWeight: 400 } }, cat.accounts.length + " account" + (cat.accounts.length > 1 ? "s" : ""))),
                matrixMonths.map((mo) => h("td", { key: mo, style: { padding: "9px 10px", textAlign: "right", fontFamily: "'JetBrains Mono',monospace", fontWeight: 700, fontSize: 11.5, color: "#0d2040" } }, cat.months[mo] > 0 ? M(cat.months[mo]) : "—")),
                h("td", { style: { padding: "9px 12px", textAlign: "right", fontFamily: "'JetBrains Mono',monospace", fontWeight: 800, color: "#0d2040", fontSize: 12 } }, M(cat.total)),
                h("td", { style: { padding: "9px 12px", textAlign: "right", fontSize: 11, color: "#6475a0" } }, (catGrand ? cat.total / catGrand * 100 : 0).toFixed(1) + "%"));
              const subRows = isExpanded ? cat.accounts.slice().sort((a, b) => b.total - a.total).map((acct) => h("tr", { key: "acct-" + cat.category + "-" + acct.account, onClick: () => window.openAccountDetail && window.openAccountDetail({ name: acct.account, category: acct.category, page: "expenses" }), title: "View account detail", onMouseEnter: (e) => { e.currentTarget.style.background = "rgba(28,78,216,.04)"; }, onMouseLeave: (e) => { e.currentTarget.style.background = "#fff"; }, style: { background: "#fff", borderTop: "1px solid rgba(13,32,64,.04)", cursor: "pointer" } },
                h("td", { style: { padding: "7px 12px 7px 36px", color: "#4a5680", fontSize: 11.5, position: "sticky", left: 0, background: "#fff", whiteSpace: "nowrap" } }, h("span", { style: { color: "#aab", marginRight: 6 } }, "└"), acct.account),
                matrixMonths.map((mo) => { const v = acct.months[mo] || 0; return h("td", { key: mo, style: { padding: "7px 10px", textAlign: "right", fontFamily: "'JetBrains Mono',monospace", fontSize: 11, color: v > 0 ? "#1a2540" : "#ddd", background: cellShade(v) } }, v > 0 ? M(v) : "—"); }),
                h("td", { style: { padding: "7px 12px", textAlign: "right", fontFamily: "'JetBrains Mono',monospace", fontSize: 11, color: "#1a2540" } }, M(acct.total)),
                h("td", { style: { padding: "7px 12px", textAlign: "right", fontSize: 11, color: "#6475a0" } }, (cat.total ? acct.total / cat.total * 100 : 0).toFixed(1) + "%"))) : [];
              return [catRow].concat(subRows);
            }))))
          : h("div", { className: "pa-card-body", style: { color: "#6475a0", fontSize: 13 } }, "No classified expense accounts in the trailing window.")),

      // Row 3b — E2: top expenses by month (GL account × month matrix, clickable)
      h(K.Card, { title: "Top expenses by month", sub: "Top 15 GL accounts · " + range.label + " (" + matrixMonths.length + " mo) · darker = higher spend · click a row for transactions", padding: 0 },
        (matrixRows.length && matrixMonths.length) ? h("div", { style: { overflowX: "auto" } },
          h("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 11.5, minWidth: 720 } },
            h("thead", null, h("tr", { style: { background: "#0d2040" } },
              h("th", { style: { textAlign: "left", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.85)", position: "sticky", left: 0, background: "#0d2040", whiteSpace: "nowrap" } }, "Account"),
              h("th", { style: { textAlign: "left", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.6)", whiteSpace: "nowrap" } }, "Category"),
              matrixMonths.map((mo) => h("th", { key: mo, style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.85)", whiteSpace: "nowrap" } }, moLabel(mo))),
              h("th", { style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "#b8921e", whiteSpace: "nowrap" } }, "Total"),
              h("th", { style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.6)", whiteSpace: "nowrap" } }, "% OpEx"))),
            h("tbody", null, matrixRows.map((r, i) => {
              const isOpen = drillAcct === r.account;
              return h("tr", { key: i, onClick: () => setDrillAcct(isOpen ? null : r.account),
                style: { cursor: "pointer", borderTop: "1px solid #eef0f4", background: isOpen ? "rgba(13,32,64,.05)" : undefined } },
                h("td", { style: { textAlign: "left", padding: "7px 12px", fontWeight: 600, color: "#1a2540", position: "sticky", left: 0, background: isOpen ? "#eaedf3" : "#fff", whiteSpace: "nowrap" } }, (isOpen ? "▾ " : "▸ ") + r.account),
                h("td", { style: { textAlign: "left", padding: "7px 12px", color: "#6475a0", fontSize: 11 } }, r.category),
                matrixMonths.map((mo) => { const v = r.months[mo] || 0; return h("td", { key: mo, style: { textAlign: "right", padding: "7px 12px", fontFamily: "'JetBrains Mono',monospace", background: cellShade(v), whiteSpace: "nowrap" } }, v ? M(v) : "—"); }),
                h("td", { style: { textAlign: "right", padding: "7px 12px", fontFamily: "'JetBrains Mono',monospace", fontWeight: 700, color: "#0d2040" } }, M(r.total)),
                h("td", { style: { textAlign: "right", padding: "7px 12px", color: "#6475a0" } }, (matrixGrand ? r.total / matrixGrand * 100 : 0).toFixed(1) + "%"));
            }))),
          drillAcct ? h("div", { style: { borderTop: "2px solid #0d2040", background: "rgba(13,32,64,.02)", padding: "10px 14px" } },
            h("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 } },
              h("div", { style: { fontSize: 12, fontWeight: 700, color: "#0d2040" } }, "GL transactions · " + drillAcct + " · " + drillTxns.length + " in trailing window"),
              h("button", { onClick: () => setDrillAcct(null), style: { background: "#fff", border: "1px solid rgba(13,32,64,.14)", borderRadius: 6, padding: "4px 10px", fontSize: 11, fontWeight: 600, color: "#1C4ED8", cursor: "pointer" } }, "✕ Close")),
            drillTxns.length ? h("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 11 } },
              h("thead", null, h("tr", null,
                h("th", { style: { textAlign: "left", padding: "5px 10px", color: "#6475a0", fontWeight: 700, borderBottom: "1px solid #e4e8f0" } }, "Date"),
                h("th", { style: { textAlign: "left", padding: "5px 10px", color: "#6475a0", fontWeight: 700, borderBottom: "1px solid #e4e8f0" } }, "Memo / Reference"),
                h("th", { style: { textAlign: "right", padding: "5px 10px", color: "#6475a0", fontWeight: 700, borderBottom: "1px solid #e4e8f0" } }, "Amount"))),
              h("tbody", null, drillTxns.slice(0, 100).map((t, j) => h("tr", { key: j },
                h("td", { style: { textAlign: "left", padding: "5px 10px", fontFamily: "'JetBrains Mono',monospace", color: "#475569" } }, (t.posted_date || "").slice(0, 10)),
                h("td", { style: { textAlign: "left", padding: "5px 10px", color: "#475569" } }, t.memo || t.reference || "—"),
                h("td", { style: { textAlign: "right", padding: "5px 10px", fontFamily: "'JetBrains Mono',monospace", color: "#0d2040" } }, M(Math.abs(num(t.amount))))))))
              : h("div", { style: { fontSize: 12, color: "#6475a0", padding: 8 } }, "No individual transactions found for this account in the trailing window."),
            drillTxns.length > 100 ? h("div", { style: { fontSize: 10.5, color: "#94a3b8", padding: "6px 10px" } }, "Showing first 100 of " + drillTxns.length + " transactions.") : null) : null)
          : h("div", { className: "pa-card-body", style: { color: "#6475a0", fontSize: 13 } }, "No classified expense accounts in the trailing window.")),

      // Row 4 — FIX 2: vendor concentration by month (heat-mapped matrix)
      h(K.Card, { title: "Vendor concentration — by month", sub: "Top 10 vendors · " + range.label + " (" + vendorMonths.length + " mo) of AP bills · darker = higher spend", padding: 0 },
        (topVendorRows.length && vendorMonths.length) ? h("div", { style: { overflowX: "auto" } },
          h("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 11.5, minWidth: 720 } },
            h("thead", null, h("tr", { style: { background: "#0d2040" } },
              h("th", { style: { textAlign: "left", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.85)", position: "sticky", left: 0, background: "#0d2040", whiteSpace: "nowrap" } }, "Vendor"),
              vendorMonths.map((mo) => h("th", { key: mo, style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.85)", whiteSpace: "nowrap" } }, vMoLabel(mo))),
              h("th", { style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "#b8921e", whiteSpace: "nowrap" } }, "Total"),
              h("th", { style: { textAlign: "right", padding: "8px 12px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "rgba(255,255,255,.6)", whiteSpace: "nowrap" } }, "% Share"))),
            h("tbody", null, topVendorRows.map((v, i) => h("tr", { key: i, style: { borderTop: "1px solid #eef0f4" } },
              h("td", { style: { textAlign: "left", padding: "7px 12px", fontWeight: 600, color: "#1a2540", position: "sticky", left: 0, background: "#fff", whiteSpace: "nowrap" } }, v.name),
              vendorMonths.map((mo) => { const x = v.months[mo] || 0; return h("td", { key: mo, style: { textAlign: "right", padding: "7px 12px", fontFamily: "'JetBrains Mono',monospace", background: vCellShade(x), whiteSpace: "nowrap" } }, x ? M(x) : "—"); }),
              h("td", { style: { textAlign: "right", padding: "7px 12px", fontFamily: "'JetBrains Mono',monospace", fontWeight: 700, color: "#0d2040" } }, M(v.total)),
              h("td", { style: { textAlign: "right", padding: "7px 12px", color: "#6475a0" } }, (vendorWinTotal ? v.total / vendorWinTotal * 100 : 0).toFixed(1) + "%"))))))
          : h("div", { className: "pa-card-body", style: { color: "#6475a0", fontSize: 13 } }, "No AP subledger connected — vendor-level spend by month appears once bills are synced.")),

      // Row 5 — recurring charges table + standardised CFO panel
      recurTable,
      commentary);
  }
  window.OpExIntelligencePage = Page;
})();
