/* FilterBar (navigation + filter chips) + SettingsDrawer + QuickAdd
   Depends on chrome-top.jsx for FilterChip.
*/

const { useState: useStateFB, useRef: useRefFB, useEffect: useEffectFB } = React;
const UU = window.mcalUtils;
const { FilterChip } = window.mcalChrome;

function FilterBar({ mode, cursor, setCursor, today, filters, setFilters, options, colorBy, setColorBy, laneGroup, setLaneGroup, timelineGroup, setTimelineGroup, openSettingsTab }) {
  // Navigation label & step depend on mode
  let label = '', step = 1;
  if (mode === 'month') {
    label = UU.monthLabel(cursor);
    step = 'month';
  } else if (mode === 'fortnight') {
    const ws = UU.weekStartSunday(cursor);
    label = `${UU.shortDate(ws)} — ${UU.shortDate(UU.addDays(ws, 13))}`;
    step = 14;
  } else if (mode === 'week' || mode === 'lanes') {
    const ws = UU.weekStartSunday(cursor);
    label = `${UU.shortDate(ws)} — ${UU.shortDate(UU.addDays(ws, 6))}`;
    step = 7;
  } else if (mode === 'timeline') {
    const ws = UU.weekStartSunday(cursor);
    label = `${UU.shortDate(ws)} — ${UU.shortDate(UU.addDays(ws, 6))}`;
    step = 7;
  }

  const nav = dir => {
    if (step === 'month') {
      const d = UU.parseISO(cursor);
      const next = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + dir, 1));
      setCursor(UU.formatISO(next));
    } else {
      setCursor(UU.addDays(cursor, step * dir));
    }
  };

  // Kanban is date-agnostic and already buckets by Stage — hide the
  // date navigation, search, Stage filter, and Holidays toggle in that
  // mode. Channel / Owner / Category / Product chips stay because
  // they're useful for slicing the board.
  const isKanban = mode === 'kanban';
  return (
    <div className="filterbar">
      {!isKanban && (
        <>
          <div className="fb-nav">
            <button className="navbtn" onClick={() => nav(-1)} title="Previous">‹</button>
            <span className="label">{label}</span>
            <button className="navbtn" onClick={() => nav(1)} title="Next">›</button>
            <button className="today-btn" onClick={() => setCursor(today)}>Today</button>
          </div>

          <div className="fb-divider"></div>

          <input
            className="search"
            placeholder="Search titles & notes…"
            value={filters.search}
            onChange={e => setFilters({ ...filters, search: e.target.value })}
          />
        </>
      )}

      <FilterChip label="Channel" type="channels" options={options}
        selected={filters.channels} setSelected={s => setFilters({ ...filters, channels: s })}
        onEditOptions={openSettingsTab} />
      <FilterChip label="Owner" type="owners" options={options}
        selected={filters.owners} setSelected={s => setFilters({ ...filters, owners: s })}
        onEditOptions={openSettingsTab} />
      <FilterChip label="Category" type="categories" options={options}
        selected={filters.categories} setSelected={s => setFilters({ ...filters, categories: s })}
        onEditOptions={openSettingsTab} />
      <FilterChip label="Product" type="products" options={options}
        selected={filters.products} setSelected={s => setFilters({ ...filters, products: s })}
        onEditOptions={openSettingsTab} />
      {!isKanban && (
        <FilterChip label="Stage" type="stages" options={options}
          selected={filters.stages} setSelected={s => setFilters({ ...filters, stages: s })}
          onEditOptions={openSettingsTab} />
      )}

      {!isKanban && (
        <HolidayChip
          holidayShow={filters.holidayShow}
          holidayTypes={filters.holidayTypes}
          conferencesShow={filters.conferencesShow}
          onToggleShow={v => setFilters({ ...filters, holidayShow: v })}
          onToggleConferences={v => setFilters({ ...filters, conferencesShow: v })}
          onToggleType={(typeId, on) => {
            const next = new Set(filters.holidayTypes);
            if (on) next.add(typeId); else next.delete(typeId);
            setFilters({ ...filters, holidayTypes: next });
          }}
          onSetAll={on => {
            const all = (window.MCAL_DEFAULTS.HOLIDAY_TYPES || []).map(t => t.id);
            setFilters({ ...filters, holidayTypes: new Set(on ? all : []) });
          }}
        />
      )}

      <div className="color-by">
        {/* Reserve a fixed-width slot for the Rows / Group selector so
            the Color picker doesn't shift left/right when switching
            between Month / 2 Weeks / Week / Channels / Timeline. */}
        <div className="color-by-mode-slot">
          {mode === 'lanes' && (
            <>
              <span>Rows</span>
              <window.mcalSelect.CustomSelect
                className="mini"
                value={laneGroup}
                options={[
                  { id: 'channels', name: 'Channel' },
                  { id: 'owners', name: 'Owner' },
                  { id: 'categories', name: 'Category' },
                  { id: 'products', name: 'Product' },
                ]}
                onChange={setLaneGroup}
              />
            </>
          )}
          {mode === 'timeline' && (
            <>
              <span>Group</span>
              <window.mcalSelect.CustomSelect
                className="mini"
                value={timelineGroup}
                options={[
                  { id: 'categories', name: 'Category' },
                  { id: 'channels', name: 'Channel' },
                  { id: 'products', name: 'Product' },
                  { id: 'owners', name: 'Owner' },
                ]}
                onChange={setTimelineGroup}
              />
            </>
          )}
        </div>
        <span>Color</span>
        <window.mcalSelect.CustomSelect
          className="mini"
          value={colorBy}
          options={[
            { id: 'categories', name: 'Category' },
            { id: 'channels', name: 'Channel' },
            { id: 'owners', name: 'Owner' },
            { id: 'products', name: 'Product' },
            { id: 'stages', name: 'Stage' },
          ]}
          onChange={setColorBy}
        />
      </div>
    </div>
  );
}

