/* eslint-disable */
// ============================================================
// GRID — config-driven data grid
//
// Renders an ActionPayload (PartialView: "Grid"). Loads:
//   • data via /api/data/list/{actionId}
//   • toolbar buttons via /api/rbac/related/{actionId}/GridToolBar
//   • row actions via /api/rbac/related/{actionId}/RowActions
//   • bulk actions via /api/rbac/related/{actionId}/BulkActions
//
// Supports: structured Filter, multi-Sort, GroupBy, Pagination, Selection,
// Bulk handlers, Inline edit (ReadOnly:false), Saved Views, Double-click → Detail,
// Column chooser, Custom column Templates.
// ============================================================

const { useState: useG_S, useMemo: useG_M, useEffect: useG_E, useRef: useG_R, useCallback: useG_CB } = React;

// ============================================================
// Custom column templates — referenced by Column.Template
// ============================================================
const CELL_TEMPLATES = {
  _BadgeNumber: ({ value }) => (
    <span className="inline-flex items-center justify-center min-w-[28px] h-5 px-1.5 rounded-full text-[11px] font-semibold tabular-nums"
          style={{ background: "var(--surface-3)", color: "var(--ink-2)" }}>
      {value ?? 0}
    </span>
  ),
  _EmailLink: ({ value }) => (
    <a href={`mailto:${value}`} className="hover:underline" style={{ color: "var(--accent)" }}>{value}</a>
  ),
  _PriorityChip: ({ value }) => <EnumPill value={value} />,
  _ImpactBar: ({ value }) => {
    const v = Math.max(0, Math.min(10, Number(value) || 0));
    const pct = (v / 10) * 100;
    return (
      <div className="flex items-center gap-2">
        <div className="font-mono tabular-nums text-[12px] w-7" style={{ color: "var(--ink-2)" }}>{v.toFixed(1)}</div>
        <div className="flex-1 h-1.5 rounded-full overflow-hidden" style={{ background: "var(--surface-3)" }}>
          <div className="h-full" style={{ width: `${pct}%`, background: "var(--primary)" }} />
        </div>
      </div>
    );
  },
  _EffortDots: ({ value }) => {
    const n = Math.max(0, Math.min(5, Number(value) || 0));
    return (
      <div className="flex items-center gap-0.5">
        {Array.from({ length: 5 }).map((_, i) => (
          <span key={i} className="w-1.5 h-1.5 rounded-full" style={{ background: i < n ? "var(--ink-2)" : "var(--border-2)" }} />
        ))}
      </div>
    );
  },
};

// ============================================================
// Cell renderer — picks template / DataType / OptionSet
// ============================================================
const Cell = ({ col, value, row }) => {
  if (value == null || value === "") {
    return <span style={{ color: "var(--faint)" }}>—</span>;
  }
  if (col.Template && CELL_TEMPLATES[col.Template]) {
    const Tpl = CELL_TEMPLATES[col.Template];
    return <Tpl value={value} row={row} col={col} />;
  }
  if (col.IsHtml) {
    return <span className="leading-snug" style={{ color: "var(--ink-2)" }} dangerouslySetInnerHTML={{ __html: value }} />;
  }
  if (col.DataType === "enum") return <EnumPill value={value} />;
  if (col.DataType === "int" || col.DataType === "decimal" || col.DataType === "number") {
    return <span className="font-mono tabular-nums" style={{ color: "var(--ink-2)" }}>{fmtCell(value, col)}</span>;
  }
  if (col.DataType === "datetime" || col.DataType === "date") {
    return <span className="whitespace-nowrap" style={{ color: "var(--ink-2)" }}>{fmtCell(value, col)}</span>;
  }
  return <span style={{ color: "var(--ink-2)" }}>{fmtCell(value, col)}</span>;
};

// ============================================================
// Inline editor cell
// ============================================================
const EditCell = ({ col, value, onCommit, onCancel }) => {
  const [v, setV] = useG_S(value ?? "");
  useG_E(() => { setV(value ?? ""); }, [value]);

  const commit = () => onCommit(coerce(v, col.DataType));
  const onKey = (e) => {
    if (e.key === "Enter") { e.preventDefault(); commit(); }
    if (e.key === "Escape") { e.preventDefault(); onCancel(); }
  };

  if (col.DataType === "enum" && col.OptionSet) {
    const opts = OPTION_SETS[col.OptionSet] || [];
    return (
      <Select
        value={v}
        onChange={(x) => { setV(x); onCommit(x); }}
        options={[{ value: "", label: "—" }, ...opts.map((o) => ({ value: o, label: o }))]}
        className="min-w-[120px]"
      />
    );
  }
  if (col.DataType === "int" || col.DataType === "decimal" || col.DataType === "number") {
    return (
      <NumberInput
        value={v}
        onChange={(x) => setV(x)}
        // commit on blur or Enter
        className="font-mono"
      />
    );
  }
  return (
    <input
      type="text"
      value={v}
      autoFocus
      onChange={(e) => setV(e.target.value)}
      onBlur={commit}
      onKeyDown={onKey}
      className="w-full h-8 px-2 text-[13px] focus:outline-none"
      style={{ background: "var(--surface)", color: "var(--ink)", border: "1.5px solid var(--primary)", borderRadius: 6 }}
    />
  );
};

function coerce(v, type) {
  if (type === "int") return parseInt(v, 10);
  if (type === "decimal" || type === "number") return Number(v);
  if (type === "bool") return !!v;
  return v;
}

