// ───────────────────────────────────────────────────────────── CRUD primitives
// Shared affordances for delete / filter / bulk action across every collection.
// Goal: every screen (Catalog, Agreements, Directory, Videos) gets the SAME
// dialog system, the SAME filter drawer chrome, and the SAME bulk selection
// hooks. Per-screen code provides domain config; this file provides UI.
//
// Exposed on window:
//   • DeleteRecordDialog   — single-record confirm with archive/hard split,
//                             impact preview, undo toast
//   • FilterDrawer         — generic filter shell + facet primitives
//                             (FacetCheckbox, FacetSegmented, FacetRange, FacetRadio)
//   • CrudActionBar        — generic bulk action bar (extends BulkActionBar)
//   • CrudActionModal      — generic bulk action modal
//   • useCrudUndo()        — undo-toast hook for delete/restore
//   • RowKebab             — ⋯ button on detail/list rows w/ delete + custom items
//
// Storage: deleted ids per scope live in window.__crudDeleted[scope] (a Set),
// archived ids in window.__crudArchived[scope]. List/detail screens consult
// these to hide rows. Undo restores from the same store.

(function () {
  // ── store ────────────────────────────────────────────────────────────
  const Bus = (window.__crud ||= {
    deleted:  {},   // scope -> Set<id>
    archived: {},   // scope -> Set<id>
    listeners: new Set(),
  });
  function set(map, scope) { return (map[scope] ||= new Set()); }
  function emit() {
    window.dispatchEvent(new CustomEvent('astro-crud-changed'));
    Bus.listeners.forEach(fn => { try { fn(); } catch {} });
  }

  window.crud = {
    isDeleted:  (scope, id) => set(Bus.deleted, scope).has(id),
    isArchived: (scope, id) => set(Bus.archived, scope).has(id),
    listDeleted:  (scope) => Array.from(set(Bus.deleted, scope)),
    listArchived: (scope) => Array.from(set(Bus.archived, scope)),
    delete: (scope, id, mode = 'archive') => {
      const target = mode === 'archive' ? Bus.archived : Bus.deleted;
      set(target, scope).add(id);
      emit();
    },
    deleteMany: (scope, ids, mode = 'archive') => {
      const target = mode === 'archive' ? Bus.archived : Bus.deleted;
      const s = set(target, scope);
      ids.forEach(id => s.add(id));
      emit();
    },
    restore: (scope, id) => {
      set(Bus.deleted,  scope).delete(id);
      set(Bus.archived, scope).delete(id);
      emit();
    },
    restoreMany: (scope, ids) => {
      const d = set(Bus.deleted, scope), a = set(Bus.archived, scope);
      ids.forEach(id => { d.delete(id); a.delete(id); });
      emit();
    },
    // Filter helper for list views — drops deleted, optionally drops archived
    visible: (scope, items, getId, opts = {}) => {
      const d = set(Bus.deleted, scope);
      const a = set(Bus.archived, scope);
      const includeArchived = opts.includeArchived !== false; // default: keep
      return items.filter(it => {
        const id = typeof getId === 'function' ? getId(it) : it[getId];
        if (d.has(id)) return false;
        if (!includeArchived && a.has(id)) return false;
        return true;
      });
    },
    on: (fn) => { Bus.listeners.add(fn); return () => Bus.listeners.delete(fn); },
  };
})();

// React hook to re-render on crud changes
function useCrudVersion() {
  const [, force] = React.useReducer(x => x + 1, 0);
  React.useEffect(() => {
    const handler = () => force();
    window.addEventListener('astro-crud-changed', handler);
    return () => window.removeEventListener('astro-crud-changed', handler);
  }, []);
}