// ─── Holiday filter chip ───
function HolidayChip({ holidayShow, holidayTypes, conferencesShow, onToggleShow, onToggleConferences, onToggleType, onSetAll }) {
  const [open, setOpen] = useStateFB(false);
  const ref = useRefFB(null);
  useEffectFB(() => {
    if (!open) return;
    const onDoc = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);

  const types = (window.MCAL_DEFAULTS && window.MCAL_DEFAULTS.HOLIDAY_TYPES) || [];
  const enabledCount = types.filter(t => holidayTypes.has(t.id)).length;
  const summary = !holidayShow
    ? 'off'
    : enabledCount === types.length ? 'all'
    : enabledCount === 0 ? 'none'
    : `${enabledCount}/${types.length}`;

  return (
    <div className="chip-wrap" ref={ref} style={{ position: 'relative' }}>
      <button
        className={'chip' + (holidayShow ? ' active' : '')}
        onClick={() => setOpen(o => !o)}
        title="Holiday visibility"
      >
        <span className="key">Holidays</span>
        <span className="val">{summary}</span>
        <span className="caret">▾</span>
      </button>
      {open && (
        <div className="popover holiday-popover" style={{ minWidth: 240 }}>
          <label className="po-item holiday-master">
            <input
              type="checkbox"
              checked={holidayShow}
              onChange={e => onToggleShow(e.target.checked)}
            />
            <span style={{ fontWeight: 700 }}>Show Holidays</span>
          </label>
          <label className="po-item holiday-master">
            <input
              type="checkbox"
              checked={!!conferencesShow}
              onChange={e => onToggleConferences(e.target.checked)}
            />
            <span style={{ fontWeight: 700 }}>Show Crypto Conferences</span>
          </label>
          <div className="po-divider"></div>
          <div className="po-section-label">By tradition</div>
          {types.map(t => {
            const checked = holidayTypes.has(t.id);
            return (
              <label key={t.id} className={'po-item holiday-type' + (!holidayShow ? ' disabled' : '')}>
                <input
                  type="checkbox"
                  checked={checked}
                  disabled={!holidayShow}
                  onChange={e => onToggleType(t.id, e.target.checked)}
                />
                <span className="swatch" style={{ background: t.color, opacity: 0.75 }}></span>
                <span style={{ flex: 1 }}>{t.name}</span>
              </label>
            );
          })}
          <div className="po-divider"></div>
          <div className="po-actions">
            <button className="po-mini" onClick={() => onSetAll(true)} disabled={!holidayShow}>All on</button>
            <button className="po-mini" onClick={() => onSetAll(false)} disabled={!holidayShow}>All off</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── SettingsDrawer ───
function SettingsDrawer({ options, setOptions, onClose, initialTab, leaving, rosterRpcStatus, updateOwner, gtmTeam, setGtmTeam }) {
  // Owners are derived from Google sign-ins (no manual creation); Stages
  // are part of the workflow contract and read-only. Both still get tabs
  // so people can see who's signed in / what the stages look like.
  const tabs = [
    { id: 'channels',    label: 'Channels' },
    { id: 'owners',      label: 'Owners' },
    { id: 'categories',  label: 'Categories' },
    { id: 'products',    label: 'Products' },
    { id: 'mafia_apps',  label: 'MegaMafia Apps' },
    { id: 'formats',     label: 'Formats' },
    { id: 'stages',      label: 'Stages' },
  ];
  const initial = initialTab && tabs.some(t => t.id === initialTab) ? initialTab : 'categories';
  const [tab, setTab] = useStateFB(initial);
  return (
    <div className={'settings' + (leaving ? ' is-leaving' : '')}>
      <div className="s-head">
        <h2>Settings · Dropdowns</h2>
        <button className="iconbtn" onClick={onClose}>×</button>
      </div>
      <div className="s-tabs">
        {tabs.map(t => (
          <button key={t.id} className={tab === t.id ? 'on' : ''} onClick={() => setTab(t.id)}>{t.label}</button>
        ))}
      </div>
      <div className="s-body">
        {tab === 'owners' ? (
          <OwnersInfo
            options={options}
            rosterRpcStatus={rosterRpcStatus}
            updateOwner={updateOwner}
            gtmTeam={gtmTeam}
            setGtmTeam={setGtmTeam}
          />
        ) : tab === 'stages' ? (
          <StagesInfo options={options} />
        ) : (
          <OptionList type={tab} options={options} setOptions={setOptions} />
        )}
      </div>
    </div>
  );
}

/* Read-only roster. Three tiers:
   • SIGNED IN — has an email + has actually completed a Google sign-in.
   • PENDING   — known teammate from the legacy roster who hasn't signed
                 in yet (will upgrade to SIGNED IN automatically).
   • REFERENCED — a name that appears on existing tasks but we have no
                  other info about. Useful in case of typos / stale ids. */
function OwnersInfo({ options, rosterRpcStatus, updateOwner, gtmTeam, setGtmTeam }) {
  const list = options.owners || [];
  const signedIn = list.filter(o => o._signedIn);
  const pending = list.filter(o => o._seed && !o._signedIn);
  const referenced = list.filter(o => o._orphan && !o._signedIn && !o._seed);
  const [colorPickerFor, setColorPickerFor] = useStateFB(null);
  const [dropHover, setDropHover] = useStateFB(false);

  // Dismiss the color picker on any outside click. Clicks on the
  // swatch itself or inside the color grid are excluded so the
  // existing toggle / pick-color flows still work.
  useEffectFB(() => {
    if (!colorPickerFor) return;
    const onDoc = e => {
      if (e.target.closest && (e.target.closest('.color-grid') || e.target.closest('.sw-input'))) return;
      setColorPickerFor(null);
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [colorPickerFor]);
  const teamIds = gtmTeam || [];
  const teamMembers = teamIds.map(id => list.find(o => o.id === id)).filter(Boolean);
  const inTeam = id => teamIds.includes(id);
  const addToTeam = id => {
    if (!setGtmTeam || inTeam(id)) return;
    setGtmTeam([...teamIds, id]);
  };
  const removeFromTeam = id => {
    if (!setGtmTeam) return;
    setGtmTeam(teamIds.filter(x => x !== id));
  };
  const renderRow = (opt, badge, badgeColor) => {
    const canRecolor = !!updateOwner && opt._signedIn;
    const pickerOpen = colorPickerFor === opt.id;
    const draggable = !!setGtmTeam && !inTeam(opt.id);
    return (
    <div
      key={opt.id}
      className="opt-row"
      draggable={draggable}
      onDragStart={draggable ? (e => {
        e.dataTransfer.effectAllowed = 'copy';
        e.dataTransfer.setData('text/plain', opt.id);
      }) : undefined}
      style={{
        cursor: draggable ? 'grab' : 'default',
        // Hoist the active row's stacking context so the popped color
        // grid renders above the sibling rows below it. The grid's
        // own z-index: 100 only wins against siblings within the same
        // stacking context. Avoid `opacity` on the row — it makes the
        // popped color picker render translucent against the rows
        // behind it.
        position: 'relative',
        zIndex: pickerOpen ? 50 : 'auto',
      }}
    >
      <button
        className="sw-input"
        style={{ background: opt.color, cursor: canRecolor ? 'pointer' : 'default' }}
        title={canRecolor ? 'Click to change color' : ''}
        disabled={!canRecolor}
        onClick={() => canRecolor && setColorPickerFor(colorPickerFor === opt.id ? null : opt.id)}
      ></button>
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 2, paddingLeft: 4 }}>
        <span style={{ fontSize: 13, color: 'var(--fg)' }}>{opt.name}</span>
        {opt.email && (
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-3)' }}>{opt.email}</span>
        )}
      </div>
      {badge && (
        <span style={{
          fontFamily: 'var(--font-mono)', fontSize: 9, letterSpacing: '0.08em',
          padding: '2px 6px', border: `1px solid ${badgeColor}`, color: badgeColor,
        }}>{badge}</span>
      )}
      {colorPickerFor === opt.id && (
        <div className="color-grid" style={{ marginTop: 28, marginLeft: -2 }} onClick={e => e.stopPropagation()}>
          {PALETTE.map(c => (
            <div
              key={c}
              className="ch"
              style={{ background: c }}
              onClick={() => { updateOwner(opt.id, { color: c }); setColorPickerFor(null); }}
            />
          ))}
        </div>
      )}
    </div>
    );
  };
  return (
    <>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)', marginBottom: 10 }}>
        {list.length} {list.length === 1 ? 'owner' : 'owners'} · derived from @megaeth.com sign-ins
      </div>
      <div style={{ fontSize: 12, color: 'var(--fg-2)', lineHeight: 1.55, marginBottom: 14 }}>
        Owners come from @megaeth.com Google sign-ins.
      </div>
      <RosterSetupCard status={rosterRpcStatus} />

      {/* GTM Team — drag-drop subset that drives @mention suggestions */}
      <div
        className={'gtm-team-box' + (dropHover ? ' is-drop-target' : '')}
        onDragOver={e => {
          if (!setGtmTeam) return;
          e.preventDefault();
          e.dataTransfer.dropEffect = 'copy';
          if (!dropHover) setDropHover(true);
        }}
        onDragLeave={e => {
          if (e.currentTarget.contains(e.relatedTarget)) return;
          setDropHover(false);
        }}
        onDrop={e => {
          if (!setGtmTeam) return;
          e.preventDefault();
          setDropHover(false);
          const id = e.dataTransfer.getData('text/plain');
          if (id) addToTeam(id);
        }}
      >
        <div className="gtm-team-head">
          <span className="gtm-team-title">GTM TEAM · {teamMembers.length}</span>
          <span className="gtm-team-hint">
            {teamMembers.length === 0
              ? 'Drag owners here. @mentions will only suggest these people once any are added.'
              : '@mentions only suggest these owners.'}
          </span>
        </div>
        <div className="gtm-team-chips">
          {teamMembers.map(o => (
            <span key={o.id} className="gtm-team-chip" style={{ borderColor: o.color }}>
              <span className="gtm-team-chip-dot" style={{ background: o.color }}></span>
              <span>{o.name}</span>
              <button
                type="button"
                className="gtm-team-chip-x"
                onClick={() => removeFromTeam(o.id)}
                title="Remove from GTM Team"
              >×</button>
            </span>
          ))}
          {teamMembers.length === 0 && (
            <span className="gtm-team-empty">— empty —</span>
          )}
        </div>
      </div>
      {signedIn.length > 0 && (
        <>
          <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-3)', letterSpacing: '0.08em', textTransform: 'uppercase', margin: '12px 0 6px' }}>
            Signed in · {signedIn.length}
          </div>
          {signedIn.map(o => renderRow(o))}
        </>
      )}
      {pending.length > 0 && (
        <>
          <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-3)', letterSpacing: '0.08em', textTransform: 'uppercase', margin: '12px 0 6px' }}>
            Pending sign-in · {pending.length}
          </div>
          {pending.map(o => renderRow(o, 'PENDING', '#7EAAD4'))}
        </>
      )}
      {referenced.length > 0 && (
        <>
          <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-3)', letterSpacing: '0.08em', textTransform: 'uppercase', margin: '12px 0 6px' }}>
            Referenced by tasks · {referenced.length}
          </div>
          {referenced.map(o => renderRow(o, 'REFERENCED', '#F5AF94'))}
        </>
      )}
      {list.length === 0 && (
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-4)', padding: 12, border: '1px dashed var(--border-2)' }}>
          No teammates yet.
        </div>
      )}
    </>
  );
}