// ============================================================
// Header cell with sort affordance
// ============================================================
const HeaderCell = ({ col, sortIdx, sortDir, multiSort, onSort, isFirst, hasSelection }) => {
  const align =
    (col.DataType === "int" || col.DataType === "decimal" || col.DataType === "number") ? "text-right" : "text-left";

  return (
    <th
      scope="col"
      style={{
        width: col.Width === "auto" ? undefined : col.Width,
        minWidth: col.Width === "auto" ? "240px" : col.Width,
        background: "var(--surface-2)",
        color: "var(--muted)",
        borderBottom: "1px solid var(--border)",
      }}
      className={`${align} px-3 py-2 text-[10.5px] uppercase tracking-wider font-semibold select-none ${isFirst && !hasSelection ? "pl-4" : ""}`}
    >
      {col.Sortable ? (
        <button
          onClick={(e) => onSort(col.PropertyName, e.shiftKey)}
          className="inline-flex items-center gap-1.5 hover:text-[var(--ink)] transition-colors group"
          title="Click to sort. Shift+click to add to multi-sort."
        >
          <span style={{ color: sortIdx != null ? "var(--ink)" : undefined }}>{col.Label}</span>
          {sortIdx != null ? (
            <span className="inline-flex items-center gap-0.5 pl-1 pr-1.5 py-0.5 rounded text-[10px] font-semibold" style={{ background: "var(--primary-bg)", color: "var(--primary)" }}>
              <Icon name={sortDir === "asc" ? "arrowUp" : "arrowDown"} size={10} />
              {multiSort && <span className="font-mono tabular-nums">{sortIdx + 1}</span>}
            </span>
          ) : (
            <Icon name="arrowUpDown" size={11} className="opacity-30 group-hover:opacity-60" />
          )}
        </button>
      ) : (
        <span>{col.Label}</span>
      )}
    </th>
  );
};

// ============================================================
// Aggregate computation
// ============================================================
const aggregate = (rows, col) => {
  if (!col?.Aggregate) return null;
  const values = rows.map((r) => r[col.PropertyName]).filter((v) => v != null && v !== "");
  switch (col.Aggregate) {
    case "Count": return values.length;
    case "Sum":   return values.reduce((s, v) => s + Number(v || 0), 0);
    case "Avg":   return values.length ? (values.reduce((s, v) => s + Number(v || 0), 0) / values.length) : 0;
    case "Min":   return values.length ? Math.min(...values.map(Number)) : null;
    case "Max":   return values.length ? Math.max(...values.map(Number)) : null;
    default:      return null;
  }
};

// ============================================================
// Column chooser
// ============================================================
const ColumnChooser = ({ open, onClose, columns, onChange, anchorRef }) => {
  return (
    <Popover open={open} onClose={onClose} anchorRef={anchorRef} className="w-72 p-2">
      <div className="px-2 py-1.5 text-[10.5px] uppercase tracking-wider font-semibold" style={{ color: "var(--muted)" }}>Visible columns</div>
      <div className="max-h-72 overflow-y-auto scrollbar-thin">
        {columns.map((c) => (
          <label key={c.PropertyName} className="flex items-center gap-2 px-2 py-1.5 hover:bg-[var(--surface-2)] rounded cursor-pointer">
            <Check checked={!!c.Visible} onChange={(v) => onChange({ ...c, Visible: v })} />
            <span className="text-[13px] flex-1 truncate" style={{ color: "var(--ink-2)" }}>{c.Label}</span>
            <span className="text-[10.5px] font-mono" style={{ color: "var(--subtle)" }}>{c.DataType}</span>
          </label>
        ))}
      </div>
    </Popover>
  );
};

// ============================================================
// Group-by popover
// ============================================================
const GroupByMenu = ({ open, onClose, groupableCols, groupBy, setGroupBy, anchorRef }) => (
  <Popover open={open} onClose={onClose} anchorRef={anchorRef} className="w-60 p-1">
    <div className="px-2 py-1 text-[10.5px] uppercase tracking-wider font-semibold" style={{ color: "var(--muted)" }}>Group by</div>
    <button
      onClick={() => { setGroupBy(null); onClose(); }}
      className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-[13px] hover:bg-[var(--surface-2)] ${!groupBy ? "font-semibold" : ""}`}
      style={{ color: !groupBy ? "var(--primary)" : "var(--ink-2)" }}
    >
      <Icon name="check" size={14} className={!groupBy ? "" : "opacity-0"} />
      No grouping
    </button>
    {groupableCols.length === 0 ? (
      <div className="px-2 py-3 text-[12px] text-center" style={{ color: "var(--subtle)" }}>No groupable columns</div>
    ) : groupableCols.map((c) => (
      <button
        key={c.PropertyName}
        onClick={() => { setGroupBy(c.PropertyName); onClose(); }}
        className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-[13px] hover:bg-[var(--surface-2)] ${groupBy === c.PropertyName ? "font-semibold" : ""}`}
        style={{ color: groupBy === c.PropertyName ? "var(--primary)" : "var(--ink-2)" }}
      >
        <Icon name="check" size={14} className={groupBy === c.PropertyName ? "" : "opacity-0"} />
        <span className="flex-1 text-left truncate">{c.Label}</span>
        {c.Aggregate && <span className="text-[10px] font-mono uppercase" style={{ color: "var(--subtle)" }}>{c.Aggregate}</span>}
      </button>
    ))}
  </Popover>
);

