/* MonthEndIQ — Dashboard Builder.
   Compose your own view: pick a field, a measure and a chart, then slice it
   with sliders. Answers "why not just build this in Power BI?" by putting the
   same construction on top of data that is already classified and reconciled. */

const { useState: useBState, useEffect: useBEffect, useMemo: useBMemo } = React;

const CHART_TYPES = [
  { key: "bar",   label: "Bar",     icon: "bar-chart-3" },
  { key: "line",  label: "Line",    icon: "line-chart" },
  { key: "donut", label: "Donut",   icon: "pie-chart" },
  { key: "kpi",   label: "Stat",    icon: "hash" },
  { key: "table", label: "Table",   icon: "table" },
];

const fmtVal = (v) => window.fmtCurrency
  ? window.fmtCurrency(v, { compact: Math.abs(v) >= 10000 })
  : "£" + Math.round(v).toLocaleString();

/* ── Chart primitives (pure SVG, theme-aware via CSS vars) ─────────────── */

function BuilderBars({ rows, onSelect, selected }) {
  if (!rows.length) return null;
  const max = Math.max(...rows.map(r => Math.abs(r.value)), 1);
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
      {rows.map((r, i) => {
        const pct = Math.abs(r.value) / max * 100;
        const neg = r.value < 0;
        const clickable = onSelect && !r.is_remainder;
        const dim = selected && selected !== r.label;
        return (
          <div key={i}
            onClick={clickable ? () => onSelect(r.label) : undefined}
            title={clickable ? `Filter the rest of the page by “${r.label}”` : r.label}
            style={{ display: "grid", gridTemplateColumns: "minmax(90px,34%) 1fr auto",
              gap: 10, alignItems: "center", cursor: clickable ? "pointer" : "default",
              opacity: dim ? 0.42 : 1, transition: "opacity .15s" }}>
            <span style={{ font: "var(--text-body)", fontSize: 12,
              fontWeight: selected === r.label ? 600 : 400,
              color: r.is_remainder ? "var(--fg-3)" : "var(--fg-1)",
              overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{r.label}</span>
            <span style={{ height: 16, background: "var(--surface-2)", borderRadius: 4, overflow: "hidden" }}>
              <span style={{ display: "block", height: "100%", width: `${pct}%`, borderRadius: 4,
                background: r.is_remainder ? "var(--fg-3)"
                  : neg ? "var(--adverse)" : "var(--c-1)", opacity: r.is_remainder ? .5 : .85 }} />
            </span>
            <span style={{ font: "600 12px var(--font-mono)", color: "var(--fg-1)",
              fontVariantNumeric: "tabular-nums" }}>{fmtVal(r.value)}</span>
          </div>
        );
      })}
    </div>
  );
}

function BuilderLine({ rows, height = 210, onSelect, selected }) {
  if (rows.length < 2) return null;
  const W = 620, H = height, padL = 52, padR = 12, padT = 12, padB = 26;
  const iw = W - padL - padR, ih = H - padT - padB;
  const vals = rows.map(r => r.value);
  const max = Math.max(...vals, 0) * 1.08;
  const rawMin = Math.min(...vals, 0);
  const min = rawMin < 0 ? rawMin * 1.08 : 0;
  const x = i => padL + (iw * i) / Math.max(rows.length - 1, 1);
  const y = v => padT + ih - (ih * (v - min)) / ((max - min) || 1);
  const axis = v => Math.abs(v) >= 1e6 ? `£${(v / 1e6).toFixed(1)}M`
    : Math.abs(v) >= 1e3 ? `£${Math.round(v / 1e3)}k` : `£${Math.round(v)}`;
  const pts = rows.map((r, i) => `${x(i).toFixed(1)},${y(r.value).toFixed(1)}`).join(" ");
  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block" }}
      role="img" aria-label={`Line chart, ${rows.length} points, from ${
        axis(rows[0]?.value ?? 0)} to ${axis(rows[rows.length - 1]?.value ?? 0)}.`}>
      {Array.from({ length: 5 }).map((_, i) => {
        const gy = padT + (ih * i) / 4;
        return (
          <g key={i}>
            <line x1={padL} x2={W - padR} y1={gy} y2={gy} stroke="var(--border)" strokeWidth="1" />
            <text x={padL - 6} y={gy + 3.5} textAnchor="end"
              style={{ font: "500 10px var(--font-mono)", fill: "var(--fg-3)" }}>
              {axis(max - ((max - min) * i) / 4)}
            </text>
          </g>
        );
      })}
      {min < 0 && <line x1={padL} x2={W - padR} y1={y(0)} y2={y(0)}
        stroke="var(--fg-3)" strokeWidth="1" opacity=".55" />}
      <polyline points={pts} fill="none" stroke="var(--c-1)" strokeWidth="2.5"
        strokeLinecap="round" strokeLinejoin="round" />
      {rows.map((r, i) => (
        <circle key={i} cx={x(i)} cy={y(r.value)} r={selected === r.label ? 6 : 3.5}
          fill="var(--c-1)" style={{ cursor: onSelect ? "pointer" : "default" }}
          onClick={onSelect ? () => onSelect(r.label) : undefined}>
          <title>{`${r.label}: ${fmtVal(r.value)}`}</title>
        </circle>
      ))}
      {rows.map((r, i) => (
        (rows.length <= 8 || i % Math.ceil(rows.length / 8) === 0) && (
          <text key={`l${i}`} x={x(i)} y={H - 8} textAnchor="middle"
            style={{ font: "500 10px var(--font-mono)", fill: "var(--fg-3)" }}>
            {String(r.label).slice(0, 7)}
          </text>
        )
      ))}
    </svg>
  );
}