// ── icons (avoid depending on Ic — keeps this file independent) ───────
const _CrudIc = {
  Trash:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M4 7h16M9 7V4h6v3M6 7l1 14h10l1-14M10 11v6M14 11v6"/></svg>,
  Archive: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M3 7h18v3H3zM5 10v10h14V10M9 14h6"/></svg>,
  Warn:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M12 3l10 18H2L12 3zM12 10v5M12 17h.01"/></svg>,
  Restore: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M3 12a9 9 0 1 0 3-6.7M3 4v5h5"/></svg>,
  X:       (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M5 5l14 14M19 5L5 19"/></svg>,
  Check:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M4 12l5 5L20 6"/></svg>,
  Kebab:   (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg>,
};

// ── undo toast ────────────────────────────────────────────────────────
function useCrudUndo() {
  // Toast lives in DOM via the existing astro-toast bus. We add a richer
  // 'astro-undo-toast' that supports an action button.
  return React.useCallback((message, onUndo, opts = {}) => {
    window.dispatchEvent(new CustomEvent('astro-undo-toast', {
      detail: { message, onUndo, kind: opts.kind || 'ok', duration: opts.duration || 6000 }
    }));
  }, []);
}

// Toast host — listens for astro-undo-toast and renders a stack
function CrudToastHost() {
  const [toasts, setToasts] = React.useState([]);
  React.useEffect(() => {
    const onToast = (e) => {
      const id = Math.random().toString(36).slice(2);
      const t = { id, ...e.detail };
      setToasts(ts => [...ts, t]);
      setTimeout(() => {
        setToasts(ts => ts.filter(x => x.id !== id));
      }, t.duration || 6000);
    };
    window.addEventListener('astro-undo-toast', onToast);
    return () => window.removeEventListener('astro-undo-toast', onToast);
  }, []);
  if (toasts.length === 0) return null;
  return (
    <div style={{
      position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)',
      zIndex: 9999, display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center',
      pointerEvents: 'none',
    }}>
      {toasts.map(t => (
        <div key={t.id} style={{
          pointerEvents: 'auto',
          background: 'var(--ink)', color: 'var(--bg)', padding: '10px 14px',
          display: 'flex', alignItems: 'center', gap: 14,
          fontSize: 13, fontWeight: 500, letterSpacing: '-0.005em',
          minWidth: 320, maxWidth: 520,
          boxShadow: '0 8px 24px rgba(0,0,0,.18)',
        }}>
          <span style={{ flex: 1 }}>{t.message}</span>
          {t.onUndo && (
            <button
              onClick={() => { try { t.onUndo(); } catch {} ; setToasts(ts => ts.filter(x => x.id !== t.id)); }}
              className="ff-mono upper"
              style={{ background: 'transparent', border: '1px solid rgba(255,255,255,.3)', color: 'var(--bg)',
                padding: '4px 10px', fontSize: 10, fontWeight: 600, letterSpacing: '.1em', cursor: 'pointer' }}>
              Undo
            </button>
          )}
        </div>
      ))}
    </div>
  );
}

// ───────────────────────────────────────────────────────────── DELETE DIALOG
// Single-record delete with archive/hard-delete split + impact preview.
//
// Props:
//   open        — bool
//   onClose     — fn
//   onConfirm   — fn(mode: 'archive' | 'hard') -> Promise|void
//   kind        — 'work' | 'recording' | 'release' | 'agreement' | 'party' | 'video' (label only)
//   title       — record's display title
//   subtitle    — record's secondary identifier
//   impacts     — [{ label, count, tone? }] — flagged "linked" records
//   allowHard   — default true; if false, only archive is offered
//   defaultMode — 'archive' (default) | 'hard'
function DeleteRecordDialog({ open, onClose, onConfirm, kind = 'record', title = '', subtitle = '', impacts = [], allowHard = true, defaultMode = 'archive' }) {
  const [mode, setMode] = React.useState(defaultMode);
  const [confirmText, setConfirmText] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => { if (open) { setMode(defaultMode); setConfirmText(''); setBusy(false); } }, [open, defaultMode]);
  if (!open) return null;
  const isHard = mode === 'hard';
  const canConfirm = isHard ? (confirmText.trim().toUpperCase() === 'DELETE') : true;
  const totalImpact = impacts.reduce((n, x) => n + (x.count || 0), 0);

  const Icon = isHard ? _CrudIc.Trash : _CrudIc.Archive;
  const accent = isHard ? '#c0392b' : 'var(--ink)';

  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,.40)', zIndex: 200,
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
    }} onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: 'var(--bg)', border: '1px solid var(--rule)',
        width: 'min(560px, 100%)', maxHeight: '90vh', overflowY: 'auto',
        boxShadow: '0 20px 60px rgba(0,0,0,.20)',
      }}>
        {/* header */}
        <div style={{ padding: '20px 24px 14px', borderBottom: '1px solid var(--rule)' }}>
          <div className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.12em', color: accent, fontWeight: 600, marginBottom: 8 }}>
            {isHard ? `DELETE ${kind.toUpperCase()} · DESTRUCTIVE` : `ARCHIVE ${kind.toUpperCase()}`}
          </div>
          <div className="ff-display" style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', lineHeight: 1.2 }}>
            {title || `Untitled ${kind}`}
          </div>
          {subtitle && (
            <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 6 }}>{subtitle}</div>
          )}
        </div>

        {/* mode toggle */}
        {allowHard && (
          <div style={{ padding: '14px 24px 0', display: 'flex', gap: 0, border: 0 }}>
            {[
              { id: 'archive', label: 'Archive', sub: 'Hide from views, preserve all links' },
              { id: 'hard',    label: 'Delete',  sub: 'Remove permanently · breaks links' },
            ].map((opt, i) => (
              <button key={opt.id} onClick={() => setMode(opt.id)} className="ff-mono"
                style={{
                  flex: 1, padding: '10px 12px', textAlign: 'left',
                  background: mode === opt.id ? 'var(--ink)' : 'transparent',
                  color: mode === opt.id ? 'var(--bg)' : 'var(--ink)',
                  border: '1px solid var(--rule)',
                  borderRight: i === 0 ? 'none' : '1px solid var(--rule)',
                  cursor: 'pointer',
                }}>
                <div className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.12em', fontWeight: 600, marginBottom: 3 }}>{opt.label}</div>
                <div style={{ fontSize: 11, opacity: mode === opt.id ? .85 : .65, fontFamily: 'inherit' }}>{opt.sub}</div>
              </button>
            ))}
          </div>
        )}

        {/* impact */}
        <div style={{ padding: '20px 24px 4px' }}>
          <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', fontWeight: 600, marginBottom: 10 }}>
            IMPACT PREVIEW
          </div>
          {impacts.length === 0 && (
            <div className="ff-mono" style={{ fontSize: 12, color: 'var(--ink-3)', padding: '8px 0' }}>
              No linked records. Safe to {isHard ? 'delete' : 'archive'}.
            </div>
          )}
          {impacts.length > 0 && (
            <div style={{ border: '1px solid var(--rule)' }}>
              {impacts.map((imp, i) => (
                <div key={i} style={{
                  display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                  padding: '10px 12px',
                  borderBottom: i < impacts.length - 1 ? '1px solid var(--rule-soft)' : 'none',
                  background: imp.tone === 'warn' ? 'rgba(192,57,43,.05)' : 'transparent',
                }}>
                  <div>
                    <div style={{ fontSize: 13 }}>{imp.label}</div>
                    {imp.note && <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 2 }}>{imp.note}</div>}
                  </div>
                  <span className="ff-mono num" style={{
                    fontSize: 12, fontWeight: 600,
                    color: imp.tone === 'warn' ? '#c0392b' : 'var(--ink)',
                  }}>{imp.count}</span>
                </div>
              ))}
            </div>
          )}

          {isHard && totalImpact > 0 && (
            <div className="ff-mono" style={{
              marginTop: 14, padding: '10px 12px', background: 'rgba(192,57,43,.08)',
              fontSize: 12, color: '#c0392b', lineHeight: 1.55,
              display: 'flex', gap: 10, alignItems: 'flex-start',
            }}>
              <_CrudIc.Warn style={{ width: 16, height: 16, flexShrink: 0, marginTop: 1 }} />
              <span>
                <b>{totalImpact} linked record{totalImpact === 1 ? '' : 's'}</b> will be flagged as orphaned.
                Existing CWR submissions, statements, and royalty allocations are preserved
                but lose their reference back to this {kind}.
              </span>
            </div>
          )}

          {!isHard && (
            <div className="ff-mono" style={{
              marginTop: 14, padding: '10px 12px', background: 'var(--bg-2)',
              fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.55,
            }}>
              Archived {kind}s are hidden from default lists but remain queryable from the Trash view,
              keep all linked records intact, and can be restored at any time.
            </div>
          )}
        </div>

        {/* hard-delete confirm */}
        {isHard && (
          <div style={{ padding: '14px 24px 0' }}>
            <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>
              TYPE "DELETE" TO CONFIRM
            </div>
            <input
              autoFocus
              value={confirmText}
              onChange={(e) => setConfirmText(e.target.value)}
              placeholder="DELETE"
              style={{
                width: '100%', padding: '10px 12px', fontSize: 13,
                background: 'transparent', color: 'var(--ink)',
                border: '1px solid var(--rule)', letterSpacing: '.1em',
                fontFamily: 'var(--ff-mono, ui-monospace, monospace)',
              }}
            />
          </div>
        )}

        {/* actions */}
        <div style={{
          display: 'flex', gap: 10, justifyContent: 'flex-end',
          padding: '20px 24px', marginTop: 8, borderTop: '1px solid var(--rule)',
        }}>
          <button onClick={onClose} disabled={busy} className="ff-mono upper" style={{
            padding: '8px 14px', background: 'transparent', color: 'var(--ink)',
            border: '1px solid var(--rule)', fontSize: 11, letterSpacing: '.12em',
            fontWeight: 600, cursor: 'pointer',
          }}>Cancel</button>
          <button
            onClick={async () => {
              if (!canConfirm || busy) return;
              setBusy(true);
              try { await onConfirm(mode); } finally { setBusy(false); onClose && onClose(); }
            }}
            disabled={!canConfirm || busy}
            className="ff-mono upper"
            style={{
              padding: '8px 16px',
              background: isHard ? '#c0392b' : 'var(--ink)',
              color: isHard ? '#fff' : 'var(--bg)',
              border: 0, fontSize: 11, letterSpacing: '.12em',
              fontWeight: 600,
              cursor: (!canConfirm || busy) ? 'not-allowed' : 'pointer',
              opacity: (!canConfirm || busy) ? 0.5 : 1,
              display: 'inline-flex', alignItems: 'center', gap: 8,
            }}>
            <Icon style={{ width: 14, height: 14 }} />
            {busy ? 'Working…' : (isHard ? 'Delete permanently' : 'Archive')}
          </button>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────── ROW KEBAB
