/* MonthEndIQ — Variance Movements page */
const { useState: useStateM, useEffect: useEffectM, useRef: useRefM, useMemo: useMemoM } = React;

const NOTES_KEY = (f) => `monthendiq_notes_f_${encodeURIComponent((f || "").trim())}`;

/* ── Inline SVG sparkline ────────────────────────────────────────────────── */
function Sparkline({ values, isFav }) {
  if (!values || values.length < 2) return null;
  const clean = values.filter(v => v != null);
  if (clean.length < 2) return null;
  const W = 56, H = 20, PAD = 2;
  const min = Math.min(...clean);
  const max = Math.max(...clean);
  const range = max === min ? 1 : max - min;
  const n = values.length;
  const pts = values
    .map((v, i) => {
      const x = PAD + (i / (n - 1)) * (W - 2 * PAD);
      const y = H - PAD - ((v ?? min) - min) / range * (H - 2 * PAD);
      return `${x.toFixed(1)},${y.toFixed(1)}`;
    })
    .join(" ");
  const colour = isFav ? "var(--favourable)" : "var(--adverse)";
  return (
    <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} aria-hidden="true" focusable="false"
      style={{ display: "block", flexShrink: 0 }}>
      <polyline points={pts} fill="none" stroke={colour}
        strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />
    </svg>
  );
}