function BuilderDonut({ rows, onSelect, selected }) {
  const data = rows.filter(r => r.value > 0);
  if (!data.length) return null;
  const total = data.reduce((a, b) => a + b.value, 0);
  const R = 70, r0 = 44, cx = 90, cy = 90;
  let angle = -Math.PI / 2;
  const slices = data.map((d, i) => {
    const theta = (d.value / total) * 2 * Math.PI;
    const x1 = cx + R * Math.cos(angle), y1 = cy + R * Math.sin(angle);
    const x2 = cx + R * Math.cos(angle + theta), y2 = cy + R * Math.sin(angle + theta);
    const large = theta > Math.PI ? 1 : 0;
    angle += theta;
    return { d, path: `M${cx},${cy} L${x1.toFixed(1)},${y1.toFixed(1)} A${R},${R} 0 ${large} 1 ${x2.toFixed(1)},${y2.toFixed(1)} Z`,
             colour: `var(--c-${(i % 8) + 1})`, pct: d.value / total * 100 };
  });
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 18, flexWrap: "wrap" }}>
      <svg viewBox="0 0 180 180" width="150" height="150" style={{ flex: "none" }}
        role="img" aria-label={`Donut chart. ${slices.map(sl =>
          `${sl.d.label} ${Math.round(sl.pct)}%`).join(", ")}.`}>
        {slices.map((s, i) => (
          <path key={i} d={s.path} fill={s.colour}
            opacity={selected && selected !== s.d.label ? .3 : .88}
            stroke="var(--surface)" strokeWidth="2"
            style={{ cursor: onSelect ? "pointer" : "default" }}
            onClick={onSelect ? () => onSelect(s.d.label) : undefined}>
            <title>{`${s.d.label}: ${fmtVal(s.d.value)} (${s.pct.toFixed(1)}%)`}</title>
          </path>
        ))}
        <circle cx={cx} cy={cy} r={r0} fill="var(--surface)" />
        <text x={cx} y={cy + 4} textAnchor="middle"
          style={{ font: "600 14px var(--font-mono)", fill: "var(--ink)" }}>{fmtVal(total)}</text>
      </svg>
      <div style={{ flex: 1, minWidth: 150, display: "flex", flexDirection: "column", gap: 5 }}>
        {slices.slice(0, 8).map((s, i) => (
          <div key={i}
            onClick={onSelect ? () => onSelect(s.d.label) : undefined}
            style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12,
              cursor: onSelect ? "pointer" : "default",
              opacity: selected && selected !== s.d.label ? .45 : 1 }}>
            <span style={{ width: 9, height: 9, borderRadius: 2, background: s.colour, flexShrink: 0 }} />
            <span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis",
              whiteSpace: "nowrap", color: "var(--fg-2)" }}>{s.d.label}</span>
            <span style={{ font: "600 11.5px var(--font-mono)", color: "var(--fg-1)" }}>{fmtVal(s.d.value)}</span>
            <span style={{ font: "var(--text-caption)", fontSize: 10.5, color: "var(--fg-3)", width: 38,
              textAlign: "right" }}>{s.pct.toFixed(0)}%</span>
          </div>
        ))}
      </div>
    </div>
  );
}

