// RevenueIntelligencePage (Stage 11) — window.RevenueIntelligencePage
// Master design: Pattern-A KPI tiles, Pattern-B multi-series chart (income bars +
// prior-year dashed + 3-mo moving-average line), Pattern-C 3-column grid, and a
// Pattern-D ranked list. YoY% suppressed when the prior base is near-zero.

(function () {
  const h = React.createElement;
  const { useState, useEffect, useMemo } = React;
  function Page(props) {
    const K = window.PerduraPageKit;
    if (!K) return h("div", { className: "pc-page" }, "Loading…");
    const { data, companyProfile, scopedCompanyId } = props;
    const archetype = (companyProfile && companyProfile.archetype) || '';
    const isProductBusiness = ['retail', 'distribution', 'manufacturing', 'ecommerce'].some(a => archetype.includes(a));
    const isServiceBusiness = ['services', 'saas', 'professional', 'medical', 'clinic'].some(a => archetype.includes(a));
    const plH = (data && (data.plHistory || data.pl)) || { labels: [], revenue: [] };
    const T = K.ttm(plH);
    const txns = (data && data.txns) || [];
    // G1/G2 — period selector + comparison drive the revenue-stream window.
    const anchor = K.anchorFromPlH(plH);
    const ps = K.usePeriodState("revenue_intelligence", "ytd");
    const range = K.resolvePeriod(ps.mode, anchor, ps.custom);
    const cmpRange = K.comparePeriod(range, ps.cmp);
    const cats = K.catBreakdownWindow(txns, companyProfile, ["revenue", "other_income"], range, cmpRange);
    const totalCur = cats.reduce((s, r) => s + r.cur, 0) || (T ? T.cur.revenue : 0);
    const totalPrior = cats.reduce((s, r) => s + r.prior, 0) || (T && T.prior ? T.prior.revenue : 0);
    const pctChg = (cur, prior) => (Math.abs(prior) > Math.abs(cur) * 0.05) ? (cur - prior) / Math.abs(prior) * 100 : null;
    const yoy = pctChg(totalCur, totalPrior);
    const M = (v) => K.moneyStr(v, { compact: true });

    const rev = (plH.revenue || []).map(Number);
    const N = rev.length;
    const labels = (plH.labels || []).map((l, i) => l + " " + String(plH.years ? plH.years[i] : "").slice(-2));
    const m12 = (plH.labels || []).slice(Math.max(0, N - 12));
    const cur12 = rev.slice(Math.max(0, N - 12));
    const lbl12 = labels.slice(Math.max(0, N - 12));
    const prior12 = N >= 24 ? rev.slice(N - 24, N - 12) : null;
    const ma3 = cur12.map((_, i) => { const a = cur12.slice(Math.max(0, i - 2), i + 1); return a.reduce((s, v) => s + v, 0) / a.length; });

    const hhiVal = K.hhi(cats);
    const hhiLabel = hhiVal > 2500 ? "Highly Concentrated" : hhiVal > 1500 ? "Moderate" : "Diversified";
    const hhiColor = hhiVal > 2500 ? "#d94f47" : hhiVal > 1500 ? "#d97706" : "#18a867";
    const topShare = cats.length ? (cats[0].cur / (totalCur || 1) * 100) : 0;
    const grower = cats.slice().sort((a, b) => (b.cur - b.prior) - (a.cur - a.prior))[0];
    const decliner = cats.slice().sort((a, b) => (a.cur - a.prior) - (b.cur - b.prior))[0];
    const accel = (() => { if (cur12.length < 6) return "stable"; const a = cur12.slice(0, cur12.length / 2 | 0).reduce((s, v) => s + v, 0); const b = cur12.slice(cur12.length / 2 | 0).reduce((s, v) => s + v, 0); return b > a * 1.05 ? "accelerating" : b < a * 0.95 ? "decelerating" : "stable"; })();

    const kpis = [
      { label: "Total Revenue", value: M(totalCur), delta: yoy == null ? null : Math.abs(yoy).toFixed(1) + "% YoY", deltaDir: yoy == null ? "flat" : yoy >= 0 ? "up" : "dn", sub: "trailing 12 months", valueColor: "navy" },
      { label: "YoY Growth", value: yoy == null ? "—" : (yoy >= 0 ? "+" : "") + yoy.toFixed(1) + "%", sub: accel, valueColor: yoy == null ? "navy" : yoy >= 0 ? "green" : "red" },
      { label: "Largest Stream", value: cats.length ? topShare.toFixed(0) + "%" : "—", sub: cats.length ? cats[0].name : "of total revenue", valueColor: "gold" },
      { label: "Active Streams", value: String(cats.length), sub: "revenue categories", valueColor: "navy" },
      { label: "Avg / Stream", value: cats.length ? M(totalCur / cats.length) : "—", sub: "TTM per category", valueColor: "teal" },
    ];

    const monthRows = cur12.map((v, i) => { const p = prior12 ? prior12[i] : null; const d = p == null ? null : v - p; return { label: m12[i], cur: v, prior: p, d: d, dp: p == null ? null : pctChg(v, p) }; });
    const dpColor = (dp) => dp == null ? "#6475a0" : dp >= 0 ? "#18a867" : "#d94f47";

    // Trend stats.
    const peakIdx = cur12.length ? cur12.indexOf(Math.max.apply(null, cur12)) : 0;
    const troughIdx = cur12.length ? cur12.indexOf(Math.min.apply(null, cur12)) : 0;
    const avg3v = cur12.length ? cur12.slice(-3).reduce((s, v) => s + v, 0) / Math.min(3, cur12.length) : 0;
    const trendStats = [
      ["Peak month", (m12[peakIdx] || "—") + " · " + M(cur12[peakIdx] || 0)],
      ["Trough month", (m12[troughIdx] || "—") + " · " + M(cur12[troughIdx] || 0)],
      ["3-mo average", M(avg3v)],
      ["YoY growth", yoy == null ? "—" : (yoy >= 0 ? "+" : "") + yoy.toFixed(1) + "%"],
      ["Trend direction", accel],
    ];

    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("#1C4ED8", "Current"), swatch("#94a3b8", "Prior year", true), swatch("#18a867", "3-mo avg"));

    const chartSeries = [
      { type: "bar", color: "#1C4ED8", data: cur12 },
      prior12 ? { type: "dashed-line", color: "#94a3b8", data: prior12 } : null,
      { type: "line", color: "#18a867", data: ma3 },
    ].filter(Boolean);

    const revTakeaway = "Revenue is <b>" + M(totalCur) + "</b> for " + range.label.toLowerCase() +
      (yoy == null ? "." : ", " + (yoy >= 0 ? "up" : "down") + " <b>" + Math.abs(yoy).toFixed(1) + "%</b> vs " + (cmpRange ? cmpRange.label.toLowerCase() : "prior") + " — trajectory is <b>" + accel + "</b>.") +
      (cats.length ? " Largest driver is <b>" + cats[0].name + "</b> at " + topShare.toFixed(0) + "% of total." : "");
    const posDelta = cats.reduce((s, r) => s + Math.max(0, r.cur - r.prior), 0);
    const negDelta = cats.reduce((s, r) => s + Math.min(0, r.cur - r.prior), 0);
    const bridgeItems = (totalPrior > 0) ? [
      { label: cmpRange ? cmpRange.label : "Prior", value: totalPrior, type: "start" },
      { label: "Growth", value: posDelta, type: "positive" },
      { label: "Declines", value: negDelta, type: "negative" },
      { label: "Current", value: totalCur, type: "end" },
    ] : null;
    const custs = (data && (data.customers || (data.dimensions && data.dimensions.customers))) || [];
    const custRows = custs.map((c) => ({ name: c.name || c.customer_name || "—", val: Number(c.lifetime_value_cents || c.lifetime_value || c.revenue || 0) })).filter((c) => c.val > 0).sort((a, b) => b.val - a.val);
    const custTotal = custRows.reduce((s, c) => s + c.val, 0);

    // ── Revenue txns inside the section, scoped to the current / prior windows ──
    const tax = window.PerduraTaxonomy;
    const isRevCat = (cat) => { if (!cat || !tax || !tax.sectionForCategory) return false; try { const sec = tax.sectionForCategory(cat, companyProfile); return sec === "revenue" || sec === "other_income"; } catch (e) { return false; } };
    const revTxns = useMemo(() => (txns || []).filter((t) => isRevCat(t.canonical_category)), [txns, companyProfile]);
    const curRevTxns = K.windowTxns(revTxns, range);
    const priorRevTxns = cmpRange ? K.windowTxns(revTxns, cmpRange) : [];

    // ── Two-level hierarchy: canonical_category → GL account (account_name) ──
    // Mirrors the Expenses pattern (pages-opex.jsx) with a green tint for revenue.
    const [expandedCats, setExpandedCats] = useState({});
    const toggleCat = (cat) => setExpandedCats((prev) => Object.assign({}, prev, { [cat]: !prev[cat] }));
    const rsMs = +range.start, reMs = +range.end;
    const revMonthCols = (() => {
      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 revAccts = {};
    for (const t of revTxns) {
      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 cat = t.canonical_category || "Revenue";
      const mon = (t.posted_date || "").slice(0, 7);
      const r = revAccts[acct] || (revAccts[acct] = { account: acct, category: cat, total: 0, months: {} });
      const amt = Math.abs(Number(t.amount) || 0);
      r.total += amt; if (mon) r.months[mon] = (r.months[mon] || 0) + amt;
    }
    const revAcctRows = Object.values(revAccts).filter((r) => r.total > 0).sort((a, b) => b.total - a.total);
    const monthsWithRev = new Set(revAcctRows.flatMap((r) => Object.keys(r.months)));
    const revMonths = revMonthCols.filter((mo) => monthsWithRev.has(mo));
    let maxRevCell = 0; for (const r of revAcctRows) for (const mo of revMonths) { const v = r.months[mo] || 0; if (v > maxRevCell) maxRevCell = v; }
    const revCellShade = (v) => { if (!v || !maxRevCell) return "transparent"; const t = Math.min(1, v / maxRevCell); return "rgba(24,168,103," + (0.06 + t * 0.30).toFixed(3) + ")"; };
    const revMoLabel = (ym) => { const p = ym.split("-"); return ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][(+p[1]) - 1] + " '" + p[0].slice(-2); };
    const revenueHierarchy = {};
    revAcctRows.forEach((acct) => {
      const cat = acct.category || "Revenue";
      const e = revenueHierarchy[cat] || (revenueHierarchy[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(revenueHierarchy).filter((c) => c.total > 0).sort((a, b) => b.total - a.total);
    const catGrand = categoryRows.reduce((s, c) => s + c.total, 0);
    const renderRevenueHierarchy = () => h(K.Card, { title: "Revenue 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"),
            revMonths.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" } }, revMoLabel(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 Revenue"))),
          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(24,168,103,.06)", 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: "#e7f4ec", 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" : ""))),
              revMonths.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: "revenue" }), title: "View account detail", onMouseEnter: (e) => { e.currentTarget.style.background = "rgba(28,78,216,.06)"; }, onMouseLeave: (e) => { e.currentTarget.style.background = "rgba(24,168,103,.03)"; }, style: { background: "rgba(24,168,103,.03)", 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: "#f4fbf6", whiteSpace: "nowrap" } }, h("span", { style: { color: "#aab", marginRight: 6 } }, "└"), acct.account),
              revMonths.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: revCellShade(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 revenue accounts in the window — GL income postings carry no account-level detail for " + range.label.toLowerCase() + "."));

    // ── By Customer — group revenue txns by `memo`, current vs prior ──
    const byCustomer = useMemo(() => {
      const rows = {};
      const add = (list, key) => { for (const t of list) { const nm = (t.memo && String(t.memo).trim()) || "Unattributed"; const r = rows[nm] || (rows[nm] = { name: nm, cur: 0, prior: 0 }); r[key] += Math.abs(Number(t.amount) || 0); } };
      add(curRevTxns, "cur"); add(priorRevTxns, "prior");
      return Object.values(rows).filter((r) => r.cur || r.prior).sort((a, b) => b.cur - a.cur);
    }, [curRevTxns, priorRevTxns]);
    const custGrossCur = byCustomer.reduce((s, r) => s + r.cur, 0);
    const hasCustomerMemos = byCustomer.length > 0 && !(byCustomer.length === 1 && byCustomer[0].name === "Unattributed");

    // ── By Channel — group revenue txns by canonical_category (only if >1) ──
    const channelCats = cats; // already current-vs-prior category breakdown over the window
    const hasMultiChannel = channelCats.length > 1;

    // ── Product / SKU & Service Type sourcing ──
    // Preferred source: already-loaded sales_line_items dimension. Fallback: a
    // best-effort connector_invoices query. Honest placeholder if neither yields rows.
    const lineItems = (data && data.dimensions && data.dimensions.salesLineItems) || [];
    const [invLines, setInvLines] = useState(null); // null = not yet attempted
    useEffect(() => {
      let cancelled = false;
      if (lineItems.length) { setInvLines([]); return; } // dimension already covers it
      const db = window.supabaseClient;
      if (!db || !scopedCompanyId) { setInvLines([]); return; }
      // connector_invoices may not exist / may be empty — guard the whole thing.
      db.from("connector_invoices")
        .select("product_name, sku, service_type, description, line_items, amount, total")
        .eq("company_id", scopedCompanyId).limit(5000)
        .then(({ data: d }) => { if (!cancelled) setInvLines(d || []); }, () => { if (!cancelled) setInvLines([]); });
      return () => { cancelled = true; };
    }, [scopedCompanyId, lineItems.length]);

    const rollup = (rows, keyFn, valFn) => {
      const m = {};
      for (const r of rows) { const k = keyFn(r); if (!k) continue; const v = valFn(r); if (!(v > 0)) continue; m[k] = (m[k] || 0) + v; }
      return Object.keys(m).map((k) => ({ name: k, cur: m[k] })).sort((a, b) => b.cur - a.cur);
    };
    const liVal = (r) => Math.abs(Number(r.line_total_cents != null ? r.line_total_cents / 100 : (r.amount != null ? r.amount : r.total)) || 0);
    // Product/SKU rollup
    const productRows = useMemo(() => {
      if (lineItems.length) return rollup(lineItems, (r) => r.product_name || r.sku, liVal);
      if (invLines && invLines.length) return rollup(invLines, (r) => r.product_name || r.sku, liVal);
      return [];
    }, [lineItems, invLines]);
    // Service-type rollup
    const serviceRows = useMemo(() => {
      if (lineItems.length) return rollup(lineItems, (r) => r.category || r.sub_category || r.product_name, liVal);
      if (invLines && invLines.length) return rollup(invLines, (r) => r.service_type || r.description || r.product_name, liVal);
      return [];
    }, [lineItems, invLines]);
    const lineItemsLoading = invLines === null && !lineItems.length;
    const productTotal = productRows.reduce((s, r) => s + r.cur, 0);
    const serviceTotal = serviceRows.reduce((s, r) => s + r.cur, 0);

    // ── Momentum — classify customers Growing / Declining / Stable (>10% band) ──
    const momentum = useMemo(() => {
      const growing = [], declining = [];
      for (const r of byCustomer) {
        if (!(r.prior > 0)) continue; // need a prior base to judge movement
        const dp = (r.cur - r.prior) / r.prior * 100;
        if (dp > 10) growing.push({ name: r.name, dp: dp });
        else if (dp < -10) declining.push({ name: r.name, dp: dp });
      }
      growing.sort((a, b) => b.dp - a.dp); declining.sort((a, b) => a.dp - b.dp);
      return { growing: growing, declining: declining };
    }, [byCustomer]);
    const hasMomentum = momentum.growing.length > 0 || momentum.declining.length > 0;

    // ── Returns & Credits — contra-revenue category or a posting that runs
    // against the dominant revenue sign (i.e. reduces revenue). ──
    const revAmtSum = useMemo(() => (revTxns || []).reduce((s, t) => s + (Number(t.amount) || 0), 0), [revTxns]);
    const revSign = revAmtSum >= 0 ? 1 : -1; // dominant sign of revenue postings
    const credits = useMemo(() => (revTxns || []).filter((t) => {
      const cat = String(t.canonical_category || "").toLowerCase();
      if (cat.indexOf("contra_revenue") >= 0 || cat.indexOf("refund") >= 0 || cat.indexOf("credit") >= 0 || cat.indexOf("discount") >= 0 || cat.indexOf("return") >= 0) return true;
      const a = Number(t.amount) || 0;
      return a !== 0 && Math.sign(a) === -revSign; // a posting that runs against the revenue direction
    }), [revTxns, revSign]);
    const hasCredits = credits.length > 0;
    // Monthly credits as % of gross revenue (last 12 months of plHistory).
    const creditSeries = useMemo(() => {
      if (!hasCredits) return [];
      const monthKey = (d) => { const dt = new Date(d); return dt.getUTCFullYear() + "-" + String(dt.getUTCMonth() + 1).padStart(2, "0"); };
      const byMon = {};
      for (const t of revTxns) { const d = t.posted_date; if (!d) continue; const k = monthKey(d); const e = byMon[k] || (byMon[k] = { gross: 0, credit: 0 }); e.gross += Math.abs(Number(t.amount) || 0); }
      for (const t of credits) { const d = t.posted_date; if (!d) continue; const k = monthKey(d); const e = byMon[k] || (byMon[k] = { gross: 0, credit: 0 }); e.credit += Math.abs(Number(t.amount) || 0); }
      return Object.keys(byMon).sort().slice(-12).map((k) => ({ label: k.slice(5), pct: byMon[k].gross ? byMon[k].credit / byMon[k].gross * 100 : 0 }));
    }, [credits, revTxns, hasCredits]);
    const creditByCustomer = useMemo(() => {
      if (!hasCredits) return [];
      const m = {};
      for (const t of credits) { const nm = (t.memo && String(t.memo).trim()) || "Unattributed"; m[nm] = (m[nm] || 0) + Math.abs(Number(t.amount) || 0); }
      return Object.keys(m).map((k) => ({ name: k, val: m[k] })).sort((a, b) => b.val - a.val).slice(0, 8);
    }, [credits, hasCredits]);
    const totalCredits = credits.reduce((s, t) => s + Math.abs(Number(t.amount) || 0), 0);

    // ── Top Revenue Sources tab control ──
    const tabDefs = [
      { id: "customer", label: "By Customer", show: true },
      { id: "product", label: "By Product/SKU", show: isProductBusiness },
      { id: "service", label: "By Service Type", show: isServiceBusiness },
      { id: "channel", label: "By Channel", show: hasMultiChannel },
    ].filter((t) => t.show);
    const [tab, setTab] = useState("customer");
    const activeTab = tabDefs.some((t) => t.id === tab) ? tab : "customer";

    const muteNote = (msg) => h("div", { style: { fontSize: 12.5, color: "#6475a0", padding: 16 } }, msg);
    const srcTable = (rows, total, col1, valLabel) => h("table", { className: "pa-table" },
      h("thead", null, h("tr", null, h("th", null, "#"), h("th", null, col1), h("th", { className: "num" }, valLabel || "Revenue"), h("th", { className: "num" }, "% of Total"), h("th", { className: "num" }, "vs Prior"), h("th", { className: "num" }, "Trend"))),
      h("tbody", null, rows.slice(0, 12).map((r, i) => {
        const dp = (r.prior != null && r.prior > 0) ? (r.cur - r.prior) / r.prior * 100 : null;
        const arrow = dp == null ? "—" : dp > 1 ? "▲" : dp < -1 ? "▼" : "▬";
        return h("tr", { key: i },
          h("td", { className: "num muted", style: { padding: "5px 12px" } }, i + 1),
          h("td", { style: { padding: "5px 12px", fontWeight: 600 } }, r.name),
          h("td", { className: "num", style: { padding: "5px 12px" } }, M(r.cur)),
          h("td", { className: "num", style: { padding: "5px 12px" } }, (total ? r.cur / total * 100 : 0).toFixed(1) + "%"),
          h("td", { className: "num", style: { padding: "5px 12px", color: dp == null ? "#6475a0" : dp >= 0 ? "#18a867" : "#d94f47" } }, dp == null ? "—" : (dp >= 0 ? "+" : "") + dp.toFixed(0) + "%"),
          h("td", { className: "num", style: { padding: "5px 12px", color: dp == null ? "#94a3b8" : dp >= 0 ? "#18a867" : "#d94f47" } }, arrow));
      })));

    const tabBody = (() => {
      if (activeTab === "customer") {
        return hasCustomerMemos
          ? srcTable(byCustomer, custGrossCur, "Customer")
          : muteNote("No customer attribution on revenue lines yet — GL income postings carry no payer/customer memo for " + range.label.toLowerCase() + ". Customer-level revenue lights up once invoices or memos are synced.");
      }
      if (activeTab === "product") {
        if (lineItemsLoading) return muteNote("Loading product line items…");
        return productRows.length
          ? srcTable(productRows, productTotal, "Product / SKU")
          : muteNote("No product or SKU line items available. Connect a sales line-item feed (sales_line_items / connector_invoices) to break revenue down by product.");
      }
      if (activeTab === "service") {
        if (lineItemsLoading) return muteNote("Loading service line items…");
        return serviceRows.length
          ? srcTable(serviceRows, serviceTotal, "Service Type")
          : muteNote("No service-level detail available. Connect invoice line items or service descriptions to break revenue down by service type.");
      }
      if (activeTab === "channel") {
        return channelCats.length
          ? srcTable(channelCats, totalCur, "Channel (category)")
          : muteNote("Only one revenue category — no channel split to show.");
      }
      return null;
    })();

    const tabBtn = (t) => h("button", {
      key: t.id, onClick: () => setTab(t.id),
      style: { border: "none", cursor: "pointer", fontSize: 11.5, fontWeight: 700, fontFamily: K.MONO, padding: "5px 12px", borderRadius: 6, background: activeTab === t.id ? "#1C4ED8" : "rgba(13,32,64,.06)", color: activeTab === t.id ? "#fff" : "#475569" },
    }, t.label);
    const tabBar = h("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" } }, tabDefs.map(tabBtn));

    return h(K.Shell, { hero: { eyebrow: "REVENUE INTELLIGENCE", title: "Revenue Analysis", subtitle: range.label + (cmpRange ? " · vs " + cmpRange.label.toLowerCase() : "") + " · from GL income accounts", controls: h(K.PeriodControls, ps) } },

      // Row 1 — KPI tiles (Pattern A)
      h("div", { className: "pa-kpi-strip pa-kpi-strip-5" }, kpis.map((k, i) => h(K.Kpi, Object.assign({ key: i, animDelay: i * 0.05, onClick: () => window.__perduraSetPage && window.__perduraSetPage("income_statement") }, k)))),

      // Row 2 — full-width multi-series chart + month table (Pattern B)
      h(K.Card, { title: "REVENUE — MONTHLY TREND", sub: "Income bars · prior-year dashed · 3-month moving average", right: legend },
        h(K.MultiSeriesBarChart, { months: m12, series: chartSeries, height: 180 }),
        h(K.KeyTakeaway, { text: revTakeaway }),
        h("div", { style: { borderTop: "1px solid rgba(13,32,64,.08)", marginTop: 8, paddingTop: 4 } },
          h("table", { className: "pa-table" },
            h("thead", null, h("tr", null, h("th", null, "Month"), h("th", { className: "num" }, "Current"), h("th", { className: "num" }, "Prior"), h("th", { className: "num" }, "Var $"), h("th", { className: "num" }, "Var %"))),
            h("tbody", null, monthRows.map((r, i) => h("tr", { key: i },
              h("td", { style: { padding: "5px 12px" } }, r.label),
              h("td", { className: "num", style: { padding: "5px 12px" } }, M(r.cur)),
              h("td", { className: "num muted", style: { padding: "5px 12px" } }, r.prior == null ? "—" : M(r.prior)),
              h("td", { className: "num", style: { padding: "5px 12px", color: dpColor(r.d) } }, r.d == null ? "—" : (r.d >= 0 ? "+" : "−") + M(Math.abs(r.d))),
              h("td", { className: "num", style: { padding: "5px 12px", color: dpColor(r.dp) } }, r.dp == null ? "—" : (r.dp >= 0 ? "+" : "") + r.dp.toFixed(1) + "%")))))) ),

      // Row 2b — two-level: revenue by category → expand to GL accounts (TB hierarchy)
      renderRevenueHierarchy(),

      // Row 3 — 3-column grid (Pattern C): mix donut · top sources ranked · concentration
      h("div", { className: "pa-grid-3" },
        h(K.Card, { title: "Revenue mix by stream", sub: "Share of TTM revenue" },
          cats.length ? h(K.Donut, { items: cats.slice(0, 6).map((r, i) => ({ label: r.name, value: r.cur, color: K.RANK_COLORS[i % K.RANK_COLORS.length] })) }) : h("div", { style: { color: K.MUTE, fontSize: 12 } }, "No classified revenue yet.")),
        h(K.RankedList, { title: "Top revenue sources", sub: "By TTM revenue", items: cats.slice(0, 7).map((r) => ({ label: r.name, pct: totalCur ? r.cur / totalCur * 100 : 0 })) }),
        h(K.Card, { title: "Concentration", sub: "Herfindahl-Hirschman index" },
          h("div", { style: { display: "flex", alignItems: "center", gap: 14, marginBottom: 12 } },
            h("div", { style: { fontSize: 36, fontWeight: 800, color: hhiColor, fontFamily: K.MONO, lineHeight: 1 } }, Math.round(hhiVal)),
            h("div", null,
              h("div", { style: { fontSize: 13, fontWeight: 800, color: hhiColor } }, hhiLabel),
              h("div", { style: { fontSize: 11, color: K.MUTE, marginTop: 2 } }, "Top stream = " + topShare.toFixed(0) + "%"))),
          h("div", { style: { display: "flex", flexDirection: "column", gap: 7 } },
            [1, 3, 5].map((n) => { const sub = cats.slice(0, n).reduce((s, r) => s + r.cur, 0); const sh = totalCur ? sub / totalCur * 100 : 0; return h("div", { key: n },
              h("div", { style: { display: "flex", justifyContent: "space-between", fontSize: 11.5, marginBottom: 2 } }, h("span", { style: { color: "#334155" } }, "Top " + n), h("b", null, sh.toFixed(0) + "%")),
              h("div", { className: "pa-bar-track" }, h("div", { className: "pa-bar-fill", style: { width: sh + "%", background: hhiColor } }))); })))),

      // Row 3b — revenue bridge (vs comparison) + top customers (subledger, guarded)
      h("div", { className: "pa-grid-2" },
        h(K.Card, { title: "Revenue bridge", sub: cmpRange ? cmpRange.label + " → current" : "Select a comparison to view the bridge" },
          bridgeItems ? h(K.BridgeChart, { items: bridgeItems })
            : h("div", { style: { color: K.MUTE, fontSize: 12, padding: 12 } }, "Choose a comparison (Prior Period / Prior Year) to decompose the revenue change into growth vs declines.")),
        custRows.length
          ? h(K.Card, { title: "Top customers", sub: "By lifetime value · share of customer base", padding: 0 },
            h("table", { className: "pa-table" },
              h("thead", null, h("tr", null, h("th", null, "#"), h("th", null, "Customer"), h("th", { className: "num" }, "Value"), h("th", { className: "num" }, "% of total"))),
              h("tbody", null, custRows.slice(0, 10).map((c, i) => h("tr", { key: i },
                h("td", { className: "num muted" }, i + 1),
                h("td", { style: { fontWeight: 600 } }, c.name),
                h("td", { className: "num" }, M(c.val)),
                h("td", { className: "num", style: { fontWeight: 700, color: (custTotal && c.val / custTotal > 0.25) ? "#d94f47" : "#475569" } }, (custTotal ? c.val / custTotal * 100 : 0).toFixed(0) + "%"))))),
            (custTotal && custRows[0].val / custTotal > 0.25) ? h("div", { style: { fontSize: 11, color: "#d94f47", fontWeight: 600, padding: "8px 14px" } }, "⚠ Concentration risk — top customer exceeds 25% of the customer base.") : null)
          : h(K.Card, { title: "Top customers", sub: "Customer-level revenue" },
            h("div", { style: { fontSize: 12.5, color: "#6475a0", padding: 8 } }, "No customer subledger connected — customer-level revenue and concentration appear once customers are synced (GL income is category-level)."))),

      // Row 3c — Top Revenue Sources (tabbed: Customer / Product / Service / Channel)
      h(K.Card, { title: "Top revenue sources", sub: range.label + (cmpRange ? " · vs " + cmpRange.label.toLowerCase() : ""), right: tabBar, padding: 0 },
        h("div", { style: { borderTop: "1px solid rgba(13,32,64,.08)" } }, tabBody)),

      // Row 3d — Revenue Momentum (growing vs declining sources)
      h(K.Card, { title: "Revenue momentum", sub: "Sources moving >10% vs " + (cmpRange ? cmpRange.label.toLowerCase() : "prior period") + " (by customer)" },
        hasMomentum
          ? h("div", { className: "pa-grid-2" },
            h("div", null,
              h("div", { style: { fontSize: 11, fontWeight: 800, letterSpacing: ".5px", textTransform: "uppercase", color: "#18a867", fontFamily: K.MONO, marginBottom: 8 } }, "▲ Growing sources (" + momentum.growing.length + ")"),
              momentum.growing.length
                ? h("div", { style: { display: "flex", flexDirection: "column", gap: 6 } }, momentum.growing.slice(0, 8).map((r, i) => h("div", { key: i, style: { display: "flex", justifyContent: "space-between", fontSize: 12.5 } },
                  h("span", { style: { color: "#1a2540", fontWeight: 600 } }, r.name),
                  h("b", { style: { color: "#18a867", fontFamily: K.MONO } }, "+" + r.dp.toFixed(0) + "%"))))
                : h("div", { style: { fontSize: 12, color: "#94a3b8" } }, "None.")),
            h("div", null,
              h("div", { style: { fontSize: 11, fontWeight: 800, letterSpacing: ".5px", textTransform: "uppercase", color: "#d94f47", fontFamily: K.MONO, marginBottom: 8 } }, "▼ Declining sources (" + momentum.declining.length + ")"),
              momentum.declining.length
                ? h("div", { style: { display: "flex", flexDirection: "column", gap: 6 } }, momentum.declining.slice(0, 8).map((r, i) => h("div", { key: i, style: { display: "flex", justifyContent: "space-between", fontSize: 12.5 } },
                  h("span", { style: { color: "#1a2540", fontWeight: 600 } }, r.name),
                  h("b", { style: { color: "#d94f47", fontFamily: K.MONO } }, r.dp.toFixed(0) + "%"))))
                : h("div", { style: { fontSize: 12, color: "#94a3b8" } }, "None.")))
          : h("div", { style: { fontSize: 12.5, color: "#6475a0", padding: 8 } }, cmpRange ? "No customer-level movement to classify — revenue lines need both a comparison window and customer memos with a prior-period base." : "Select a comparison (Prior Period / Prior Year) to classify sources as growing or declining.")),

      // Row 3e — Returns & Credits (only when contra-revenue / credits exist)
      hasCredits ? h("div", { className: "pa-grid-2" },
        h(K.Card, { title: "Credits & returns", sub: "Credits as % of gross revenue · monthly" },
          creditSeries.length
            ? h("div", null,
              h("div", { style: { display: "flex", alignItems: "flex-end", gap: 4, height: 120, padding: "8px 0" } },
                creditSeries.map((p, i) => { const mx = Math.max.apply(null, creditSeries.map((q) => q.pct)) || 1; return h("div", { key: i, style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-end", height: "100%" } },
                  h("div", { title: p.pct.toFixed(1) + "%", style: { width: "70%", background: "#d97706", borderRadius: "3px 3px 0 0", height: Math.max(2, p.pct / mx * 100) + "%" } }),
                  h("div", { style: { fontSize: 9, color: "#94a3b8", marginTop: 4, fontFamily: K.MONO } }, p.label)); })),
              h("div", { style: { fontSize: 11.5, color: "#6475a0", marginTop: 4 } }, "Total credits " + M(totalCredits) + " · " + (custGrossCur || totalCur ? (totalCredits / ((custGrossCur || totalCur) + totalCredits) * 100).toFixed(1) : "0") + "% of gross over window."))
            : h("div", { style: { fontSize: 12, color: "#94a3b8", padding: 8 } }, "Credits detected but no dated history to chart.")),
        h(K.Card, { title: "Top customers by credits/returns", sub: "Refund & credit volume", padding: 0 },
          creditByCustomer.length
            ? h("table", { className: "pa-table" },
              h("thead", null, h("tr", null, h("th", null, "#"), h("th", null, "Customer"), h("th", { className: "num" }, "Credits"), h("th", { className: "num" }, "% of credits"))),
              h("tbody", null, creditByCustomer.map((c, i) => h("tr", { key: i },
                h("td", { className: "num muted", style: { padding: "5px 12px" } }, i + 1),
                h("td", { style: { padding: "5px 12px", fontWeight: 600 } }, c.name),
                h("td", { className: "num", style: { padding: "5px 12px", color: "#d94f47" } }, M(c.val)),
                h("td", { className: "num", style: { padding: "5px 12px" } }, (totalCredits ? c.val / totalCredits * 100 : 0).toFixed(0) + "%")))))
            : h("div", { style: { fontSize: 12, color: "#94a3b8", padding: 8 } }, "Credits are not attributed to specific customers."))) : null,

      // Row 4 — 24-month trend (60) + CFO intelligence & trend stats (40)
      h("div", { className: "pa-grid-64" },
        h(K.Card, { title: "Revenue trend & seasonality", sub: "24-month revenue with 3-month moving average" },
          h(K.Line, { values: rev, labels: labels, color: "#1C4ED8", movingAvg: true, height: 180 })),
        h("div", { style: { display: "flex", flexDirection: "column", gap: 12 } },
          h(K.Card, { title: "Trend stats" },
            h("div", { style: { display: "flex", flexDirection: "column", gap: 7 } },
              trendStats.map((s, i) => h("div", { key: i, style: { display: "flex", justifyContent: "space-between", fontSize: 12 } },
                h("span", { style: { color: "#6475a0" } }, s[0]), h("b", { style: { color: "#0d2040" } }, s[1]))))),
          h(K.Commentary, { title: "CFO revenue intelligence", items: [
            { icon: "▣", text: "Revenue is <b>" + M(totalCur) + "</b> TTM" + (yoy == null ? "." : ", " + (yoy >= 0 ? "up" : "down") + " <b>" + Math.abs(yoy).toFixed(1) + "%</b> YoY — trajectory is <b>" + accel + "</b>.") },
            { icon: hhiVal > 2500 ? "▼" : "◆", text: "Concentration is <b>" + hhiLabel.toLowerCase() + "</b> (HHI " + Math.round(hhiVal) + "); largest stream is " + topShare.toFixed(0) + "% of revenue." + (topShare > 25 ? " <b>Key risk.</b>" : "") },
            grower ? { icon: "▲", text: "Biggest grower: <b>" + grower.name + "</b> (" + ((grower.cur - grower.prior) >= 0 ? "+" : "−") + M(Math.abs(grower.cur - grower.prior)) + " YoY)." } : null,
            { icon: "➜", text: "<b>Recommended action:</b> " + (topShare > 25 ? "Diversify — grow mid-tier streams to reduce reliance on " + (cats[0] ? cats[0].name : "the top stream") + "." : decliner && (decliner.cur - decliner.prior) < 0 ? "Investigate the decline in " + decliner.name + "." : "Double down on the fastest-accelerating streams.") },
          ].filter(Boolean) }))));
  }
  window.RevenueIntelligencePage = Page;
})();
