// Shared utility components — Council Phase 2 P2
// Base copied verbatim from AdminOverhaul/components.jsx, extended with
// Sidebar, Topbar, MigrationBanner, ConflictBanner, RestoredSessionNotice,
// and formatShortDate / formatRelTime helpers.

const { useState, useEffect, useRef, useMemo, useCallback } = React;

// --------------------------------------------------------------------------
// Utilities
// --------------------------------------------------------------------------

function formatShortDate(dateStr) {
  if (!dateStr) return '—';
  const d = new Date(dateStr + 'T12:00:00');
  if (isNaN(d.getTime())) return dateStr;
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}

function formatRelTime(date) {
  if (!date) return '—';
  const d = (date instanceof Date) ? date : new Date(date);
  if (isNaN(d.getTime())) return String(date);
  const now = Date.now();
  const diff = now - d.getTime();
  const sec = Math.floor(diff / 1000);
  if (sec < 60) return 'just now';
  if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
  if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`;
  if (sec < 604800) return `${Math.floor(sec / 86400)}d ago`;
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}

// --------------------------------------------------------------------------
// Primitives (verbatim from AdminOverhaul/components.jsx)
// --------------------------------------------------------------------------

function Icon({ name, size, className = '', filled }) {
  const style = {};
  if (size) style.fontSize = size + 'px';
  return <span className={`material-symbols-rounded ${filled ? 'filled' : ''} ${className}`} style={style}>{name}</span>;
}

function Checkbox({ checked, indeterminate, onChange, onClick }) {
  const cls = indeterminate ? 'checkbox indeterminate' : (checked ? 'checkbox checked' : 'checkbox');
  return <button className={cls} onClick={(e) => { e.stopPropagation(); if (onClick) onClick(e); if (onChange) onChange(!checked); }} aria-checked={checked}></button>;
}

function Avatar({ name, variant = 'primary' }) {
  const initials = (name || '?').split(' ').map(s => s[0]).slice(0, 2).join('').toUpperCase();
  return <div className={`avatar ${variant}`}>{initials}</div>;
}

function Badge({ tone = 'neutral', children, dotOnly, mono }) {
  const cls = `badge ${tone}${dotOnly ? ' dot-only' : ''}${mono ? ' badge-mono' : ''}`;
  return <span className={cls}>{children}</span>;
}

function Stat({ label, value, meta, icon, tone = 'default', trend }) {
  return (
    <div className={`stat ${tone}`}>
      <div className="stat-label">{label}</div>
      <div className="stat-value">{value}</div>
      {meta && <div className="stat-meta">
        {trend && <span className={`stat-trend ${trend}`}>{trend === 'up' ? '↑' : '↓'}</span>}
        {meta}
      </div>}
      {icon && <div className="stat-icon"><Icon name={icon} size={18} /></div>}
    </div>
  );
}

function PageHeader({ title, subtitle, actions }) {
  return (
    <div className="page-header">
      <div className="page-header-text">
        <h1>{title}</h1>
        {subtitle && <p>{subtitle}</p>}
      </div>
      {actions && <div className="page-header-actions">{actions}</div>}
    </div>
  );
}

function BulkBar({ count, total, onClear, actions }) {
  if (count === 0) return null;
  return (
    <div className="bulk-bar">
      <div className="bulk-bar-count"><strong>{count}</strong> selected of {total}</div>
      <div className="bulk-bar-actions">{actions}</div>
      <button className="btn-icon bulk-bar-close" onClick={onClear} style={{ color: 'var(--md-sys-color-inverse-on-surface)' }}><Icon name="close" /></button>
    </div>
  );
}

function Toolbar({ children }) { return <div className="toolbar">{children}</div>; }

function Chip({ active, onClick, icon, children }) {
  return (
    <button className={`chip ${active ? 'active' : ''}`} onClick={onClick}>
      {icon && <Icon name={icon} />}{children}
    </button>
  );
}

function SearchInput({ value, onChange, placeholder = 'Search…', wide }) {
  return (
    <div className="search-input" style={wide ? { width: 'min(100%, 480px)' } : {}}>
      <Icon name="search" size={20} />
      <input value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} />
    </div>
  );
}

function Modal({ open, onClose, title, subtitle, children, footer, wide }) {
  if (!open) return null;
  return (<>
    <div className="scrim" onClick={onClose}></div>
    <div className="modal" style={wide ? { width: 720 } : {}}>
      <div className="modal-header">
        <h2>{title}</h2>
        {subtitle && <p>{subtitle}</p>}
      </div>
      <div className="modal-body">{children}</div>
      {footer && <div className="modal-footer">{footer}</div>}
    </div>
  </>);
}

function Drawer({ open, onClose, title, eyebrow, children, footer }) {
  if (!open) return null;
  return (<>
    <div className="scrim" onClick={onClose}></div>
    <div className="drawer">
      <div className="drawer-header">
        <div style={{ flex: 1 }}>
          {eyebrow && <div className="card-header-eyebrow">{eyebrow}</div>}
          <h3>{title}</h3>
        </div>
        <button className="btn-icon" onClick={onClose}><Icon name="close" /></button>
      </div>
      <div className="drawer-body">{children}</div>
      {footer && <div className="drawer-footer">{footer}</div>}
    </div>
  </>);
}

function DetailRow({ label, children }) {
  return <div className="detail-row"><div className="detail-label">{label}</div><div className="detail-value">{children}</div></div>;
}

function EmptyState({ icon = 'inbox', title, children, action }) {
  return (
    <div className="empty-state">
      <Icon name={icon} />
      <h4>{title}</h4>
      {children && <p>{children}</p>}
      {action && <div style={{ marginTop: 16 }}>{action}</div>}
    </div>
  );
}

// Toast system (minimal)
const ToastContext = React.createContext(null);
function ToastProvider({ children }) {
  const [toasts, setToasts] = useState([]);
  const push = useCallback((msg, opts = {}) => {
    const id = Math.random().toString(36).slice(2);
    setToasts(t => [...t, { id, msg, ...opts }]);
    setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 4000);
  }, []);
  return (
    <ToastContext.Provider value={push}>
      {children}
      <div className="toast-stack">
        {toasts.map(t => (
          <div key={t.id} className="toast">
            <Icon name={t.icon || 'check_circle'} />
            <span>{t.msg}</span>
            {t.action && <button onClick={t.action.onClick}>{t.action.label}</button>}
          </div>
        ))}
      </div>
    </ToastContext.Provider>
  );
}
function useToast() { return React.useContext(ToastContext); }

// --------------------------------------------------------------------------
// Sidebar / Topbar — accept nav as prop, no hardcoded NAV
// --------------------------------------------------------------------------

function Sidebar({ nav = [], active, setActive, collapsed, onToggle, pendingRequestCount, user }) {
  return (
    <aside className="sidebar">
      <div className="sidebar-brand">
        <div className="sidebar-brand-logo">B</div>
        <div className="sidebar-brand-name">
          <strong>BookATeeTime</strong>
          <span>Admin · BCC</span>
        </div>
      </div>
      <nav className="sidebar-nav">
        {nav.map((item, i) => {
          if (item.section) return <div key={`sec-${i}`} className="sidebar-section">{item.section}</div>;
          let badge = null;
          if (typeof item.badge === 'function') badge = item.badge();
          else if (item.badge != null) badge = item.badge;
          else if (item.id === 'requests' && pendingRequestCount > 0) badge = pendingRequestCount;
          return (
            <div
              key={item.id}
              className={`sidebar-nav-item ${active === item.id ? 'active' : ''}`}
              onClick={() => setActive && setActive(item.id)}
              title={item.label}
            >
              <Icon name={item.icon} />
              <span className="sidebar-nav-item-label">{item.label}</span>
              {badge ? <span className="sidebar-nav-badge">{badge}</span> : null}
            </div>
          );
        })}
      </nav>
      <div className="sidebar-footer">
        {(() => {
          // Derive name + initials from whatever the auth gate gave us.
          // Order: explicit user.name → user.displayName → email local-part → '?'.
          const email = (user && user.email) || '';
          const explicitName = (user && (user.name || user.displayName)) || '';
          const localPart = email.includes('@') ? email.split('@')[0] : email;
          // Pretty-print the local part: "brian.byrne" → "Brian Byrne"
          const prettyLocal = localPart
            .replace(/[._-]+/g, ' ')
            .split(' ')
            .filter(Boolean)
            .map(w => w[0].toUpperCase() + w.slice(1))
            .join(' ');
          const displayName = explicitName || prettyLocal || '(unnamed)';
          const initials = (user && user.initials) ||
            displayName.split(/\s+/).filter(Boolean).map(w => w[0]).slice(0, 2).join('').toUpperCase() ||
            '?';
          return (
            <>
              <div className="sidebar-avatar">{initials}</div>
              <div className="sidebar-user-info">
                <strong>{displayName}</strong>
                <span>{email || '—'}</span>
              </div>
            </>
          );
        })()}
        <button className="btn btn-icon btn-sm" title="Sign out" onClick={user && user.onSignOut}>
          <Icon name="logout" />
        </button>
      </div>
    </aside>
  );
}

function Topbar({ active, nav = [], onToggle, onSearch }) {
  // Resolve title from nav array (find item by id) so callers don't have to
  // ship a parallel titles object.
  const item = nav.find(n => !n.section && n.id === active);
  const title = item ? item.label : '';
  return (
    <header className="topbar">
      <button className="btn btn-icon" onClick={onToggle}><Icon name="menu" /></button>
      <div className="topbar-breadcrumb">
        <span className="muted">Admin</span>
        <Icon name="chevron_right" size={16} />
        <strong>{title}</strong>
      </div>
      <div className="topbar-spacer"></div>
      <div className="topbar-search">
        <Icon name="search" size={20} />
        <input
          placeholder="Search anything…"
          onChange={(e) => onSearch && onSearch(e.target.value)}
        />
        <kbd>⌘K</kbd>
      </div>
      <button className="btn btn-icon" title="Notifications">
        <Icon name="notifications" />
        <span className="notification-dot"></span>
      </button>
      <button className="btn btn-icon" title="Help"><Icon name="help" /></button>
    </header>
  );
}

// --------------------------------------------------------------------------
// NEW: MigrationBanner — top-of-main banner when migrationV2Complete === false
// --------------------------------------------------------------------------

function MigrationBanner({ visible, onDismiss }) {
  if (!visible) return null;
  return (
    <div
      className="banner banner-warn"
      role="alert"
      style={{
        background: 'var(--md-sys-color-tertiary-container)',
        borderLeft: '4px solid var(--md-sys-color-tertiary)',
        padding: '12px 18px',
        margin: '0 0 16px 0',
        display: 'flex',
        alignItems: 'center',
        gap: 12,
        borderRadius: 8
      }}
    >
      <Icon name="warning" />
      <div style={{ flex: 1 }}>
        <strong>Person migration is incomplete.</strong>{' '}
        Dispatch and certain features are blocked until migration runs. Contact admin.
      </div>
      {onDismiss && (
        <button onClick={onDismiss} className="btn-icon" aria-label="Dismiss">
          <Icon name="close" />
        </button>
      )}
    </div>
  );
}

// --------------------------------------------------------------------------
// NEW: ConflictBanner — 403 ASSIGNMENT_CONFLICT inside GroupBuilder
// --------------------------------------------------------------------------

function ConflictBanner({
  visible,
  message,
  conflicts = [],
  currentAdminEmail = '',
  onRefresh,
  onDismiss,
  onEditConflicting,
  onCancelConflicting,
  onPickDifferent,
  onOverrideSlot,
  // Optional lookup: given priorAssignmentId, returns the assignment doc
  // (so we can detect "your own" without an extra Firestore read).
  lookupAssignment
}) {
  if (!visible) return null;

  // E.6.3 — group conflicts by priorAssignmentId. SLOT_TARGET_COLLISION conflicts
  // get their own group key per prior assignment as well.
  const groups = [];
  const idx = new Map();
  for (const c of conflicts) {
    const key = c.priorAssignmentId || `__slot:${c.priorTime || ''}:${c.priorSniperName || ''}`;
    if (!idx.has(key)) {
      idx.set(key, groups.length);
      groups.push({
        priorAssignmentId: c.priorAssignmentId || null,
        priorTime: c.priorTime || '',
        priorSniperName: c.priorSniperName || '',
        conflictType: c.conflictType || '',
        people: []
      });
    }
    const g = groups[idx.get(key)];
    // For person-overlap types, accumulate the colliding people.
    if (c.conflictType !== 'SLOT_TARGET_COLLISION' && (c.rosterName || c.memberName || c.rosterId)) {
      g.people.push({
        rosterId: c.rosterId,
        rosterName: c.rosterName || c.memberName || c.rosterId,
        role: _conflictRole(c.conflictType)
      });
    }
  }

  return (
    <div
      className="banner banner-error"
      role="alert"
      style={{
        background: 'var(--md-sys-color-error-container)',
        borderLeft: '4px solid var(--md-sys-color-error)',
        padding: '14px 18px',
        margin: '12px 0',
        borderRadius: 8
      }}
    >
      <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
        <Icon name="error" />
        <div style={{ flex: 1 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <strong>Scheduling conflict</strong>
            <div style={{ flex: 1 }} />
            {onDismiss && (
              <button onClick={onDismiss} className="btn btn-icon btn-sm" aria-label="Dismiss banner">
                <Icon name="close" />
              </button>
            )}
          </div>
          {message && (
            <div style={{ marginTop: 4, fontSize: 13 }}>{message}</div>
          )}

          {groups.map((g, gi) => {
            const isSlot = g.conflictType === 'SLOT_TARGET_COLLISION';
            const priorAsg = (g.priorAssignmentId && typeof lookupAssignment === 'function')
              ? lookupAssignment(g.priorAssignmentId)
              : null;
            const isOwn = !!(priorAsg && priorAsg.createdBy && currentAdminEmail &&
              String(priorAsg.createdBy).toLowerCase() === String(currentAdminEmail).toLowerCase());

            const header = isSlot
              ? `Same time slot — another group already targets ${g.priorTime || 'this time'}`
              : `Conflict with ${g.priorSniperName || 'another'}’s group at ${g.priorTime || '?'}`;

            return (
              <div
                key={gi}
                style={{
                  marginTop: 10,
                  padding: 10,
                  background: 'var(--md-sys-color-surface)',
                  border: '1px solid var(--md-sys-color-outline-variant)',
                  borderRadius: 6
                }}
              >
                <div style={{ fontSize: 13, fontWeight: 600 }}>{header}</div>

                {isSlot ? (
                  <div style={{ marginTop: 4, fontSize: 13 }}>
                    Sniper: <strong>{g.priorSniperName || '—'}</strong>
                  </div>
                ) : (
                  <ul style={{ margin: '6px 0 0 0', padding: '0 0 0 18px', fontSize: 13 }}>
                    {g.people.map((p, pi) => (
                      <li key={pi}>
                        <strong>{p.rosterName}</strong>
                        {p.role ? <span className="muted"> ({p.role})</span> : null}
                      </li>
                    ))}
                  </ul>
                )}

                {isOwn && (
                  <div style={{
                    marginTop: 6, fontSize: 12,
                    color: 'var(--md-sys-color-error)'
                  }}>
                    This is <strong>your own</strong> assignment.
                  </div>
                )}

                <div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
                  {onEditConflicting && g.priorAssignmentId && (
                    <button
                      className="btn btn-text btn-sm"
                      onClick={() => onEditConflicting(g.priorAssignmentId)}
                    >Edit that group</button>
                  )}
                  {onCancelConflicting && g.priorAssignmentId && (
                    <button
                      className="btn btn-text btn-sm danger"
                      onClick={() => onCancelConflicting(g.priorAssignmentId, g.priorSniperName, g.priorTime, isOwn)}
                    >Cancel that group</button>
                  )}
                  {!isSlot && onPickDifferent && g.people.length > 0 && (
                    <button
                      className="btn btn-text btn-sm"
                      onClick={() => g.people.forEach(p => p.rosterId && onPickDifferent(p.rosterId))}
                    >Pick different from my slot</button>
                  )}
                  {isSlot && onOverrideSlot && (
                    <button
                      className="btn btn-filled btn-sm"
                      onClick={() => onOverrideSlot(g.priorSniperName)}
                      title="Dispatch anyway — only one group will actually win the booking"
                    >Override and dispatch</button>
                  )}
                </div>
              </div>
            );
          })}

          {onRefresh && (
            <div style={{ marginTop: 10 }}>
              <button onClick={onRefresh} className="btn btn-text btn-sm">
                <Icon name="refresh" /> Refresh picker
              </button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function _conflictRole(t) {
  switch (t) {
    case 'SNIPER_SNIPER':    return 'sniper';
    case 'SNIPER_AS_PLAYER': return 'sniper-as-player';
    case 'PLAYER_AS_SNIPER': return 'player-as-sniper';
    case 'PLAYER_PLAYER':    return 'player';
    default: return '';
  }
}

// --------------------------------------------------------------------------
// NEW: RestoredSessionNotice — GroupBuilder hydrated from saved session
// --------------------------------------------------------------------------

function RestoredSessionNotice({ visible, onClear, onContinue }) {
  if (!visible) return null;
  return (
    <div
      className="banner banner-warn"
      style={{
        background: 'var(--md-sys-color-tertiary-container)',
        borderLeft: '4px solid var(--md-sys-color-tertiary)',
        padding: '12px 18px',
        margin: '0 0 16px 0',
        borderRadius: 8,
        display: 'flex',
        alignItems: 'center',
        gap: 12
      }}
    >
      <Icon name="restore" />
      <div style={{ flex: 1 }}>
        <strong>Restored in-progress group</strong> from earlier session.
      </div>
      <button onClick={onClear} className="btn-secondary">Clear</button>
      <button onClick={onContinue} className="btn-primary">Continue</button>
    </div>
  );
}

// --------------------------------------------------------------------------
// AE-7 — ActorBadge: REMOVED 2026-05-09.
// (sniper-activity-only view — admin/system rows live in admin Audit tab elsewhere)
// The `actor` field is still written by the backend for analysis but is no
// longer surfaced in the admin UI. If a future feature needs source labeling,
// reintroduce here rather than scattering parser logic across pages.
// --------------------------------------------------------------------------
// AE-8 — DateHeader: full-width row separator showing Today/Yesterday/Weekday, Month Day
// in the club's timezone (clubTz prop). Renders a <tr> so it slots into a table.
// --------------------------------------------------------------------------

function _clubDateParts(date, clubTz) {
  // Returns { yyyy, mm, dd, weekday, monthDay } in clubTz (or browser local if clubTz falsy).
  const opts = clubTz ? { timeZone: clubTz } : {};
  const yyyy = Number(new Intl.DateTimeFormat('en-US', { ...opts, year: 'numeric' }).format(date));
  const mm = Number(new Intl.DateTimeFormat('en-US', { ...opts, month: '2-digit' }).format(date));
  const dd = Number(new Intl.DateTimeFormat('en-US', { ...opts, day: '2-digit' }).format(date));
  const weekday = new Intl.DateTimeFormat('en-US', { ...opts, weekday: 'long' }).format(date);
  const monthDay = new Intl.DateTimeFormat('en-US', { ...opts, month: 'long', day: 'numeric' }).format(date);
  return { yyyy, mm, dd, weekday, monthDay };
}

function _clubDateKey(date, clubTz) {
  const p = _clubDateParts(date, clubTz);
  return `${p.yyyy}-${String(p.mm).padStart(2, '0')}-${String(p.dd).padStart(2, '0')}`;
}

function _clubDateLabel(date, clubTz) {
  const today = new Date();
  const yesterday = new Date(Date.now() - 86400000);
  const k = _clubDateKey(date, clubTz);
  if (k === _clubDateKey(today, clubTz)) {
    const p = _clubDateParts(date, clubTz);
    return `Today, ${p.monthDay}`;
  }
  if (k === _clubDateKey(yesterday, clubTz)) {
    const p = _clubDateParts(date, clubTz);
    return `Yesterday, ${p.monthDay}`;
  }
  const p = _clubDateParts(date, clubTz);
  return `${p.weekday}, ${p.monthDay}`;
}

function DateHeader({ date, clubTz, colSpan = 5 }) {
  return (
    <tr className="date-header-row">
      <td
        colSpan={colSpan}
        style={{
          background: 'var(--md-sys-color-surface-container-low)',
          color: 'var(--md-sys-color-on-surface-variant)',
          fontSize: 12,
          fontWeight: 600,
          letterSpacing: 0.4,
          textTransform: 'uppercase',
          padding: '6px 12px',
          borderTop: '1px solid var(--md-sys-color-outline-variant)',
          borderBottom: '1px solid var(--md-sys-color-outline-variant)'
        }}
      >
        {_clubDateLabel(date, clubTz)}
      </td>
    </tr>
  );
}

// --------------------------------------------------------------------------
// AE-9 — AssignmentDetailDrawer: side panel showing full assignment doc + its event timeline.
// Props:
//   open: boolean
//   onClose: () => void
//   assignment: object | null    — the assignment doc (already loaded by caller)
//   events: array<event>         — events filtered to this assignmentId (already filtered)
//   rosterById: { [rosterId]: personDoc }  — name lookup
//   clubTz: string               — for time formatting
// --------------------------------------------------------------------------

function _fmtClubTime(d, clubTz) {
  if (!d) return '—';
  const date = (d instanceof Date) ? d : new Date(d);
  if (isNaN(date.getTime())) return '—';
  try {
    return new Intl.DateTimeFormat('en-US', {
      timeZone: clubTz || undefined,
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit'
    }).format(date);
  } catch (_) {
    return date.toLocaleTimeString();
  }
}

function AssignmentDetailDrawer({ open, onClose, assignment, events = [], rosterById = {}, clubTz }) {
  if (!open) return null;
  const a = assignment || {};
  const sniperName = a.sniperRosterId && rosterById[a.sniperRosterId]
    ? rosterById[a.sniperRosterId].name
    : (a.sniper || a.sniperRosterId || '—');
  const players = Array.isArray(a.players) ? a.players : [];
  const fdId = a.firingDeviceId || '';
  const fdTail = fdId ? String(fdId).slice(-8) : '';

  return (
    <>
      <div className="scrim" onClick={onClose}></div>
      <div className="drawer">
        <div className="drawer-header">
          <div style={{ flex: 1 }}>
            <div className="card-header-eyebrow">Assignment</div>
            <h3>{a.date || '—'} · {a.time || '—'}</h3>
          </div>
          <button className="btn-icon" onClick={onClose}><Icon name="close" /></button>
        </div>
        <div className="drawer-body">
          <DetailRow label="Status">
            <Badge tone={a.status === 'confirmed' ? 'success' : a.status === 'failed' ? 'danger' : 'info'}>
              {a.status || '—'}
            </Badge>
          </DetailRow>
          <DetailRow label="Sniper">{sniperName}</DetailRow>
          <DetailRow label="Players">
            {players.length === 0
              ? <span className="muted">none</span>
              : players.map((p, i) => {
                  const pid = (p && p.rosterId) || p;
                  const name = (pid && rosterById[pid] && rosterById[pid].name) || (p && p.name) || pid || '—';
                  return <span key={i} style={{ marginRight: 8 }}>{name}{i < players.length - 1 ? ',' : ''}</span>;
                })}
          </DetailRow>
          <DetailRow label="Firing device">
            {fdId
              ? <span className="mono" title={fdId}>…{fdTail}</span>
              : <span className="muted">—</span>}
          </DetailRow>
          {a.confirmationNumber && (
            <DetailRow label="Confirmation">
              <span className="mono" style={{ color: 'var(--md-sys-color-secondary)' }}>#{a.confirmationNumber}</span>
            </DetailRow>
          )}
          <h4 style={{ marginTop: 24, marginBottom: 8 }}>Event timeline ({events.length})</h4>
          {events.length === 0 ? (
            <div className="muted text-sm">No events recorded for this assignment.</div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {events.map((e) => (
                <div
                  key={e.id}
                  style={{
                    padding: '8px 10px',
                    background: 'var(--md-sys-color-surface-container-low)',
                    borderRadius: 8,
                    borderLeft: '3px solid var(--md-sys-color-outline-variant)'
                  }}
                >
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                    <Badge tone="neutral">{e.type}</Badge>
                    <span className="text-xs muted">{_fmtClubTime(e.receivedAt, clubTz)}</span>
                  </div>
                  {e.payload && (
                    <div className="mono text-xs" style={{ marginTop: 4, color: 'var(--md-sys-color-on-surface-variant)' }}>
                      {JSON.stringify({
                        slotTime: e.payload.slotTime,
                        target: e.payload.targetTime,
                        fallback: e.payload.fallbackTime,
                        reason: e.payload.failureReason || e.payload.reason
                      }).slice(0, 200)}
                    </div>
                  )}
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </>
  );
}

// --------------------------------------------------------------------------
// EventSummary — humanized rendering of a sniper event payload.
//
// Source of truth for failure-reason humanization is functions/lib/failureReasons.js
// (FAILURE_REASON_DETAILS). It cannot be imported here (different deploy artifact),
// so it is mirrored below. Keep in sync when adding/changing canonical reason codes.
// --------------------------------------------------------------------------

const FAILURE_REASON_TEXT = Object.freeze({
  CONFIG_MISSING: 'Required extension configuration was unavailable.',
  AUTH_TOKEN_MISSING: 'Required backend authentication token was missing.',
  AUTH_TOKEN_INVALID: 'Backend authentication token was invalid or expired.',
  ACCOUNT_REVOKED: 'Backend rejected the member account as revoked/inactive.',
  ASSIGNMENT_MISSING: 'No assignment or target was available when execution required one.',
  ASSIGNMENT_INVALID: 'Assignment data was present but incomplete or invalid.',
  FORETEES_TAB_OPEN_FAILED: 'The extension could not open or focus the ForeTees tab.',
  FORETEES_LOGIN_REQUIRED: 'ForeTees required interactive login before execution could continue.',
  FORETEES_LOGIN_FAILED: 'ForeTees login automation or user login did not complete.',
  FORETEES_SESSION_EXPIRED: 'ForeTees session expired during the run.',
  PAGE_TYPE_UNSAFE: 'The current ForeTees page type was not safe for sniper execution.',
  TEE_SHEET_NOT_REACHED: 'The tee-sheet page was not reached before execution was required.',
  REGISTRATION_PAGE_NOT_REACHED: 'Clicking a slot did not reach the registration page.',
  UNEXPECTED_FORETEES_URL: 'ForeTees navigated to an unexpected URL.',
  SERVER_TIME_NOT_FOUND: 'ForeTees server time was not visible on the page.',
  SERVER_TIME_PARSE_FAILED: 'ForeTees server time was visible but could not be parsed.',
  SERVER_TIME_OFFSET_STALE: 'The server-time offset was too old to trust at fire time.',
  RELEASE_WINDOW_NOT_OPEN: 'Execution was attempted before the configured release window.',
  RELEASE_TIME_MISCOMPUTED: 'The computed release timestamp was invalid or inconsistent.',
  TARGET_SLOT_NOT_VISIBLE: 'The target tee time was not visible in the available slots.',
  OPEN_SLOT_SCAN_EMPTY: 'No open tee-time slots were found during scan.',
  SLOT_BUTTON_NOT_FOUND: 'The target slot existed as text but no clickable button/link was found.',
  SLOT_MATCH_AMBIGUOUS: 'Multiple possible matching slot controls were found.',
  CLICK_TARGET_DISABLED: 'The target slot control was present but disabled.',
  CLICK_EXCEPTION: 'The extension threw while attempting to click the slot.',
  CLICK_DID_NOT_NAVIGATE: 'Slot click did not produce the expected page transition.',
  REGISTRATION_SLOT_MISMATCH: 'The registration page did not show the target tee time/date.',
  PLAYER_FIELD_MISSING: 'A required player field was missing on the registration page.',
  PLAYER_FILL_FAILED: 'The extension could not fill one or more player fields.',
  SUBMIT_BUTTON_MISSING: 'The final submit/confirm button was missing.',
  SUBMIT_BUTTON_DISABLED: 'The final submit/confirm button was present but disabled.',
  SUBMIT_CLICK_FAILED: 'Clicking the final submit/confirm button failed.',
  CONFIRMATION_NOT_FOUND: 'No booking confirmation text or number was found after submit.',
  BOOKING_REJECTED_BY_FORETEES: 'ForeTees rejected the booking and showed an error.',
  NETWORK_REQUEST_FAILED: 'A required extension/backend/ForeTees network request failed.',
  STORAGE_WRITE_FAILED: 'Required local extension storage write failed.',
  TELEMETRY_REJECTED: 'Backend rejected required telemetry for the run.',
  UNHANDLED_EXCEPTION: 'A caught but otherwise uncategorized exception stopped execution.',
});

// Legacy short codes still emitted by older extension builds. Map to canonical
// codes (or to ad-hoc plain text where no canonical exists).
const LEGACY_REASON_ALIAS = Object.freeze({
  no_slots: 'OPEN_SLOT_SCAN_EMPTY',
  network_error: 'NETWORK_REQUEST_FAILED',
  form_not_found: 'PLAYER_FIELD_MISSING',
  session_expired: 'FORETEES_SESSION_EXPIRED',
  already_fired: '__ALREADY_FIRED__',
});

const LEGACY_REASON_TEXT = Object.freeze({
  __ALREADY_FIRED__: 'Already fired for this assignment.',
});

function _titleCaseCode(code) {
  return String(code)
    .replace(/[_\-]+/g, ' ')
    .toLowerCase()
    .replace(/\b\w/g, c => c.toUpperCase());
}

// friendlyReason(code) → { text, code } where text is plain English.
// Handles canonical FAILURE_REASON codes, legacy short aliases, and unknowns.
function friendlyReason(code) {
  if (!code) return null;
  const raw = String(code);
  // Canonical first.
  if (FAILURE_REASON_TEXT[raw]) return { text: FAILURE_REASON_TEXT[raw], code: raw };
  const upper = raw.toUpperCase();
  if (FAILURE_REASON_TEXT[upper]) return { text: FAILURE_REASON_TEXT[upper], code: upper };
  // Legacy alias.
  const aliasKey = raw.toLowerCase();
  if (LEGACY_REASON_ALIAS[aliasKey]) {
    const target = LEGACY_REASON_ALIAS[aliasKey];
    if (FAILURE_REASON_TEXT[target]) return { text: FAILURE_REASON_TEXT[target], code: target };
    if (LEGACY_REASON_TEXT[target]) return { text: LEGACY_REASON_TEXT[target], code: raw };
  }
  // Unknown — title-case as best-effort.
  return { text: _titleCaseCode(raw), code: raw, unknown: true };
}

// humanizeEvent(event, assignment) → { headline, lines: [{text, color?, mono?, secondary?}], raw }
// Pure data — no JSX. EventSummary renders the result.
function humanizeEvent(event, assignment) {
  const e = event || {};
  const p = (e.payload && typeof e.payload === 'object') ? e.payload : e;
  const type = e.type || p.type || p.auditType || 'UNKNOWN';
  const a = assignment || {};

  const tgt = p.targetTime || a.time || '';
  const slot = p.slotTime || '';
  const fb = p.fallbackTime || '';

  // Color tokens
  const ERR = 'var(--md-sys-color-error)';
  const OK = 'var(--md-sys-color-secondary)';
  const WARN = 'var(--md-sys-color-tertiary)';
  const MUTED = 'var(--md-sys-color-on-surface-variant)';

  let headline = '';
  const lines = [];

  function pushReason(code, color) {
    const fr = friendlyReason(code);
    if (!fr) return;
    lines.push({ text: fr.text, color: color || ERR, mono: false });
    if (fr.unknown) {
      lines.push({ text: `(code: ${fr.code})`, color: MUTED, mono: true, secondary: true });
    }
  }

  switch (type) {
    case 'BOOKING_CONFIRMED': {
      const t = p.confirmedTime || p.slotTime || tgt || '?';
      const d = p.confirmedDate || a.date || '';
      headline = d ? `✓ Booking confirmed at ${t} on ${d}` : `✓ Booking confirmed at ${t}`;
      if (p.confirmationNumber) {
        lines.push({ text: `Confirmation #${p.confirmationNumber}`, color: OK, mono: true });
      }
      if (p.finalUrl) {
        lines.push({ text: `via ${p.finalUrl}`, color: MUTED, mono: true, secondary: true });
      }
      break;
    }
    case 'BOOKING_FAILED':
    case 'BOOKING_REJECTED_BY_FORETEES':
    case 'BOOKING_REJECTED': {
      headline = `✗ Booking failed${tgt ? ` at ${tgt}` : ''}`;
      pushReason(p.failureReason || p.reason, ERR);
      if (p.errorMessage) {
        lines.push({ text: p.errorMessage, color: ERR, mono: true, secondary: true });
      }
      break;
    }
    case 'SNIPER_TIMEOUT': {
      headline = `⏱ Timed out trying to lock ${tgt || 'target'}`;
      pushReason(p.failureReason || p.reason, ERR);
      break;
    }
    case 'SNIPER_ENGAGED':
      headline = `Sniper engaged${tgt ? ` for ${tgt}` : ''}`;
      break;
    case 'SLOT_CLICK_CONFIRMED':
      headline = `Slot click landed${tgt ? ` at ${tgt}` : ''}`;
      break;
    case 'FALLBACK_SLOT_CLICKED':
      headline = `Fallback slot ${slot || '?'} clicked${tgt ? ` (target ${tgt})` : ''}`;
      break;
    case 'SUBMIT_CLICKED':
      headline = 'Submit button pressed';
      break;
    case 'ASSIGNED_PLAYER_FILL_SUCCEEDED': {
      const who = p.playerName || (p.playerIndex != null ? `#${p.playerIndex}` : '?');
      headline = `Player ${who} filled`;
      break;
    }
    case 'ASSIGNED_PLAYER_FILL_FAILED': {
      const who = p.playerName || (p.playerIndex != null ? `#${p.playerIndex}` : '?');
      const fr = friendlyReason(p.failureReason || p.reason);
      headline = `Player ${who} fill failed${fr ? `: ${fr.text}` : ''}`;
      if (fr && fr.unknown) {
        lines.push({ text: `(code: ${fr.code})`, color: MUTED, mono: true, secondary: true });
      }
      break;
    }
    case 'ASSIGNED_PLAYER_FILL_NOT_VERIFIED': {
      const who = p.playerName || (p.playerIndex != null ? `#${p.playerIndex}` : '?');
      headline = `Player ${who} fill could not be verified`;
      break;
    }
    case 'ASSIGNED_PLAYERS_CAPTURED_ON_PAGE': {
      const n = Array.isArray(p.players) ? p.players.length : '?';
      headline = `Page shows ${n} player${n === 1 ? '' : 's'}`;
      break;
    }
    case 'SERVER_TIME_SYNCED': {
      const off = (p.offsetMs != null) ? `${p.offsetMs}ms` : '?';
      headline = `Server time synced (offset ${off})`;
      break;
    }
    case 'PREROLL_TRIGGERED':
      headline = `Pre-roll triggered${tgt ? ` for ${tgt}` : ''}`;
      break;
    case 'RELEASE_WINDOW_TRIGGERED':
      headline = `Release window opened${tgt ? ` for ${tgt}` : ''}`;
      break;
    case 'FORETEES_TAB_READY':
    case 'TEE_SHEET_READY':
    case 'PAGE_DETECTED':
      headline = `ForeTees ready${p.pageType ? `: ${p.pageType}` : ''}`;
      break;
    case 'TARGET_ARMED':
    case 'REMOTE_ASSIGNMENT_AUTO_ARMED':
    case 'REMOTE_ASSIGNMENT_WAITING_FOR_RELEASE':
      headline = `Sniper armed${tgt ? ` for ${tgt}` : ''}`;
      break;
    case 'TARGET_DISARMED':
      headline = 'Sniper disarmed';
      break;
    case 'FIRING_DEVICE_LOST':
      headline = 'Stood down — another device claimed firing';
      break;
    case 'FIRE_SKIPPED_OTHER_DEVICE_OWNS_CLAIM': {
      const fd = p.firingDeviceId ? String(p.firingDeviceId).slice(-8) : '?';
      headline = `Skipped fire — claim held by …${fd}`;
      break;
    }
    case 'FIRE_SKIPPED_ALREADY_FIRED':
      headline = 'Skipped fire — already fired this assignment';
      break;
    case 'ACCESS_REVOKED_LOCAL_CLEAR':
      headline = 'Access revoked — local state cleared';
      break;
    case 'EXTENSION_ACTIVATED':
      headline = `Extension activated${p.version ? ` (v${p.version})` : ''}`;
      break;
    case 'TIMEZONE_HARDCODED':
      headline = 'Timezone fallback fired (clubTimezone missing in /config)';
      break;
    case 'DEBUG_PAGE_STATE_CAPTURED':
      headline = 'Page state captured for debugging';
      break;
    case 'BETA_AUDIT_REJECTED_LOCALLY': {
      const fr = friendlyReason(p.reason || p.failureReason);
      headline = `Beta audit rejected locally${fr ? ` — ${fr.text}` : ''}`;
      break;
    }
    case 'SLOT_SCAN':
      headline = `Slot scan: ${p.slotsFound != null ? p.slotsFound : 'N/A'} found`;
      break;
    case 'SLOT_FOUND':
      headline = `Slot found${slot ? ` at ${slot}` : ''}`;
      break;
    default: {
      // Catch-all: humanize the canonical name.
      const nice = _titleCaseCode(type);
      const parts = [nice];
      if (tgt) parts.push(`(${tgt})`);
      headline = parts.join(' ');
      const fr = friendlyReason(p.failureReason || p.reason);
      if (fr) {
        lines.push({ text: fr.text, color: WARN, mono: false });
        if (fr.unknown) {
          lines.push({ text: `(code: ${fr.code})`, color: MUTED, mono: true, secondary: true });
        }
      }
      // Surface fallback time if present and not already shown.
      if (fb && !headline.includes(fb)) {
        lines.push({ text: `Fallback: ${fb}`, color: MUTED, secondary: true });
      }
      break;
    }
  }

  return { headline, lines, raw: p };
}