function BuilderTable({ rows, onSelect, selected }) {
  return (
    <div style={{ overflowX: "auto" }}>
      <table className="var" style={{ fontSize: 12.5 }}>
        <thead><tr><th className="l">Label</th><th>Value</th></tr></thead>
        <tbody>
          {rows.map((r, i) => (
            <tr key={i}
              onClick={onSelect && !r.is_remainder ? () => onSelect(r.label) : undefined}
              style={{ cursor: onSelect && !r.is_remainder ? "pointer" : "default",
                opacity: selected && selected !== r.label ? .45 : 1 }}>
              <td className="l">{r.label}</td>
              <td className={r.value < 0 ? "adv" : ""}>{fmtVal(r.value)}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function BuilderStat({ rows, meta }) {
  const total = meta?.total != null ? meta.total : rows.reduce((a, b) => a + b.value, 0);
  return (
    <div style={{ padding: "6px 0" }}>
      <div style={{ font: "var(--text-metric)", fontSize: 30, color: "var(--ink)",
        fontVariantNumeric: "tabular-nums" }}>{fmtVal(total)}</div>
      <div style={{ font: "var(--text-caption)", fontSize: 11.5, color: "var(--fg-3)", marginTop: 4 }}>
        {meta?.measure_label || "Total"} · {rows.length} {rows.length === 1 ? "row" : "rows"}
      </div>
    </div>
  );
}

/* ── One widget ────────────────────────────────────────────────────────── */

/* A cross-filter selection maps onto the query engine's filter vocabulary.
   Clicking "Staff Costs" on a category chart filters every other widget to
   that category — the same gesture as a Power BI visual interaction. */
function crossFilterToQuery(cf) {
  if (!cf) return {};
  switch (cf.dimension) {
    case "category": return { categories: [cf.value] };
    case "account":  return { accounts:   [cf.value] };
    case "section":  return { sections:   [cf.value] };
    case "period":   return { period_from: cf.value, period_to: cf.value };
    default:         return {};
  }
}

function Widget({ widget, sessionId, fields, onEdit, onRemove, globalFilters,
                  crossFilter, onCrossFilter, onCycleWidth,
                  dragHandlers, isDragging, isDropTarget }) {
  const { Icon } = window;
  const [data, setData] = useBState(null);
  const [err, setErr]   = useBState(null);
  const [busy, setBusy] = useBState(true);

  // A widget is never filtered by its own selection — otherwise clicking a bar
  // would collapse the chart you just clicked to a single row.
  const incomingCf = crossFilter && crossFilter.widgetId !== widget.id ? crossFilter : null;
  const ownSelection = crossFilter && crossFilter.widgetId === widget.id
    ? crossFilter.value : null;

  const spec = useBMemo(() => ({
    dimension: widget.dimension, measure: widget.measure,
    aggregate: widget.aggregate || "sum",
    top_n: widget.top_n || null,
    sort: widget.sort || "value_desc",
    filters: {
      ...(widget.filters || {}),
      // slicers set at page level apply to every widget, Power BI style
      ...(globalFilters.categories?.length ? { categories: globalFilters.categories } : {}),
      ...(globalFilters.period_from ? { period_from: globalFilters.period_from } : {}),
      ...(globalFilters.period_to ? { period_to: globalFilters.period_to } : {}),
      ...(globalFilters.min_abs ? { min_abs: globalFilters.min_abs } : {}),
      ...crossFilterToQuery(incomingCf),
    },
  }), [widget, globalFilters, incomingCf]);

  useBEffect(() => {
    let live = true;
    setBusy(true);
    fetch(apiUrl(`/api/builder/query/${sessionId}`), {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify(spec),
    })
      .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(new Error(j.detail || r.status))))
      .then(j => { if (live) { setData(j); setErr(null); setBusy(false); } })
      .catch(e => { if (live) { setErr(e.message); setBusy(false); } });
    return () => { live = false; };
  }, [sessionId, JSON.stringify(spec)]);

  const rows = data?.rows || [];
  // Stat tiles have no marks to click, so they are filter targets only
  const select = widget.chart === "kpi" ? null
    : (label) => onCrossFilter({ widgetId: widget.id, dimension: widget.dimension, value: label });

  const body = err ? (
    <div style={{ font: "var(--text-body)", fontSize: 12.5, color: "var(--adverse-text)" }}>{err}</div>
  ) : busy ? (
    <div style={{ height: 60, display: "flex", alignItems: "center", justifyContent: "center" }}>
      <div className="spinner" style={{ width: 18, height: 18 }} />
    </div>
  ) : !rows.length ? (
    <div style={{ font: "var(--text-body)", fontSize: 12.5, color: "var(--fg-3)" }}>
      No rows match these filters.
    </div>
  ) : widget.chart === "line" ? <BuilderLine rows={rows} onSelect={select} selected={ownSelection} />
    : widget.chart === "donut" ? <BuilderDonut rows={rows} onSelect={select} selected={ownSelection} />
    : widget.chart === "table" ? <BuilderTable rows={rows} onSelect={select} selected={ownSelection} />
    : widget.chart === "kpi"   ? <BuilderStat rows={rows} meta={data.meta} />
    : <BuilderBars rows={rows} onSelect={select} selected={ownSelection} />;

  return (
    <div className="card"
      draggable
      {...(dragHandlers || {})}
      style={{ padding: "14px 16px", gridColumn: `span ${widget.width || 6}`,
        opacity: isDragging ? 0.4 : 1,
        outline: isDropTarget ? "2px dashed var(--primary)" : "none",
        outlineOffset: 3, transition: "opacity .15s" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
        <span title="Drag to reorder" style={{ cursor: "grab", color: "var(--fg-3)",
          display: "inline-flex", flexShrink: 0 }}>
          <Icon name="grip-vertical" size={13} />
        </span>
        <span style={{ font: "var(--text-body-strong)", fontSize: 12.5, color: "var(--fg-1)",
          flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
          {widget.title || `${widget.measure} by ${widget.dimension}`}
        </span>
        {incomingCf && (
          <span title={`Filtered by ${incomingCf.value}`} style={{
            font: "var(--text-label)", fontSize: 9.5, textTransform: "uppercase",
            letterSpacing: ".04em", color: "var(--primary-text)", background: "var(--primary-soft)",
            borderRadius: 20, padding: "2px 7px", flexShrink: 0 }}>filtered</span>
        )}
        <button title="Change width" onClick={() => onCycleWidth(widget.id)} style={iconBtn}>
          <Icon name="move-horizontal" size={13} />
        </button>
        <button title="Edit widget" onClick={() => onEdit(widget)} style={iconBtn}>
          <Icon name="settings-2" size={13} />
        </button>
        <button title="Remove widget" onClick={() => onRemove(widget.id)} style={iconBtn}>
          <Icon name="x" size={13} />
        </button>
      </div>
      {body}
      {data?.meta?.folded_into_other > 0 && (
        <div style={{ marginTop: 8, font: "var(--text-caption)", fontSize: 10.5, color: "var(--fg-3)" }}>
          {data.meta.folded_into_other} smaller rows folded into “Other” — the total still reconciles.
        </div>
      )}
    </div>
  );
}

const iconBtn = {
  background: "var(--surface-2)", border: "1px solid var(--border)",
  borderRadius: "var(--radius-sm)", padding: "4px 6px", cursor: "pointer",
  color: "var(--fg-2)", display: "inline-flex", alignItems: "center",
};

/* ── Widget editor ─────────────────────────────────────────────────────── */

function WidgetEditor({ widget, fields, onSave, onClose }) {
  const { Icon } = window;
  const [w, setW] = useBState(widget);
  const set = (k, v) => setW(p => ({ ...p, [k]: v }));
  const sel = { width: "100%", padding: "7px 9px", fontSize: 13, borderRadius: "var(--radius-sm)",
    border: "1px solid var(--border-strong)", background: "var(--surface)", color: "var(--ink)" };
  const lbl = { font: "var(--text-label)", fontSize: 10.5, fontWeight: 600, textTransform: "uppercase",
    letterSpacing: ".05em", color: "var(--fg-3)", display: "block", marginBottom: 5 };

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.45)",
      zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center", padding: 20 }}>
      <div onClick={e => e.stopPropagation()} className="card"
        style={{ width: "100%", maxWidth: 460, padding: "22px 24px" }}>
        <div style={{ display: "flex", alignItems: "center", marginBottom: 16 }}>
          <span style={{ font: "700 16px var(--font-display)", color: "var(--ink)", flex: 1 }}>
            {widget.isNew ? "Add widget" : "Edit widget"}
          </span>
          <button onClick={onClose} style={iconBtn}><Icon name="x" size={14} /></button>
        </div>

        <div style={{ display: "grid", gap: 13 }}>
          <div>
            <label style={lbl}>Title</label>
            <input style={sel} value={w.title || ""} placeholder="e.g. Cost by category"
              onChange={e => set("title", e.target.value)} />
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            <div>
              <label style={lbl}>Group by</label>
              <select style={sel} value={w.dimension} onChange={e => set("dimension", e.target.value)}>
                {(fields.dimensions || []).map(d => (
                  <option key={d.key} value={d.key}>{d.label} ({d.distinct})</option>
                ))}
              </select>
            </div>
            <div>
              <label style={lbl}>Measure</label>
              <select style={sel} value={w.measure} onChange={e => set("measure", e.target.value)}>
                {(fields.measures || []).map(m => (
                  <option key={m.key} value={m.key}>{m.label}</option>
                ))}
              </select>
            </div>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            <div>
              <label style={lbl}>Aggregate</label>
              <select style={sel} value={w.aggregate || "sum"} onChange={e => set("aggregate", e.target.value)}>
                {(fields.aggregates || ["sum"]).map(a => <option key={a} value={a}>{a}</option>)}
              </select>
            </div>
            <div>
              <label style={lbl}>Width</label>
              <select style={sel} value={w.width || 6} onChange={e => set("width", parseInt(e.target.value))}>
                <option value={4}>One third</option>
                <option value={6}>Half</option>
                <option value={12}>Full width</option>
              </select>
            </div>
          </div>
          <div>
            <label style={lbl}>Chart</label>
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
              {CHART_TYPES.map(c => (
                <button key={c.key} onClick={() => set("chart", c.key)} style={{
                  display: "inline-flex", alignItems: "center", gap: 6, padding: "7px 12px",
                  borderRadius: "var(--radius-sm)", cursor: "pointer", fontSize: 12.5,
                  border: `1px solid ${w.chart === c.key ? "var(--primary)" : "var(--border)"}`,
                  background: w.chart === c.key ? "var(--primary-soft)" : "var(--surface)",
                  color: w.chart === c.key ? "var(--primary-text)" : "var(--fg-2)",
                  fontWeight: w.chart === c.key ? 600 : 400,
                }}><Icon name={c.icon} size={13} /> {c.label}</button>
              ))}
            </div>
          </div>
          <div>
            <label style={lbl}>Show top {w.top_n || "all"}</label>
            <input type="range" min="0" max="25" step="1" value={w.top_n || 0}
              onChange={e => set("top_n", parseInt(e.target.value) || null)}
              style={{ width: "100%", accentColor: "var(--primary)" }} />
            <div style={{ font: "var(--text-caption)", fontSize: 10.5, color: "var(--fg-3)" }}>
              0 = show every row. Remaining rows fold into “Other”.
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 10, justifyContent: "flex-end", marginTop: 20 }}>
          <button onClick={onClose} style={{ padding: "9px 18px", borderRadius: "var(--radius-sm)",
            border: "1px solid var(--border-strong)", background: "var(--surface)",
            color: "var(--fg-2)", fontSize: 13.5, cursor: "pointer" }}>Cancel</button>
          <button onClick={() => onSave(w)} style={{ padding: "9px 20px", borderRadius: "var(--radius-sm)",
            border: "none", background: "var(--primary)", color: "var(--on-primary)", fontSize: 13.5,
            cursor: "pointer" }}>{widget.isNew ? "Add" : "Save"}</button>
        </div>
      </div>
    </div>
  );
}

/* ── The builder page ──────────────────────────────────────────────────── */

function DashboardBuilder({ sessionId, onToast }) {
  const { Icon } = window;
  const [fields, setFields]   = useBState({});
  const [widgets, setWidgets] = useBState([]);
  const [editing, setEditing] = useBState(null);
  const [layouts, setLayouts] = useBState([]);
  const [name, setName]       = useBState("My dashboard");
  const [loadErr, setLoadErr] = useBState(null);
  const [gf, setGf] = useBState({ categories: [], period_from: "", period_to: "", min_abs: 0 });
  const [crossFilter, setCrossFilter] = useBState(null);   // {widgetId, dimension, value}
  const [dragId, setDragId] = useBState(null);
  const [overId, setOverId] = useBState(null);

  // Clicking the same mark twice clears the cross-filter, like a toggle
  const applyCrossFilter = (cf) => setCrossFilter(prev =>
    (prev && prev.widgetId === cf.widgetId && prev.value === cf.value) ? null : cf);

  const cycleWidth = (id) => setWidgets(prev => prev.map(w =>
    w.id === id ? { ...w, width: w.width === 12 ? 4 : w.width === 4 ? 6 : 12 } : w));

  // Reorder on drop — HTML5 drag events, no library needed
  const dropOn = (targetId) => {
    if (!dragId || dragId === targetId) { setDragId(null); setOverId(null); return; }
    setWidgets(prev => {
      const from = prev.findIndex(w => w.id === dragId);
      const to   = prev.findIndex(w => w.id === targetId);
      if (from < 0 || to < 0) return prev;
      const next = [...prev];
      const [moved] = next.splice(from, 1);
      next.splice(to, 0, moved);
      return next;
    });
    setDragId(null); setOverId(null);
  };

  useBEffect(() => {
    if (!sessionId) return;
    fetch(apiUrl(`/api/builder/fields/${sessionId}`))
      .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(new Error(j.detail || r.status))))
      .then(f => {
        setFields(f);
        setLoadErr(null);
        const ps = f.periods || [];
        setGf(g => ({ ...g, period_from: ps[0] || "", period_to: ps[ps.length - 1] || "" }));
        if (!widgets.length) setWidgets(defaultWidgets());
      })
      .catch(e => setLoadErr(e.message));
    fetch(apiUrl("/api/builder/layouts"))
      .then(r => r.json()).then(j => setLayouts(j.layouts || [])).catch(() => {});
  }, [sessionId]);

  const defaultWidgets = () => ([
    { id: "w1", title: "Cost by category", dimension: "category", measure: "value",
      chart: "bar", width: 6, top_n: 8 },
    { id: "w2", title: "Value by period", dimension: "period", measure: "value",
      chart: "line", width: 6 },
    { id: "w3", title: "Biggest movements", dimension: "account", measure: "abs_variance",
      chart: "bar", width: 12, top_n: 10 },
  ]);

  const saveWidget = (w) => {
    setWidgets(prev => {
      const clean = { ...w }; delete clean.isNew;
      return prev.some(p => p.id === w.id)
        ? prev.map(p => (p.id === w.id ? clean : p))
        : [...prev, clean];
    });
    setEditing(null);
  };

  const addWidget = () => setEditing({
    id: `w${Date.now()}`, title: "", dimension: "category", measure: "value",
    chart: "bar", width: 6, top_n: 8, isNew: true,
  });

  const saveLayout = () => {
    fetch(apiUrl("/api/builder/layouts"), {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, spec: { widgets, filters: gf } }),
    }).then(r => r.ok ? r.json() : Promise.reject(new Error("save failed")))
      .then(() => {
        onToast && onToast("Layout saved");
        return fetch(apiUrl("/api/builder/layouts")).then(r => r.json());
      })
      .then(j => setLayouts(j.layouts || []))
      .catch(() => onToast && onToast("Could not save layout"));
  };

  const applyLayout = (l) => {
    setWidgets(l.spec.widgets || []);
    if (l.spec.filters) setGf(l.spec.filters);
    setName(l.name);
  };

  if (!sessionId) return (
    <div className="card" style={{ padding: 28, textAlign: "center" }}>
      <div style={{ font: "var(--text-h3)", color: "var(--ink)", marginBottom: 6 }}>Dashboard builder</div>
      <div style={{ font: "var(--text-body)", color: "var(--fg-3)" }}>
        Upload a P&L or open a client to start building.
      </div>
    </div>
  );

  if (loadErr) return (
    <div className="card" style={{ padding: 24 }}>
      <div style={{ font: "var(--text-body-strong)", color: "var(--ink)", marginBottom: 6 }}>
        Builder unavailable for this session
      </div>
      <div style={{ font: "var(--text-body)", fontSize: 13, color: "var(--fg-3)" }}>{loadErr}</div>
    </div>
  );

  const periods = fields.periods || [];
  const pIdx = (p) => Math.max(0, periods.indexOf(p));

  return (
    <div>
      {/* Toolbar */}
      <div className="card" style={{ padding: "14px 18px", marginBottom: 16 }}>
        <div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
          <input value={name} onChange={e => setName(e.target.value)}
            style={{ font: "var(--text-body-strong)", fontSize: 14, padding: "7px 10px",
              border: "1px solid var(--border)", borderRadius: "var(--radius-sm)",
              background: "var(--surface)", color: "var(--ink)", minWidth: 180 }} />
          <button onClick={addWidget} style={{ display: "inline-flex", alignItems: "center", gap: 6,
            padding: "8px 14px", borderRadius: "var(--radius-sm)", border: "none",
            background: "var(--primary)", color: "var(--on-primary)", fontSize: 13, cursor: "pointer" }}>
            <Icon name="plus" size={14} /> Add widget
          </button>
          <button onClick={saveLayout} style={{ display: "inline-flex", alignItems: "center", gap: 6,
            padding: "8px 14px", borderRadius: "var(--radius-sm)",
            border: "1px solid var(--border-strong)", background: "var(--surface)",
            color: "var(--fg-2)", fontSize: 13, cursor: "pointer" }}>
            <Icon name="save" size={14} /> Save layout
          </button>
          {layouts.length > 0 && (
            <select onChange={e => {
              const l = layouts.find(x => x.id === e.target.value);
              if (l) applyLayout(l);
            }} defaultValue="" style={{ padding: "8px 10px", fontSize: 13,
              border: "1px solid var(--border)", borderRadius: "var(--radius-sm)",
              background: "var(--surface)", color: "var(--ink)" }}>
              <option value="" disabled>Load saved…</option>
              {layouts.map(l => <option key={l.id} value={l.id}>{l.name}</option>)}
            </select>
          )}
        </div>

        {/* Slicers — apply to every widget on the page */}
        <div style={{ marginTop: 14, paddingTop: 14, borderTop: "1px solid var(--border)",
          display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(210px, 1fr))", gap: 16 }}>
          {periods.length > 1 && (
            <React.Fragment>
              <div>
                <label style={{ font: "var(--text-label)", fontSize: 10.5, fontWeight: 600,
                  textTransform: "uppercase", letterSpacing: ".05em", color: "var(--fg-3)",
                  display: "block", marginBottom: 6 }}>
                  From — {String(gf.period_from).slice(0, 10)}
                </label>
                <input type="range" min="0" max={periods.length - 1} value={pIdx(gf.period_from)}
                  onChange={e => setGf(g => ({ ...g, period_from: periods[parseInt(e.target.value)] }))}
                  style={{ width: "100%", accentColor: "var(--primary)" }} />
              </div>
              <div>
                <label style={{ font: "var(--text-label)", fontSize: 10.5, fontWeight: 600,
                  textTransform: "uppercase", letterSpacing: ".05em", color: "var(--fg-3)",
                  display: "block", marginBottom: 6 }}>
                  To — {String(gf.period_to).slice(0, 10)}
                </label>
                <input type="range" min="0" max={periods.length - 1} value={pIdx(gf.period_to)}
                  onChange={e => setGf(g => ({ ...g, period_to: periods[parseInt(e.target.value)] }))}
                  style={{ width: "100%", accentColor: "var(--primary)" }} />
              </div>
            </React.Fragment>
          )}
          <div>
            <label style={{ font: "var(--text-label)", fontSize: 10.5, fontWeight: 600,
              textTransform: "uppercase", letterSpacing: ".05em", color: "var(--fg-3)",
              display: "block", marginBottom: 6 }}>
              Minimum size — {gf.min_abs ? fmtVal(gf.min_abs) : "none"}
            </label>
            <input type="range" min="0" max="20000" step="500" value={gf.min_abs}
              onChange={e => setGf(g => ({ ...g, min_abs: parseInt(e.target.value) }))}
              style={{ width: "100%", accentColor: "var(--primary)" }} />
          </div>
        </div>

        {(fields.categories || []).length > 0 && (
          <div style={{ marginTop: 12, display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
            <span style={{ font: "var(--text-caption)", fontSize: 11, color: "var(--fg-3)", marginRight: 2 }}>
              Categories:
            </span>
            {fields.categories.map(c => {
              const on = gf.categories.includes(c);
              return (
                <button key={c} onClick={() => setGf(g => ({
                  ...g, categories: on ? g.categories.filter(x => x !== c) : [...g.categories, c],
                }))} style={{
                  padding: "3px 10px", borderRadius: 20, fontSize: 11.5, cursor: "pointer",
                  border: `1px solid ${on ? "var(--primary)" : "var(--border)"}`,
                  background: on ? "var(--primary-soft)" : "var(--surface)",
                  color: on ? "var(--primary-text)" : "var(--fg-2)",
                }}>{c}</button>
              );
            })}
            {gf.categories.length > 0 && (
              <button onClick={() => setGf(g => ({ ...g, categories: [] }))} style={{
                background: "none", border: "none", cursor: "pointer", fontSize: 11.5,
                color: "var(--fg-3)", textDecoration: "underline" }}>clear</button>
            )}
          </div>
        )}
      </div>

      {/* Active cross-filter — visible and clearable, never a hidden state */}
      {crossFilter && (
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14,
          padding: "10px 14px", borderRadius: "var(--radius-md)",
          background: "var(--primary-soft)", border: "1px solid var(--primary)" }}>
          <Icon name="filter" size={14} color="var(--primary)" />
          <span style={{ font: "var(--text-body)", fontSize: 13, color: "var(--fg-1)", flex: 1 }}>
            Cross-filtered by <b>{crossFilter.value}</b>
            <span style={{ color: "var(--fg-3)" }}> — every other widget shows only this {crossFilter.dimension}.</span>
          </span>
          <button onClick={() => setCrossFilter(null)} style={{
            background: "var(--surface)", border: "1px solid var(--border-strong)",
            borderRadius: "var(--radius-sm)", padding: "5px 11px", cursor: "pointer",
            fontSize: 12.5, color: "var(--fg-2)" }}>Clear</button>
        </div>
      )}

      {/* Widget grid — drag a card onto another to reorder */}
      <div style={{ display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap: 14 }}>
        {widgets.map(w => (
          <Widget key={w.id} widget={w} sessionId={sessionId} fields={fields}
            globalFilters={gf}
            crossFilter={crossFilter}
            onCrossFilter={applyCrossFilter}
            onCycleWidth={cycleWidth}
            isDragging={dragId === w.id}
            isDropTarget={overId === w.id && dragId !== w.id}
            dragHandlers={{
              onDragStart: () => setDragId(w.id),
              onDragEnd:   () => { setDragId(null); setOverId(null); },
              onDragOver:  (e) => { e.preventDefault(); setOverId(w.id); },
              onDragLeave: () => setOverId(prev => (prev === w.id ? null : prev)),
              onDrop:      (e) => { e.preventDefault(); dropOn(w.id); },
            }}
            onEdit={setEditing}
            onRemove={id => setWidgets(prev => prev.filter(p => p.id !== id))} />
        ))}
      </div>

      {!widgets.length && (
        <div className="card" style={{ padding: 30, textAlign: "center" }}>
          <div style={{ font: "var(--text-body)", color: "var(--fg-3)" }}>
            No widgets yet — use “Add widget” to build your view.
          </div>
        </div>
      )}

      {editing && (
        <WidgetEditor widget={editing} fields={fields}
          onSave={saveWidget} onClose={() => setEditing(null)} />
      )}
    </div>
  );
}

Object.assign(window, { DashboardBuilder });