/* One-tap SQL setup for the list_megaeth_users() RPC. Shown when the
   client detects the function isn't deployed yet (status 'needs-setup').
   On 'ok' we show a compact green check. */
function RosterSetupCard({ status }) {
  const [copied, setCopied] = useStateFB(false);
  const sql = window.mcalRosterSQL || '';
  const editorUrl = window.mcalSupabaseSqlEditorUrl || '';

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(sql);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (e) {
      console.warn('Clipboard copy failed', e);
    }
  };

  if (status === 'ok') {
    return (
      <div className="roster-setup ok">
        <div className="roster-setup-h">
          <span className="roster-setup-dot ok" />
          <span>Auth roster sync · live</span>
        </div>
        <div className="roster-setup-sub">
          Pulling signed-in users from Supabase auth.users every 5 minutes.
        </div>
      </div>
    );
  }

  if (status === 'no-supabase') return null;

  // 'unknown' / 'needs-setup' / 'error' all surface the setup card so
  // users can fix the RPC end-to-end. 'unknown' just hasn't fetched yet,
  // but showing the SQL is still useful as documentation.
  return (
    <div className={'roster-setup' + (status === 'needs-setup' ? ' warn' : '')}>
      <div className="roster-setup-h">
        <span className={'roster-setup-dot ' + (status === 'needs-setup' ? 'warn' : 'idle')} />
        <span>{status === 'needs-setup' ? 'Auth roster sync · not set up' : 'Auth roster sync · SQL'}</span>
      </div>
      <div className="roster-setup-sub">
        Run this once in your Supabase SQL editor to pull the real signed-in user list. Until then we fall back to the hardcoded team.
      </div>
      <div className="roster-setup-actions">
        <button type="button" className="roster-setup-btn primary" onClick={copy}>
          {copied ? '✓ Copied' : 'Copy SQL'}
        </button>
        {editorUrl && (
          <a className="roster-setup-btn" href={editorUrl} target="_blank" rel="noopener noreferrer">
            Open SQL Editor ↗
          </a>
        )}
      </div>
      <pre className="roster-setup-sql">{sql}</pre>
    </div>
  );
}