// Generic ⋯ menu for detail-screen action bars and list rows.
// items: [{ id, label, icon?, danger?, onClick, divider? }]
function RowKebab({ items = [], align = 'right', size = 'md' }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
  }, [open]);
  const dim = size === 'sm' ? 26 : 30;
  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
      <button onClick={(e) => { e.stopPropagation(); setOpen(o => !o); }}
        aria-label="More actions"
        style={{
          width: dim, height: dim, padding: 0,
          background: open ? 'var(--bg-2)' : 'transparent',
          border: '1px solid var(--rule)',
          color: 'var(--ink)', cursor: 'pointer',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        }}>
        <_CrudIc.Kebab style={{ width: 16, height: 16 }} />
      </button>
      {open && (
        <div style={{
          position: 'absolute', top: dim + 4,
          [align]: 0,
          minWidth: 200, background: 'var(--bg)',
          border: '1px solid var(--rule)',
          boxShadow: '0 8px 24px rgba(0,0,0,.12)',
          zIndex: 50,
        }}>
          {items.map((it, i) => it.divider ? (
            <div key={`d${i}`} style={{ height: 1, background: 'var(--rule)' }} />
          ) : (
            <button key={it.id || i}
              onClick={(e) => { e.stopPropagation(); setOpen(false); it.onClick && it.onClick(); }}
              disabled={it.disabled}
              className="ff-mono"
              style={{
                display: 'flex', alignItems: 'center', gap: 10, width: '100%',
                padding: '9px 12px', textAlign: 'left',
                background: 'transparent', border: 0,
                color: it.danger ? '#c0392b' : 'var(--ink)',
                fontSize: 12, cursor: it.disabled ? 'not-allowed' : 'pointer',
                opacity: it.disabled ? 0.5 : 1,
              }}
              onMouseEnter={(e) => { if (!it.disabled) e.currentTarget.style.background = 'var(--bg-2)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
              {it.icon && <span style={{ display: 'inline-flex', width: 14, height: 14 }}>{it.icon}</span>}
              <span style={{ flex: 1 }}>{it.label}</span>
              {it.shortcut && <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{it.shortcut}</span>}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────── FILTER DRAWER
// Generic right-side filter drawer. Children render the facets; the drawer
// provides the chrome (header, count, reset, apply, close).
//
// Usage:
//   <FilterDrawer open={open} onClose={…} onReset={…} activeCount={n}>
//     <FacetSection title="Status">
//       <FacetCheckbox value={...} onChange={...} options={[…]}/>
//     </FacetSection>
//     ...
//   </FilterDrawer>
function FilterDrawer({ open, onClose, onReset, activeCount = 0, children, title = 'Filter' }) {
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose && onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 180 }}>
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,.35)' }} />
      <aside style={{
        position: 'absolute', top: 0, right: 0, bottom: 0, width: 'min(420px,100%)',
        background: 'var(--bg)', borderLeft: '1px solid var(--rule)',
        display: 'flex', flexDirection: 'column',
      }}>
        <div style={{ padding: '18px 24px 14px', borderBottom: '1px solid var(--rule)',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div>
            <div className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.12em', color: 'var(--ink-3)', fontWeight: 600 }}>
              FILTER
            </div>
            <div className="ff-display" style={{ fontSize: 20, fontWeight: 600, letterSpacing: '-0.02em', marginTop: 4 }}>
              {title}
              {activeCount > 0 && (
                <span className="ff-mono num" style={{ marginLeft: 10, padding: '1px 8px',
                  background: 'var(--ink)', color: 'var(--bg)', fontSize: 11, fontWeight: 600 }}>
                  {activeCount}
                </span>
              )}
            </div>
          </div>
          <button onClick={onClose} aria-label="Close" style={{
            width: 32, height: 32, padding: 0, background: 'transparent',
            border: '1px solid var(--rule)', cursor: 'pointer',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--ink)',
          }}>
            <_CrudIc.X style={{ width: 14, height: 14 }} />
          </button>
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: '8px 0' }}>
          {children}
        </div>
        <div style={{
          padding: '14px 24px', borderTop: '1px solid var(--rule)',
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        }}>
          <button onClick={onReset} disabled={activeCount === 0} className="ff-mono upper" style={{
            padding: '7px 12px', background: 'transparent', color: activeCount > 0 ? 'var(--ink)' : 'var(--ink-3)',
            border: '1px solid var(--rule)', fontSize: 10, letterSpacing: '.12em', fontWeight: 600,
            cursor: activeCount > 0 ? 'pointer' : 'not-allowed',
          }}>Reset</button>
          <button onClick={onClose} className="ff-mono upper" style={{
            padding: '7px 16px', background: 'var(--ink)', color: 'var(--bg)',
            border: 0, fontSize: 10, letterSpacing: '.12em', fontWeight: 600, cursor: 'pointer',
          }}>Apply</button>
        </div>
      </aside>
    </div>
  );
}

function FacetSection({ title, hint, children }) {
  return (
    <div style={{ padding: '14px 24px', borderBottom: '1px solid var(--rule-soft)' }}>
      <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', fontWeight: 600, marginBottom: 10 }}>
        {title}
      </div>
      {children}
      {hint && <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 8 }}>{hint}</div>}
    </div>
  );
}