// composeEventSentence(event, assignment, sniperName, clubTz)
// → { icon, iconColor, sentence, secondary: [{text, mono?, color?}] }
// Builds a single natural-language sentence per event, folding in slot/group
// context and friendly failure reason text. Pure data — caller renders JSX.
function composeEventSentence(event, assignment, sniperName, clubTz) {
  const e = event || {};
  const p = (e.payload && typeof e.payload === 'object') ? e.payload : e;
  const type = e.type || p.type || p.auditType || 'UNKNOWN';
  const a = (assignment && !assignment._missing && !assignment._error) ? assignment : null;

  const tgt = p.targetTime || (a && a.time) || '';
  const slot = p.slotTime || '';

  const ERR = 'var(--md-sys-color-error)';
  const OK = 'var(--md-sys-color-secondary)';
  const WARN = 'var(--md-sys-color-tertiary)';
  const MUTED = 'var(--md-sys-color-on-surface-variant)';

  // Resolve player names from assignment (best effort — caller passes
  // rosterById hydration via the assignment's already-enriched players if
  // available; otherwise we fall back to whatever name lives on the entry).
  function _playerNames() {
    if (!a || !Array.isArray(a.players)) return [];
    return a.players.map(function (pl) {
      if (!pl) return null;
      if (typeof pl === 'string') return pl;
      return pl.name || pl.rosterId || null;
    }).filter(Boolean);
  }
  function _playerListText(limit) {
    const names = _playerNames();
    if (names.length === 0) return '';
    const cap = limit || 3;
    if (names.length <= cap) return names.join(', ');
    return names.slice(0, cap).join(', ') + ', …';
  }

  let icon = '';
  let iconColor = '';
  let sentence = '';
  const secondary = [];

  function withReason(prefix, sep) {
    const fr = friendlyReason(p.failureReason || p.reason);
    if (!fr) return prefix;
    return prefix + (sep || ': ') + fr.text;
  }
  function appendGroupTerminal(base) {
    const list = _playerListText(3);
    if (list) return base + ' with ' + list;
    return base;
  }
  function appendGroupArmed(base) {
    const list = _playerListText(3);
    if (list) return base + ' (group: ' + list + ')';
    return base;
  }

  switch (type) {
    case 'BOOKING_CONFIRMED': {
      icon = '✓';
      iconColor = OK;
      const t = p.confirmedTime || p.slotTime || tgt || '?';
      const d = p.confirmedDate || (a && a.date) || '';
      let base = d ? `Booked ${t} on ${d}` : `Booked ${t}`;
      sentence = appendGroupTerminal(base);
      if (p.confirmationNumber) {
        secondary.push({ text: `Confirmation #${p.confirmationNumber}`, mono: true, color: OK });
      }
      if (p.finalUrl) {
        secondary.push({ text: `via ${p.finalUrl}`, mono: true, color: MUTED, truncate: true });
      }
      break;
    }
    case 'BOOKING_FAILED':
    case 'BOOKING_REJECTED_BY_FORETEES':
    case 'BOOKING_REJECTED': {
      icon = '✗';
      iconColor = ERR;
      let base = `Booking failed${tgt ? ` at ${tgt}` : ''}`;
      sentence = withReason(appendGroupTerminal(base), ': ');
      if (p.errorMessage) {
        secondary.push({ text: p.errorMessage, mono: true, color: ERR, truncate: true });
      }
      break;
    }
    case 'SNIPER_TIMEOUT':
    case 'BOOKING_CONFIRMATION_NOT_DETECTED': {
      icon = '⏱';
      iconColor = WARN;
      let base = `Timed out trying to lock ${tgt || 'target'}`;
      const fr = friendlyReason(p.failureReason || p.reason);
      sentence = appendGroupTerminal(base) + (fr ? ` (${fr.text})` : '');
      break;
    }
    case 'SNIPER_ENGAGED':
      sentence = appendGroupArmed(`Sniper engaged${tgt ? ` for ${tgt}` : ''}`);
      break;
    case 'SLOT_CLICK_CONFIRMED':
      sentence = `Slot click landed${tgt ? ` at ${tgt}` : ''}`;
      break;
    case 'FALLBACK_SLOT_CLICKED':
      sentence = `Fallback slot ${slot || '?'} clicked${tgt ? ` (target ${tgt})` : ''}`;
      break;
    case 'SUBMIT_CLICKED':
      sentence = `Submit pressed${tgt ? ` for ${tgt}` : ''}`;
      break;
    case 'ASSIGNED_PLAYER_FILL_SUCCEEDED': {
      const who = p.playerName || (p.playerIndex != null ? `${p.playerIndex}` : '?');
      sentence = `Player ${who} filled`;
      break;
    }
    case 'ASSIGNED_PLAYER_FILL_FAILED': {
      const who = p.playerName || (p.playerIndex != null ? `${p.playerIndex}` : '?');
      icon = '✗';
      iconColor = ERR;
      sentence = withReason(`Player ${who} fill failed`, ': ');
      break;
    }
    case 'ASSIGNED_PLAYER_FILL_NOT_VERIFIED': {
      const who = p.playerName || (p.playerIndex != null ? `${p.playerIndex}` : '?');
      sentence = `Player ${who} fill could not be verified`;
      break;
    }
    case 'TARGET_ARMED':
    case 'REMOTE_ASSIGNMENT_AUTO_ARMED':
    case 'REMOTE_ASSIGNMENT_WAITING_FOR_RELEASE':
      sentence = appendGroupArmed(`Sniper armed${tgt ? ` for ${tgt}` : ''}`);
      break;
    case 'TARGET_DISARMED':
      sentence = 'Sniper disarmed';
      break;
    case 'FIRING_DEVICE_LOST': {
      const fd = p.firingDeviceId ? String(p.firingDeviceId).slice(-8) : '';
      sentence = `Stood down — another device claimed firing${fd ? ` (dev:${fd})` : ''}`;
      break;
    }
    case 'FIRE_SKIPPED_OTHER_DEVICE_OWNS_CLAIM':
      sentence = 'Skipped fire — claim held by other device';
      break;
    case 'FIRE_SKIPPED_ALREADY_FIRED':
      sentence = 'Skipped fire — already fired this assignment';
      break;
    case 'EXTENSION_ACTIVATED':
      sentence = `Extension activated${p.version ? ` (v${p.version})` : ''}`;
      break;
    case 'ACCESS_REVOKED_LOCAL_CLEAR':
      sentence = 'Access revoked — local state cleared';
      break;
    case 'PREROLL_TRIGGERED':
      sentence = `Pre-roll triggered${tgt ? ` for ${tgt}` : ''}`;
      break;
    case 'RELEASE_WINDOW_TRIGGERED':
      sentence = `Release window opened${tgt ? ` for ${tgt}` : ''}`;
      break;
    case 'SERVER_TIME_SYNCED': {
      if (p.offsetMs != null) {
        sentence = `Server time synced (offset ${p.offsetMs}ms)`;
      } else {
        sentence = 'Server time synced';
      }
      break;
    }
    case 'FORETEES_TAB_READY':
    case 'TEE_SHEET_READY':
    case 'PAGE_DETECTED':
      sentence = `ForeTees ready${p.pageType ? `: ${p.pageType}` : ''}`;
      break;
    case 'WARMUP_HEARTBEAT':
      sentence = 'Warmup heartbeat';
      break;
    case 'BETA_AUDIT_REJECTED_LOCALLY':
      sentence = withReason('Audit event rejected locally', ' — ');
      break;
    case 'TIMEZONE_HARDCODED':
      sentence = 'Timezone fallback fired';
      break;
    case 'DEBUG_PAGE_STATE_CAPTURED':
      sentence = 'Debug page state captured';
      break;
    default: {
      const nice = _titleCaseCode(type);
      sentence = nice + (tgt ? ` for ${tgt}` : '');
      const fr = friendlyReason(p.failureReason || p.reason);
      if (fr) sentence += ` (${fr.text})`;
      break;
    }
  }

  return { icon, iconColor, sentence, secondary, raw: p };
}