/* Stages are part of the workflow itself (Ideation → Creation → Ready →
   Done) — read-only by design. We surface them here so users can see what
   the order looks like, but rename / delete / reorder are disabled. */
function StagesInfo({ options }) {
  const list = options.stages || [];
  return (
    <>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)', marginBottom: 10 }}>
        {list.length} stages · locked
      </div>
      <div style={{ fontSize: 12, color: 'var(--fg-2)', lineHeight: 1.55, marginBottom: 14 }}>
        Stages are the workflow contract used across the dashboard. They
        can't be renamed, reordered, or removed — every task moves through
        Ideation → Creation → Ready → Done.
      </div>
      {list.map(opt => (
        <div key={opt.id} className="opt-row" style={{ opacity: 0.9, cursor: 'default' }}>
          <span style={{ width: 18, height: 18, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-3)' }}>{opt.order != null ? opt.order + 1 : '·'}</span>
          <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8, paddingLeft: 4 }}>
            <span style={{ display: 'inline-flex', color: opt.color }}>
              {window.mcalStageGlyph
                ? <window.mcalStageGlyph pct={opt.fillPct != null ? opt.fillPct : ((opt.order + 1) / 4) * 100} size={13} />
                : opt.glyph}
            </span>
            <span style={{ fontSize: 13, color: 'var(--fg)' }}>{opt.name}</span>
          </div>
        </div>
      ))}
    </>
  );
}

