// CashForecastPage — window.CashForecastPage
//
// 13-Week Cash Forecast (rolling liquidity planning). Reads weekly rows from the
// cash_forecast table (one row per forecast week 1..13). Renders a stacked
// inflow/outflow waterfall chart with a closing-cash line and a minimum-cash
// floor, a full 13-week detail table with a running opening→closing chain, an
// assumptions panel, and an upload/edit flow. When no manual forecast exists and
// GL transactions are present, it seeds an estimated forecast from a trailing
// 4-week average of cash activity. Honest empty state when there is no data at
// all — never fabricates numbers.
//
// Props: { data, companyProfile, scopedCompanyId, globalPeriod, setPage }
//
// cash_forecast columns: company_id, week_number (1-13), week_start_date,
// forecast_label, opening_cash, ar_collections, other_inflows, ap_payments,
// payroll, tax_vat, capex_payments, loan_repayment, other_outflows,
// closing_cash, is_actual, notes.

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

  const HORIZON = 13;
  const MS_WEEK = 7 * 86400000;
  const MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

  const nz = (v) => Number(v) || 0;

  // Inflow / outflow field maps so chart + table stay in sync.
  const INFLOW_KEYS = ["ar_collections", "other_inflows"];
  const OUTFLOW_KEYS = ["ap_payments", "payroll", "tax_vat", "capex_payments", "loan_repayment", "other_outflows"];

  const inflowsOf = (r) => INFLOW_KEYS.reduce((s, k) => s + nz(r[k]), 0);
  const outflowsOf = (r) => OUTFLOW_KEYS.reduce((s, k) => s + nz(r[k]), 0);

  // Format a YYYY-MM-DD style date into "12 Jun".
  function fmtDate(v) {
    if (!v) return "—";
    const d = new Date(v);
    if (isNaN(d)) return String(v);
    return d.getUTCDate() + " " + MONTH_ABBR[d.getUTCMonth()];
  }

  // Build a running opening/closing chain. opening[1] is taken from the first
  // row's stored opening_cash (or a supplied seed); each subsequent opening is
  // the prior week's computed closing. closing = opening + inflows − outflows.
  function chainRows(rows, seedOpening) {
    const out = [];
    let prevClose = null;
    rows.forEach((r, i) => {
      const inflow = inflowsOf(r);
      const outflow = outflowsOf(r);
      let opening;
      if (i === 0) opening = r.opening_cash != null ? nz(r.opening_cash) : nz(seedOpening);
      else opening = prevClose;
      const closing = (r.closing_cash != null && r.opening_cash != null)
        ? nz(r.closing_cash)
        : opening + inflow - outflow;
      prevClose = closing;
      out.push(Object.assign({}, r, { _opening: opening, _closing: closing, _inflow: inflow, _outflow: outflow, _net: inflow - outflow }));
    });
    return out;
  }

  // Build a 13-week baseline from trailing GL activity + balance-sheet balances.
  // Uses the last ~12 weeks of cash txns for weekly inflow/outflow averages,
  // derives AR collections from the AR balance (6-week cycle) and AP payments
  // from the AP balance (4-week cycle), then spreads them with front-loaded
  // collections, lumpy AP, bi-weekly payroll and periodic tax. Returns 13
  // chained rows flagged as estimates, or null when there is no usable signal.
  // Sign convention here matches the GL feed: inflows are NEGATIVE, costs
  // POSITIVE. The trailing window + forecast start anchor on the latest GL date
  // (robust to historical datasets), not the wall-clock "now".
  function buildAutoEstimate(txns, bs, seedCash) {
    const list = txns || [];
    let maxD = 0;
    for (const t of list) { if (t.posted_date) { const d = +new Date(t.posted_date); if (d > maxD) maxD = d; } }
    const arBal = bs && bs.accounts_receivable != null ? Math.abs(nz(bs.accounts_receivable)) : 0;
    const apBal = bs && bs.accounts_payable != null ? Math.abs(nz(bs.accounts_payable)) : 0;
    const bsCash = bs && bs.cash != null ? Math.abs(nz(bs.cash)) : null;
    // Need a flow signal to model from: recent GL txns, or an AR / AP balance.
    // A bare cash balance alone isn't enough (it would draw a flat line).
    if (!maxD && !arBal && !apBal) return null;

    // Trailing 12-week window anchored on the latest GL date.
    const cut = maxD ? maxD - 12 * MS_WEEK : 0;
    let inSum = 0, outSum = 0;
    for (const t of list) {
      const d = t.posted_date ? +new Date(t.posted_date) : 0;
      if (!d || (maxD && (d <= cut || d > maxD))) continue;
      const amt = Number(t.amount) || 0;
      if (amt < 0) inSum += Math.abs(amt); else outSum += amt;
    }
    const avgWeeklyInflow = inSum / 12;
    const avgWeeklyOutflow = outSum / 12;
    const weeklyAP = apBal / 4;   // ~4-week payment cycle
    const weeklyAR = arBal / 6;   // ~6-week collection cycle

    const base = maxD ? new Date(maxD) : new Date();
    const openingCash = bsCash != null ? bsCash : nz(seedCash);
    const rows = [];
    for (let w = 1; w <= HORIZON; w++) {
      const arColl = weeklyAR * (w <= 6 ? 1.1 : 0.9);              // front-loaded collections
      const apPay = (w % 4 === 0) ? weeklyAP * 2 : weeklyAP * 0.5;  // lumpy payments
      const payroll = (w % 2 === 0) ? avgWeeklyOutflow * 0.4 : 0;   // bi-weekly payroll
      const otherOut = avgWeeklyOutflow * 0.3;
      const otherIn = avgWeeklyInflow * 0.1;
      const taxVat = (w === 4 || w === 8 || w === 12) ? avgWeeklyOutflow * 0.15 : 0;
      rows.push({
        week_number: w,
        week_start_date: new Date(+base + (w - 1) * MS_WEEK).toISOString().slice(0, 10),
        forecast_label: "W" + w,
        ar_collections: Math.round(arColl),
        other_inflows: Math.round(otherIn),
        ap_payments: Math.round(apPay),
        payroll: Math.round(payroll),
        tax_vat: Math.round(taxVat),
        capex_payments: 0,
        loan_repayment: 0,
        other_outflows: Math.round(otherOut),
        is_actual: false, _estimated: true,
      });
    }
    return chainRows(rows, openingCash);
  }

  // Sample CSV — exact header order the importer expects, with example weeks.
  function downloadForecastTemplate() {
    const csv = [
      "week_number,week_start_date,ar_collections,other_inflows,ap_payments,payroll,tax_vat,capex_payments,loan_repayment,other_outflows",
      "1,2026-01-05,120000,5000,60000,0,0,0,0,8000",
      "2,2026-01-12,110000,0,30000,40000,0,0,0,8000",
      "3,2026-01-19,115000,0,30000,0,0,25000,0,8000",
      "4,2026-01-26,100000,0,90000,40000,18000,0,12000,8000",
    ].join("\n");
    const a = document.createElement("a");
    a.href = "data:text/csv;charset=utf-8," + encodeURIComponent(csv);
    a.download = "cash_forecast_template.csv";
    a.click();
  }

  // ── CSV parsing (forecast upload) ───────────────────────────────────────────
  const UPLOAD_COLS = ["week_number", "week_start_date", "ar_collections", "ap_payments", "payroll", "tax_vat", "capex_payments", "loan_repayment", "other_inflows", "other_outflows"];

  function parseCsv(text) {
    const lines = String(text || "").split(/\r?\n/).filter((l) => l.trim() !== "");
    if (lines.length < 2) return { rows: [], error: "CSV needs a header row and at least one data row." };
    const splitLine = (line) => {
      const out = []; let cur = "", q = false;
      for (let i = 0; i < line.length; i++) {
        const c = line[i];
        if (q) { if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; } else if (c === '"') q = false; else cur += c; }
        else { if (c === '"') q = true; else if (c === ",") { out.push(cur); cur = ""; } else cur += c; }
      }
      out.push(cur);
      return out.map((s) => s.trim());
    };
    const header = splitLine(lines[0]).map((c) => c.toLowerCase());
    const idx = {};
    UPLOAD_COLS.forEach((c) => { idx[c] = header.indexOf(c); });
    if (idx.week_number < 0) return { rows: [], error: "CSV must include a week_number column." };
    const numKeys = ["ar_collections", "ap_payments", "payroll", "tax_vat", "capex_payments", "loan_repayment", "other_inflows", "other_outflows"];
    const rows = [];
    for (let i = 1; i < lines.length; i++) {
      const cells = splitLine(lines[i]);
      const wn = parseInt(cells[idx.week_number], 10);
      if (!(wn >= 1 && wn <= HORIZON)) continue;
      const row = { week_number: wn, week_start_date: idx.week_start_date >= 0 ? cells[idx.week_start_date] : null };
      numKeys.forEach((k) => { row[k] = idx[k] >= 0 ? (Number(String(cells[idx[k]]).replace(/[$,]/g, "")) || 0) : 0; });
      rows.push(row);
    }
    rows.sort((a, b) => a.week_number - b.week_number);
    if (!rows.length) return { rows: [], error: "No valid week rows (1-13) found in the CSV." };
    return { rows: rows, error: null };
  }

  function Page(props) {
    const K = window.PerduraPageKit;
    if (!K) return h("div", { className: "pa-page" }, "Loading…");
    const { data, companyProfile, scopedCompanyId } = props || {};
    const M = (v) => K.moneyStr(v, { compact: true });
    const cash = (data && data.cash) || {};
    const plH = (data && (data.plHistory || data.pl)) || { opex: [] };
    const txns = (data && data.txns) || [];

    const [rows, setRows] = useState(null);     // raw fetched rows (or [])
    const [loadErr, setLoadErr] = useState(false);
    const [refreshTick, setRefreshTick] = useState(0);

    // Upload / preview state.
    const [preview, setPreview] = useState(null); // { rows, error }
    const [saving, setSaving] = useState(false);
    const [saveErr, setSaveErr] = useState(null);

    useEffect(() => {
      let cancelled = false;
      const db = window.supabaseClient;
      if (!db || !scopedCompanyId) { setRows([]); return; }
      setRows(null); setLoadErr(false);
      db.from("cash_forecast").select("*").eq("company_id", scopedCompanyId).order("week_number")
        .then(({ data: d, error }) => { if (cancelled) return; if (error) { setLoadErr(true); setRows([]); } else setRows(d || []); },
              () => { if (!cancelled) { setLoadErr(true); setRows([]); } });
      return () => { cancelled = true; };
    }, [scopedCompanyId, refreshTick]);

    // Minimum-cash floor. Prefer an explicit profile field; else 4 weeks of OpEx
    // where weekly OpEx ≈ latest monthly opex / 4.3.
    const latestOpex = useMemo(() => {
      const arr = (plH && plH.opex) || [];
      for (let i = arr.length - 1; i >= 0; i--) { const v = Number(arr[i]); if (v) return v; }
      return null;
    }, [plH]);
    const weeklyOpex = latestOpex != null ? latestOpex / 4.3 : null;
    const bsCash = (data && data.bs && data.bs.cash != null) ? Math.abs(nz(data.bs.cash)) : null;
    const profileFloor = companyProfile && companyProfile.min_cash_floor != null ? nz(companyProfile.min_cash_floor) : null;
    // Floor: explicit profile value → else 25% of current cash → else ≈4 weeks OpEx.
    const floor = profileFloor != null ? profileFloor
      : (bsCash != null && bsCash > 0 ? bsCash * 0.25
        : (weeklyOpex != null ? weeklyOpex * 4 : null));
    const floorBasis = profileFloor != null ? "profile" : (bsCash != null && bsCash > 0 ? "25% of cash" : (weeklyOpex != null ? "≈4 weeks OpEx" : null));
    const floorKnown = floor != null;

    // Build the working forecast: stored rows when present, else an estimate.
    // Opening cash prefers the balance-sheet cash balance.
    const startCash = bsCash != null ? bsCash : (cash.startCash != null ? nz(cash.startCash) : null);
    const stored = rows && rows.length ? chainRows(rows.slice().sort((a, b) => nz(a.week_number) - nz(b.week_number)), startCash) : null;
    const estimated = (!stored) ? buildAutoEstimate(txns, data && data.bs, startCash) : null;
    const weeks = stored || estimated || [];
    const isEstimate = !stored && !!estimated;
    const hasForecast = weeks.length > 0;

    // Current week = first non-actual week, else week 1.
    const curWeekIdx = useMemo(() => {
      const i = weeks.findIndex((w) => !w.is_actual);
      return i < 0 ? 0 : i;
    }, [weeks]);

    // ── KPI tiles ─────────────────────────────────────────────────────────────
    const opening = hasForecast ? weeks[0]._opening : null;
    const closing13 = hasForecast ? weeks[weeks.length - 1]._closing : null;
    const minClosing = hasForecast ? Math.min.apply(null, weeks.map((w) => w._closing)) : null;
    const weeksAboveFloor = (hasForecast && floorKnown) ? weeks.filter((w) => w._closing > floor).length : null;

    const minColor = (minClosing == null || !floorKnown) ? "navy" : (minClosing < floor ? "red" : minClosing < floor * 1.1 ? "amber" : "green");
    const aboveColor = weeksAboveFloor == null ? "navy" : weeksAboveFloor === HORIZON ? "green" : weeksAboveFloor >= HORIZON * 0.7 ? "amber" : "red";

    const kpis = [
      { label: "Opening Cash", value: opening == null ? "—" : M(opening), valueColor: "navy", sub: "Week 1 start balance" },
      { label: "Closing Cash (W13)", value: closing13 == null ? "—" : M(closing13), valueColor: closing13 == null ? "navy" : closing13 >= (opening || 0) ? "green" : "amber", sub: "End of 13-week horizon" },
      { label: "Minimum Cash", value: minClosing == null ? "—" : M(minClosing), valueColor: minColor, sub: floorKnown ? "Lowest weekly close · floor " + M(floor) : "Lowest weekly close" },
      { label: "Weeks Above Floor", value: weeksAboveFloor == null ? "—" : weeksAboveFloor + " / " + HORIZON, valueColor: aboveColor, sub: floorKnown ? "Closing above min-cash floor" : "Set a min-cash floor to track" },
    ];

    // ── Section 1 — cash flow chart (SVG) ───────────────────────────────────────
    function renderChart() {
      if (!hasForecast) return null;
      const W = 860, H = 320, padL = 56, padR = 16, padT = 18, padB = 40;
      const chartW = W - padL - padR, chartH = H - padT - padB;
      const n = weeks.length;
      const colW = chartW / n;

      const inflowVals = weeks.map((w) => w._inflow);
      const outflowVals = weeks.map((w) => w._outflow);
      const closeVals = weeks.map((w) => w._closing);
      const maxClose = Math.max.apply(null, closeVals);
      const minClose = Math.min.apply(null, closeVals);
      const maxFlow = Math.max.apply(null, inflowVals.concat(outflowVals).concat([1]));

      // Two visual bands: top band = cash bars (inflows up / outflows down around a
      // flow-zero line); the closing-cash line + floor map onto the same value
      // range so they read together. We scale everything to a shared range that
      // covers flows (±) and the closing balance / floor.
      const lo = Math.min(0, -maxFlow, minClose, floorKnown ? floor : minClose);
      const hi = Math.max(maxFlow, maxClose, floorKnown ? floor : maxClose, 1);
      const range = (hi - lo) || 1;
      const toY = (v) => padT + chartH * (1 - (v - lo) / range);
      const fmtN = (v) => { const a = Math.abs(v); const s = v < 0 ? "-" : ""; return a >= 1e6 ? s + (a / 1e6).toFixed(1) + "M" : a >= 1e3 ? s + (a / 1e3).toFixed(0) + "K" : s + Math.round(a); };

      const els = [];
      // Gridlines.
      [0, .25, .5, .75, 1].forEach((p, i) => {
        const y = padT + chartH * p; const val = hi - range * p;
        els.push(h("line", { key: "g" + i, x1: padL, x2: W - padR, y1: y, y2: y, stroke: "rgba(13,32,64,.07)", strokeWidth: .8 }));
        els.push(h("text", { key: "gl" + i, x: padL - 6, y: y + 4, textAnchor: "end", fontSize: 9, fill: "#94a3b8", fontFamily: K.MONO }, fmtN(val)));
      });
      // Below-floor column tints.
      if (floorKnown) {
        weeks.forEach((w, i) => {
          if (w._closing < floor) els.push(h("rect", { key: "tint" + i, x: padL + i * colW, y: padT, width: colW, height: chartH, fill: "rgba(217,79,71,.08)" }));
        });
      }
      // Zero (flow) line.
      els.push(h("line", { key: "zero", x1: padL, x2: W - padR, y1: toY(0), y2: toY(0), stroke: "rgba(13,32,64,.22)", strokeWidth: 1 }));
      // Inflow (green, above zero) + outflow (red, below zero) bars.
      const bw = colW * 0.5;
      weeks.forEach((w, i) => {
        const cx = padL + i * colW + colW / 2;
        const inTop = toY(w._inflow), inBot = toY(0);
        els.push(h("rect", { key: "in" + i, x: cx - bw / 2, y: inTop, width: bw, height: Math.max(inBot - inTop, 0.5), fill: "#18a867", rx: 2, opacity: 0.9 }));
        const outTop = toY(0), outBot = toY(-w._outflow);
        els.push(h("rect", { key: "out" + i, x: cx - bw / 2, y: outTop, width: bw, height: Math.max(outBot - outTop, 0.5), fill: "#d94f47", rx: 2, opacity: 0.9 }));
      });
      // Min-cash floor (red dashed horizontal).
      if (floorKnown) {
        els.push(h("line", { key: "floor", x1: padL, x2: W - padR, y1: toY(floor), y2: toY(floor), stroke: "#d94f47", strokeWidth: 1.4, strokeDasharray: "6 4" }));
        els.push(h("text", { key: "floorlbl", x: W - padR, y: toY(floor) - 5, textAnchor: "end", fontSize: 9, fontWeight: 700, fill: "#d94f47", fontFamily: K.MONO }, "FLOOR " + M(floor)));
      }
      // Net-cash-flow line overlay (navy solid through the per-week net).
      const netPts = weeks.map((w, i) => (padL + i * colW + colW / 2) + "," + toY(w._net)).join(" ");
      els.push(h("polyline", { key: "netline", points: netPts, fill: "none", stroke: "#1C4ED8", strokeWidth: 1.8, strokeLinejoin: "round", strokeLinecap: "round", opacity: 0.85 }));
      // Closing-cash line (navy dashed).
      const closePts = weeks.map((w, i) => (padL + i * colW + colW / 2) + "," + toY(w._closing)).join(" ");
      els.push(h("polyline", { key: "closeline", points: closePts, fill: "none", stroke: "#009fa0", strokeWidth: 2.4, strokeLinejoin: "round", strokeLinecap: "round" }));
      weeks.forEach((w, i) => {
        els.push(h("circle", { key: "cd" + i, cx: padL + i * colW + colW / 2, cy: toY(w._closing), r: 2.6, fill: "#009fa0" }));
      });
      // X labels W1..W13.
      weeks.forEach((w, i) => {
        els.push(h("text", { key: "xl" + i, x: padL + i * colW + colW / 2, y: H - 22, textAnchor: "middle", fontSize: 9, fontWeight: i === curWeekIdx ? 800 : 600, fill: i === curWeekIdx ? "#b8921e" : "#475569", fontFamily: K.MONO }, "W" + nz(w.week_number || i + 1)));
        els.push(h("text", { key: "xd" + i, x: padL + i * colW + colW / 2, y: H - 9, textAnchor: "middle", fontSize: 8, fill: "#94a3b8" }, fmtDate(w.week_start_date)));
      });

      const legendItem = (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);

      return h(K.Card, {
        title: "13-WEEK CASH FLOW",
        sub: "Inflows / outflows · closing-cash trajectory" + (floorKnown ? " vs minimum-cash floor" : ""),
        right: h("div", { style: { display: "flex", gap: 12, flexWrap: "wrap" } },
          legendItem("#18a867", "Inflows"), legendItem("#d94f47", "Outflows"),
          legendItem("#1C4ED8", "Net flow"), legendItem("#009fa0", "Closing"),
          floorKnown ? legendItem("#d94f47", "Floor", true) : null),
      },
        h("div", { style: { overflowX: "auto" } },
          h("svg", { viewBox: "0 0 " + W + " " + H, style: { width: "100%", height: "auto", maxHeight: H + "px", display: "block", minWidth: 640 } }, els)),
        h(K.KeyTakeaway, { text: takeawayText() }));
    }

    function takeawayText() {
      if (!hasForecast) return "Upload or sync a 13-week forecast to project the cash trajectory.";
      const dir = (closing13 != null && opening != null) ? (closing13 >= opening ? "builds to" : "draws down to") : "ends at";
      let t = "Cash " + dir + " <b>" + M(closing13) + "</b> over the 13 weeks (from " + M(opening) + ").";
      if (floorKnown && minClosing != null) {
        if (minClosing < floor) {
          const breach = weeks.find((w) => w._closing < floor);
          t += " <b style='color:#d94f47'>Floor breached</b> — closing dips to " + M(minClosing) + (breach ? " around week " + nz(breach.week_number) : "") + ", below the " + M(floor) + " minimum.";
        } else if (minClosing < floor * 1.1) {
          t += " The trough of " + M(minClosing) + " runs close to the " + M(floor) + " floor — watch collection timing.";
        } else {
          t += " The forecast stays clear of the " + M(floor) + " minimum-cash floor throughout.";
        }
      }
      if (isEstimate) t += " These figures are <b>estimated from a trailing 4-week average</b> — upload a forecast to refine.";
      return t;
    }

    // ── Section 2 — 13-week detail table ────────────────────────────────────────
    function statusFor(w) {
      if (!floorKnown) return { icon: "•", color: "#94a3b8", label: "" };
      if (w._closing < floor) return { icon: "🔴", color: "#d94f47", label: "below floor" };
      if (w._closing < floor * 1.15) return { icon: "⚠️", color: "#d97706", label: "near floor" };
      return { icon: "✅", color: "#18a867", label: "above floor" };
    }

    function renderTable() {
      if (!hasForecast) return null;
      const cols = ["WEEK", "DATE", "OPENING", "AR COLL", "OTHER IN", "AP PAY", "PAYROLL", "TAX", "CAPEX", "OTHER OUT", "NET FLOW", "CLOSING", "STATUS"];
      const th = (txt, i) => h("th", { key: i, style: { textAlign: i <= 1 ? "left" : (i === 12 ? "center" : "right"), padding: "8px 10px", fontSize: 9.5, fontWeight: 700, letterSpacing: 0.4, textTransform: "uppercase", color: "rgba(255,255,255,.85)", whiteSpace: "nowrap", position: i === 0 ? "sticky" : undefined, left: i === 0 ? 0 : undefined, background: i === 0 ? "#0d2040" : undefined, zIndex: i === 0 ? 2 : undefined } }, txt);
      const numCell = (v, style) => h("td", { style: Object.assign({ textAlign: "right", padding: "7px 10px", fontFamily: K.MONO, fontSize: 11, whiteSpace: "nowrap", color: "#1a2540" }, style || {}) }, v ? M(v) : "—");

      const closingColor = (w) => !floorKnown ? "#0d2040" : (w._closing < floor ? "#d94f47" : w._closing < floor * 1.15 ? "#d97706" : "#18a867");
      const estBadge = h("span", { style: { marginLeft: 5, padding: "1px 5px", borderRadius: 999, fontSize: 8.5, fontWeight: 800, letterSpacing: 0.3, textTransform: "uppercase", color: "#6475a0", background: "rgba(100,117,160,.14)", fontStyle: "normal", fontFamily: K.MONO } }, "est.");
      const body = weeks.map((w, i) => {
        const st = statusFor(w);
        const isCur = i === curWeekIdx;
        const est = isEstimate || w._estimated;
        const rowBg = floorKnown && w._closing < floor ? "rgba(217,79,71,.05)" : (i % 2 ? "#fafbfe" : "#fff");
        const stickyBg = floorKnown && w._closing < floor ? "#fbecec" : (i % 2 ? "#fafbfe" : "#fff");
        return h("tr", { key: i, style: { background: rowBg, borderLeft: isCur ? "3px solid #b8921e" : "3px solid transparent", fontStyle: est ? "italic" : "normal", opacity: est ? 0.9 : 1 } },
          h("td", { style: { textAlign: "left", padding: "7px 10px", fontWeight: 700, fontSize: 11, color: isCur ? "#b8921e" : "#0d2040", position: "sticky", left: 0, background: isCur ? "#fdf7e6" : stickyBg, whiteSpace: "nowrap", zIndex: 1 } }, "W" + nz(w.week_number || i + 1) + (w.is_actual ? " ·a" : ""), est ? estBadge : null),
          h("td", { style: { textAlign: "left", padding: "7px 10px", fontSize: 11, color: "#6475a0", whiteSpace: "nowrap" } }, fmtDate(w.week_start_date)),
          numCell(w._opening, { fontWeight: 600 }),
          numCell(w.ar_collections, { color: "#18a867" }),
          numCell(w.other_inflows, { color: "#18a867" }),
          numCell(w.ap_payments, { color: "#d94f47" }),
          numCell(w.payroll, { color: "#d94f47" }),
          numCell(w.tax_vat, { color: "#d94f47" }),
          numCell(w.capex_payments, { color: "#d94f47" }),
          numCell(w.other_outflows, { color: "#d94f47" }),
          h("td", { style: { textAlign: "right", padding: "7px 10px", fontFamily: K.MONO, fontSize: 11, fontWeight: 700, color: w._net >= 0 ? "#18a867" : "#d94f47", whiteSpace: "nowrap" } }, (w._net >= 0 ? "+" : "−") + M(Math.abs(w._net))),
          h("td", { style: { textAlign: "right", padding: "7px 10px", fontFamily: K.MONO, fontSize: 11.5, fontWeight: 800, color: closingColor(w), whiteSpace: "nowrap" } }, M(w._closing)),
          h("td", { style: { textAlign: "center", padding: "7px 10px", whiteSpace: "nowrap", title: st.label } }, st.icon));
      });

      return h(K.Card, { title: "13-WEEK DETAIL", sub: "Running cash chain · current week highlighted in gold" + (isEstimate ? " · estimated figures" : ""), padding: 0 },
        h("div", { style: { overflowX: "auto" } },
          h("table", { style: { width: "100%", borderCollapse: "collapse", minWidth: 920 } },
            h("thead", null, h("tr", { style: { background: "#0d2040" } }, cols.map(th))),
            h("tbody", null, body))));
    }

    // ── Section 3 — assumptions panel ───────────────────────────────────────────
    function renderAssumptions() {
      const get = (k) => (companyProfile && companyProfile[k] != null) ? companyProfile[k] : null;
      const arRate = get("ar_collection_rate");
      const payFreq = get("payroll_frequency");
      const vatDue = get("vat_due_day") || get("vat_due");
      const assumptions = [
        ["AR Collection Rate", arRate != null ? (Number(arRate) <= 1 ? (Number(arRate) * 100).toFixed(0) + "%" : Number(arRate).toFixed(0) + "%") : "Assumed full collection on stated weeks"],
        ["Payroll Frequency", payFreq != null ? String(payFreq) : "Bi-weekly (assumed)"],
        ["VAT / Tax Due", vatDue != null ? String(vatDue) : "Per stated tax weeks in forecast"],
        ["Minimum Cash Floor", floorKnown ? M(floor) + (floorBasis ? " (" + floorBasis + ")" : "") : "Not set"],
        ["Forecast Horizon", HORIZON + " weeks"],
      ];
      return h(K.Card, { title: "ASSUMPTIONS", sub: "Drivers behind the forecast", padding: 0 },
        h("table", { style: { width: "100%", borderCollapse: "collapse" } },
          h("tbody", null, assumptions.map((a, i) => h("tr", { key: i, style: { borderBottom: "1px solid rgba(13,32,64,.05)" } },
            h("td", { style: { padding: "9px 14px", fontSize: 12, color: "#4a5680", fontWeight: 600, width: "45%" } }, a[0]),
            h("td", { style: { padding: "9px 14px", fontSize: 12, color: "#1a2540", fontFamily: K.MONO, textAlign: "right" } }, a[1]))))));
    }

    // ── Section 4 — upload / edit ───────────────────────────────────────────────
    function onFile(e) {
      const file = e.target.files && e.target.files[0];
      if (!file) return;
      setSaveErr(null);
      const reader = new FileReader();
      reader.onload = () => { setPreview(parseCsv(reader.result)); };
      reader.onerror = () => { setPreview({ rows: [], error: "Could not read the file." }); };
      reader.readAsText(file);
    }

    function confirmUpload() {
      if (!preview || !preview.rows || !preview.rows.length) return;
      const db = window.supabaseClient;
      if (!db || !scopedCompanyId) { setSaveErr("Not connected to the data store — cannot save."); return; }
      const chained = chainRows(preview.rows, startCash);
      const payload = chained.map((w) => ({
        company_id: scopedCompanyId,
        week_number: w.week_number,
        week_start_date: w.week_start_date || null,
        forecast_label: w.forecast_label || null,
        opening_cash: w._opening,
        ar_collections: nz(w.ar_collections),
        other_inflows: nz(w.other_inflows),
        ap_payments: nz(w.ap_payments),
        payroll: nz(w.payroll),
        tax_vat: nz(w.tax_vat),
        capex_payments: nz(w.capex_payments),
        loan_repayment: nz(w.loan_repayment),
        other_outflows: nz(w.other_outflows),
        closing_cash: w._closing,
        is_actual: false,
        notes: null,
      }));
      setSaving(true); setSaveErr(null);
      // Idempotent replace: clear this company's forecast then insert the 13
      // weeks. The table has no unique constraint on (company_id, week_number),
      // so an upsert/onConflict would error — delete-then-insert keeps the
      // forecast to a single clean set rather than appending duplicate weeks.
      db.from("cash_forecast").delete().eq("company_id", scopedCompanyId)
        .then(({ error }) => { if (error) throw new Error(error.message); return db.from("cash_forecast").insert(payload); })
        .then(({ error }) => {
          setSaving(false);
          if (error) { setSaveErr("Save failed — " + (error.message || "please retry.")); return; }
          setPreview(null); setRefreshTick((t) => t + 1);
        })
        .catch((e) => { setSaving(false); setSaveErr("Save failed — " + ((e && e.message) || "please retry.")); });
    }

    function renderUpload() {
      // Only show the upload affordance when there is no stored forecast.
      if (stored) return null;
      const btn = { padding: "8px 16px", fontSize: 12.5, fontWeight: 600, borderRadius: 8, cursor: "pointer", border: "1px solid rgba(13,32,64,.14)", background: "#fff", color: "#1C4ED8" };
      const previewTable = (pv) => h("div", { style: { marginTop: 14, overflowX: "auto" } },
        pv.error ? h("div", { style: { color: "#d94f47", fontSize: 12.5, fontWeight: 600 } }, pv.error)
          : h(React.Fragment, null,
            h("div", { style: { fontSize: 12, color: "#4a5680", marginBottom: 8 } }, pv.rows.length + " week(s) parsed — opening / closing computed as a running chain. Review then confirm."),
            h("table", { style: { width: "100%", borderCollapse: "collapse", minWidth: 720, fontSize: 11 } },
              h("thead", null, h("tr", { style: { background: "#0d2040" } }, ["WK", "DATE", "OPENING", "IN", "OUT", "NET", "CLOSING"].map((c, i) => h("th", { key: i, style: { color: "rgba(255,255,255,.85)", padding: "7px 10px", fontSize: 9.5, fontWeight: 700, textTransform: "uppercase", textAlign: i <= 1 ? "left" : "right" } }, c)))),
              h("tbody", null, chainRows(pv.rows, startCash).map((w, i) => h("tr", { key: i, style: { borderBottom: "1px solid rgba(13,32,64,.05)" } },
                h("td", { style: { padding: "6px 10px", fontWeight: 700, color: "#0d2040" } }, "W" + w.week_number),
                h("td", { style: { padding: "6px 10px", color: "#6475a0" } }, fmtDate(w.week_start_date)),
                h("td", { style: { padding: "6px 10px", textAlign: "right", fontFamily: K.MONO } }, M(w._opening)),
                h("td", { style: { padding: "6px 10px", textAlign: "right", fontFamily: K.MONO, color: "#18a867" } }, M(w._inflow)),
                h("td", { style: { padding: "6px 10px", textAlign: "right", fontFamily: K.MONO, color: "#d94f47" } }, M(w._outflow)),
                h("td", { style: { padding: "6px 10px", textAlign: "right", fontFamily: K.MONO, fontWeight: 700, color: w._net >= 0 ? "#18a867" : "#d94f47" } }, (w._net >= 0 ? "+" : "−") + M(Math.abs(w._net))),
                h("td", { style: { padding: "6px 10px", textAlign: "right", fontFamily: K.MONO, fontWeight: 800, color: "#0d2040" } }, M(w._closing)))))),
            h("div", { style: { display: "flex", gap: 10, marginTop: 14, alignItems: "center" } },
              h("button", { onClick: confirmUpload, disabled: saving, style: Object.assign({}, btn, { background: "#1C4ED8", color: "#fff", border: "1px solid #1C4ED8", opacity: saving ? 0.6 : 1 }) }, saving ? "Saving…" : "Confirm & save forecast"),
              h("button", { onClick: () => { setPreview(null); setSaveErr(null); }, style: btn }, "Cancel"),
              saveErr ? h("span", { style: { color: "#d94f47", fontSize: 12, fontWeight: 600 } }, saveErr) : null)));

      return h(K.Card, { title: "UPLOAD FORECAST", sub: isEstimate ? "Replace the estimate with your own 13-week forecast" : "Import a 13-week forecast via CSV" },
        h("div", { style: { fontSize: 12.5, color: "#4a5680", lineHeight: 1.6 } },
          "CSV columns: ", h("code", { style: { fontFamily: K.MONO, fontSize: 11, background: "rgba(13,32,64,.05)", padding: "2px 5px", borderRadius: 4 } }, UPLOAD_COLS.join(", ")),
          ". One row per week (1-13). Opening and closing balances are computed automatically as a running chain from the period's starting cash."),
        h("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10, marginTop: 12 } },
          h("button", { onClick: downloadForecastTemplate, style: btn }, "⭳ Download sample CSV"),
          h("label", { style: Object.assign({}, btn, { display: "inline-block" }) },
            "Choose CSV file",
            h("input", { type: "file", accept: ".csv", onChange: onFile, style: { display: "none" } }))),
        preview ? previewTable(preview) : null);
    }

    // ── empty state ─────────────────────────────────────────────────────────────
    const loading = rows === null;

    const hero = {
      eyebrow: "PLANNING",
      title: "13-Week Cash Forecast",
      subtitle: "Rolling liquidity outlook · inflows, outflows & runway to the cash floor"
        + (isEstimate ? " · estimated" : ""),
    };

    if (loading) {
      return h(K.Shell, { hero: hero }, h(K.Card, { title: "13-Week Cash Forecast" }, h("div", { style: { padding: 16, color: "#6475a0", fontSize: 13 } }, "Loading forecast…")));
    }

    if (!hasForecast) {
      return h(K.Shell, { hero: hero },
        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, onClick: () => window.__perduraSetPage && window.__perduraSetPage("cash_flow_statement") }, k)))),
        h(K.Card, { title: "13-Week Cash Forecast" },
          h("div", { style: { padding: 18, fontSize: 13, color: "#6475a0", lineHeight: 1.6 } },
            h("b", null, "No forecast yet. "),
            loadErr ? "The forecast store could not be reached. " : "",
            "Upload a 13-week forecast below to project weekly inflows, outflows and the closing-cash trajectory against your minimum-cash floor. With a transaction feed connected, an estimate can also be seeded from your trailing cash activity.")),
        renderAssumptions(),
        renderUpload());
    }

    return h(K.Shell, { hero: hero },
      // Row 1 — KPI tiles
      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, onClick: () => window.__perduraSetPage && window.__perduraSetPage("cash_flow_statement") }, k)))),

      // Auto-estimate banner with a refine CTA.
      isEstimate ? h("div", { style: { margin: "0 0 12px", display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10, padding: "9px 14px", borderRadius: 10, background: "rgba(0,159,160,.07)", border: "1px solid rgba(0,159,160,.28)", borderLeft: "4px solid #009fa0" } },
        h("span", { style: { fontSize: 10, fontWeight: 800, letterSpacing: 0.5, textTransform: "uppercase", color: "#0d2040", fontFamily: K.MONO } }, "📡 Auto-estimated from trailing GL"),
        h("span", { style: { fontSize: 11.5, color: "#4a5680", flex: 1, minWidth: 200 } }, "Built from your trailing 12-week cash activity and AR / AP balances — directional only. Upload a 13-week forecast to replace it."),
        h("button", { onClick: () => { const el = document.getElementById("cf-upload"); if (el && el.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "start" }); }, style: { padding: "6px 14px", fontSize: 12, fontWeight: 700, borderRadius: 8, cursor: "pointer", border: "1px solid #009fa0", background: "#009fa0", color: "#fff", whiteSpace: "nowrap" } }, "Refine forecast →")) : null,

      // Section 1 — chart
      renderChart(),
      // Section 2 — detail table
      renderTable(),
      // Section 3 — assumptions
      renderAssumptions(),
      // Section 4 — upload / edit
      h("div", { id: "cf-upload" }, renderUpload()));
  }

  window.CashForecastPage = Page;
})();
