// KPIScorecardPage (Stage 11) — window.KPIScorecardPage
// Performance scorecard. Financial KPIs are computed live from data (revenue
// growth, margins, runway, cost ratio) and compared to targets stored in
// kpi_targets (falling back to kpi_library benchmark_p50). Super admins can
// edit targets inline. Non-financial categories (customer/ops/people) list the
// company's library KPIs with "—" actuals until their data source is connected.

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

  function Page(props) {
    const K = window.PerduraPageKit;
    if (!K) return h("div", { className: "pc-page" }, "Loading…");
    const { data, companyProfile, scopedCompanyId } = props;
    const plH = (data && (data.plHistory || data.pl)) || {};
    const T = K.ttm(plH);
    const cash = (data && data.cash) || null;

    // G1 — period selector windows the financial actuals. Default LTM keeps the
    // prior trailing-12-month behaviour. Roll up plHistory over the chosen window
    // (and its comparison) so the actuals genuinely reflect the period, not just a
    // relabel.
    const anchor = K.anchorFromPlH(plH);
    const ps = K.usePeriodState("kpi_scorecard", "ltm");
    const range = K.resolvePeriod(ps.mode, anchor, ps.custom);
    const cmpRange = K.comparePeriod(range, ps.cmp);
    const roll = (span) => (span && span[1] >= span[0]) ? {
      revenue: K.sumIdx(plH.revenue, span), cogs: K.sumIdx(plH.cogs, span),
      opex: K.sumIdx(plH.opex, span), gp: K.sumIdx(plH.gp, span), ebitda: K.sumIdx(plH.ebitda, span),
    } : null;
    const W = {
      cur: roll(K.plIdxRange(plH, range)) || (T ? T.cur : { revenue: 0, cogs: 0, opex: 0, gp: 0, ebitda: 0 }),
      prior: (cmpRange ? roll(K.plIdxRange(plH, cmpRange)) : null) || (T ? T.prior : null),
    };

    const [targets, setTargets] = useState({});      // kpi_key -> {target_value, owner_name, owner_initials}
    const [canEdit, setCanEdit] = useState(false);
    const [editing, setEditing] = useState(false);
    const [draft, setDraft] = useState({});
    const [saving, setSaving] = useState(false);
    // Payroll / CAPEX aggregates feed the Headcount + CAPEX KPI groups. Fetched
    // best-effort from the new tables; absent → those group gates stay closed.
    const [opsAgg, setOpsAgg] = useState({});

    useEffect(() => {
      const db = window.supabaseClient; if (!db) return;
      let alive = true;
      (async () => {
        try { const { data: { session } } = await db.auth.getSession(); if (alive) setCanEdit(!!(session && session.user && session.user.app_metadata && session.user.app_metadata.super_admin)); } catch (e) {}
        if (!scopedCompanyId) return;
        try {
          const { data: rows } = await db.from("kpi_targets").select("kpi_key, target_value, owner_name, owner_initials").eq("company_id", scopedCompanyId);
          if (!alive || !rows) return;
          const m = {}; rows.forEach((r) => { m[r.kpi_key] = r; }); setTargets(m);
        } catch (e) {}
      })();
      return () => { alive = false; };
    }, [scopedCompanyId]);

    // Latest payroll_summary + capex_projects aggregates → Headcount/CAPEX KPIs.
    useEffect(() => {
      const db = window.supabaseClient; if (!db || !scopedCompanyId) { setOpsAgg({}); return; }
      let alive = true;
      (async () => {
        const agg = {};
        try {
          const { data: pr } = await db.from("payroll_summary").select("period_end, headcount_current, fte, turnover_pct, total_payroll_cost").eq("company_id", scopedCompanyId).order("period_end", { ascending: false }).limit(200);
          if (pr && pr.length) {
            const latest = pr[0].period_end;
            const rows = pr.filter((r) => r.period_end === latest);
            const sum = (f) => rows.reduce((s, r) => s + (Number(r[f]) || 0), 0);
            const tav = rows.filter((r) => r.turnover_pct != null);
            agg.headcount = sum("headcount_current") || null;
            agg.fte = sum("fte") || null;
            agg.payroll_total = sum("total_payroll_cost") || null;
            agg.turnover_pct = tav.length ? tav.reduce((s, r) => s + Number(r.turnover_pct), 0) / tav.length : null;
          }
        } catch (e) {}
        try {
          const { data: cx } = await db.from("capex_projects").select("budget_amount, spent_ytd").eq("company_id", scopedCompanyId).limit(2000);
          if (cx && cx.length) {
            agg.capex = cx.reduce((s, r) => s + (Number(r.spent_ytd) || 0), 0) || null;
            agg.capex_budget = cx.reduce((s, r) => s + (Number(r.budget_amount) || 0), 0) || null;
          }
        } catch (e) {}
        if (alive) setOpsAgg(agg);
      })();
      return () => { alive = false; };
    }, [scopedCompanyId]);

    // ── Declarative KPI catalog drives the entire scorecard. No archetype
    // conditionals here: getKPIsForArchetype gates on archetype + data presence,
    // getBenchmark supplies the industry band, getKPIStatus colours vs p50. ──
    const KC = window.KPICatalog || {};
    const archetype = (companyProfile && (companyProfile.archetype || (companyProfile.industry && companyProfile.industry.archetype))) || "default";
    const money = (window.PerduraFormat && window.PerduraFormat.money) ? window.PerduraFormat.money : (v) => "$" + Math.round(v || 0).toLocaleString();

    // Point-in-time sources for the balance-sheet / working-capital KPIs. AR/AP
    // totals come from the live aging subledger (real). Balance-sheet totals
    // (assets, equity, debt…) are not carried on the `data` object today — they
    // are computed only inside the Balance Sheet page from txns — so we read
    // them defensively from any BS object that may be present and otherwise
    // leave them null (those KPIs then render as a graceful "—" row).
    const aging = (data && data.aging) || {};
    const bsData = (data && (data.bs || data.balance_sheet || data.balanceSheet || data.bsTotals)) || {};
    const cfData = (data && (data.cashflow || data.cash_flow)) || {};
    const arOut = (aging.ar && aging.ar.outstanding != null) ? Number(aging.ar.outstanding) : null;
    const apOut = (aging.ap && aging.ap.outstanding != null) ? Number(aging.ap.outstanding) : null;
    const ar61 = (aging.ar && Array.isArray(aging.ar.buckets)) ? (Number(aging.ar.buckets[2] || 0) + Number(aging.ar.buckets[3] || 0)) : null;

    // Build the canonical data shape the catalog formulas consume from the
    // windowed rollup (W) plus the point-in-time sources above.
    const buildShape = (w, isCur) => {
      const s = {
        revenue: w.revenue || 0, cogs: w.cogs || 0, operating_expenses: w.opex || 0,
        gross_profit: w.gp || 0, ebitda: w.ebitda || 0,
        monthly_revenue: w.revenue ? w.revenue / 12 : null,
        monthly_opex: w.opex ? w.opex / 12 : null,
      };
      if (isCur) {
        // NOTE: absent fields are left `undefined`, never `null`. Some catalog
        // formulas divide by revenue/EBITDA without guarding the numerator
        // (e.g. net_margin = net_income/revenue); `null/rev` coerces to a fake
        // 0, whereas `undefined/rev` is NaN → rendered as a graceful "—".
        s.cash = (cash && Number.isFinite(cash.endCash)) ? cash.endCash : (bsData.cash || bsData.cash_and_equivalents || undefined);
        s.revenue_prior_year = (W.prior && W.prior.revenue) || undefined;
        s.gp_prior_year = (W.prior && W.prior.gp) || undefined;
        const burn = (s.monthly_opex || 0) - (s.monthly_revenue || 0);
        s.monthly_burn = burn > 0 ? burn : 0;
        // AR/AP from the live aging subledger (real, point-in-time).
        s.ar_total = arOut != null ? arOut : (bsData.accounts_receivable || bsData.ar || undefined);
        s.ap_total = apOut != null ? apOut : (bsData.accounts_payable || bsData.ap || undefined);
        s.ar_61plus = ar61 != null ? ar61 : undefined;
        // Balance-sheet / cash-flow totals — defensive lookups (undefined today).
        s.total_assets = bsData.total_assets || bsData.totalAssets || undefined;
        s.total_liabilities = bsData.total_liabilities || bsData.totalLiabilities || undefined;
        s.current_assets = bsData.current_assets || bsData.currentAssets || undefined;
        s.current_liabilities = bsData.current_liabilities || bsData.currentLiabilities || undefined;
        s.equity = bsData.equity || bsData.total_equity || undefined;
        s.total_debt = bsData.total_debt || bsData.long_term_debt || undefined;
        s.fixed_assets = bsData.fixed_assets || bsData.net_fixed_assets || undefined;
        s.inventory = bsData.inventory || undefined;
        s.net_income = (data && data.net_income_ltm != null) ? data.net_income_ltm : undefined;
        s.operating_cf = cfData.operating || cfData.operating_cf || undefined;
        // Headcount / CAPEX from the payroll_summary + capex_projects feeds.
        s.headcount = opsAgg.headcount || undefined;
        s.fte = opsAgg.fte || undefined;
        s.payroll_total = opsAgg.payroll_total || undefined;
        s.capex = opsAgg.capex || cfData.investing || cfData.capex || undefined;
        s.capex_budget = opsAgg.capex_budget || undefined;
      }
      return s;
    };
    const dataShape = buildShape(W.cur, true);
    const priorShape = W.prior ? buildShape(W.prior, false) : null;

    const targetOf = (id) => { const t = targets[id]; return (t && t.target_value != null) ? Number(t.target_value) : null; };
    const applicable = (KC.getKPIsForArchetype ? KC.getKPIsForArchetype(archetype, dataShape) : []) || [];
    const rowsEval = applicable.map((kpi) => {
      const value = kpi.formula ? kpi.formula(dataShape) : null;
      const prior = (priorShape && kpi.formula) ? kpi.formula(priorShape) : null;
      const target = targetOf(kpi.id);
      const status = KC.getKPIStatus ? KC.getKPIStatus(value, kpi.id, archetype, target) : "none";
      const bm = KC.getBenchmark ? KC.getBenchmark(kpi.id, archetype) : null;
      return { kpi: kpi, key: kpi.id, name: KC.getKPILabel ? KC.getKPILabel(kpi.id, archetype) : kpi.label, value: value, prior: prior, target: target, status: status, bm: bm, category: kpi.category };
    });

    // Distinct left-border accent per category group, so the scorecard scans.
    const CATEGORY_COLORS = {
      "Profitability": "#18a867", "Working Capital": "#1C4ED8", "Cash Flow": "#009fa0",
      "Leverage": "#d97706", "Efficiency": "#6366f1", "Growth": "#ec4899",
      "SaaS Metrics": "#8b5cf6", "Services Metrics": "#0ea5e9", "Clinical Metrics": "#14b8a6",
      "Nonprofit Metrics": "#f59e0b", "Retail & Inventory": "#f97316", "Headcount": "#64748b",
      "CAPEX": "#854d0e", "Tax": "#475569", "Investor Metrics": "#0d2040",
    };

    // Group by category, preserving the archetype-ordered sequence.
    const catOrder = []; const byCat = {};
    rowsEval.forEach((r) => { if (!byCat[r.category]) { byCat[r.category] = []; catOrder.push(r.category); } byCat[r.category].push(r); });

    const counts = { green: 0, amber: 0, red: 0, none: 0 };
    rowsEval.forEach((r) => { counts[r.status] = (counts[r.status] || 0) + 1; });
    const dot = (s) => s === "green" ? "🟢" : s === "amber" ? "🟡" : s === "red" ? "🔴" : "⚪";

    // Format a catalog value / benchmark band by the KPI's declared fmt.
    const fmtKpi = (v, fmt) => {
      if (v == null || !Number.isFinite(v)) return "—";
      if (fmt === "pct") return (v * 100).toFixed(1) + "%";
      if (fmt === "days") return Math.round(v) + "d";
      if (fmt === "months") return v.toFixed(1) + "mo";
      if (fmt === "multiple") return v.toFixed(2) + "×";
      if (fmt === "dollar") return money(v);
      return String(v);
    };
    // Trend arrow vs the prior window, respecting each KPI's good direction.
    const trendOf = (r) => {
      if (r.value == null || r.prior == null || !Number.isFinite(r.value) || !Number.isFinite(r.prior)) return { t: "→", c: K.MUTE };
      const d = r.value - r.prior, mag = Math.abs(r.prior) || 1;
      if (Math.abs(d) < mag * 0.01) return { t: "→", c: K.MUTE };
      const good = (r.kpi.direction === "dn") ? d < 0 : d > 0;
      const big = Math.abs(d) > mag * 0.1;
      return { t: d > 0 ? (big ? "↑↑" : "↑") : (big ? "↓↓" : "↓"), c: good ? K.POS : K.NEG };
    };
    // Inline [p25 ──●── p75] benchmark range bar with the company's position.
    const RangeBar = (r) => {
      if (!r.bm) return h("span", { style: { color: "#cbd5e1", fontSize: 10 } }, "—");
      const lo = r.bm.p25, hi = r.bm.p75, fmt = r.kpi.fmt;
      const pos = (r.value == null || !Number.isFinite(r.value) || hi === lo) ? null : Math.max(0, Math.min(1, (r.value - lo) / (hi - lo)));
      const p50pos = hi === lo ? 0.5 : Math.max(0, Math.min(1, (r.bm.p50 - lo) / (hi - lo)));
      const col = r.status === "green" ? K.POS : r.status === "red" ? K.NEG : "#D97706";
      return h("div", { title: "p25 " + fmtKpi(lo, fmt) + " · p50 " + fmtKpi(r.bm.p50, fmt) + " · p75 " + fmtKpi(hi, fmt) },
        h("div", { style: { display: "flex", justifyContent: "space-between", fontSize: 8, color: "#9ca3af", marginBottom: 2 } },
          h("span", null, fmtKpi(lo, fmt)), h("span", null, fmtKpi(hi, fmt))),
        h("div", { style: { position: "relative", height: 6, background: "#EEF2F7", borderRadius: 999 } },
          h("div", { style: { position: "absolute", left: (p50pos * 100) + "%", top: -1, width: 1, height: 8, background: "#94a3b8" } }),
          pos != null ? h("div", { style: { position: "absolute", left: "calc(" + (pos * 100) + "% - 4px)", top: -1, width: 8, height: 8, borderRadius: "50%", background: col, border: "1px solid #fff" } }) : null));
    };

    async function save() {
      const db = window.supabaseClient; if (!db || !scopedCompanyId) { setEditing(false); return; }
      setSaving(true);
      try {
        const { data: { session } } = await db.auth.getSession();
        const rows = Object.keys(draft).filter((k) => draft[k] !== "" && draft[k] != null).map((k) => ({
          company_id: scopedCompanyId, kpi_key: k, target_value: Number(draft[k]), period_type: "annual", effective_from: new Date().toISOString().slice(0, 10), created_by: session && session.user ? session.user.id : null,
        }));
        if (rows.length) {
          await db.from("kpi_targets").upsert(rows, { onConflict: "company_id,kpi_key,period_type,effective_from" });
          const m = Object.assign({}, targets); rows.forEach((r) => { m[r.kpi_key] = Object.assign({}, m[r.kpi_key], { target_value: r.target_value }); }); setTargets(m);
        }
      } catch (e) { alert("Save failed: " + (e.message || e)); }
      setSaving(false); setEditing(false); setDraft({});
    }

    const Summary = ({ label, n, color, emoji }) => h("div", { style: Object.assign({}, K.CARD, { padding: "12px 16px", flex: 1, minWidth: 130 }) },
      h("div", { style: { fontSize: 22, fontWeight: 800, color: color } }, n),
      h("div", { style: { fontSize: 11.5, color: K.MUTE, marginTop: 2 } }, emoji + " " + label));

    const editControl = canEdit ? (editing
      ? h("div", { style: { display: "flex", gap: 8 } }, h("button", { className: "pc-btn-mini ghost", onClick: () => { setEditing(false); setDraft({}); } }, "Cancel"), h("button", { className: "pc-btn-mini", disabled: saving, onClick: save }, saving ? "Saving…" : "Save targets"))
      : h("button", { className: "pc-btn-mini", onClick: () => setEditing(true) }, "✎ Edit Targets")) : null;

    return h(K.Shell, { hero: { eyebrow: "PERFORMANCE MANAGEMENT", title: "KPI Scorecard", subtitle: range.label + " actuals vs targets" + (cmpRange ? " · vs " + cmpRange.label.toLowerCase() : ""),
        controls: h("div", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" } },
          K.PeriodControls ? h(K.PeriodControls, ps) : null, editControl) } },

      h("div", { style: { display: "flex", gap: 12, flexWrap: "wrap" } },
        h(Summary, { label: "On Track", n: counts.green || 0, color: K.POS, emoji: "🟢" }),
        h(Summary, { label: "Watch", n: counts.amber || 0, color: "#D97706", emoji: "🟡" }),
        h(Summary, { label: "Below Target", n: counts.red || 0, color: K.NEG, emoji: "🔴" }),
        h(Summary, { label: "No Target Set", n: counts.none || 0, color: K.MUTE, emoji: "⚪" })),

      h("div", { style: { fontSize: 10, color: "#6475a0", padding: "8px 2px 4px", display: "flex", alignItems: "center", gap: 6 } },
        h("span", null, "💡"), "Click any metric to navigate to the relevant detail page"),

      rowsEval.length ? h(K.Card, { padding: 0 },
        h("div", { style: { overflowX: "auto" } },
          h("table", { className: "pc-table", style: { fontSize: 12, width: "100%", minWidth: 900 } },
            h("thead", null, h("tr", null, ["KPI", "Actual", "Benchmark", "Range (p25–p75)", "Status", "Target", "Trend", "What it means"].map((c, i) => h("th", { key: i, style: { textAlign: (i === 1 || i === 2 || i === 5) ? "right" : (i === 4 || i === 6) ? "center" : "left" } }, c)))),
            h("tbody", null,
              catOrder.map((cat) => h(React.Fragment, { key: cat },
                h("tr", null, h("td", { colSpan: 8, style: { background: "#0d2040", color: "#fff", fontSize: 11, textTransform: "uppercase", letterSpacing: "0.08em", fontWeight: 700, padding: "8px 16px", borderLeft: "4px solid " + (CATEGORY_COLORS[cat] || "#0d2040") } }, cat)),
                byCat[cat].map((r, i) => {
                  const tr = trendOf(r);
                  // Graceful "no data" rows: dim the whole row, tooltip the actual
                  // cell, and surface a "connect data →" hint instead of hiding the
                  // KPI — the user still learns what's possible for their archetype.
                  const noData = r.value == null || !Number.isFinite(r.value);
                  const missing = (r.kpi.required_data || []).filter((f) => dataShape[f] == null || dataShape[f] === 0);
                  const tip = noData ? ("Data not available — connect the underlying feed to unlock" + (missing.length ? " (needs: " + missing.join(", ") + ")" : "")) : undefined;
                  // Computed KPIs navigate to the most relevant detail page. Skip
                  // while editing targets so the inline inputs stay usable.
                  const kctx = ((r.category || "") + " " + (r.name || "")).toLowerCase();
                  const navTo = /cash|runway|burn/.test(kctx) ? "cash_flow_statement"
                    : /liquid|working|current ratio|quick|receivable|payable|inventory|days /.test(kctx) ? "working_capital"
                    : "income_statement";
                  return h("tr", { key: r.key + i, onClick: editing ? undefined : () => window.__perduraSetPage && window.__perduraSetPage(navTo), title: editing ? undefined : "Open detail page", style: { cursor: editing ? undefined : "pointer", opacity: noData ? 0.55 : 1, background: r.status === "amber" ? "rgba(217,119,6,0.04)" : r.status === "red" ? "rgba(220,38,38,0.04)" : undefined } },
                    h("td", { style: { fontWeight: 600 } }, r.name),
                    h("td", { style: { textAlign: "right" }, title: tip },
                      h("div", { style: { fontWeight: 800, fontFamily: "'JetBrains Mono','Geist Mono',monospace", color: noData ? "#9ca3af" : undefined } }, fmtKpi(r.value, r.kpi.fmt)),
                      h("div", { style: { fontSize: 8, color: "#9ca3af", marginTop: 1 } }, noData ? "no data" : range.label)),
                    h("td", { style: { textAlign: "right", color: K.MUTE } }, r.bm ? "p50 " + fmtKpi(r.bm.p50, r.kpi.fmt) : "—"),
                    h("td", { style: { minWidth: 110 } }, RangeBar(r)),
                    h("td", { style: { textAlign: "center", fontSize: 14 } }, dot(r.status)),
                    h("td", { style: { textAlign: "right" } }, editing
                      ? h("input", { type: "number", step: "any", defaultValue: r.target != null ? r.target : "", onChange: (e) => setDraft((d) => Object.assign({}, d, { [r.key]: e.target.value })), style: { width: 70, fontSize: 12, padding: "3px 6px", border: "1px solid " + K.LINE, borderRadius: 5, textAlign: "right" } })
                      : (r.target != null ? fmtKpi(r.target, r.kpi.fmt) : "—")),
                    h("td", { style: { textAlign: "center", color: tr.c, fontWeight: 700 } }, tr.t),
                    h("td", { style: { color: K.MUTE, fontSize: 11, maxWidth: 280, lineHeight: 1.4 } },
                      h("span", null, r.kpi.plain_english || r.kpi.description || ""),
                      noData ? h("span", { title: tip, style: { color: "#94a3b8", fontSize: 10.5, marginLeft: 6, fontWeight: 600, whiteSpace: "nowrap", cursor: "default" } }, "· connect data →") : null));
                }))))))) : h(K.Card, null, h("div", { style: { padding: 20, color: K.MUTE, fontSize: 13 } }, "No KPIs have sufficient data for this period yet — connect more of your ledger / subledgers to populate the scorecard.")),

      h("div", { style: { fontSize: 12, color: "var(--text-3)", padding: "4px 2px", lineHeight: 1.5 } },
        "KPIs, ordering, and benchmarks are driven by your business archetype (" + archetype + ") from the declarative KPI catalog. A KPI only appears when its underlying data is present; benchmark bands (p25/p50/p75) are industry medians for your archetype."),

      (function () {
        // BLOCK D — standardised CFO panel (shared window.CFOCommentaryPanel).
        const Panel = K.CFOCommentaryPanel;
        if (!Panel) return null;
        const redK = rowsEval.filter((r) => r.status === "red");
        const insights = [
          { type: "neutral", text: "<b>" + (counts.green || 0) + " of " + rowsEval.length + "</b> KPIs at or above the " + archetype + " p50 benchmark" + ((counts.amber || 0) ? ", <b>" + counts.amber + "</b> watching" : "") + ((counts.red || 0) ? ", <b>" + counts.red + "</b> below target" : "") + "." },
        ].concat(redK.slice(0, 3).map((k) => ({ type: "warning", text: "<b>" + k.name + "</b> is below its " + archetype + " benchmark.", detail: k.kpi.plain_english || k.kpi.description || "" })));
        if (!redK.length) insights.push({ type: "positive", text: "Every measured KPI is at or above its industry benchmark — set stretch targets via Edit Targets to keep pushing." });
        return h(Panel, { title: "Scorecard Summary", insights: insights,
          savings: redK.length ? "Focus on <b>" + redK[0].name + "</b> first — it is furthest below the " + archetype + " median and most likely to move the overall scorecard." : null });
      })());
  }
  window.KPIScorecardPage = Page;
})();