const PALETTE = [
  '#37B77C','#6DD0A9','#90D79F','#7EAAD4','#70BAD2',
  '#F7519B','#FF177F','#FF8AA8','#F5949D','#F5AF94',
  '#ABA2A2','#DFD9D9','#C9C2B2','#5C5C5F','#19191A',
  '#F9AB00','#FFC72C','#C8A2C8','#9B6FBC','#5B5BD6',
  '#FF4040','#1E8E3E','#1A73E8','#4A4A4D','#000000',
];

/* Category IDs that are wired into category-specific logic elsewhere
   (Product category drives the Product dropdown; MegaMafia drives the
   apps sub-dropdown). Deleting either would silently break those flows,
   so we lock them in the settings panel. */
const PROTECTED_OPTION_IDS = {
  categories: ['ecosystem', 'megamafia'],
};

/* Local-state input — keeps keystrokes off the App-level setOptions
   path which used to re-render every view on every character. The
   committed value only fires on blur or Enter; Escape reverts. */
function OptionNameInput({ value, onCommit }) {
  const [draft, setDraft] = useStateFB(value);
  // If the canonical value changes externally (rename via storage sync,
  // etc.), sync local state when we're not actively focused.
  const ref = useRefFB(null);
  useEffectFB(() => {
    if (document.activeElement !== ref.current) setDraft(value);
  }, [value]);
  const commit = () => {
    const v = draft;
    if (v !== value) onCommit(v);
  };
  return (
    <input
      ref={ref}
      className="name-input"
      value={draft}
      onChange={e => setDraft(e.target.value)}
      onBlur={commit}
      onKeyDown={e => {
        if (e.key === 'Enter') { e.currentTarget.blur(); }
        if (e.key === 'Escape') { setDraft(value); e.currentTarget.blur(); }
      }}
    />
  );
}