function FacetCheckbox({ value = [], onChange, options }) {
  // value: string[]; options: [{id,label,count?}]
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      {options.map(opt => {
        const id = typeof opt === 'string' ? opt : opt.id;
        const label = typeof opt === 'string' ? opt : opt.label;
        const count = typeof opt === 'string' ? null : opt.count;
        const checked = value.includes(id);
        return (
          <label key={id} style={{
            display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer',
            padding: '6px 8px',
            background: checked ? 'var(--bg-2)' : 'transparent',
            border: '1px solid ' + (checked ? 'var(--ink)' : 'transparent'),
          }}>
            <span style={{
              width: 14, height: 14, border: '1px solid ' + (checked ? 'var(--ink)' : 'var(--rule)'),
              background: checked ? 'var(--ink)' : 'transparent',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              flexShrink: 0,
            }}>
              {checked && <_CrudIc.Check style={{ width: 10, height: 10, color: 'var(--bg)' }} />}
            </span>
            <input type="checkbox" checked={checked} onChange={() => {
              const next = checked ? value.filter(x => x !== id) : [...value, id];
              onChange && onChange(next);
            }} style={{ display: 'none' }} />
            <span style={{ flex: 1, fontSize: 12 }}>{label}</span>
            {count != null && <span className="ff-mono num" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{count}</span>}
          </label>
        );
      })}
    </div>
  );
}