function EventSummary({ event, assignment }) {
  const { headline, lines, raw } = humanizeEvent(event, assignment);
  return (
    <div className="text-sm">
      {headline && (
        <div style={{ fontWeight: 500, marginBottom: lines.length ? 4 : 0 }}>
          {headline}
        </div>
      )}
      {lines.map((ln, i) => (
        <div
          key={i}
          className={ln.mono ? 'mono text-xs' : (ln.secondary ? 'text-xs' : '')}
          style={{
            color: ln.color || 'inherit',
            marginBottom: 2,
            ...(ln.secondary ? { opacity: 0.85 } : {})
          }}
        >
          {ln.text}
        </div>
      ))}
      <details style={{ marginTop: 4 }}>
        <summary
          className="text-xs muted"
          style={{ cursor: 'pointer', color: 'var(--md-sys-color-on-surface-variant)' }}
        >
          raw payload
        </summary>
        <pre
          className="mono text-xs"
          style={{
            margin: '4px 0 0 0',
            padding: 8,
            background: 'var(--md-sys-color-surface-container-low)',
            color: 'var(--md-sys-color-on-surface-variant)',
            borderRadius: 6,
            whiteSpace: 'pre-wrap',
            wordBreak: 'break-all',
            maxHeight: 220,
            overflow: 'auto'
          }}
        >
          {JSON.stringify(raw, null, 2)}
        </pre>
      </details>
    </div>
  );
}

// --------------------------------------------------------------------------
// Window exports — Babel script tags don't share scope, so window is the bridge.
// --------------------------------------------------------------------------

Object.assign(window, {
  // From AdminOverhaul (existing)
  Icon, Checkbox, Avatar, Badge, Stat, PageHeader, BulkBar, Toolbar, Chip,
  SearchInput, Modal, Drawer, DetailRow, EmptyState,
  ToastProvider, useToast,
  Sidebar, Topbar,
  // NEW
  MigrationBanner, ConflictBanner, RestoredSessionNotice,
  // AE-8/9 — ActorBadge removed 2026-05-09 (sniper-only view)
  DateHeader, AssignmentDetailDrawer,
  // Humanized event rendering
  FAILURE_REASON_TEXT, LEGACY_REASON_ALIAS, friendlyReason, humanizeEvent, EventSummary,
  composeEventSentence,
  // Utilities
  formatShortDate, formatRelTime
});