// ============================================================
// Row action menu (loaded from related actions)
// ============================================================
const RowMenu = ({ rowActions, onAction, row }) => {
  const [open, setOpen] = useG_S(false);
  const ref = useG_R(null);
  useG_E(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [open]);
  if (!rowActions || rowActions.length === 0) return null;
  return (
    <div className="relative" ref={ref}>
      <button
        onClick={(e) => { e.stopPropagation(); setOpen((v) => !v); }}
        className="h-7 w-7 inline-flex items-center justify-center rounded hover:bg-[var(--surface-2)]"
        style={{ color: "var(--muted)" }}
      >
        <Icon name="more" size={15} />
      </button>
      {open && (
        <div className="absolute right-0 top-8 z-20 w-44 card backdrop-fade p-1" style={{ boxShadow: "var(--shadow-md)" }}>
          {rowActions.map((a) => {
            const mp = a.MergedParams || {};
            return (
              <button
                key={a.ActionId}
                onClick={(e) => { e.stopPropagation(); onAction(a, row); setOpen(false); }}
                className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-[13px] hover:bg-[var(--surface-2)]`}
                style={{ color: mp.Danger ? "var(--danger)" : "var(--ink-2)" }}
              >
                <Icon name={mp.Icon} size={14} />{mp.Label}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
};

// ============================================================
// Pagination
// ============================================================
const Pagination = ({ page, pageCount, setPage, pageSize, total }) => {
  if (pageCount <= 1) {
    return (
      <div className="flex items-center justify-between px-4 py-2.5 border-t text-[12px]" style={{ borderColor: "var(--border)", color: "var(--muted)" }}>
        <span>Showing {total} of {total}</span>
      </div>
    );
  }
  const from = (page - 1) * pageSize + 1;
  const to = Math.min(page * pageSize, total);
  let start = Math.max(1, page - 2);
  let end = Math.min(pageCount, start + 4);
  if (end - start < 4) start = Math.max(1, end - 4);
  const range = [];
  for (let i = start; i <= end; i++) range.push(i);

  return (
    <div className="flex items-center justify-between px-4 py-2.5 border-t text-[12px]" style={{ borderColor: "var(--border)", color: "var(--muted)" }}>
      <span>Showing <span className="font-medium" style={{ color: "var(--ink)" }}>{from}–{to}</span> of <span className="font-medium" style={{ color: "var(--ink)" }}>{total}</span></span>
      <div className="flex items-center gap-1">
        <Btn variant="ghost" size="icon" onClick={() => setPage(Math.max(1, page - 1))} disabled={page === 1}>
          <Icon name="chevronLeft" size={14} />
        </Btn>
        {start > 1 && <span className="px-1" style={{ color: "var(--subtle)" }}>…</span>}
        {range.map((p) => (
          <button
            key={p}
            onClick={() => setPage(p)}
            className={`h-8 min-w-[2rem] px-2 rounded-md text-[13px] font-medium transition-colors`}
            style={{ background: p === page ? "var(--primary)" : "transparent", color: p === page ? "white" : "var(--ink-2)" }}
          >{p}</button>
        ))}
        {end < pageCount && <span className="px-1" style={{ color: "var(--subtle)" }}>…</span>}
        <Btn variant="ghost" size="icon" onClick={() => setPage(Math.min(pageCount, page + 1))} disabled={page === pageCount}>
          <Icon name="chevronRight" size={14} />
        </Btn>
      </div>
    </div>
  );
};

// ============================================================
// MAIN GRID
// ============================================================
const Grid = ({ actionPayload, role, onToast, onOpenDetail, onAdminPatch, mergedParamsOverride }) => {
  const action = actionPayload;
  // Local MergedParams — the user can mutate sort/filter/columns; admin can save via mockApi
  const baseParams = mergedParamsOverride || action.MergedParams;
  const [mergedParams, setMergedParams] = useG_S(baseParams);
  useG_E(() => { setMergedParams(baseParams); }, [baseParams, action.ActionId]);

  // Data state
  const [data, setData] = useG_S([]);
  const [dataLoading, setDataLoading] = useG_S(true);

  // Related actions
  const [toolbarActions, setToolbarActions] = useG_S([]);
  const [rowActions, setRowActions] = useG_S([]);
  const [bulkActions, setBulkActions] = useG_S([]);

  // UI state
  const [search, setSearch] = useG_S("");
  const [page, setPage] = useG_S(1);
  const [filterOpen, setFilterOpen] = useG_S(false);
  const [chooserOpen, setChooserOpen] = useG_S(false);
  const [groupMenuOpen, setGroupMenuOpen] = useG_S(false);
  const [groupBy, setGroupBy] = useG_S(null);
  const [collapsed, setCollapsed] = useG_S(() => new Set());
  const [selection, setSelection] = useG_S(() => new Set());
  const [editingCell, setEditingCell] = useG_S(null); // { rowId, propertyName }
  const chooserBtnRef = useG_R(null);
  const groupBtnRef = useG_R(null);

  // ---- Load data + related actions when actionId changes ----
  useG_E(() => {
    let cancelled = false;
    setDataLoading(true);
    setSelection(new Set());
    setEditingCell(null);
    setPage(1);
    Promise.all([
      mockApi.listData(action.ActionId, { page: 1, pageSize: 200 }),
      mockApi.getRelatedActions(action.ActionId, "GridToolBar", role),
      mockApi.getRelatedActions(action.ActionId, "RowActions", role),
      mockApi.getRelatedActions(action.ActionId, "BulkActions", role),
    ]).then(([dataResp, tb, ra, ba]) => {
      if (cancelled) return;
      setData(dataResp.Items || []);
      setToolbarActions(tb);
      setRowActions(ra);
      setBulkActions(ba);
      setDataLoading(false);
    }).catch((e) => {
      if (cancelled) return;
      onToast({ kind: "err", text: e.message });
      setDataLoading(false);
    });
    return () => { cancelled = true; };
  }, [action.ActionId, role]);

  // ---- Columns (resolved against actual data shape) ----
  const allCols = useG_M(
    () => resolveColumns(mergedParams, data[0]),
    [mergedParams, data]
  );
  const visibleCols = useG_M(() => allCols.filter((c) => c.Visible !== false), [allCols]);
  const groupableCols = useG_M(() => visibleCols.filter((c) => c.Groupable), [visibleCols]);
  const groupCol = groupBy ? allCols.find((c) => c.PropertyName === groupBy) : null;
  const renderCols = useG_M(
    () => (groupBy ? visibleCols.filter((c) => c.PropertyName !== groupBy) : visibleCols),
    [visibleCols, groupBy]
  );

  // ---- Apply filter / search / sort ----
  const filtered = useG_M(() => {
    let rows = data;
    rows = applyFilter(rows, mergedParams.Filter || []);
    if (search.trim()) {
      const q = search.toLowerCase();
      rows = rows.filter((r) => Object.values(r).some((v) => v != null && String(v).toLowerCase().includes(q)));
    }
    rows = applySort(rows, mergedParams.Sort || [], allCols);
    return rows;
  }, [data, search, mergedParams, allCols]);

  // ---- Pagination ----
  const paginationEnabled = mergedParams.Pagination?.Enabled !== false;
  const pageSize = paginationEnabled ? (mergedParams.Pagination?.PageSize || 10) : filtered.length || 1;
  const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
  useG_E(() => { if (page > pageCount) setPage(1); }, [pageCount]);
  const pageRows = filtered.slice((page - 1) * pageSize, page * pageSize);

  // ---- Grouping ----
  const groups = useG_M(() => {
    if (!groupBy || !groupCol) return null;
    const map = new Map();
    filtered.forEach((r) => {
      const k = r[groupBy];
      if (!map.has(k)) map.set(k, []);
      map.get(k).push(r);
    });
    return [...map.entries()]
      .sort((a, b) => cmp(a[0], b[0], "asc", groupCol.DataType))
      .map(([key, rows]) => ({ key, rows }));
  }, [filtered, groupBy, groupCol]);

  // ---- Helpers ----
  const toggleGroup = (key) => {
    setCollapsed((s) => { const n = new Set(s); const k = String(key); if (n.has(k)) n.delete(k); else n.add(k); return n; });
  };
  const updateMergedParams = (patch) => {
    const next = { ...mergedParams, ...patch };
    setMergedParams(next);
    if (action.ActionId) mockApi.putActionConfig(action.ActionId, next).catch(() => {});
    onAdminPatch?.(next);
  };
  const onSort = (prop, additive) => {
    const col = allCols.find((c) => c.PropertyName === prop);
    if (!col?.Sortable) return;
    const current = mergedParams.Sort || [];
    const existing = current.find((s) => s.PropertyName === prop);
    let nextSort;
    if (existing) {
      // flip direction; if already flipped, remove
      const dirIsAsc = existing.SortDirection === "1" || existing.SortDirection === "asc";
      if (dirIsAsc) {
        nextSort = current.map((s) => s.PropertyName === prop ? { ...s, SortDirection: "0" } : s);
      } else {
        nextSort = current.filter((s) => s.PropertyName !== prop);
      }
    } else {
      const newSort = { PropertyName: prop, ColOrder: String((current.length + 1) * 10), SortDirection: "1", Grouping: "0" };
      nextSort = additive ? [...current, newSort] : [newSort];
    }
    updateMergedParams({ Sort: nextSort });
  };
  const sortIndexOf = (prop) => {
    const arr = mergedParams.Sort || [];
    const sorted = [...arr].sort((a, b) => Number(a.ColOrder) - Number(b.ColOrder));
    return sorted.findIndex((s) => s.PropertyName === prop);
  };
  const sortDirOf = (prop) => {
    const s = (mergedParams.Sort || []).find((x) => x.PropertyName === prop);
    if (!s) return null;
    return (s.SortDirection === "1" || s.SortDirection === "asc") ? "asc" : "desc";
  };
  const onChangeFilter = (next) => updateMergedParams({ Filter: next });

  // ---- Selection ----
  const allOnPage = (groups ? filtered : pageRows).map((r) => r.Id);
  const allSelectedOnPage = allOnPage.length > 0 && allOnPage.every((id) => selection.has(id));
  const someSelectedOnPage = allOnPage.some((id) => selection.has(id));
  const toggleAllOnPage = () => {
    setSelection((s) => {
      const n = new Set(s);
      if (allSelectedOnPage) allOnPage.forEach((id) => n.delete(id));
      else allOnPage.forEach((id) => n.add(id));
      return n;
    });
  };
  const toggleOne = (id) => {
    setSelection((s) => { const n = new Set(s); if (n.has(id)) n.delete(id); else n.add(id); return n; });
  };
  const clearSelection = () => setSelection(new Set());
  const hasSelection = selection.size > 0;
  const showRowMenu = rowActions.length > 0;

  // ---- Handlers (toolbar / row / bulk handler dispatcher) ----
  const dispatchHandler = useG_CB(async (handler, ctx) => {
    switch (handler) {
      case "refresh": {
        setDataLoading(true);
        const dataResp = await mockApi.listData(action.ActionId, { page: 1, pageSize: 200 });
        setData(dataResp.Items || []);
        setDataLoading(false);
        onToast({ kind: "ok", text: "Refreshed" });
        break;
      }
      case "openColumnChooser": setChooserOpen(true); break;
      case "openGroupBy":       setGroupMenuOpen(true); break;
      case "exportCsv": {
        const headers = visibleCols.map((c) => c.Label);
        const csvEsc = (s) => { const v = s == null ? "" : String(s); return /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v; };
        const lines = [headers.map(csvEsc).join(",")];
        filtered.forEach((row) => { lines.push(visibleCols.map((c) => csvEsc(c.IsHtml ? String(row[c.PropertyName] || "").replace(/<[^>]+>/g, "") : fmtCell(row[c.PropertyName], c))).join(",")); });
        const blob = new Blob([lines.join("\n")], { type: "text/csv;charset=utf-8;" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a"); a.href = url; a.download = `${action.ActionName.toLowerCase().replace(/\s+/g, "_")}.csv`;
        document.body.appendChild(a); a.click(); document.body.removeChild(a);
        URL.revokeObjectURL(url);
        onToast({ kind: "ok", text: `Exported ${filtered.length} rows` });
        break;
      }
      case "addRow":
        onToast({ kind: "info", text: "Add-new form would open here." });
        break;

      // Row handlers
      case "viewRow":
        if (action.MergedParams.DetailActionId) onOpenDetail({ detailActionId: action.MergedParams.DetailActionId, row: ctx.row });
        else onToast({ kind: "info", text: `View ${JSON.stringify(ctx.row).slice(0, 60)}…` });
        break;
      case "editRow":
        onToast({ kind: "info", text: "Edit form would open here. (Try double-clicking a non-readonly cell to inline-edit.)" });
        break;
      case "approveRow":
        await mockApi.patchRow(action.ActionId, ctx.row.Id, { Recommendation_Value: "Approved" });
        await dispatchHandler("refresh");
        onToast({ kind: "ok", text: `Approved #${ctx.row.IdeaNumber || ctx.row.Id}` });
        break;
      case "rejectRow":
        await mockApi.patchRow(action.ActionId, ctx.row.Id, { Recommendation_Value: "Rejected" });
        await dispatchHandler("refresh");
        onToast({ kind: "warn", text: `Rejected #${ctx.row.IdeaNumber || ctx.row.Id}` });
        break;
      case "deleteRow":
        await mockApi.runBulk(action.ActionId, "bulkDelete", [ctx.row.Id]);
        await dispatchHandler("refresh");
        onToast({ kind: "warn", text: "Row deleted" });
        break;
      case "assignToMe":
        onToast({ kind: "info", text: "Assignment would happen server-side." });
        break;

      // Bulk handlers
      case "bulkApprove":
      case "bulkReject":
      case "bulkHold":
      case "bulkDelete":
        if (selection.size === 0) return;
        await mockApi.runBulk(action.ActionId, handler, [...selection]);
        clearSelection();
        await dispatchHandler("refresh");
        onToast({ kind: handler === "bulkDelete" ? "warn" : "ok", text: `${handler.replace("bulk","")} applied to ${selection.size} item${selection.size === 1 ? "" : "s"}` });
        break;

      default:
        onToast({ kind: "info", text: `Handler "${handler}" not implemented.` });
    }
  }, [action, visibleCols, filtered, selection, onOpenDetail]);

  const onRowDblClick = (row) => {
    if (editingCell) return;
    if (action.MergedParams.DetailActionId) {
      onOpenDetail({ detailActionId: action.MergedParams.DetailActionId, row });
    }
  };

  const onCellDblClick = (row, col) => {
    if (col.ReadOnly !== false) return; // not editable
    setEditingCell({ rowId: row.Id, propertyName: col.PropertyName });
  };

  const commitEdit = async (row, col, newValue) => {
    setEditingCell(null);
    try {
      await mockApi.patchRow(action.ActionId, row.Id, { [col.PropertyName]: newValue });
      // mutate local data optimistically
      setData((d) => d.map((r) => (r.Id === row.Id ? { ...r, [col.PropertyName]: newValue } : r)));
      onToast({ kind: "ok", text: `Updated ${col.Label}` });
    } catch (e) {
      onToast({ kind: "err", text: e.message });
    }
  };

  return (
    <div className="card relative" style={{ overflow: "visible" }}>
      <Toolbar
        action={action}
        mergedParams={mergedParams}
        onUpdate={updateMergedParams}
        toolbarActions={toolbarActions}
        bulkActions={bulkActions}
        search={search}
        setSearch={setSearch}
        groupableCols={groupableCols}
        groupBy={groupBy}
        setGroupBy={setGroupBy}
        groupMenuOpen={groupMenuOpen}
        setGroupMenuOpen={setGroupMenuOpen}
        groupBtnRef={groupBtnRef}
        chooserBtnRef={chooserBtnRef}
        chooserOpen={chooserOpen}
        setChooserOpen={setChooserOpen}
        filterOpen={filterOpen}
        setFilterOpen={setFilterOpen}
        selection={selection}
        clearSelection={clearSelection}
        total={filtered.length}
        dispatchHandler={dispatchHandler}
        onToast={onToast}
      />

      {/* Column chooser anchored to its button */}
      <ColumnChooser
        open={chooserOpen}
        onClose={() => setChooserOpen(false)}
        columns={allCols}
        onChange={(next) => updateMergedParams({ Columns: mergedParams.Columns.map((c) => c.PropertyName === next.PropertyName ? next : c) })}
        anchorRef={chooserBtnRef}
      />

      {/* Group-by */}
      <GroupByMenu
        open={groupMenuOpen}
        onClose={() => setGroupMenuOpen(false)}
        groupableCols={groupableCols}
        groupBy={groupBy}
        setGroupBy={setGroupBy}
        anchorRef={groupBtnRef}
      />

      <FilterEditor
        open={filterOpen}
        onClose={() => setFilterOpen(false)}
        columns={allCols}
        filter={mergedParams.Filter || []}
        optionSets={OPTION_SETS}
        onApply={onChangeFilter}
      />

      <div className="overflow-x-auto scrollbar-thin">
        {dataLoading ? (
          <div className="p-6 space-y-2">
            <Skeleton className="h-7 w-full" />
            <Skeleton className="h-5 w-3/4" />
            <Skeleton className="h-5 w-2/3" />
            <Skeleton className="h-5 w-4/5" />
            <Skeleton className="h-5 w-1/2" />
          </div>
        ) : visibleCols.length === 0 ? (
          <Empty icon="columns" title="All columns are hidden" subtitle="Use the Columns picker in the toolbar to show one." />
        ) : (
          <table className="w-full border-collapse">
            <thead>
              <tr>
                {bulkActions.length > 0 && (
                  <th className="w-10 pl-4 text-center" style={{ background: "var(--surface-2)", borderBottom: "1px solid var(--border)" }}>
                    <input
                      type="checkbox"
                      checked={allSelectedOnPage}
                      ref={(el) => { if (el) el.indeterminate = !allSelectedOnPage && someSelectedOnPage; }}
                      onChange={toggleAllOnPage}
                      className="w-4 h-4 rounded border"
                      style={{ accentColor: "var(--primary)", borderColor: "var(--border-2)" }}
                    />
                  </th>
                )}
                {renderCols.map((c, i) => (
                  <HeaderCell
                    key={c.PropertyName}
                    col={c}
                    sortIdx={sortIndexOf(c.PropertyName) === -1 ? null : sortIndexOf(c.PropertyName)}
                    sortDir={sortDirOf(c.PropertyName)}
                    multiSort={(mergedParams.Sort || []).length > 1}
                    onSort={onSort}
                    isFirst={i === 0}
                    hasSelection={bulkActions.length > 0}
                  />
                ))}
                {showRowMenu && <th className="w-10" style={{ background: "var(--surface-2)", borderBottom: "1px solid var(--border)" }}></th>}
              </tr>
            </thead>
            <tbody>
              {groups ? renderGroupedBody({
                groups, groupCol, renderCols, showRowMenu, bulkActions, collapsed, toggleGroup,
                selection, toggleOne, onRowDblClick, onCellDblClick, editingCell, commitEdit, setEditingCell,
                rowActions, dispatchHandler,
              }) : pageRows.length === 0 ? (
                <tr>
                  <td colSpan={renderCols.length + (showRowMenu ? 1 : 0) + (bulkActions.length > 0 ? 1 : 0)}>
                    <Empty icon="search" title="No results" subtitle={(mergedParams.Filter?.length || search) ? "Try clearing filters or search." : "No data on this page."} />
                  </td>
                </tr>
              ) : pageRows.map((row, idx) => (
                <RowTr
                  key={row.Id}
                  row={row}
                  idx={idx}
                  renderCols={renderCols}
                  bulkActions={bulkActions}
                  selected={selection.has(row.Id)}
                  toggleOne={() => toggleOne(row.Id)}
                  onRowDblClick={() => onRowDblClick(row)}
                  onCellDblClick={onCellDblClick}
                  editingCell={editingCell}
                  commitEdit={commitEdit}
                  setEditingCell={setEditingCell}
                  rowActions={rowActions}
                  dispatchHandler={dispatchHandler}
                  showRowMenu={showRowMenu}
                />
              ))}
            </tbody>
          </table>
        )}
      </div>

      {paginationEnabled && !groups && !dataLoading && (
        <Pagination page={page} pageCount={pageCount} setPage={setPage} pageSize={pageSize} total={filtered.length} />
      )}
      {groups && !dataLoading && (
        <div className="flex items-center justify-between px-4 py-2.5 border-t text-[12px]" style={{ borderColor: "var(--border)", color: "var(--muted)" }}>
          <span><span className="font-medium" style={{ color: "var(--ink)" }}>{groups.length}</span> group{groups.length === 1 ? "" : "s"} · <span className="font-medium" style={{ color: "var(--ink)" }}>{filtered.length}</span> record{filtered.length === 1 ? "" : "s"}</span>
          <div className="flex items-center gap-1">
            <button onClick={() => setCollapsed(new Set(groups.map((g) => String(g.key))))} className="h-7 px-2 rounded text-[12px] hover:bg-[var(--surface-2)]" style={{ color: "var(--ink-2)" }}>Collapse all</button>
            <button onClick={() => setCollapsed(new Set())} className="h-7 px-2 rounded text-[12px] hover:bg-[var(--surface-2)]" style={{ color: "var(--ink-2)" }}>Expand all</button>
          </div>
        </div>
      )}
    </div>
  );
};

// ============================================================
// TOOLBAR (separated for clarity)
// ============================================================
const Toolbar = ({
  action, mergedParams, onUpdate, toolbarActions, bulkActions,
  search, setSearch, groupableCols, groupBy, setGroupBy,
  groupBtnRef, groupMenuOpen, setGroupMenuOpen,
  chooserBtnRef, chooserOpen, setChooserOpen,
  filterOpen, setFilterOpen,
  selection, clearSelection,
  total, dispatchHandler, onToast,
}) => {
  const hasSel = selection.size > 0;
  const groupedColLabel = groupBy ? (mergedParams.Columns?.find((c) => c.PropertyName === groupBy)?.Label || groupBy) : null;

  // If we have selection, show the bulk action bar instead of the regular toolbar
  if (hasSel && bulkActions.length > 0) {
    return (
      <div className="flex flex-wrap items-center gap-2 px-4 py-2.5 border-b" style={{ background: "var(--primary-bg)", borderColor: "var(--primary-ring)", borderTopLeftRadius: "var(--radius-lg)", borderTopRightRadius: "var(--radius-lg)" }}>
        <div className="inline-flex items-center gap-2 text-[13px] font-semibold" style={{ color: "var(--primary)" }}>
          <span className="inline-flex items-center justify-center min-w-6 h-6 px-1.5 rounded-full text-[11px] font-semibold" style={{ background: "var(--primary)", color: "white" }}>{selection.size}</span>
          selected
        </div>
        <button onClick={clearSelection} className="text-[12px] underline" style={{ color: "var(--primary)" }}>clear</button>
        <div className="flex-1" />
        <div className="flex items-center gap-1.5">
          {bulkActions.map((a) => {
            const mp = a.MergedParams;
            return (
              <Btn
                key={a.ActionId}
                variant={mp.Variant || "secondary"}
                onClick={async () => {
                  if (mp.ConfirmMessage && !window.confirm(mp.ConfirmMessage)) return;
                  await dispatchHandler(mp.Handler);
                }}
              >
                <Icon name={mp.Icon} size={13} /> {mp.Label}
              </Btn>
            );
          })}
        </div>
      </div>
    );
  }

  return (
    <div className="flex flex-wrap items-center gap-2 px-4 py-2.5 border-b" style={{ background: "var(--surface)", borderColor: "var(--border)", borderTopLeftRadius: "var(--radius-lg)", borderTopRightRadius: "var(--radius-lg)" }}>
      <div className="relative flex-1 min-w-[200px] max-w-sm">
        <Icon name="search" size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: "var(--subtle)" }} />
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Search..."
          className="w-full h-8 pl-8 pr-3 text-[13px] focus:outline-none focus:border-[var(--primary)] focus:bg-[var(--surface)]"
          style={{ background: "var(--surface-2)", color: "var(--ink)", border: "1px solid var(--border)", borderRadius: 7 }}
        />
      </div>
      <span className="text-[11.5px] hidden sm:inline-flex items-center gap-1 px-2 h-7 rounded font-mono" style={{ background: "var(--surface-2)", color: "var(--muted)" }}>
        {total} <span className="hidden md:inline">{total === 1 ? "record" : "records"}</span>
      </span>
      {groupedColLabel && (
        <span className="inline-flex items-center gap-1 pl-2 pr-1 h-7 rounded-md text-[12px] font-medium pop">
          <Icon name="layers" size={12} />
          Grouped by {groupedColLabel}
          <button onClick={() => setGroupBy(null)} className="ml-0.5 h-5 w-5 inline-flex items-center justify-center rounded hover:bg-[var(--primary-bg)]" title="Clear grouping">
            <Icon name="x" size={11} />
          </button>
        </span>
      )}

      <FilterPill
        filter={mergedParams.Filter}
        onClick={() => setFilterOpen(true)}
        onClear={() => onUpdate({ Filter: [] })}
      />

      <div className="flex-1" />

      <div className="flex items-center gap-1.5">
        <ViewsMenu
          actionId={action.ActionId}
          currentMergedParams={mergedParams}
          onApply={onUpdate}
          onToast={onToast}
        />

        {toolbarActions.map((a) => {
          const mp = a.MergedParams;
          const ref = mp.Handler === "openGroupBy" ? groupBtnRef : mp.Handler === "openColumnChooser" ? chooserBtnRef : null;
          const btn = (
            <Btn
              variant={mp.Variant || "secondary"}
              onClick={() => dispatchHandler(mp.Handler)}
            >
              <Icon name={mp.Icon} size={13} /> <span className="hidden sm:inline">{mp.Label}</span>
            </Btn>
          );
          // wrap in a span when we need a ref handle for popover anchoring
          return ref ? (
            <span key={a.ActionId} ref={ref} className="inline-flex">{btn}</span>
          ) : (
            <React.Fragment key={a.ActionId}>{btn}</React.Fragment>
          );
        })}
      </div>
    </div>
  );
};

// ============================================================
// ROW (extracted so grouped + flat share the same logic)
// ============================================================
const RowTr = ({ row, idx, renderCols, bulkActions, selected, toggleOne, onRowDblClick, onCellDblClick, editingCell, commitEdit, setEditingCell, rowActions, dispatchHandler, showRowMenu, indent }) => {
  return (
    <tr
      className="row-anim transition-colors"
      onDoubleClick={onRowDblClick}
      style={{
        background: selected ? "var(--primary-bg)" : (idx % 2 === 1 ? "var(--row-zebra)" : "transparent"),
        animationDelay: `${Math.min(idx, 8) * 16}ms`,
        cursor: "default",
      }}
      onMouseEnter={(e) => { if (!selected) e.currentTarget.style.background = "var(--row-hover)"; }}
      onMouseLeave={(e) => { if (!selected) e.currentTarget.style.background = idx % 2 === 1 ? "var(--row-zebra)" : "transparent"; }}
    >
      {bulkActions.length > 0 && (
        <td className="pl-4 text-center align-middle" style={{ borderBottom: "1px solid var(--border)" }}>
          <input
            type="checkbox"
            checked={selected}
            onChange={toggleOne}
            onClick={(e) => e.stopPropagation()}
            className="w-4 h-4 rounded border"
            style={{ accentColor: "var(--primary)", borderColor: "var(--border-2)" }}
          />
        </td>
      )}
      {renderCols.map((c, i) => {
        const align = (c.DataType === "int" || c.DataType === "decimal" || c.DataType === "number") ? "text-right" : "text-left";
        const value = row[c.PropertyName];
        const isEditing = editingCell && editingCell.rowId === row.Id && editingCell.propertyName === c.PropertyName;
        const editable = c.ReadOnly === false;
        return (
          <td
            key={c.PropertyName}
            onDoubleClick={(e) => { e.stopPropagation(); onCellDblClick(row, c); }}
            className={`${align} px-3 py-2 text-[13px] align-middle ${i === 0 && !bulkActions.length ? "pl-4 font-medium" : ""}`}
            style={{ width: c.Width === "auto" ? undefined : c.Width, borderBottom: "1px solid var(--border)", paddingLeft: indent && i === 0 ? "2.5rem" : undefined, color: "var(--ink)", position: "relative" }}
            title={editable ? "Double-click to edit" : undefined}
          >
            {isEditing ? (
              <EditCell col={c} value={value} onCommit={(v) => commitEdit(row, c, v)} onCancel={() => setEditingCell(null)} />
            ) : (
              <Cell col={c} value={value} row={row} />
            )}
            {editable && !isEditing && (
              <span className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 hover:opacity-100" style={{ color: "var(--subtle)" }}>
                <Icon name="pencil" size={10} />
              </span>
            )}
          </td>
        );
      })}
      {showRowMenu && (
        <td className="px-2 py-1 align-middle text-right" style={{ borderBottom: "1px solid var(--border)" }}>
          <RowMenu rowActions={rowActions} onAction={(act, r) => dispatchHandler(act.MergedParams.Handler, { row: r })} row={row} />
        </td>
      )}
    </tr>
  );
};

function renderGroupedBody({ groups, groupCol, renderCols, showRowMenu, bulkActions, collapsed, toggleGroup, selection, toggleOne, onRowDblClick, onCellDblClick, editingCell, commitEdit, setEditingCell, rowActions, dispatchHandler }) {
  if (groups.length === 0) {
    return (
      <tr>
        <td colSpan={renderCols.length + (showRowMenu ? 1 : 0) + (bulkActions.length > 0 ? 1 : 0)}>
          <Empty icon="search" title="No results" />
        </td>
      </tr>
    );
  }
  return groups.map((g, gIdx) => {
    const k = String(g.key);
    const isOpen = !collapsed.has(k);
    const aggLabel = groupCol?.Aggregate || "Count";
    const aggValue = aggLabel === "Count" ? g.rows.length : aggregate(g.rows, groupCol);
    return (
      <React.Fragment key={k}>
        <tr
          onClick={() => toggleGroup(g.key)}
          className="row-anim cursor-pointer transition-colors"
          style={{ background: "var(--surface-2)", borderTop: "1px solid var(--border)", animationDelay: `${Math.min(gIdx, 8) * 14}ms` }}
        >
          <td colSpan={renderCols.length + (showRowMenu ? 1 : 0) + (bulkActions.length > 0 ? 1 : 0)} className="px-3 py-2 pl-4">
            <div className="flex items-center gap-2.5">
              <Icon name={isOpen ? "chevronDown" : "chevronRight"} size={14} style={{ color: "var(--muted)" }} />
              <span className="text-[10.5px] uppercase tracking-wider font-semibold" style={{ color: "var(--muted)" }}>{groupCol?.Label || "Group"}</span>
              <span style={{ color: "var(--faint)" }}>·</span>
              <span className="text-[13px] font-semibold inline-flex items-center" style={{ color: "var(--ink)" }}>
                {groupCol?.DataType === "enum"
                  ? <EnumPill value={g.key} />
                  : (g.key == null || g.key === "" ? <em style={{ color: "var(--subtle)" }}>(none)</em> : fmtCell(g.key, groupCol))}
              </span>
              <span className="ml-auto inline-flex items-center gap-1 px-2 py-0.5 rounded text-[11px] font-medium" style={{ background: "var(--surface)", color: "var(--muted)", border: "1px solid var(--border)" }}>
                {aggLabel}: <span className="font-mono tabular-nums" style={{ color: "var(--ink)" }}>{aggValue}</span>
              </span>
            </div>
          </td>
        </tr>
        {isOpen && g.rows.map((row, idx) => (
          <RowTr
            key={row.Id}
            row={row}
            idx={idx}
            renderCols={renderCols}
            bulkActions={bulkActions}
            selected={selection.has(row.Id)}
            toggleOne={() => toggleOne(row.Id)}
            onRowDblClick={() => onRowDblClick(row)}
            onCellDblClick={onCellDblClick}
            editingCell={editingCell}
            commitEdit={commitEdit}
            setEditingCell={setEditingCell}
            rowActions={rowActions}
            dispatchHandler={dispatchHandler}
            showRowMenu={showRowMenu}
            indent
          />
        ))}
      </React.Fragment>
    );
  });
}

Object.assign(window, { Grid, Cell, CELL_TEMPLATES });