function OptionList({ type, options, setOptions }) {
  // Pseudo-type "mafia_apps" edits a filtered slice of options.apps where parent='megamafia'.
  const isMafiaApps = type === 'mafia_apps';
  const realKey = isMafiaApps ? 'apps' : type;
  const allList = options[realKey] || [];
  const list = isMafiaApps
    ? allList.filter(a => a.parent === 'megamafia')
    : allList;
  const protectedIds = PROTECTED_OPTION_IDS[realKey] || [];
  const [newName, setNewName] = useStateFB('');
  const [colorPickerFor, setColorPickerFor] = useStateFB(null);
  // Pending delete — holds the option id awaiting themed-confirm action.
  // Native confirm() was off-theme + blocking; this matches the rest of
  // the app's modal styling and keeps focus inside the panel.
  const [pendingDelete, setPendingDelete] = useStateFB(null);
  const pendingItem = pendingDelete ? list.find(o => o.id === pendingDelete) : null;
  // Drag-to-reorder state. dragId = id being dragged; overId = id whose
  // row is being hovered (drop position is "before this row"). We persist
  // the new order via writeList on drop.
  const [dragId, setDragId] = useStateFB(null);
  const [overId, setOverId] = useStateFB(null);
  const reorder = (fromId, beforeId) => {
    if (fromId === beforeId) return;
    const next = list.slice();
    const fromIdx = next.findIndex(o => o.id === fromId);
    if (fromIdx === -1) return;
    const [moved] = next.splice(fromIdx, 1);
    const toIdx = beforeId == null ? next.length : next.findIndex(o => o.id === beforeId);
    next.splice(toIdx === -1 ? next.length : toIdx, 0, moved);
    // For stages, also re-sequence the .order field so downstream
    // stageOrder() calls reflect the new arrangement.
    if (realKey === 'stages') {
      next.forEach((s, i) => { s.order = i; });
    }
    writeList(next);
  };

  // Helper: write a transformed list back to options (preserving non-MegaMafia apps when in mafia_apps mode)
  const writeList = (next) => {
    if (isMafiaApps) {
      const others = allList.filter(a => a.parent !== 'megamafia');
      setOptions({ ...options, apps: [...others, ...next] });
    } else {
      setOptions({ ...options, [realKey]: next });
    }
  };

  const updateName = (id, name) => {
    writeList(list.map(o => o.id === id ? { ...o, name } : o));
  };
  const updateColor = (id, color) => {
    writeList(list.map(o => o.id === id ? { ...o, color } : o));
  };
  const del = id => setPendingDelete(id);
  const confirmDel = () => {
    if (!pendingDelete) return;
    writeList(list.filter(o => o.id !== pendingDelete));
    setPendingDelete(null);
  };
  const add = () => {
    const n = newName.trim();
    if (!n) return;
    const idBase = isMafiaApps ? 'mafia' : realKey.slice(0, 3);
    const id = idBase + '_' + Math.random().toString(36).slice(2, 6);
    const color = PALETTE[Math.floor(Math.random() * PALETTE.length)];
    const next = isMafiaApps
      ? { id, name: n, color, parent: 'megamafia' }
      : { id, name: n, color };
    writeList([...list, next]);
    setNewName('');
  };

  const noun = isMafiaApps ? 'MegaMafia apps' : type;
  const addLabel = isMafiaApps ? 'Add a MegaMafia app…' : `Add new ${type.replace(/s$/, '')}…`;

  return (
    <>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)', marginBottom: 10 }}>
        {list.length} {noun} · click swatch to recolor, name to rename
      </div>
      {list.map(opt => {
        const isDragging = dragId === opt.id;
        const isOver = overId === opt.id && dragId && dragId !== opt.id;
        const rowCls = ['opt-row'];
        if (isDragging) rowCls.push('is-dragging');
        if (isOver) rowCls.push('is-drop-target');
        return (
          <div
            key={opt.id}
            className={rowCls.join(' ')}
            onDragOver={e => {
              if (!dragId) return;
              e.preventDefault();
              if (overId !== opt.id) setOverId(opt.id);
            }}
            onDrop={e => {
              e.preventDefault();
              if (dragId) reorder(dragId, opt.id);
              setDragId(null);
              setOverId(null);
            }}
          >
            {type === 'stages' ? (
              <span style={{ width: 18, height: 18, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-3)' }}>{opt.order != null ? opt.order + 1 : '·'}</span>
            ) : (
              <button className="sw-input" style={{ background: opt.color }} onClick={() => setColorPickerFor(colorPickerFor === opt.id ? null : opt.id)} title="Recolor"></button>
            )}
            <OptionNameInput value={opt.name} onCommit={v => updateName(opt.id, v)} />
            <button
              className="opt-btn opt-drag"
              title="Drag to reorder"
              draggable
              onDragStart={e => {
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', opt.id);
                setDragId(opt.id);
              }}
              onDragEnd={() => { setDragId(null); setOverId(null); }}
            >
              <span className="opt-drag-icon" aria-hidden="true" />
            </button>
            {protectedIds.includes(opt.id) ? (
              <button className="opt-btn del" title="Built-in · cannot be deleted" disabled style={{ opacity: 0.3, cursor: 'not-allowed' }}>×</button>
            ) : (
              <button className="opt-btn del" title="Delete" onClick={() => del(opt.id)}>×</button>
            )}

            {colorPickerFor === opt.id && (
              <div className="color-grid" style={{ marginTop: 28, marginLeft: -2 }} onClick={e => e.stopPropagation()}>
                {PALETTE.map(c => (
                  <div key={c} className="ch" style={{ background: c }} onClick={() => { updateColor(opt.id, c); setColorPickerFor(null); }}></div>
                ))}
              </div>
            )}
          </div>
        );
      })}
      <div className="add-opt">
        <input value={newName} placeholder={addLabel} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && add()} />
        <button onClick={add}>+ Add</button>
      </div>
      {pendingItem && (
        <div className="confirm-overlay" onClick={() => setPendingDelete(null)}>
          <div className="confirm-card" onClick={e => e.stopPropagation()}>
            <div className="confirm-tag">Delete {noun.replace(/s$/, '')}</div>
            <div className="confirm-title">{pendingItem.name}</div>
            <div className="confirm-sub">
              Events tagged with this {noun.replace(/s$/, '')} will keep the orphaned ID until you re-tag or re-create.
            </div>
            <div className="confirm-actions">
              <button className="confirm-btn" onClick={() => setPendingDelete(null)}>Cancel</button>
              <button className="confirm-btn danger" onClick={confirmDel}>Delete</button>
            </div>
          </div>
        </div>
      )}
    </>
  );
}

// ─── Quick-add popover ───
function QuickAdd({ at, defaultDate, options, onCommit, onCancel }) {
  const [title, setTitle] = useStateFB('');
  const ref = useRefFB();
  useEffectFB(() => {
    const t = setTimeout(() => ref.current && ref.current.focus(), 30);
    return () => clearTimeout(t);
  }, []);
  useEffectFB(() => {
    const onKey = e => { if (e.key === 'Escape') onCancel(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);
  const commit = () => {
    if (!title.trim()) { onCancel(); return; }
    onCommit({ title: title.trim(), start: defaultDate, end: defaultDate });
  };
  return (
    <div className="quickadd-overlay" onClick={onCancel}>
      <div className="quickadd centered" onClick={e => e.stopPropagation()}>
        <h4>New event · {UU.longDate(defaultDate)}</h4>
        <input
          ref={ref}
          placeholder="Title…"
          value={title}
          onChange={e => setTitle(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && commit()}
        />
        <div className="qa-foot">
          <span>↵ to create · Esc to cancel</span>
          <button onClick={commit}>Create</button>
        </div>
      </div>
    </div>
  );
}

window.mcalChromeFB = { FilterBar, SettingsDrawer, QuickAdd };