function FacetSegmented({ value, onChange, options }) {
  return (
    <div style={{ display: 'flex', border: '1px solid var(--rule)', width: 'fit-content' }}>
      {options.map((opt, i) => {
        const id = typeof opt === 'string' ? opt : opt.id;
        const label = typeof opt === 'string' ? opt : opt.label;
        const active = value === id;
        return (
          <button key={id} onClick={() => onChange && onChange(id)} className="ff-mono upper" style={{
            padding: '6px 12px', fontSize: 10, letterSpacing: '.1em', fontWeight: 600,
            background: active ? 'var(--ink)' : 'transparent',
            color: active ? 'var(--bg)' : 'var(--ink-2)',
            border: 0,
            borderLeft: i === 0 ? 'none' : '1px solid var(--rule)',
            cursor: 'pointer',
          }}>{label}</button>
        );
      })}
    </div>
  );
}

function FacetRange({ min, max, value, onChange, format = (v) => v }) {
  // value: [lo, hi]
  const [lo, hi] = value;
  return (
    <div>
      <div className="ff-mono num" style={{ fontSize: 12, marginBottom: 8, color: 'var(--ink)' }}>
        {format(lo)} <span style={{ color: 'var(--ink-3)', margin: '0 6px' }}>→</span> {format(hi)}
      </div>
      <div style={{ display: 'flex', gap: 10 }}>
        <input type="range" min={min} max={max} value={lo}
          onChange={(e) => onChange && onChange([Math.min(+e.target.value, hi), hi])}
          style={{ flex: 1 }}/>
        <input type="range" min={min} max={max} value={hi}
          onChange={(e) => onChange && onChange([lo, Math.max(+e.target.value, lo)])}
          style={{ flex: 1 }}/>
      </div>
    </div>
  );
}