function Movements({ sessionId, initialData, periodMode, controlledPeriod, onDataChange, analysisType, onNavigateCopilot, fileName }) {
  const { Icon, Card, Button, Delta, Chip } = window;
  const [data, setData]               = useStateM(initialData);
  const [loading, setLoading]         = useStateM(false);
  const [activeTab, setActiveTab]     = useStateM("top");
  const [search, setSearch]           = useStateM("");
  const [sortCol, setSortCol]         = useStateM("abs_variance");
  const [sortAsc, setSortAsc]         = useStateM(false);
  const [expandedRow, setExpandedRow]     = useStateM(null);
  const [categories, setCategories]       = useStateM([]);
  const [reclassState, setReclassState]   = useStateM({});
  const [alertThreshold, setAlertThreshold] = useStateM(null); // null = off, number = %
  const [showPctRev, setShowPctRev] = useStateM(false);
  const [notes, setNotes]           = useStateM({});
  const [noteEditing, setNoteEditing] = useStateM({}); // account → draft text
  const lastFetched = useRefM({ period: initialData?.selected_period, mode: periodMode });

  // Load available categories once on mount
  useEffectM(() => {
    fetch(apiUrl("/api/categories"))
      .then(r => r.json())
      .then(d => setCategories(d.categories || []))
      .catch(() => {});
  }, []);

  // Load notes from localStorage when fileName is available
  useEffectM(() => {
    if (!fileName) return;
    try {
      const raw = localStorage.getItem(NOTES_KEY(fileName));
      if (raw) setNotes(JSON.parse(raw));
    } catch {}
  }, [fileName]);

  const saveNote = (account, text) => {
    const next = { ...notes };
    if (text.trim()) {
      next[account] = text.trim();
    } else {
      delete next[account];
    }
    setNotes(next);
    setNoteEditing((prev) => { const n = { ...prev }; delete n[account]; return n; });
    try { if (fileName) localStorage.setItem(NOTES_KEY(fileName), JSON.stringify(next)); } catch {}
  };

  const fmtGBP       = (v) => window.fmtCurrency(v);
  const fmtSignedGBP = (v) => window.fmtCurrency(v, { signed: true });
  const fmtPct = (v) => {
    if (v == null || isNaN(v)) return "—";
    return (v > 0 ? "+" : v < 0 ? "-" : "") + Math.abs(v).toFixed(1) + "%";
  };

  // Category chip colours — deterministic by first keyword
  const catChip = (cat) => {
    if (!cat) return null;
    const lc = cat.toLowerCase();
    if (lc.includes("revenue") || lc.includes("turnover") || lc.includes("sales") || lc.includes("income"))
      return { bg: "var(--favourable-soft)", color: "var(--favourable-text)" };
    // The --chip-* tokens carry a light and a dark step; hardcoding the light
    // one here left these chips at ~2.4:1 on the dark canvas.
    if (lc.includes("staff") || lc.includes("payroll") || lc.includes("wage") || lc.includes("salary") || lc.includes("hr"))
      return { bg: "var(--chip-staff-bg)", color: "var(--chip-staff-text)" };
    if (lc.includes("direct") || lc.includes("cogs") || lc.includes("material") || lc.includes("production"))
      return { bg: "var(--chip-direct-bg)", color: "var(--chip-direct-text)" };
    if (lc.includes("market") || lc.includes("advertis"))
      return { bg: "var(--chip-marketing-bg)", color: "var(--chip-marketing-text)" };
    if (lc.includes("admin") || lc.includes("overhead") || lc.includes("general"))
      return { bg: "var(--surface-2)", color: "var(--fg-2)" };
    if (lc.includes("finance") || lc.includes("interest") || lc.includes("bank"))
      return { bg: "var(--chip-finance-bg)", color: "var(--chip-finance-text)" };
    // Fallback: a stable hue per category, lightness set by theme so it never
    // becomes a pale wash on dark
    const hue = ((cat.charCodeAt(0) || 0) * 47) % 360;
    return { bg: `hsla(${hue},55%,50%,var(--chip-fallback-bg-a))`,
             color: `hsl(${hue},var(--chip-fallback-s),var(--chip-fallback-l))` };
  };

  const fetchPeriod = async (period, mode) => {
    setLoading(true);
    try {
      const params = new URLSearchParams({ period, mode });
      const res = await fetch(apiUrl(`/api/data/${sessionId}?${params}`));
      if (res.ok) {
        const newData = await res.json();
        setData(newData);
        onDataChange && onDataChange(newData);
        lastFetched.current = { period: newData.selected_period, mode };
      }
    } finally { setLoading(false); }
  };

  useEffectM(() => {
    if (!controlledPeriod) return;
    if (controlledPeriod === lastFetched.current.period && periodMode === lastFetched.current.mode) return;
    fetchPeriod(controlledPeriod, periodMode);
  }, [controlledPeriod, periodMode]);

  if (!data) return <div className="loading"><div className="spinner" />Loading...</div>;

  const isBvA = (analysisType || data.analysis_type) === "budget_vs_actual";
  const { movements, period, selected_period } = data;
  const allRows = movements || [];

  const tabFiltered = useMemoM(() => {
    if (activeTab === "revenue") return allRows.filter(m => m.category === "Revenue");
    if (activeTab === "costs")   return allRows.filter(m => m.category !== "Revenue");
    return allRows;
  }, [allRows, activeTab]);

  const searched = useMemoM(() => {
    if (!search.trim()) return tabFiltered;
    const q = search.toLowerCase();
    return tabFiltered.filter(m =>
      m.account.toLowerCase().includes(q) || m.category.toLowerCase().includes(q)
    );
  }, [tabFiltered, search]);

  const sorted = useMemoM(() => {
    const copy = [...searched];
    const dir = sortAsc ? 1 : -1;
    copy.sort((a, b) => {
      let va, vb;
      switch (sortCol) {
        case "account":      va = a.account.toLowerCase(); vb = b.account.toLowerCase(); return va < vb ? -dir : va > vb ? dir : 0;
        case "category":     va = a.category.toLowerCase(); vb = b.category.toLowerCase(); return va < vb ? -dir : va > vb ? dir : 0;
        case "actual":       return dir * ((a.value || 0) - (b.value || 0));
        case "budget":       return dir * ((a.prior_value || 0) - (b.prior_value || 0));
        case "variance":     return dir * ((a.variance || 0) - (b.variance || 0));
        case "variance_pct": return dir * ((a.variance_pct || 0) - (b.variance_pct || 0));
        case "abs_variance": default: return dir * (Math.abs(a.variance || 0) - Math.abs(b.variance || 0));
      }
    });
    return copy;
  }, [searched, sortCol, sortAsc]);

  const handleSort = (col) => {
    if (sortCol === col) { setSortAsc(!sortAsc); }
    else { setSortCol(col); setSortAsc(col === "account" || col === "category"); }
  };

  const SortHeader = ({ col, label, align, className }) => {
    const active = sortCol === col;
    const cls = [align || "", className || ""].filter(Boolean).join(" ");
    return (
      <th className={cls || undefined} onClick={() => handleSort(col)}
        style={{ cursor: "pointer", userSelect: "none", whiteSpace: "nowrap" }}>
        {label}
        <span style={{ marginLeft: 4, opacity: active ? 1 : 0.3, fontSize: 10 }}>
          {active ? (sortAsc ? "▲" : "▼") : "▼"}
        </span>
      </th>
    );
  };

  // Summary stats
  const favRows = allRows.filter(m => m.is_fav && m.variance !== 0);
  const advRows = allRows.filter(m => !m.is_fav && m.variance !== 0);
  const largestFav = favRows.length ? favRows.reduce((a, b) => Math.abs(a.variance) > Math.abs(b.variance) ? a : b) : null;
  const largestAdv = advRows.length ? advRows.reduce((a, b) => Math.abs(a.variance) > Math.abs(b.variance) ? a : b) : null;
  const totalFav = favRows.reduce((s, m) => s + Math.abs(m.variance || 0), 0);
  const totalAdv = advRows.reduce((s, m) => s + Math.abs(m.variance || 0), 0);
  const maxAbsVariance = allRows.reduce((max, m) => Math.max(max, Math.abs(m.variance || 0)), 0);
  const totalRevenue   = allRows.filter(m => m.category === "Revenue").reduce((s, m) => s + Math.abs(m.value || 0), 0);

  // Insights: most volatile category
  const catVariances = {};
  allRows.forEach(m => {
    if (!catVariances[m.category]) catVariances[m.category] = 0;
    catVariances[m.category] += Math.abs(m.variance || 0);
  });
  const mostVolatile = Object.entries(catVariances).sort((a, b) => b[1] - a[1])[0];

  const generateExplanation = (m) => {
    const dir = m.is_fav ? "favourable" : "adverse";
    const pctStr = m.variance_pct != null ? ` (${Math.abs(m.variance_pct).toFixed(1)}%)` : "";
    if (isBvA) {
      if (m.category === "Revenue") {
        return m.is_fav
          ? `${m.account} exceeded budget by ${fmtGBP(Math.abs(m.variance))}${pctStr} and was a favourable revenue variance.`
          : `${m.account} fell short of budget by ${fmtGBP(Math.abs(m.variance))}${pctStr} and was an adverse revenue variance.`;
      }
      return m.is_fav
        ? `${m.account} came in under budget by ${fmtGBP(Math.abs(m.variance))}${pctStr}, a favourable cost variance.`
        : `${m.account} exceeded budget by ${fmtGBP(Math.abs(m.variance))}${pctStr} and was the largest ${dir} variance in the period.`;
    }
    return m.is_fav
      ? `${m.account} improved by ${fmtGBP(Math.abs(m.variance))}${pctStr} versus the prior period — a ${dir} movement.`
      : `${m.account} worsened by ${fmtGBP(Math.abs(m.variance))}${pctStr} versus the prior period — a ${dir} movement.`;
  };

  const handleReclassify = async (account, category) => {
    setReclassState(prev => ({ ...prev, [account]: { saving: true, category } }));
    try {
      const res = await fetch(apiUrl(`/api/reclassify/${sessionId}`), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ account, category }),
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({ detail: "Reclassify failed." }));
        throw new Error(err.detail || "Reclassify failed.");
      }
      // Reload current period data to reflect the updated category
      const period = data?.selected_period || data?.selected_bva_period || "";
      await fetchPeriod(period, periodMode);
      setReclassState(prev => ({ ...prev, [account]: { saved: true, category } }));
      setTimeout(() => setReclassState(prev => {
        const n = { ...prev };
        delete n[account];
        return n;
      }), 2500);
    } catch (e) {
      setReclassState(prev => ({ ...prev, [account]: { error: e.message } }));
    }
  };

  const handleExplainVariance = (m) => {
    if (!onNavigateCopilot) return;
    const context = [
      `Analysis Type: ${isBvA ? "Budget vs Actual" : "Month-on-Month"}`,
      `Period: ${period?.label || selected_period || ""}`,
      `Account: ${m.account}`,
      `Category: ${m.category}`,
      `Actual: ${fmtGBP(m.value)}`,
      `${isBvA ? "Budget" : "Prior"}: ${fmtGBP(m.prior_value)}`,
      `Variance: ${fmtSignedGBP(m.variance)}`,
      `Variance %: ${fmtPct(m.variance_pct)}`,
    ].join("\n");
    onNavigateCopilot(`Given this context:\n${context}\n\nExplain this variance.`);
  };

  const downloadCSV = () => {
    const headers = ["Account", "Category", isBvA ? "Actual" : "Current", isBvA ? "Budget" : "Prior", "Variance", "Var %", "Direction"];
    if (showPctRev) headers.push("% of Revenue");
    const escape = (v) => `"${String(v ?? "").replace(/"/g, '""')}"`;
    const rows = sorted.map(m => [
      escape(m.account),
      escape(m.category),
      m.value        != null ? Math.round(m.value)        : "",
      m.prior_value  != null ? Math.round(m.prior_value)  : "",
      m.variance     != null ? Math.round(m.variance)     : "",
      m.variance_pct != null ? m.variance_pct.toFixed(1)  : "",
      m.variance !== 0 ? (m.is_fav ? "Favourable" : "Adverse") : "No change",
      ...(showPctRev ? [totalRevenue > 0 && m.value != null ? ((m.value / totalRevenue) * 100).toFixed(1) : ""] : []),
    ].join(","));
    const csv = [headers.join(","), ...rows].join("\n");
    const blob = new Blob([csv], { type: "text/csv" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `movements-${selected_period || "export"}.csv`;
    a.click();
    URL.revokeObjectURL(url);
  };

  const tabs = [
    { id: "top",    label: "Top Movements" },
    { id: "revenue",label: "Revenue" },
    { id: "costs",  label: "Costs" },
    { id: "matrix", label: "Heatmap" },
  ];

  /* ── Heatmap helpers ─────────────────────────────────────── */
  const [matrixMode, setMatrixMode] = useStateM("change"); // "change" | "value"

  const allPeriods  = data?.periods || [];
  const histLen     = Math.max(0, ...allRows.map(r => (r.history || []).length));
  const heatPeriods = allPeriods.slice(-histLen).map((p) => {
    try { return new Date(String(p)).toLocaleDateString("en-GB", { month: "short", year: "2-digit" }); }
    catch (_) { return String(p).slice(0, 7); }
  });

  const pctChange = (values, idx) => {
    if (!values || idx === 0 || values[idx] == null || values[idx - 1] == null || values[idx - 1] === 0) return null;
    return ((values[idx] - values[idx - 1]) / Math.abs(values[idx - 1])) * 100;
  };

  const heatBg = (pct, isRev) => {
    if (pct == null) return "transparent";
    const fav   = isRev ? pct >= 0 : pct <= 0;
    const alpha = Math.min(Math.abs(pct) / 20, 1) * 0.65;
    return fav
      ? `rgba(14,138,87,${0.07 + alpha})`
      : `rgba(208,43,69,${0.07 + alpha})`;
  };

  const fmtCompact = (v) => window.fmtCurrency(v, { compact: true });

  const heatRows = useMemoM(() => {
    if (activeTab === "revenue") return allRows.filter(m => m.category === "Revenue");
    if (activeTab === "costs")   return allRows.filter(m => m.category !== "Revenue");
    return allRows;
  }, [allRows, activeTab]);

  try { void allRows.length; void sorted.length; void heatRows.length; } catch(__e) {
    return <div className="content" style={{padding:"40px 32px"}}><b>Movements render error:</b><pre style={{marginTop:8,fontSize:12,color: "red"}}>{String(__e)}</pre></div>;
  }

  return (
    <div className="content">
      <div className="content-inner reveal" style={{ opacity: loading ? 0.6 : 1, transition: "opacity .2s", position: "relative" }}>
        {loading && (
          <div style={{
            position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center",
            background: "rgba(var(--surface-rgb, 255,255,255), 0.5)", zIndex: 5, borderRadius: "var(--radius-md)",
          }}>
            <div className="spinner" />
          </div>
        )}

        {/* Page header */}
        <div style={{ marginBottom: 20 }}>
          <h2 style={{ font: "var(--text-metric)", fontSize: 22, color: "var(--ink)", margin: 0 }}>
            Variance Movements
          </h2>
          <p style={{ font: "var(--text-body)", fontSize: 13.5, color: "var(--fg-3)", margin: "4px 0 0" }}>
            {isBvA ? "Largest variances versus budget" : "Largest movements between periods"}
          </p>
        </div>

        {/* Summary bar */}
        <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12, marginBottom: 20 }}>
          {[
            { label: "Largest Favourable", value: largestFav ? fmtSignedGBP(largestFav.variance) : "—", sub: largestFav?.account || "", tone: "fav" },
            { label: "Largest Adverse", value: largestAdv ? fmtSignedGBP(largestAdv.variance) : "—", sub: largestAdv?.account || "", tone: "adv" },
            { label: "Total Favourable", value: fmtGBP(totalFav), sub: `${favRows.length} items`, tone: "fav" },
            { label: "Total Adverse", value: fmtGBP(totalAdv), sub: `${advRows.length} items`, tone: "adv" },
          ].map((c) => (
            <div key={c.label} className="card" style={{ padding: "14px 16px" }}>
              <div style={{ font: "var(--text-label)", fontSize: 10.5, textTransform: "uppercase", letterSpacing: ".05em", color: "var(--fg-3)", marginBottom: 6 }}>
                {c.label}
              </div>
              <div style={{
                font: "var(--text-metric)", fontSize: 20, fontVariantNumeric: "tabular-nums",
                color: c.tone === "fav" ? "var(--favourable-text)" : "var(--adverse-text)",
              }}>
                {c.value}
              </div>
              <div style={{ font: "var(--text-caption)", fontSize: 11, color: "var(--fg-3)", marginTop: 4 }}>
                {c.sub}
              </div>
            </div>
          ))}
        </div>

        {/* Insights panel */}
        <div style={{
          display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12, marginBottom: 20,
        }}>
          {largestFav && (
            <div style={{
              padding: "12px 16px", borderRadius: "var(--radius-sm)",
              background: "var(--favourable-soft)", borderLeft: "3px solid var(--favourable)",
            }}>
              <div style={{ font: "var(--text-label)", fontSize: 10.5, textTransform: "uppercase", color: "var(--favourable-text)", marginBottom: 4 }}>
                Largest Favourable
              </div>
              <div style={{ font: "var(--text-body-strong)", fontSize: 13, color: "var(--ink)" }}>{largestFav.account}</div>
              <div style={{ font: "var(--text-body)", fontSize: 12, color: "var(--fg-2)", marginTop: 2 }}>
                {fmtSignedGBP(largestFav.variance)} ({fmtPct(largestFav.variance_pct)})
              </div>
            </div>
          )}
          {largestAdv && (
            <div style={{
              padding: "12px 16px", borderRadius: "var(--radius-sm)",
              background: "var(--adverse-soft)", borderLeft: "3px solid var(--adverse)",
            }}>
              <div style={{ font: "var(--text-label)", fontSize: 10.5, textTransform: "uppercase", color: "var(--adverse-text)", marginBottom: 4 }}>
                Largest Adverse
              </div>
              <div style={{ font: "var(--text-body-strong)", fontSize: 13, color: "var(--ink)" }}>{largestAdv.account}</div>
              <div style={{ font: "var(--text-body)", fontSize: 12, color: "var(--fg-2)", marginTop: 2 }}>
                {fmtSignedGBP(largestAdv.variance)} ({fmtPct(largestAdv.variance_pct)})
              </div>
            </div>
          )}
          {mostVolatile && (
            <div style={{
              padding: "12px 16px", borderRadius: "var(--radius-sm)",
              background: "var(--primary-soft)", borderLeft: "3px solid var(--primary)",
            }}>
              <div style={{ font: "var(--text-label)", fontSize: 10.5, textTransform: "uppercase", color: "var(--primary-text)", marginBottom: 4 }}>
                Most Volatile Category
              </div>
              <div style={{ font: "var(--text-body-strong)", fontSize: 13, color: "var(--ink)" }}>{mostVolatile[0]}</div>
              <div style={{ font: "var(--text-body)", fontSize: 12, color: "var(--fg-2)", marginTop: 2 }}>
                {fmtGBP(mostVolatile[1])} total absolute variance
              </div>
            </div>
          )}
        </div>

        {/* Tabs + search */}
        <Card title="" style={{ padding: 0 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16, flexWrap: "wrap" }}>
            <div className="tab-strip" style={{
              display: "flex", gap: 4,
              borderBottom: "1px solid var(--border)", paddingBottom: 0, flex: 1,
            }}>
              {tabs.map(t => (
                <button key={t.id} onClick={() => setActiveTab(t.id)}
                  style={{
                    font: "var(--text-body-strong)", fontSize: 13, padding: "8px 14px",
                    border: "none", background: "transparent",
                    color: activeTab === t.id ? "var(--primary-text)" : "var(--fg-3)",
                    borderBottom: activeTab === t.id ? "2px solid var(--primary)" : "2px solid transparent",
                    cursor: "pointer",
                  }}>
                  {t.label}
                </button>
              ))}
            </div>
            {/* Alert threshold toggle */}
            <div style={{
              display: "flex", alignItems: "center", gap: 6,
              background: alertThreshold != null ? "var(--adverse-soft)" : "var(--surface)",
              border: `1px solid ${alertThreshold != null ? "var(--adverse)" : "var(--border)"}`,
              borderRadius: "var(--radius-sm)", padding: "0 10px", height: 34, cursor: "pointer",
            }}
              title="Flag rows where |Var %| exceeds this threshold"
            >
              <Icon name="bell" size={13} color={alertThreshold != null ? "var(--adverse)" : "var(--fg-3)"} />
              <select
                value={alertThreshold ?? ""}
                onChange={e => setAlertThreshold(e.target.value === "" ? null : Number(e.target.value))}
                style={{
                  font: "var(--text-body)", fontSize: 13,
                  color: alertThreshold != null ? "var(--adverse-text)" : "var(--fg-2)",
                  background: "transparent", border: "none", outline: "none", cursor: "pointer",
                }}>
                <option value="">Alerts off</option>
                <option value={5}>Flag &gt;5%</option>
                <option value={10}>Flag &gt;10%</option>
                <option value={20}>Flag &gt;20%</option>
                <option value={50}>Flag &gt;50%</option>
              </select>
            </div>

            {/* % of revenue toggle — only meaningful when revenue rows exist */}
            {totalRevenue > 0 && (
              <button
                onClick={() => setShowPctRev(v => !v)}
                title="Show each account as % of total revenue"
                style={{
                  display: "inline-flex", alignItems: "center", gap: 5,
                  font: "var(--text-body-strong)", fontSize: 12.5,
                  padding: "0 10px", height: 34, borderRadius: "var(--radius-sm)",
                  border: `1px solid ${showPctRev ? "var(--primary)" : "var(--border)"}`,
                  background: showPctRev ? "var(--primary-soft)" : "var(--surface)",
                  color: showPctRev ? "var(--primary-text)" : "var(--fg-2)",
                  cursor: "pointer", whiteSpace: "nowrap",
                }}
              >
                <Icon name="percent" size={13} />% Rev
              </button>
            )}

            {/* CSV export */}
            <button
              onClick={downloadCSV}
              title="Download visible rows as CSV"
              style={{
                display: "inline-flex", alignItems: "center", gap: 5,
                font: "var(--text-body-strong)", fontSize: 12.5,
                padding: "0 10px", height: 34, borderRadius: "var(--radius-sm)",
                border: "1px solid var(--border)",
                background: "var(--surface)", color: "var(--fg-2)",
                cursor: "pointer",
              }}
            >
              <Icon name="download" size={13} />CSV
            </button>

            <div style={{
              display: "flex", alignItems: "center", gap: 6,
              background: "var(--surface)", border: "1px solid var(--border)",
              borderRadius: "var(--radius-sm)", padding: "0 10px", height: 34,
            }}>
              <Icon name="search" size={14} color="var(--fg-3)" />
              <input
                type="text" placeholder="Search accounts..."
                value={search} onChange={e => setSearch(e.target.value)}
                style={{
                  font: "var(--text-body)", fontSize: 13, color: "var(--ink)",
                  background: "transparent", border: "none", outline: "none", width: 180,
                }}
              />
              {search && (
                <button
                  onClick={() => setSearch("")}
                  title="Clear search"
                  style={{
                    display: "inline-flex", alignItems: "center",
                    background: "none", border: "none", padding: "0 0 0 2px",
                    cursor: "pointer", color: "var(--fg-3)", lineHeight: 0,
                  }}
                >
                  <Icon name="x" size={13} />
                </button>
              )}
            </div>
          </div>

          {/* Alert count banner */}
          {alertThreshold != null && (() => {
            const count = sorted.filter(m => m.variance_pct != null && Math.abs(m.variance_pct) >= alertThreshold).length;
            return count > 0 ? (
              <div style={{
                display: "flex", alignItems: "center", gap: 8, marginBottom: 10,
                padding: "8px 14px", borderRadius: "var(--radius-sm)",
                background: "var(--adverse-soft)", border: "1px solid var(--adverse)",
                font: "var(--text-body-strong)", fontSize: 13, color: "var(--adverse-text)",
              }}>
                <Icon name="bell" size={14} color="var(--adverse)" />
                {count} account{count !== 1 ? "s" : ""} flagged with variance &gt;{alertThreshold}%
              </div>
            ) : (
              <div style={{
                display: "flex", alignItems: "center", gap: 8, marginBottom: 10,
                padding: "8px 14px", borderRadius: "var(--radius-sm)",
                background: "var(--favourable-soft)", border: "1px solid var(--favourable)",
                font: "var(--text-body-strong)", fontSize: 13, color: "var(--favourable-text)",
              }}>
                <Icon name="check-circle" size={14} color="var(--favourable)" />
                No accounts exceed the {alertThreshold}% threshold
              </div>
            );
          })()}

          {/* Table — hidden when matrix tab is active */}
          {activeTab !== "matrix" && (<div style={{ overflowX: "auto" }}>
            <table className="var">
              <thead>
                <tr>
                  <SortHeader col="account" label="Account" align="l" />
                  <SortHeader col="category" label="Category" align="l" />
                  {!isBvA && <th className="col-sparkline" style={{ width: 64 }}>Trend</th>}
                  <SortHeader col="actual" label={isBvA ? "Actual" : "Current"} />
                  <SortHeader col="budget" label={isBvA ? "Budget" : "Prior"} className="col-prior" />
                  <SortHeader col="variance" label="Variance" />
                  <SortHeader col="variance_pct" label="Var %" className="col-varpct" />
                  <th>Impact</th>
                  {showPctRev && <th style={{ whiteSpace: "nowrap" }}>% Rev</th>}
                  <th style={{ width: 40 }}></th>
                </tr>
              </thead>
              <tbody>
                {sorted.map((m, i) => {
                  const isExpanded = expandedRow === i;
                  const isAlert = alertThreshold != null
                    && m.variance_pct != null
                    && Math.abs(m.variance_pct) >= alertThreshold;
                  return (
                    <React.Fragment key={i}>
                      <tr onClick={() => setExpandedRow(isExpanded ? null : i)}
                        style={{ cursor: "pointer", background: isExpanded ? "var(--surface-2)" : (isAlert ? "var(--adverse-soft)" : undefined) }}>
                        <td className="l" style={{ display: "flex", alignItems: "center", gap: 6 }}>
                          {isAlert && (
                            <span title={`Variance ${Math.abs(m.variance_pct).toFixed(1)}% exceeds ${alertThreshold}% threshold`}>
                              <Icon name="bell" size={13} color="var(--adverse)" />
                            </span>
                          )}
                          {notes[m.account] && (
                            <span title={notes[m.account]} style={{ opacity: 0.65, flexShrink: 0 }}>
                              <Icon name="sticky-note" size={12} color="var(--caution)" />
                            </span>
                          )}
                          {m.account}
                        </td>
                        <td className="l">
                          {(() => {
                            const chip = catChip(m.category);
                            return chip ? (
                              <span style={{
                                display: "inline-flex", alignItems: "center",
                                font: "var(--text-label)", fontSize: 10.5, fontWeight: 600,
                                padding: "2px 8px", borderRadius: 20,
                                background: chip.bg, color: chip.color,
                                whiteSpace: "nowrap",
                              }}>{m.category}</span>
                            ) : m.category;
                          })()}
                        </td>
                        {!isBvA && (
                          <td className="col-sparkline" style={{ padding: "6px 8px" }}>
                            <Sparkline values={m.history} isFav={m.is_fav} />
                          </td>
                        )}
                        <td>{fmtGBP(m.value)}</td>
                        <td className="col-prior">{fmtGBP(m.prior_value)}</td>
                        <td className={m.is_fav ? "fav" : m.variance !== 0 ? "adv" : ""}>{fmtSignedGBP(m.variance)}</td>
                        <td className={`col-varpct${m.is_fav ? " fav" : m.variance !== 0 ? " adv" : ""}`}>{fmtPct(m.variance_pct)}</td>
                        <td style={{ minWidth: 110 }}>
                          {m.variance !== 0 ? (
                            <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                              <div style={{ flex: 1, height: 6, background: "var(--surface-2)", borderRadius: 3, overflow: "hidden", minWidth: 50 }}>
                                <div style={{
                                  height: "100%", borderRadius: 3,
                                  width: `${maxAbsVariance > 0 ? Math.round(Math.abs(m.variance || 0) / maxAbsVariance * 100) : 0}%`,
                                  background: m.is_fav ? "var(--favourable)" : "var(--adverse)",
                                  transition: "width .2s",
                                }} />
                              </div>
                              <span style={{
                                font: "var(--text-body-strong)", fontSize: 10, padding: "1px 6px",
                                borderRadius: "var(--radius-pill)", whiteSpace: "nowrap",
                                background: m.is_fav ? "var(--favourable-soft)" : "var(--adverse-soft)",
                                color: m.is_fav ? "var(--favourable-text)" : "var(--adverse-text)",
                              }}>
                                {m.is_fav ? "Fav" : "Adv"}
                              </span>
                            </div>
                          ) : (
                            <span style={{ color: "var(--fg-3)", fontSize: 12 }}>—</span>
                          )}
                        </td>
                        {showPctRev && (
                          <td style={{ fontVariantNumeric: "tabular-nums", color: "var(--fg-2)", fontSize: 12 }}>
                            {totalRevenue > 0 && m.value != null
                              ? ((m.value / totalRevenue) * 100).toFixed(1) + "%"
                              : "—"}
                          </td>
                        )}
                        <td>
                          <Icon name={isExpanded ? "chevron-up" : "chevron-down"} size={14} color="var(--fg-3)" />
                        </td>
                      </tr>
                      {isExpanded && (
                        <tr>
                          <td colSpan={(isBvA ? 8 : 9) + (showPctRev ? 1 : 0)} style={{ padding: 0, border: "none" }}>
                            <div style={{
                              padding: "16px 20px", background: "var(--surface)",
                              borderBottom: "1px solid var(--border)",
                            }}>
                              <div style={{ display: "grid", gridTemplateColumns: "repeat(5, auto)", gap: "12px 24px", marginBottom: 12 }}>
                                <div>
                                  <div style={{ font: "var(--text-label)", fontSize: 10, textTransform: "uppercase", color: "var(--fg-3)" }}>
                                    {isBvA ? "Actual" : "Current"}
                                  </div>
                                  <div style={{ font: "var(--text-body-strong)", fontSize: 14, color: "var(--ink)" }}>{fmtGBP(m.value)}</div>
                                </div>
                                <div>
                                  <div style={{ font: "var(--text-label)", fontSize: 10, textTransform: "uppercase", color: "var(--fg-3)" }}>
                                    {isBvA ? "Budget" : "Prior"}
                                  </div>
                                  <div style={{ font: "var(--text-body-strong)", fontSize: 14, color: "var(--ink)" }}>{fmtGBP(m.prior_value)}</div>
                                </div>
                                <div>
                                  <div style={{ font: "var(--text-label)", fontSize: 10, textTransform: "uppercase", color: "var(--fg-3)" }}>Variance</div>
                                  <div style={{
                                    font: "var(--text-body-strong)", fontSize: 14,
                                    color: m.is_fav ? "var(--favourable-text)" : "var(--adverse-text)",
                                  }}>{fmtSignedGBP(m.variance)}</div>
                                </div>
                                <div>
                                  <div style={{ font: "var(--text-label)", fontSize: 10, textTransform: "uppercase", color: "var(--fg-3)" }}>Var %</div>
                                  <div style={{
                                    font: "var(--text-body-strong)", fontSize: 14,
                                    color: m.is_fav ? "var(--favourable-text)" : "var(--adverse-text)",
                                  }}>{fmtPct(m.variance_pct)}</div>
                                </div>
                                <div>
                                  <div style={{ font: "var(--text-label)", fontSize: 10, textTransform: "uppercase", color: "var(--fg-3)" }}>Category</div>
                                  <div style={{ font: "var(--text-body-strong)", fontSize: 14, color: "var(--ink)" }}>{m.category}</div>
                                </div>
                              </div>

                              <div style={{
                                padding: "10px 14px", background: "var(--surface-2)",
                                borderRadius: "var(--radius-sm)", marginBottom: 12,
                                font: "var(--text-body)", fontSize: 13, color: "var(--fg-2)", lineHeight: 1.5,
                              }}>
                                {generateExplanation(m)}
                              </div>

                              {/* ── Category reclassification ── */}
                              {categories.length > 0 && (
                                <div style={{
                                  marginBottom: 12, padding: "10px 14px",
                                  background: "var(--surface)", borderRadius: "var(--radius-sm)",
                                  border: "1px solid var(--border)",
                                  display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap",
                                }}
                                  onClick={e => e.stopPropagation()}
                                >
                                  <span style={{ font: "var(--text-label)", fontSize: 11, textTransform: "uppercase", color: "var(--fg-3)", letterSpacing: ".06em", flexShrink: 0 }}>
                                    Reclassify
                                  </span>
                                  <select
                                    defaultValue={m.category}
                                    id={`cat-sel-${i}`}
                                    style={{
                                      font: "var(--text-body)", fontSize: 13, color: "var(--ink)",
                                      background: "var(--surface-2)", border: "1px solid var(--border)",
                                      borderRadius: "var(--radius-sm)", padding: "4px 8px", cursor: "pointer",
                                    }}
                                  >
                                    {categories.map(c => (
                                      <option key={c} value={c}>{c}</option>
                                    ))}
                                  </select>
                                  <button
                                    onClick={(e) => {
                                      e.stopPropagation();
                                      const sel = document.getElementById(`cat-sel-${i}`);
                                      if (sel) handleReclassify(m.account, sel.value);
                                    }}
                                    disabled={reclassState[m.account]?.saving}
                                    style={{
                                      display: "inline-flex", alignItems: "center", gap: 5,
                                      font: "var(--text-body-strong)", fontSize: 12.5,
                                      padding: "5px 12px", borderRadius: "var(--radius-sm)",
                                      border: "1px solid var(--border)",
                                      background: "var(--surface-2)", color: "var(--fg-2)",
                                      cursor: "pointer", opacity: reclassState[m.account]?.saving ? 0.6 : 1,
                                    }}
                                  >
                                    {reclassState[m.account]?.saving
                                      ? <><div className="spinner" style={{ width: 12, height: 12, borderWidth: 2 }} />Saving…</>
                                      : <><Icon name="tag" size={13} />Save</>
                                    }
                                  </button>
                                  {reclassState[m.account]?.saved && (
                                    <span style={{ display: "inline-flex", alignItems: "center", gap: 4, font: "var(--text-body)", fontSize: 12.5, color: "var(--favourable-text)" }}>
                                      <Icon name="check-circle" size={13} color="var(--favourable)" />
                                      Updated to {reclassState[m.account]?.category}
                                    </span>
                                  )}
                                  {reclassState[m.account]?.error && (
                                    <span style={{ font: "var(--text-body)", fontSize: 12.5, color: "var(--adverse-text)" }}>
                                      {reclassState[m.account].error}
                                    </span>
                                  )}
                                </div>
                              )}

                              {/* ── Variance annotation note ── */}
                              <div
                                onClick={e => e.stopPropagation()}
                                style={{
                                  marginBottom: 12, padding: "12px 14px",
                                  background: "var(--surface)", border: "1px solid var(--border)",
                                  borderRadius: "var(--radius-sm)",
                                }}
                              >
                                <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 6 }}>
                                  <Icon name="sticky-note" size={13} color="var(--caution)" />
                                  <span style={{ font: "var(--text-label)", fontSize: 10.5, textTransform: "uppercase", letterSpacing: ".06em", color: "var(--fg-3)" }}>
                                    Annotation
                                  </span>
                                  {notes[m.account] && noteEditing[m.account] === undefined && (
                                    <span style={{ font: "var(--text-caption)", fontSize: 11, color: "var(--favourable-text)", marginLeft: "auto", display: "flex", alignItems: "center", gap: 4 }}>
                                      <Icon name="check" size={11} color="var(--favourable)" /> Saved
                                    </span>
                                  )}
                                </div>
                                <textarea
                                  placeholder="Add a note explaining this variance…"
                                  value={noteEditing[m.account] !== undefined ? noteEditing[m.account] : (notes[m.account] || "")}
                                  onChange={e => setNoteEditing(prev => ({ ...prev, [m.account]: e.target.value }))}
                                  style={{
                                    width: "100%", minHeight: 60, resize: "vertical",
                                    padding: "7px 10px", boxSizing: "border-box",
                                    font: "var(--text-body)", fontSize: 13, color: "var(--ink)",
                                    background: "var(--surface-2)", border: "1px solid var(--border)",
                                    borderRadius: "var(--radius-sm)", outline: "none",
                                  }}
                                />
                                {noteEditing[m.account] !== undefined && (
                                  <div style={{ display: "flex", gap: 8, marginTop: 8 }}>
                                    <button
                                      onClick={() => saveNote(m.account, noteEditing[m.account])}
                                      style={{
                                        display: "inline-flex", alignItems: "center", gap: 5,
                                        padding: "5px 12px", borderRadius: "var(--radius-sm)",
                                        border: "1.5px solid var(--primary)", background: "var(--primary-soft)",
                                        color: "var(--primary-text)", font: "var(--text-body-strong)", fontSize: 12.5, cursor: "pointer",
                                      }}>
                                      <Icon name="save" size={13} /> Save note
                                    </button>
                                    <button
                                      onClick={() => setNoteEditing(prev => { const n = {...prev}; delete n[m.account]; return n; })}
                                      style={{
                                        padding: "5px 12px", borderRadius: "var(--radius-sm)",
                                        border: "1px solid var(--border)", background: "transparent",
                                        font: "var(--text-body)", fontSize: 12.5, color: "var(--fg-3)", cursor: "pointer",
                                      }}>
                                      Cancel
                                    </button>
                                    {noteEditing[m.account] === "" && notes[m.account] && (
                                      <span style={{ font: "var(--text-caption)", fontSize: 11.5, color: "var(--caution-text)", alignSelf: "center" }}>
                                        Saving empty text will delete the note
                                      </span>
                                    )}
                                  </div>
                                )}
                              </div>

                              <button onClick={(e) => { e.stopPropagation(); handleExplainVariance(m); }}
                                style={{
                                  display: "inline-flex", alignItems: "center", gap: 6,
                                  font: "var(--text-body-strong)", fontSize: 12.5,
                                  padding: "6px 14px", borderRadius: "var(--radius-sm)",
                                  border: "1px solid var(--primary)", background: "var(--primary-soft)",
                                  color: "var(--primary-text)", cursor: "pointer",
                                }}>
                                <Icon name="sparkles" size={13} />
                                Explain this variance
                              </button>
                            </div>
                          </td>
                        </tr>
                      )}
                    </React.Fragment>
                  );
                })}
                {sorted.length === 0 && (
                  <tr>
                    <td colSpan={(isBvA ? 8 : 9) + (showPctRev ? 1 : 0)}>
                      <div style={{ textAlign: "center", padding: "36px 24px" }}>
                        <Icon name="search-x" size={28} color="var(--fg-4, var(--fg-3))" style={{ marginBottom: 10 }} />
                        <div style={{ font: "var(--text-body-strong)", fontSize: 13.5, color: "var(--ink)", marginBottom: 5 }}>
                          No accounts match your search
                        </div>
                        <div style={{ font: "var(--text-body)", fontSize: 12.5, color: "var(--fg-3)", marginBottom: 12 }}>
                          Try a different keyword or clear the search to see all accounts
                        </div>
                        {search && (
                          <button onClick={() => setSearch("")} style={{
                            display: "inline-flex", alignItems: "center", gap: 6, padding: "6px 14px",
                            borderRadius: "var(--radius-sm)", background: "var(--surface-2)",
                            border: "1px solid var(--border)", cursor: "pointer",
                            font: "var(--text-body)", fontSize: 12.5, color: "var(--fg-2)",
                          }}>
                            <Icon name="x" size={13} />Clear search
                          </button>
                        )}
                      </div>
                    </td>
                  </tr>
                )}
              </tbody>
            </table>
          </div>
        )}

        {/* ── Heatmap / Matrix view ─────────────────────────── */}
        {activeTab === "matrix" && !isBvA && (
          <div>
            {/* Mode toggle + legend */}
            <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 14, flexWrap: "wrap" }}>
              <div className="seg" style={{ display: "inline-flex" }}>
                <button className={matrixMode === "change" ? "on" : ""} onClick={() => setMatrixMode("change")}>
                  MoM % change
                </button>
                <button className={matrixMode === "value" ? "on" : ""} onClick={() => setMatrixMode("value")}>
                  Absolute value
                </button>
              </div>
              {matrixMode === "change" && (
                <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                  {[
                    { label: "Favourable", bg: "rgba(14,138,87,0.55)", color: "var(--favourable-text)" },
                    { label: "Adverse",    bg: "rgba(208,43,69,0.55)",  color: "var(--adverse-text)" },
                    { label: "No change",  bg: "var(--surface-2)",       color: "var(--fg-3)" },
                  ].map((l) => (
                    <span key={l.label} style={{ display: "inline-flex", alignItems: "center", gap: 5, font: "var(--text-caption)", fontSize: 11, color: l.color }}>
                      <span style={{ width: 12, height: 12, borderRadius: 3, background: l.bg, display: "inline-block" }} />
                      {l.label}
                    </span>
                  ))}
                  <span style={{ font: "var(--text-caption)", fontSize: 11, color: "var(--fg-3)" }}>
                    · intensity = magnitude (full at ±20%)
                  </span>
                </div>
              )}
            </div>

            {heatPeriods.length < 2 ? (
              <div style={{ textAlign: "center", padding: "24px 0", color: "var(--fg-3)", font: "var(--text-body)", fontSize: 13 }}>
                Not enough periods for a heatmap — upload a dataset with at least 2 months.
              </div>
            ) : (
              <div style={{ overflowX: "auto" }}>
                <table style={{ borderCollapse: "collapse", minWidth: "100%", tableLayout: "fixed" }}>
                  <colgroup>
                    <col style={{ width: 160 }} />
                    {heatPeriods.map((_, i) => <col key={i} style={{ width: Math.max(64, Math.floor(500 / heatPeriods.length)) }} />)}
                  </colgroup>
                  <thead>
                    <tr>
                      <th style={{
                        textAlign: "left", padding: "6px 10px",
                        font: "var(--text-label)", fontSize: 10.5, textTransform: "uppercase",
                        letterSpacing: ".06em", color: "var(--fg-3)",
                        borderBottom: "1px solid var(--border)",
                        position: "sticky", left: 0, background: "var(--surface)", zIndex: 1,
                      }}>Account</th>
                      {heatPeriods.map((p, i) => (
                        <th key={i} style={{
                          textAlign: "center", padding: "6px 4px",
                          font: "var(--text-label)", fontSize: 10, textTransform: "uppercase",
                          letterSpacing: ".05em", color: "var(--fg-3)",
                          borderBottom: "1px solid var(--border)", whiteSpace: "nowrap",
                        }}>{p}</th>
                      ))}
                    </tr>
                  </thead>
                  <tbody>
                    {heatRows.map((m, ri) => {
                      const hist = m.history || [];
                      const isRev = m.category === "Revenue";
                      return (
                        <tr key={ri} style={{ borderBottom: "1px solid var(--border)" }}>
                          <td style={{
                            padding: "5px 10px", font: "var(--text-body)", fontSize: 12,
                            color: "var(--fg-1)", overflow: "hidden", textOverflow: "ellipsis",
                            whiteSpace: "nowrap", maxWidth: 160,
                            position: "sticky", left: 0, background: "var(--surface)", zIndex: 1,
                          }} title={m.account}>
                            {m.account}
                          </td>
                          {Array.from({ length: histLen }).map((_, pi) => {
                            const v   = hist[pi] ?? null;
                            const pct = pctChange(hist, pi);
                            const bg  = matrixMode === "change" ? heatBg(pct, isRev) : "transparent";
                            const txt = matrixMode === "change"
                              ? (pct == null ? (pi === 0 ? "base" : "—") : (pct >= 0 ? "+" : "") + pct.toFixed(1) + "%")
                              : fmtCompact(v);
                            const textColor = matrixMode === "change" && pct != null
                              ? (isRev ? (pct >= 0 ? "var(--favourable-text)" : "var(--adverse-text)") : (pct <= 0 ? "var(--favourable-text)" : "var(--adverse-text)"))
                              : "var(--fg-2)";
                            return (
                              <td key={pi} style={{
                                textAlign: "center", padding: "5px 4px",
                                background: bg, transition: "background .15s",
                                font: "500 11px var(--font-mono)", fontVariantNumeric: "tabular-nums",
                                color: pi === 0 && matrixMode === "change" ? "var(--fg-3)" : textColor,
                                fontSize: 10.5,
                              }}>
                                {txt}
                              </td>
                            );
                          })}
                        </tr>
                      );
                    })}
                    {heatRows.length === 0 && (
                      <tr><td colSpan={histLen + 1} style={{ textAlign: "center", padding: 24, color: "var(--fg-3)" }}>No accounts to display</td></tr>
                    )}
                  </tbody>
                </table>
              </div>
            )}
          </div>
        )}

        {activeTab === "matrix" && isBvA && (
          <div style={{ textAlign: "center", padding: "24px 0", color: "var(--fg-3)", font: "var(--text-body)", fontSize: 13 }}>
            Heatmap is not available for Budget vs Actual datasets.
          </div>
        )}
        </Card>
      </div>
    </div>
  );
}
Object.assign(window, { Movements });