function FacetRadio({ value, onChange, options }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      {options.map(opt => {
        const id = typeof opt === 'string' ? opt : opt.id;
        const label = typeof opt === 'string' ? opt : opt.label;
        const active = value === id;
        return (
          <label key={id} style={{
            display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer',
            padding: '6px 8px',
            background: active ? 'var(--bg-2)' : 'transparent',
          }}>
            <span style={{
              width: 14, height: 14, borderRadius: '50%',
              border: '1px solid ' + (active ? 'var(--ink)' : 'var(--rule)'),
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              flexShrink: 0,
            }}>
              {active && <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--ink)' }}/>}
            </span>
            <span style={{ fontSize: 12 }}>{label}</span>
          </label>
        );
      })}
    </div>
  );
}

// ───────────────────────────────────────────────────────────── BULK ACTION BAR
// Lighter wrapper around the existing bulk-select store, with a delete-aware
// default action set. Pass `actions` to override.
function CrudActionBar({ scope, kind, label, actions, onAction }) {
  const sel = (typeof useBulkSel === 'function') ? useBulkSel(scope) : null;
  if (!sel || sel.count === 0) return null;
  const defaults = [
    { id: 'export',  label: 'Export CSV' },
    { id: 'archive', label: 'Archive' },
    { id: 'delete',  label: 'Delete', danger: true },
  ];
  const acts = actions || defaults;
  const plural = label || kind || 'items';
  return (
    <div style={{
      position: 'sticky', top: 0, zIndex: 40,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '10px 14px', background: 'var(--ink)', color: 'var(--bg)',
      borderBottom: '1px solid var(--ink)',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
        <span className="ff-mono num" style={{ fontSize: 13, fontWeight: 600 }}>
          {sel.count} <span style={{ opacity: .65, fontWeight: 500 }}>{plural} selected</span>
        </span>
        <button onClick={() => sel.clear()} className="ff-mono upper" style={{
          background: 'transparent', border: '1px solid rgba(255,255,255,.3)',
          color: 'var(--bg)', padding: '4px 10px', fontSize: 10, fontWeight: 600,
          letterSpacing: '.1em', cursor: 'pointer',
        }}>Clear</button>
      </div>
      <div style={{ display: 'flex', gap: 8 }}>
        {acts.map(a => (
          <button key={a.id}
            onClick={() => onAction && onAction(a.id, sel.selected)}
            className="ff-mono upper"
            style={{
              padding: '5px 12px', fontSize: 10, letterSpacing: '.1em', fontWeight: 600,
              background: a.danger ? '#c0392b' : 'transparent',
              color: 'var(--bg)',
              border: a.danger ? 'none' : '1px solid rgba(255,255,255,.3)',
              cursor: 'pointer',
            }}>{a.label}</button>
        ))}
      </div>
    </div>
  );
}

// Minimal generic bulk action modal — for kinds NOT in bulk-select.jsx's
// hardcoded list. Catalog already has BulkActionModal; this is for the rest.
function CrudActionModal({ open, onClose, action, kind, label, ids, onConfirm }) {
  const [confirmText, setConfirmText] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => { if (open) { setConfirmText(''); setBusy(false); } }, [open]);
  if (!open) return null;
  const plural = label || kind || 'items';
  const isDelete  = action === 'delete';
  const isArchive = action === 'archive';
  const danger = isDelete;
  const canConfirm = isDelete ? confirmText.trim().toUpperCase() === 'DELETE' : true;
  const titleMap = {
    'delete':  `Delete ${ids.length} ${plural}`,
    'archive': `Archive ${ids.length} ${plural}`,
    'export':  `Export ${ids.length} ${plural}`,
    'tag':     `Tag ${ids.length} ${plural}`,
  };
  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,.40)', zIndex: 200,
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
    }} onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: 'var(--bg)', border: '1px solid var(--rule)',
        width: 'min(480px, 100%)', boxShadow: '0 20px 60px rgba(0,0,0,.20)',
      }}>
        <div style={{ padding: '20px 24px 14px', borderBottom: '1px solid var(--rule)' }}>
          <div className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.12em',
            color: danger ? '#c0392b' : 'var(--ink-3)', fontWeight: 600, marginBottom: 8 }}>
            BULK ACTION{danger ? ' · DESTRUCTIVE' : ''}
          </div>
          <div className="ff-display" style={{ fontSize: 20, fontWeight: 600, letterSpacing: '-0.02em' }}>
            {titleMap[action] || action}
          </div>
        </div>
        <div style={{ padding: '18px 24px' }}>
          {isDelete && (
            <>
              <div className="ff-mono" style={{ fontSize: 12, color: '#c0392b', lineHeight: 1.6, marginBottom: 14 }}>
                <b>Destructive.</b> {ids.length} {plural} will be removed.
                Linked records are preserved but flagged as orphaned.
              </div>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em',
                color: 'var(--ink-3)', marginBottom: 8 }}>
                TYPE "DELETE" TO CONFIRM
              </div>
              <input autoFocus value={confirmText} onChange={e => setConfirmText(e.target.value)}
                placeholder="DELETE"
                style={{ width: '100%', padding: '10px 12px', fontSize: 13,
                  background: 'transparent', color: 'var(--ink)',
                  border: '1px solid var(--rule)', letterSpacing: '.1em',
                  fontFamily: 'var(--ff-mono, ui-monospace, monospace)',
                }}/>
            </>
          )}
          {isArchive && (
            <div className="ff-mono" style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.6 }}>
              {ids.length} {plural} will be hidden from default views and moved to the Archive.
              All linked records remain intact. You can restore at any time.
            </div>
          )}
          {!isDelete && !isArchive && (
            <div className="ff-mono" style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.6 }}>
              Apply this action to {ids.length} selected {plural}.
            </div>
          )}
        </div>
        <div style={{
          display: 'flex', gap: 10, justifyContent: 'flex-end',
          padding: '14px 24px', borderTop: '1px solid var(--rule)',
        }}>
          <button onClick={onClose} className="ff-mono upper" style={{
            padding: '7px 12px', background: 'transparent', color: 'var(--ink)',
            border: '1px solid var(--rule)', fontSize: 10, letterSpacing: '.12em',
            fontWeight: 600, cursor: 'pointer',
          }}>Cancel</button>
          <button
            disabled={!canConfirm || busy}
            onClick={async () => {
              if (!canConfirm || busy) return;
              setBusy(true);
              try { await onConfirm(action, ids); } finally { setBusy(false); onClose && onClose(); }
            }}
            className="ff-mono upper"
            style={{
              padding: '7px 16px',
              background: danger ? '#c0392b' : 'var(--ink)',
              color: danger ? '#fff' : 'var(--bg)',
              border: 0, fontSize: 10, letterSpacing: '.12em', fontWeight: 600,
              cursor: (!canConfirm || busy) ? 'not-allowed' : 'pointer',
              opacity: (!canConfirm || busy) ? 0.5 : 1,
            }}>
            {busy ? 'Working…' : (titleMap[action] || 'Apply')}
          </button>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────── helpers
// Build impact list for delete dialogs by counting linked records.
// kind: 'work' | 'recording' | 'release' | 'agreement' | 'party' | 'video'
window.crudImpacts = function (kind, record) {
  const out = [];
  const safe = (fn, fallback = 0) => { try { return fn(); } catch { return fallback; } };
  const arr = (x) => Array.isArray(x) ? x : [];
  if (!record) return out;
  const id = record.id || record.aid || record['Video ID'] || record.code;

  if (kind === 'work') {
    const recs = safe(() => arr(window.RECORDINGS).filter(r => r.workId === id || r.work === id || (record.recordings || []).includes(r.id)));
    const regs = safe(() => arr(record.registrations));
    const splits = safe(() => arr(record.writers || record.splits));
    if (recs.length)   out.push({ label: 'Linked recordings',   count: recs.length });
    if (splits.length) out.push({ label: 'Writer splits',       count: splits.length });
    if (regs.length)   out.push({ label: 'Society registrations', count: regs.length, tone: 'warn', note: 'CWR submissions retained as orphan' });
  }
  if (kind === 'recording') {
    const releases = safe(() => arr(record.releases || record.releaseGroups));
    const platforms = safe(() => arr(record.platforms || record.dsp));
    const fingerprint = !!record.audioPrintHash;
    if (releases.length)  out.push({ label: 'Releases',          count: releases.length });
    if (platforms.length) out.push({ label: 'DSP linkages',      count: platforms.length });
    if (fingerprint)      out.push({ label: 'Audio fingerprint', count: 1, note: 'will be unmatched on next scan' });
  }
  if (kind === 'release') {
    const tracks = safe(() => arr(record.tracks || record.recordings));
    const dsp = safe(() => arr(record.platforms));
    if (tracks.length) out.push({ label: 'Track listings', count: tracks.length });
    if (dsp.length)    out.push({ label: 'DSP releases',   count: dsp.length });
  }
  if (kind === 'agreement') {
    const works = safe(() => arr(record.works));
    const stmts = safe(() => arr(record.statements));
    const adv   = safe(() => arr(record.advanceSchedule));
    if (works.length) out.push({ label: 'Works covered',  count: works.length });
    if (adv.length)   out.push({ label: 'Advance lines',  count: adv.length, tone: 'warn', note: 'recoupment ledger preserved' });
    if (stmts.length) out.push({ label: 'Statement runs', count: stmts.length });
  }
  if (kind === 'party') {
    // Record may be a profile/publisher/label/society — count refs across catalog
    const pid = id;
    const inWriters    = safe(() => arr(window.WORKS).filter(w => arr(w.writers).some(x => x.id === pid || x.ipi === record.ipi)).length);
    const inPublishers = safe(() => arr(window.WORKS).filter(w => arr(w.publishers).some(x => x.id === pid)).length);
    const inLabels     = safe(() => arr(window.RELEASE_GROUPS).filter(g => g.label === record.name || g.labelId === pid).length);
    const inAgs        = safe(() => arr(window.AGREEMENTS).filter(a => a.a === record.name || a.b === record.name).length);
    if (inWriters)    out.push({ label: 'Works as writer',    count: inWriters });
    if (inPublishers) out.push({ label: 'Works as publisher', count: inPublishers });
    if (inLabels)     out.push({ label: 'Releases',           count: inLabels });
    if (inAgs)        out.push({ label: 'Agreements',         count: inAgs, tone: 'warn' });
  }
  if (kind === 'video') {
    const cidClaim = !!record['Content ID Claimants'];
    const yt = !!record['YouTube Link'];
    const fb = !!record['Facebook Link'];
    const hits = (cidClaim ? 1 : 0) + (yt ? 1 : 0) + (fb ? 1 : 0);
    if (hits) out.push({ label: 'Linked DSP IDs', count: hits });
    if (cidClaim) out.push({ label: 'Content ID claim', count: 1, tone: 'warn', note: 'manual release required' });
  }
  return out;
};

// Mount the toast host once into <body>
(function mountCrudToastHost() {
  if (document.getElementById('crud-toast-root')) return;
  const div = document.createElement('div');
  div.id = 'crud-toast-root';
  document.body && document.body.appendChild(div);
  // Lazy mount once React + ReactDOM are present
  const tryMount = () => {
    if (window.ReactDOM && window.React) {
      try {
        const root = window.ReactDOM.createRoot ? window.ReactDOM.createRoot(div) : null;
        if (root) root.render(<CrudToastHost/>);
        else window.ReactDOM.render(<CrudToastHost/>, div);
      } catch {}
    } else {
      setTimeout(tryMount, 50);
    }
  };
  tryMount();
})();

Object.assign(window, {
  DeleteRecordDialog,
  RowKebab,
  FilterDrawer,
  FacetSection, FacetCheckbox, FacetSegmented, FacetRange, FacetRadio,
  CrudActionBar,
  CrudActionModal,
  useCrudUndo,
  useCrudVersion,
  CrudToastHost,
});

// ───────────────────────────────────────────────────────────── DETAIL DELETE
// One-shot button + dialog + undo-toast for detail screens.
//   <DetailDelete kind="work" record={m} title={m.title} subtitle={m.iswc}
//                 onDeleted={() => go('catalog')} />
// Resolves scope from `kind`, computes impacts from window.crudImpacts(),
// stores the delete in window.crud, fires an undo toast.
function DetailDelete({ kind, record, title, subtitle, onDeleted, scope, allowHard = true, label, variant = 'ghost' }) {
  const [open, setOpen] = React.useState(false);
  const undoToast = useCrudUndo();
  const scopeKey = scope || `crud:${kind}`;
  const id = (record && (record.id || record.aid || record.code || record['Video ID'])) || null;
  const BtnC = window.Btn;
  const Ic   = window.Ic || {};
  const Trash = (Ic.Trash) || _CrudIc.Trash;
  if (!id) return null;
  const impacts = (window.crudImpacts && window.crudImpacts(kind, record)) || [];
  return (
    <>
      {BtnC ? (
        <BtnC variant={variant} icon={<Trash />} onClick={() => setOpen(true)}>
          {label || `Delete ${kind}`}
        </BtnC>
      ) : (
        <button onClick={() => setOpen(true)} className="ff-mono upper" style={{
          padding: '6px 12px', background: 'transparent', color: '#c0392b',
          border: '1px solid var(--rule)', fontSize: 10, letterSpacing: '.12em',
          fontWeight: 600, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6,
        }}>
          <Trash style={{ width: 14, height: 14 }} />
          {label || `Delete ${kind}`}
        </button>
      )}
      <DeleteRecordDialog
        open={open}
        onClose={() => setOpen(false)}
        kind={kind}
        title={title || (record && (record.title || record.name || record['Video Title'])) || ''}
        subtitle={subtitle || ''}
        impacts={impacts}
        allowHard={allowHard}
        onConfirm={(mode) => {
          window.crud.delete(scopeKey, id, mode);
          const verb = mode === 'hard' ? 'Deleted' : 'Archived';
          undoToast(
            `${verb} ${kind} "${title || id}"`,
            () => { window.crud.restore(scopeKey, id); }
          );
          if (onDeleted) setTimeout(onDeleted, 50);
        }}
      />
    </>
  );
}
window.DetailDelete = DetailDelete;
