// tab-admin.jsx — admin-only panel: roster / targets / notifications.
// Most writes go through callables (addAnnotator / removeAnnotator); the
// rest hit Firestore directly thanks to the admin-only rules on
// `benchmarks` and `config`.

const { useState: adUseState, useEffect: adUseEffect, useMemo: adUseMemo } = React;

function AdminToast({ msg, onClose }) {
  adUseEffect(() => {
    const t = setTimeout(onClose, 2400);
    return () => clearTimeout(t);
  }, [onClose]);
  return <div className="toast">{msg}</div>;
}

// ── Roster: search ALL users, soft-delete, reactivate, edit, add ─
function RosterAdmin({ onToast }) {
  const [users, setUsers] = adUseState(null);
  const [err, setErr] = adUseState(null);
  const [q, setQ] = adUseState('');
  const [showInactive, setShowInactive] = adUseState(false);
  const [busy, setBusy] = adUseState(null);
  const [showAdd, setShowAdd] = adUseState(false);
  const [showImport, setShowImport] = adUseState(false);
  const [shiftsTarget, setShiftsTarget] = adUseState(null);
  const [roleTarget, setRoleTarget] = adUseState(null); // single-row OR { bulk: [...] }
  const [selected, setSelected] = adUseState(new Set());

  const load = () => {
    setErr(null);
    setUsers(null);
    window.YDApp.firestore.collection('users').get()
      .then((snap) => {
        const list = snap.docs.map((d) => {
          const data = d.data() || {};
          return {
            id: d.id,
            email: data.email || d.id,
            name: data.name || d.id,
            team: data.team || '',
            role: data.role || 'annotator',
            active: data.active !== false,
          };
        }).sort((a, b) => a.name.localeCompare(b.name));
        setUsers(list);
      })
      .catch((e) => setErr(e.message || String(e)));
  };
  adUseEffect(load, []);

  const filtered = adUseMemo(() => {
    if (!users) return [];
    let list = showInactive ? users : users.filter(u => u.active);
    const qq = q.trim().toLowerCase();
    if (qq) {
      list = list.filter(u =>
        u.name.toLowerCase().includes(qq) ||
        u.email.toLowerCase().includes(qq) ||
        u.team.toLowerCase().includes(qq)
      );
    }
    return list.slice(0, 200);
  }, [users, q, showInactive]);

  const totalShown = filtered.length;
  const totalAvail = users ? (showInactive ? users.length : users.filter(u => u.active).length) : 0;

  const toggle = (email) => {
    setSelected(prev => {
      const n = new Set(prev);
      if (n.has(email)) n.delete(email); else n.add(email);
      return n;
    });
  };

  const removeOne = async (u) => {
    if (!confirm(`Remove ${u.name} (${u.email})?\nThey'll be marked inactive but their history is preserved. You can reactivate later.`)) return;
    setBusy(u.email);
    try {
      const viewer = window.YDApp.auth.currentUser?.email || 'admin';
      await window.YDApp.call('removeAnnotator', { reviewer: viewer, email: u.email });
      setUsers(users.map(x => x.email === u.email ? { ...x, active: false } : x));
      onToast(`Removed ${u.name}`);
    } catch (e) {
      alert(e.message || String(e));
    } finally {
      setBusy(null);
    }
  };

  const reactivateOne = async (u) => {
    setBusy(u.email);
    try {
      await window.YDApp.call('updateUser', { email: u.email, active: true });
      setUsers(users.map(x => x.email === u.email ? { ...x, active: true } : x));
      onToast(`Reactivated ${u.name}`);
    } catch (e) {
      alert(e.message || String(e));
    } finally {
      setBusy(null);
    }
  };

  const bulkChangeRole = () => {
    const list = filtered.filter(u => selected.has(u.email));
    if (!list.length) return;
    setRoleTarget({ bulk: list });
  };

  const bulkRemove = async () => {
    const list = filtered.filter(u => selected.has(u.email) && u.active);
    if (!list.length) return;
    if (!confirm(`Remove ${list.length} selected ${list.length === 1 ? 'user' : 'users'}? Soft-delete only, can be reactivated later.`)) return;
    const viewer = window.YDApp.auth.currentUser?.email || 'admin';
    for (const u of list) {
      try { await window.YDApp.call('removeAnnotator', { reviewer: viewer, email: u.email }); } catch (e) { /* keep going */ }
    }
    setUsers(users.map(x => list.some(l => l.email === x.email) ? { ...x, active: false } : x));
    setSelected(new Set());
    onToast(`Removed ${list.length} ${list.length === 1 ? 'user' : 'users'}`);
  };

  return (
    <div className="admin-section">
      <div className="admin-section-hd">
        <div>
          <h2>Roster</h2>
          <div className="meta">
            {users ? (
              <>{totalAvail} {showInactive ? 'total' : 'active'} users · {totalShown} shown</>
            ) : 'Loading…'}
          </div>
        </div>
        <div className="flex g6">
          <button className="btn" onClick={() => setShowImport(true)}>Import CSV</button>
          <button className="btn btn-dark" onClick={() => setShowAdd(true)}>+ Add annotator</button>
        </div>
      </div>

      <div className="flex g8 ac" style={{ flexWrap: 'wrap' }}>
        <div className="admin-search" style={{ flex: 1, minWidth: 280 }}>
          <svg width="14" height="14" viewBox="0 0 14 14" style={{ opacity: 0.5 }}>
            <circle cx="6" cy="6" r="4" stroke="currentColor" fill="none" strokeWidth="1.5"/>
            <path d="M9 9l3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
          </svg>
          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search name, email, or team…"/>
        </div>
        <label className="admin-checkbox">
          <input type="checkbox" checked={showInactive} onChange={(e) => setShowInactive(e.target.checked)}/>
          Show inactive
        </label>
        <button className="btn btn-sm btn-ghost" onClick={load}>Refresh</button>
      </div>

      {selected.size > 0 && (
        <div className="ci-bulk-bar">
          <div className="ci-bulk-bar-lbl">
            <strong>{selected.size}</strong> selected
            <button className="btn btn-ghost btn-sm" onClick={() => setSelected(new Set())}>Clear</button>
          </div>
          <div className="ci-bulk-bar-actions">
            <button className="btn btn-sm" onClick={bulkChangeRole}>Change role</button>
            <button className="btn btn-sm" style={{ color: '#b91c1c' }} onClick={bulkRemove}>Remove all</button>
          </div>
        </div>
      )}

      {err && <div className="modal-err">{err}</div>}

      <div className="card">
        <div className="card-bd" style={{ padding: 0 }}>
          <table className="admin-table">
            <thead>
              <tr>
                <th style={{ width: 32 }}/>
                <th>Name</th>
                <th>Team</th>
                <th>Role</th>
                <th>Status</th>
                <th/>
              </tr>
            </thead>
            <tbody>
              {!users && (
                <tr><td colSpan={6} style={{ padding: 24, textAlign: 'center', color: '#9ca3af' }}>Loading roster…</td></tr>
              )}
              {users && filtered.length === 0 && (
                <tr><td colSpan={6} style={{ padding: 24, textAlign: 'center', color: '#9ca3af' }}>No matches.</td></tr>
              )}
              {filtered.map((u) => (
                <tr key={u.email} style={{ opacity: u.active ? 1 : 0.55 }}>
                  <td><input type="checkbox" checked={selected.has(u.email)} onChange={() => toggle(u.email)}/></td>
                  <td>
                    <div className="flex ac g8">
                      <Avatar name={u.name} size={24}/>
                      <div>
                        <div style={{ fontWeight: 600 }}>{u.name}</div>
                        <div style={{ fontSize: 10, color: '#9ca3af' }}>{u.email}</div>
                      </div>
                    </div>
                  </td>
                  <td>{u.team}</td>
                  <td>
                    <button className="chip chip-mini" style={{ cursor: 'pointer' }} onClick={() => setRoleTarget(u)}>
                      {u.role} ✎
                    </button>
                  </td>
                  <td>
                    {u.active
                      ? <span className="chip chip-mini chip-ok">Active</span>
                      : <span className="chip chip-mini chip-warn">Inactive</span>}
                  </td>
                  <td style={{ textAlign: 'right' }}>
                    <button className="btn btn-sm btn-ghost" onClick={() => setShiftsTarget(u)} disabled={busy === u.email}>
                      Edit shifts
                    </button>
                    {u.active ? (
                      <button
                        className="btn btn-sm btn-ghost"
                        style={{ color: '#b91c1c' }}
                        onClick={() => removeOne(u)}
                        disabled={busy === u.email}>
                        {busy === u.email ? 'Removing…' : 'Remove'}
                      </button>
                    ) : (
                      <button
                        className="btn btn-sm btn-ghost"
                        style={{ color: '#15803d' }}
                        onClick={() => reactivateOne(u)}
                        disabled={busy === u.email}>
                        {busy === u.email ? 'Reactivating…' : 'Reactivate'}
                      </button>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>

      {showAdd && (
        <AddAnnotatorModal
          onClose={() => setShowAdd(false)}
          onSaved={(name) => { setShowAdd(false); onToast(`Added ${name}`); load(); }}/>
      )}
      {showImport && (
        <ImportCsvModal
          existingEmails={new Set((users || []).map(u => u.email.toLowerCase()))}
          onClose={() => setShowImport(false)}
          onDone={(addedCount) => {
            setShowImport(false);
            onToast(`Imported ${addedCount} annotator${addedCount === 1 ? '' : 's'}`);
            load();
          }}/>
      )}
      {shiftsTarget && (
        <ShiftsModal
          target={shiftsTarget}
          onClose={() => setShiftsTarget(null)}
          onSaved={(name) => { setShiftsTarget(null); onToast(`Saved shifts for ${name}`); }}/>
      )}
      {roleTarget && (
        <RoleModal
          target={roleTarget}
          onClose={() => setRoleTarget(null)}
          onSaved={(count, role) => {
            setRoleTarget(null);
            // Update local list to reflect the change without a full reload.
            const targets = roleTarget.bulk || [roleTarget];
            const targetEmails = new Set(targets.map(t => t.email));
            setUsers(users.map(x => targetEmails.has(x.email) ? { ...x, role } : x));
            setSelected(new Set());
            onToast(`Changed role for ${count} ${count === 1 ? 'person' : 'people'}`);
          }}/>
      )}
    </div>
  );
}

// ── Shifts editor modal ─────────────────────────────────────
const DAY_KEYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
const DAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];

function ShiftsModal({ target, onClose, onSaved }) {
  const [shifts, setShifts] = adUseState(null);
  const [busy, setBusy] = adUseState(false);
  const [err, setErr] = adUseState(null);

  adUseEffect(() => {
    window.YDApp.firestore
      .collection('users').doc(target.email)
      .collection('shifts').doc('current').get()
      .then((d) => {
        const data = d.exists ? d.data() : {};
        const init = {};
        DAY_KEYS.forEach((k) => { init[k] = data?.[k] || ''; });
        setShifts(init);
      })
      .catch((e) => setErr(e.message || String(e)));
  }, [target.email]);

  const save = async () => {
    setBusy(true);
    setErr(null);
    try {
      await window.YDApp.call('updateShifts', { email: target.email, shifts });
      onSaved(target.name);
    } catch (e) {
      setErr(e.message || String(e));
      setBusy(false);
    }
  };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card modal-card-wide" onClick={(e) => e.stopPropagation()}>
        <div className="modal-hd">
          <h3>Edit shifts · {target.name}</h3>
          <button className="modal-x" onClick={onClose}>×</button>
        </div>
        <div className="modal-bd">
          <div className="meta" style={{ marginBottom: 12 }}>
            Comma-separated list of committed hours per day. Format: <code>8:00pm, 9:00pm, 10:00pm</code>.
            Use <code>N/A</code> or leave blank for days off.
          </div>
          {!shifts ? <div className="meta">Loading current schedule…</div> : (
            <div style={{ display: 'grid', gap: 10 }}>
              {DAY_KEYS.map((k, i) => (
                <div key={k} className="flex ac g8">
                  <div style={{ width: 50, fontSize: 11, fontWeight: 700, textTransform: 'uppercase', color: '#6b7280', letterSpacing: '0.06em' }}>
                    {DAY_LABELS[i]}
                  </div>
                  <input
                    className="modal-input"
                    style={{ flex: 1 }}
                    value={shifts[k]}
                    onChange={(e) => setShifts({ ...shifts, [k]: e.target.value })}
                    disabled={busy}
                    placeholder="N/A"/>
                </div>
              ))}
            </div>
          )}
          {err && <div className="modal-err">{err}</div>}
        </div>
        <div className="modal-actions">
          <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="btn btn-dark" onClick={save} disabled={busy || !shifts}>
            {busy ? 'Saving…' : 'Save'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Role-change modal (single OR bulk) ──────────────────────
function RoleModal({ target, onClose, onSaved }) {
  const isBulk = !!target.bulk;
  const list = isBulk ? target.bulk : [target];
  const [role, setRole] = adUseState(isBulk ? 'annotator' : target.role);
  const [busy, setBusy] = adUseState(false);
  const [err, setErr] = adUseState(null);

  const save = async () => {
    setBusy(true);
    setErr(null);
    try {
      for (const u of list) {
        await window.YDApp.call('updateUser', { email: u.email, role });
      }
      onSaved(list.length, role);
    } catch (e) {
      setErr(e.message || String(e));
      setBusy(false);
    }
  };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-hd">
          <h3>Change role {isBulk ? `· ${list.length} people` : `· ${target.name}`}</h3>
          <button className="modal-x" onClick={onClose}>×</button>
        </div>
        <div className="modal-bd">
          <label className="modal-lbl">Role</label>
          <select className="modal-input" value={role} onChange={(e) => setRole(e.target.value)} disabled={busy}>
            <option value="annotator">Annotator</option>
            <option value="teamLead">Team lead</option>
            <option value="admin">Admin</option>
          </select>
          <div className="meta" style={{ marginTop: 10 }}>
            Custom claims are re-minted on save. The change takes effect on the user's next sign-in or token refresh.
          </div>
          {err && <div className="modal-err">{err}</div>}
        </div>
        <div className="modal-actions">
          <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="btn btn-dark" onClick={save} disabled={busy}>
            {busy ? 'Saving…' : 'Save'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Bulk CSV import ──────────────────────────────────────────
// Tolerant CSV parser — accepts the Google-Form shape that most admins
// already use ("Timestamp, Email Address, Name, Monday, Tuesday, ...,
// Team, Role") as well as the minimal "email,name" form.  Header
// detection normalizes column names so common variants ("Email
// Address", "Full Name", "Mon"/"Monday") all map cleanly.
const DAY_KEYS_FOR_IMPORT = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];

function classifyImportHeader(h) {
  const norm = (h || '').toLowerCase().replace(/[^a-z]/g, '');
  // Day columns first — "team name" contains "name" so days take priority
  // for any standalone day word.
  for (const day of DAY_KEYS_FOR_IMPORT) {
    if (norm === day || norm === day.slice(0, 3)) return day;
  }
  if (norm.includes('email')) return 'email';
  if (norm === 'team' || norm.includes('teamname') || norm.includes('project')) return 'team';
  if (norm === 'role') return 'role';
  if (norm.includes('phone')) return 'phone';
  if (norm === 'name' || norm.includes('fullname')) return 'name';
  // Anything else (timestamp, notes, etc) is ignored.
  return null;
}

// "Annotator" / "Team lead" / "team-lead" → canonical Role string.
function normalizeRoleString(s) {
  const norm = (s || '').toLowerCase().replace(/[\s_-]/g, '');
  if (norm === 'annotator') return 'annotator';
  if (norm === 'teamlead') return 'teamLead';
  if (norm === 'admin') return 'admin';
  return ''; // unknown
}

// "N/A" or empty → off day (empty string). Otherwise the raw shift string
// passes through; the backend's parseHourList already handles the comma-
// separated "5:00am, 6:00am, ..." syntax + "5:00 PM" single-time forms.
function normalizeShiftCell(s) {
  const t = (s || '').trim();
  if (!t) return '';
  if (/^n\/?a$/i.test(t)) return '';
  return t;
}

function parseCsv(text) {
  const lines = (text || '').replace(/^﻿/, '').trim().split(/\r?\n/);
  if (!lines.length) return [];
  const splitLine = (line) => {
    const out = [];
    let cur = '';
    let inQ = false;
    for (let i = 0; i < line.length; i++) {
      const c = line[i];
      if (c === '"') {
        if (inQ && line[i + 1] === '"') { cur += '"'; i++; }
        else inQ = !inQ;
      } else if (c === ',' && !inQ) {
        out.push(cur); cur = '';
      } else if (c === '\t' && !inQ) {
        // Tolerate tab-separated paste from spreadsheets.
        out.push(cur); cur = '';
      } else {
        cur += c;
      }
    }
    out.push(cur);
    return out.map((s) => s.trim());
  };
  // Detect header by trying to classify the first row. If at least one
  // column is recognized as `email` or `name`, treat row 0 as headers.
  const firstCells = splitLine(lines[0]);
  const headerKinds = firstCells.map(classifyImportHeader);
  const hasHeader = headerKinds.includes('email') || headerKinds.includes('name');
  const rows = [];
  const kindByIdx = hasHeader
    ? headerKinds
    : ['email', 'name', 'role', 'phone']; // legacy minimal form
  const dataLines = hasHeader ? lines.slice(1) : lines;
  for (const line of dataLines) {
    if (!line.trim()) continue;
    const cells = splitLine(line);
    const row = { email: '', name: '', role: '', phone: '', team: '', shifts: {} };
    cells.forEach((cell, i) => {
      const kind = kindByIdx[i];
      if (!kind) return;
      if (DAY_KEYS_FOR_IMPORT.includes(kind)) {
        row.shifts[kind] = normalizeShiftCell(cell);
      } else if (kind === 'email') {
        row.email = (cell || '').toLowerCase().trim();
      } else {
        row[kind] = (cell || '').trim();
      }
    });
    rows.push(row);
  }
  return rows;
}

function classifyRow(row, existingEmails, seenInCsv, defaultTeam) {
  if (!row.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(row.email)) {
    return { status: 'invalid', reason: 'Bad email' };
  }
  if (!row.name) {
    return { status: 'invalid', reason: 'Missing name' };
  }
  if (seenInCsv.has(row.email)) {
    return { status: 'invalid', reason: 'Duplicate in CSV' };
  }
  if (existingEmails.has(row.email)) {
    return { status: 'skip', reason: 'Already in roster' };
  }
  // Role: blank means use UI default; non-blank must be a known role.
  if (row.role && !normalizeRoleString(row.role)) {
    return { status: 'invalid', reason: 'Bad role (use annotator, teamLead, or admin)' };
  }
  // Team: must come from CSV row OR UI fallback.
  const team = (row.team || defaultTeam || '').trim();
  if (!team) return { status: 'invalid', reason: 'No team (CSV column blank and no default selected)' };
  return { status: 'ready' };
}

function ImportCsvModal({ existingEmails, onClose, onDone }) {
  const [csvText, setCsvText] = adUseState('');
  const [team, setTeam] = adUseState('');
  const [defaultRole, setDefaultRole] = adUseState('annotator');
  const [teams, setTeams] = adUseState([]);
  const [busy, setBusy] = adUseState(false);
  const [err, setErr] = adUseState(null);
  const [progress, setProgress] = adUseState(null); // { done, total }
  const [results, setResults] = adUseState(null); // { added, skipped, failed: [{row, error}] }

  adUseEffect(() => {
    window.YDApp.call('getTeamList').then(r => setTeams((r.teams || []).map(t => t.name))).catch(() => {});
  }, []);

  const onFile = (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = () => setCsvText(String(reader.result || ''));
    reader.onerror = () => setErr('Failed to read file.');
    reader.readAsText(f);
  };

  const parsed = adUseMemo(() => parseCsv(csvText), [csvText]);
  const csvHasTeamColumn = adUseMemo(() => parsed.some((r) => r.team), [parsed]);
  const csvHasShifts = adUseMemo(
    () => parsed.some((r) => Object.values(r.shifts || {}).some((v) => v && v.length > 0)),
    [parsed],
  );
  const classified = adUseMemo(() => {
    const seen = new Set();
    return parsed.map((row) => {
      const c = classifyRow(row, existingEmails, seen, team.trim());
      if (c.status !== 'invalid') seen.add(row.email);
      return { row, ...c };
    });
  }, [parsed, existingEmails, team]);

  const counts = adUseMemo(() => {
    const out = { ready: 0, skip: 0, invalid: 0 };
    classified.forEach((r) => { out[r.status] = (out[r.status] || 0) + 1; });
    return out;
  }, [classified]);

  const runImport = async () => {
    if (counts.ready === 0) { setErr('No valid rows to import.'); return; }
    setBusy(true); setErr(null); setResults(null);
    const ready = classified.filter((c) => c.status === 'ready');
    setProgress({ done: 0, total: ready.length });
    let added = 0;
    const failed = [];
    for (let i = 0; i < ready.length; i++) {
      const r = ready[i].row;
      const shiftsPayload = r.shifts && Object.values(r.shifts).some((v) => v)
        ? r.shifts
        : undefined;
      const rowTeam = (r.team || team).trim();
      const rowRole = normalizeRoleString(r.role) || defaultRole;
      try {
        await window.YDApp.call('addAnnotator', {
          email: r.email,
          name: r.name,
          team: rowTeam,
          role: rowRole,
          phone: r.phone || undefined,
          shifts: shiftsPayload,
        });
        added += 1;
      } catch (e) {
        failed.push({ row: r, error: e.message || String(e) });
      }
      setProgress({ done: i + 1, total: ready.length });
    }
    setResults({ added, skipped: counts.skip, failed });
    setBusy(false);
  };

  return (
    <div className="modal-overlay" onClick={busy ? undefined : onClose}>
      <div
        className="modal-card modal-card-xwide"
        onClick={(e) => e.stopPropagation()}
        style={{ maxHeight: 'calc(100vh - 40px)', display: 'flex', flexDirection: 'column' }}>
        <div className="modal-hd" style={{ flex: '0 0 auto' }}>
          <h3>Import annotators from CSV</h3>
          <button className="modal-x" onClick={busy ? undefined : onClose} aria-label="Close">×</button>
        </div>
        <div className="modal-bd" style={{ overflowY: 'auto', flex: '1 1 auto', minHeight: 0 }}>
          {!results && (
            <>
              <div className="meta" style={{ marginBottom: 12 }}>
                Recognised columns: <code>Email Address</code> (or <code>email</code>), <code>Name</code>, <code>Team</code>, <code>Role</code>, <code>Phone</code>, plus per-day shifts (<code>Monday</code> … <code>Sunday</code>). Other columns like <code>Timestamp</code> are ignored. Shift cells use the comma-separated hour list (<code>5:00am, 6:00am, 7:00pm</code>) or <code>N/A</code> for off days.
              </div>

              <label className="modal-lbl">Target team {csvHasTeamColumn && <span className="meta">— optional, CSV Team column overrides per-row</span>}</label>
              <input
                className="modal-input"
                list="import-teams"
                value={team}
                onChange={(e) => setTeam(e.target.value)}
                disabled={busy}
                placeholder="e.g. Shop Everything UGC B"/>
              <datalist id="import-teams">
                {teams.map(t => <option key={t} value={t}/>)}
              </datalist>

              <label className="modal-lbl">Default role (when CSV row has none)</label>
              <select className="modal-input" value={defaultRole} onChange={(e) => setDefaultRole(e.target.value)} disabled={busy}>
                <option value="annotator">Annotator</option>
                <option value="teamLead">Team lead</option>
                <option value="admin">Admin</option>
              </select>

              <label className="modal-lbl" style={{ marginTop: 14 }}>CSV file or paste</label>
              <input type="file" accept=".csv,text/csv,text/plain" onChange={onFile} disabled={busy}/>
              <textarea
                className="modal-input"
                style={{ marginTop: 8, fontFamily: 'Inconsolata, monospace', fontSize: 11, minHeight: 100 }}
                placeholder={"email,name,role\njohn.doe@gmail.com,John Doe,annotator"}
                value={csvText}
                onChange={(e) => setCsvText(e.target.value)}
                disabled={busy}/>

              {parsed.length > 0 && (
                <>
                  <div className="meta" style={{ marginTop: 14, marginBottom: 6 }}>
                    Preview · <b>{counts.ready}</b> ready · <b>{counts.skip}</b> skip (already in roster) · <b>{counts.invalid}</b> invalid
                    {csvHasShifts && <> · shifts will be imported per row</>}
                  </div>
                  <div style={{ maxHeight: 240, overflowY: 'auto', border: '1px solid var(--line)', borderRadius: 8 }}>
                    <table className="admin-table" style={{ fontSize: 11 }}>
                      <thead>
                        <tr>
                          <th>Status</th>
                          <th>Email</th>
                          <th>Name</th>
                          <th>Team</th>
                          <th>Role</th>
                          {csvHasShifts && <th>Days</th>}
                          <th>Reason</th>
                        </tr>
                      </thead>
                      <tbody>
                        {classified.map((c, i) => {
                          const tone = c.status === 'ready'
                            ? { bg: '#f0fdf4', color: '#15803d', label: 'ready' }
                            : c.status === 'skip'
                              ? { bg: '#f3f4f6', color: '#6b7280', label: 'skip' }
                              : { bg: '#fef2f2', color: '#b91c1c', label: 'invalid' };
                          const shiftDays = c.row.shifts
                            ? Object.entries(c.row.shifts).filter(([, v]) => v).map(([d]) => d.slice(0, 3)).join(' ')
                            : '';
                          return (
                            <tr key={i}>
                              <td>
                                <span className="chip chip-mini" style={{ background: tone.bg, color: tone.color, borderColor: 'transparent' }}>
                                  {tone.label}
                                </span>
                              </td>
                              <td className="mono">{c.row.email || <span className="meta">—</span>}</td>
                              <td>{c.row.name || <span className="meta">—</span>}</td>
                              <td>
                                {c.row.team
                                  ? c.row.team
                                  : team
                                    ? <span className="meta">{team} (default)</span>
                                    : <span className="meta" style={{ color: '#b91c1c' }}>—</span>}
                              </td>
                              <td>{normalizeRoleString(c.row.role) || <span className="meta">{defaultRole}</span>}</td>
                              {csvHasShifts && (
                                <td className="meta" style={{ fontFamily: 'Inconsolata, monospace', fontSize: 10 }}>
                                  {shiftDays || <span style={{ color: '#9ca3af' }}>none</span>}
                                </td>
                              )}
                              <td className="meta">{c.reason || ''}</td>
                            </tr>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                </>
              )}
              {err && <div className="modal-err" style={{ marginTop: 10 }}>{err}</div>}
            </>
          )}

          {busy && progress && (
            <div style={{ marginTop: 10 }}>
              <div className="meta" style={{ marginBottom: 6 }}>
                Importing… <b>{progress.done}</b> / {progress.total}
              </div>
              <div style={{ height: 8, background: '#f3f4f6', borderRadius: 4, overflow: 'hidden' }}>
                <div style={{
                  height: '100%',
                  width: `${(progress.done / Math.max(1, progress.total)) * 100}%`,
                  background: '#15803d',
                  transition: 'width 0.2s ease',
                }}/>
              </div>
            </div>
          )}

          {results && (
            <div>
              <div style={{ fontSize: 14, fontWeight: 700, marginBottom: 10 }}>
                ✓ Added {results.added} · skipped {results.skipped} · failed {results.failed.length}
              </div>
              {results.failed.length > 0 && (
                <div className="meta" style={{ marginBottom: 10, color: '#b91c1c' }}>
                  These rows didn't import — fix and re-run:
                </div>
              )}
              {results.failed.length > 0 && (
                <table className="admin-table" style={{ fontSize: 11 }}>
                  <thead><tr><th>Email</th><th>Name</th><th>Error</th></tr></thead>
                  <tbody>
                    {results.failed.map((f, i) => (
                      <tr key={i}>
                        <td className="mono">{f.row.email}</td>
                        <td>{f.row.name}</td>
                        <td className="meta" style={{ color: '#b91c1c' }}>{f.error}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              )}
            </div>
          )}
        </div>
        <div className="modal-actions" style={{ flex: '0 0 auto' }}>
          {!results ? (
            <>
              <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
              <button
                className="btn btn-dark"
                onClick={runImport}
                disabled={busy || counts.ready === 0}>
                {busy ? 'Importing…' : `Import ${counts.ready} annotator${counts.ready === 1 ? '' : 's'}`}
              </button>
            </>
          ) : (
            <button className="btn btn-dark" onClick={() => onDone(results.added)}>Done</button>
          )}
        </div>
      </div>
    </div>
  );
}

function AddAnnotatorModal({ onClose, onSaved }) {
  const [email, setEmail] = adUseState('');
  const [name, setName] = adUseState('');
  const [team, setTeam] = adUseState('');
  const [role, setRole] = adUseState('annotator');
  const [busy, setBusy] = adUseState(false);
  const [err, setErr] = adUseState(null);

  // Pull team list once for the dropdown so admins pick a real team.
  const [teams, setTeams] = adUseState([]);
  adUseEffect(() => {
    window.YDApp.call('getTeamList').then(r => setTeams((r.teams || []).map(t => t.name))).catch(() => {});
  }, []);

  const submit = async () => {
    setBusy(true);
    setErr(null);
    try {
      await window.YDApp.call('addAnnotator', {
        email: email.trim().toLowerCase(),
        name: name.trim(),
        team: team.trim(),
        role,
      });
      onSaved(name);
    } catch (e) {
      setErr(e.message || String(e));
      setBusy(false);
    }
  };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-hd">
          <h3>Add annotator</h3>
          <button className="modal-x" onClick={onClose}>×</button>
        </div>
        <div className="modal-bd">
          <label className="modal-lbl">Email</label>
          <input className="modal-input" type="email" value={email} onChange={e => setEmail(e.target.value)} disabled={busy} placeholder="someone@gmail.com"/>
          <label className="modal-lbl">Full name</label>
          <input className="modal-input" value={name} onChange={e => setName(e.target.value)} disabled={busy} placeholder="Firstname Lastname"/>
          <label className="modal-lbl">Team</label>
          <input className="modal-input" list="add-anno-teams" value={team} onChange={e => setTeam(e.target.value)} disabled={busy} placeholder="Shop Everything ugc"/>
          <datalist id="add-anno-teams">
            {teams.map(t => <option key={t} value={t}/>)}
          </datalist>
          <label className="modal-lbl">Role</label>
          <select className="modal-input" value={role} onChange={e => setRole(e.target.value)} disabled={busy}>
            <option value="annotator">Annotator</option>
            <option value="teamLead">Team lead</option>
            <option value="admin">Admin</option>
          </select>
          <div className="meta" style={{ marginTop: 10 }}>
            Shifts default to empty — set them later via the bulk import script or a future shift editor.
          </div>
          {err && <div className="modal-err">{err}</div>}
        </div>
        <div className="modal-actions">
          <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button
            className="btn btn-dark"
            onClick={submit}
            disabled={busy || !email || !name || !team}>
            {busy ? 'Adding…' : 'Add'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Targets: per-project pace + hours, written direct to Firestore ─
function TargetsAdmin({ onToast }) {
  const [rows, setRows] = adUseState(null);
  const [err, setErr] = adUseState(null);
  const [savingId, setSavingId] = adUseState(null);

  const load = () => {
    setErr(null);
    window.YDApp.firestore.collection('benchmarks').get()
      .then(snap => {
        const list = snap.docs.map(d => ({
          id: d.id,
          project: d.get('project') || d.id,
          targetPace: Number(d.get('targetPace') || 0),
          targetHours: Number(d.get('targetHours') || 25),
          minAccuracy: Number(d.get('minAccuracy') || 0.95),
          // Defaults to true if missing — preserves legacy behaviour for any
          // benchmark doc that pre-dates this field.
          bonusEligible: d.get('bonusEligible') !== false,
        })).sort((a, b) => a.project.localeCompare(b.project));
        setRows(list);
      })
      .catch(e => setErr(e.message || String(e)));
  };
  adUseEffect(load, []);

  const updateRow = (id, patch) => {
    setRows(rows.map(r => r.id === id ? { ...r, ...patch } : r));
  };

  const saveRow = async (r) => {
    setSavingId(r.id);
    try {
      await window.YDApp.firestore.collection('benchmarks').doc(r.id).set({
        project: r.project,
        targetPace: r.targetPace,
        targetHours: r.targetHours,
        targetResources: r.targetPace * r.targetHours,
        minAccuracy: r.minAccuracy,
        bonusEligible: r.bonusEligible !== false,
        updatedAt: new Date(),
      }, { merge: true });
      onToast(`Saved ${r.project}`);
    } catch (e) {
      alert(e.message || String(e));
    } finally {
      setSavingId(null);
    }
  };

  const addRow = () => {
    const project = prompt('Project name (must match the project_name column in KPI files exactly, casing aside):');
    if (!project || !project.trim()) return;
    const id = project.trim().toLowerCase();
    if (rows.some(r => r.id === id)) {
      alert('That project already exists below.');
      return;
    }
    setRows([{ id, project: project.trim(), targetPace: 100, targetHours: 25, minAccuracy: 0.95, bonusEligible: true }, ...rows]);
  };

  if (err) return <div className="card"><div className="card-bd" style={{ color: '#b91c1c' }}>{err}</div></div>;
  if (!rows) return <div className="meta">Loading targets…</div>;

  return (
    <div className="admin-section">
      <div className="admin-section-hd">
        <div>
          <h2>Pace targets</h2>
          <div className="meta">Per-project resources/hour. Drives expected pace and weekly target across all dashboards.</div>
        </div>
        <button className="btn" onClick={addRow}>+ Add project</button>
      </div>
      <div className="card">
        <div className="card-bd" style={{ padding: 0 }}>
          <table className="admin-table">
            <thead>
              <tr>
                <th>Project</th>
                <th className="num">Target pace (/hr)</th>
                <th className="num">Hours/wk</th>
                <th className="num">Weekly resources</th>
                <th title="Resources from this project count toward the daily 850-floor bonus tier">Bonus</th>
                <th/>
              </tr>
            </thead>
            <tbody>
              {rows.map(r => (
                <tr key={r.id}>
                  <td style={{ fontWeight: 600 }}>{r.project}</td>
                  <td className="num">
                    <input
                      className="admin-num"
                      type="number"
                      min="1"
                      value={r.targetPace}
                      onChange={e => updateRow(r.id, { targetPace: Number(e.target.value) || 0 })}/>
                  </td>
                  <td className="num">
                    <input
                      className="admin-num"
                      type="number"
                      min="1"
                      value={r.targetHours}
                      onChange={e => updateRow(r.id, { targetHours: Number(e.target.value) || 0 })}/>
                  </td>
                  <td className="num mono">{(r.targetPace * r.targetHours).toLocaleString()}</td>
                  <td>
                    <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
                      <input
                        type="checkbox"
                        checked={r.bonusEligible !== false}
                        onChange={e => updateRow(r.id, { bonusEligible: e.target.checked })}/>
                      <span className="meta" style={{ fontSize: 12 }}>
                        {r.bonusEligible !== false ? 'Eligible' : 'Excluded'}
                      </span>
                    </label>
                  </td>
                  <td style={{ textAlign: 'right' }}>
                    <button
                      className="btn btn-sm btn-dark"
                      onClick={() => saveRow(r)}
                      disabled={savingId === r.id}>
                      {savingId === r.id ? 'Saving…' : 'Save'}
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

// ── Chat webhooks: per-team Space URLs for the auto + manual reminders ──
// Stored in `config/chat`:
//   { webhooks: { '<team name>': '<url>', ... }, default: '<url>' }
// URLs contain auth tokens — masked in the UI by default. Each row has a
// Test button that posts a one-off "🧪 Webhook test" card via the
// testChatWebhook callable so non-technical admins can confirm a paste worked.
function WebhooksAdmin({ onToast }) {
  const [rows, setRows] = adUseState(null); // [{ team, url }]
  const [defaultUrl, setDefaultUrl] = adUseState('');
  const [teams, setTeams] = adUseState([]); // for the Add Project picker
  const [revealed, setRevealed] = adUseState(new Set());
  const [busy, setBusy] = adUseState(false);
  const [testingKey, setTestingKey] = adUseState(null);
  const [err, setErr] = adUseState(null);

  adUseEffect(() => {
    setErr(null);
    Promise.all([
      window.YDApp.firestore.collection('config').doc('chat').get(),
      window.YDApp.call('getTeamList').catch(() => ({ teams: [] })),
    ])
      .then(([snap, teamResp]) => {
        const data = snap.exists ? snap.data() : {};
        const wh = data?.webhooks || {};
        setRows(Object.keys(wh).sort().map((team) => ({ team, url: wh[team] || '' })));
        setDefaultUrl(data?.default || '');
        setTeams((teamResp.teams || []).map((t) => t.name));
      })
      .catch((e) => setErr(e.message || String(e)));
  }, []);

  const setRowAt = (i, patch) => setRows(rows.map((r, idx) => (idx === i ? Object.assign({}, r, patch) : r)));
  const removeRow = (i) => setRows(rows.filter((_, idx) => idx !== i));
  const toggleReveal = (key) => setRevealed((prev) => {
    const next = new Set(prev);
    if (next.has(key)) next.delete(key); else next.add(key);
    return next;
  });

  const addRow = () => {
    const team = prompt(
      'Team name (must match exactly what appears on user docs):' + (teams.length ? `\n\nKnown teams:\n${teams.join('\n')}` : ''),
    );
    if (!team || !team.trim()) return;
    const t = team.trim();
    if (rows.some((r) => r.team === t)) {
      alert('That team already has a row below.');
      return;
    }
    setRows([...rows, { team: t, url: '' }]);
  };

  const save = async () => {
    setBusy(true); setErr(null);
    // Validate URLs that aren't blank — blanks just mean "no override for this team".
    for (const r of rows) {
      if (r.url && !/^https:\/\/chat\.googleapis\.com\//.test(r.url)) {
        setErr(`URL for "${r.team}" doesn't look like a Google Chat webhook (https://chat.googleapis.com/...)`);
        setBusy(false);
        return;
      }
    }
    if (defaultUrl && !/^https:\/\/chat\.googleapis\.com\//.test(defaultUrl)) {
      setErr('Default URL doesn\'t look like a Google Chat webhook');
      setBusy(false);
      return;
    }
    try {
      const webhooks = {};
      for (const r of rows) if (r.team && r.url) webhooks[r.team] = r.url;
      await window.YDApp.firestore.collection('config').doc('chat').set({
        webhooks,
        default: defaultUrl || null,
        updatedAt: new Date(),
        updatedBy: window.YDApp.auth.currentUser?.email || 'admin',
      }, { merge: true });
      onToast(`Saved ${Object.keys(webhooks).length} team webhook${Object.keys(webhooks).length === 1 ? '' : 's'}`);
    } catch (e) {
      setErr(e.message || String(e));
    } finally {
      setBusy(false);
    }
  };

  const testRow = async (key, url, label) => {
    if (!url) { onToast('Add a URL first'); return; }
    setTestingKey(key);
    try {
      await window.YDApp.call('testChatWebhook', { url, label });
      onToast(`✓ Test posted to ${label || 'webhook'}. Check the Space.`);
    } catch (e) {
      onToast(`✗ Test failed: ${e.message || String(e)}`);
    } finally {
      setTestingKey(null);
    }
  };

  if (rows == null) return <div className="meta">Loading webhooks…</div>;

  const maskedView = (url) => {
    if (!url) return '';
    if (url.length <= 60) return url;
    return url.slice(0, 50) + '…' + url.slice(-12);
  };

  return (
    <>
      <div className="admin-section-hd" style={{ marginTop: 24 }}>
        <div>
          <h2>Chat webhooks</h2>
          <div className="meta">
            One Google Chat <i>incoming webhook</i> URL per team Space. Used by both manual Clock-in reminders and the auto reminders below. Anyone with the URL can post — keep these confidential.
          </div>
        </div>
        <button className="btn" onClick={addRow}>+ Add project</button>
      </div>

      <div className="card">
        <div className="card-bd">
          {err && <div className="modal-err" style={{ marginBottom: 12 }}>{err}</div>}

          <div className="meta" style={{ marginBottom: 8 }}>
            <b>How to get a webhook:</b> in Google Chat, open the Space → <b>Manage webhooks</b> → <b>Add webhook</b> → name it (e.g. "Yenda alerts") → copy the URL → paste below.
          </div>

          {rows.length === 0 && (
            <div className="meta" style={{ padding: '12px 0' }}>
              No team webhooks yet. Click <b>+ Add project</b> above to add one.
            </div>
          )}

          {rows.map((r, i) => {
            const key = `team-${i}`;
            const isRevealed = revealed.has(key);
            return (
              <div key={key} style={{ borderTop: i === 0 ? 'none' : '1px solid var(--line-2)', padding: '12px 0' }}>
                <div className="flex ac jb" style={{ marginBottom: 6 }}>
                  <label className="modal-lbl" style={{ margin: 0, fontSize: 12, fontWeight: 700 }}>{r.team}</label>
                  <div className="flex g6">
                    <button
                      className="btn btn-sm btn-ghost"
                      onClick={() => toggleReveal(key)}
                      disabled={busy}>
                      {isRevealed ? 'Hide' : 'Reveal'}
                    </button>
                    <button
                      className="btn btn-sm"
                      onClick={() => testRow(key, r.url, r.team)}
                      disabled={busy || testingKey === key || !r.url}>
                      {testingKey === key ? 'Sending…' : 'Test'}
                    </button>
                    <button
                      className="btn btn-sm btn-ghost"
                      style={{ color: '#b91c1c' }}
                      onClick={() => removeRow(i)}
                      disabled={busy}>
                      Remove
                    </button>
                  </div>
                </div>
                {isRevealed ? (
                  <input
                    className="modal-input"
                    style={{ fontFamily: 'Inconsolata, monospace', fontSize: 11 }}
                    value={r.url}
                    onChange={(e) => setRowAt(i, { url: e.target.value })}
                    disabled={busy}
                    placeholder="https://chat.googleapis.com/v1/spaces/.../messages?key=...&token=..."/>
                ) : (
                  <div
                    className="modal-input"
                    style={{
                      fontFamily: 'Inconsolata, monospace',
                      fontSize: 11,
                      color: r.url ? '#6b7280' : '#9ca3af',
                      background: '#fafafa',
                      cursor: 'pointer',
                      userSelect: 'none',
                    }}
                    onClick={() => toggleReveal(key)}>
                    {r.url ? maskedView(r.url) : 'Click Reveal to paste a URL'}
                  </div>
                )}
              </div>
            );
          })}

          <div style={{ borderTop: '1px solid var(--line-2)', marginTop: 16, paddingTop: 12 }}>
            <div className="flex ac jb" style={{ marginBottom: 6 }}>
              <label className="modal-lbl" style={{ margin: 0, fontSize: 12, fontWeight: 700 }}>
                Default <span className="meta" style={{ fontWeight: 400 }}>(used when a team has no specific webhook above)</span>
              </label>
              <div className="flex g6">
                <button
                  className="btn btn-sm btn-ghost"
                  onClick={() => toggleReveal('default')}
                  disabled={busy}>
                  {revealed.has('default') ? 'Hide' : 'Reveal'}
                </button>
                <button
                  className="btn btn-sm"
                  onClick={() => testRow('default', defaultUrl, 'default')}
                  disabled={busy || testingKey === 'default' || !defaultUrl}>
                  {testingKey === 'default' ? 'Sending…' : 'Test'}
                </button>
              </div>
            </div>
            {revealed.has('default') ? (
              <input
                className="modal-input"
                style={{ fontFamily: 'Inconsolata, monospace', fontSize: 11 }}
                value={defaultUrl}
                onChange={(e) => setDefaultUrl(e.target.value)}
                disabled={busy}
                placeholder="(optional) https://chat.googleapis.com/..."/>
            ) : (
              <div
                className="modal-input"
                style={{
                  fontFamily: 'Inconsolata, monospace',
                  fontSize: 11,
                  color: defaultUrl ? '#6b7280' : '#9ca3af',
                  background: '#fafafa',
                  cursor: 'pointer',
                }}
                onClick={() => toggleReveal('default')}>
                {defaultUrl ? maskedView(defaultUrl) : 'Click Reveal to paste a fallback URL (optional)'}
              </div>
            )}
          </div>

          <div className="flex jb ac" style={{ marginTop: 16 }}>
            <div className="meta">Saves to <code>config/chat</code>. Takes effect immediately for the next reminder.</div>
            <button className="btn btn-dark" onClick={save} disabled={busy}>
              {busy ? 'Saving…' : 'Save webhooks'}
            </button>
          </div>
        </div>
      </div>
    </>
  );
}

// ── Auto-reminder config: incomplete-shift + clock-in ─────────
// Edits config/notifications. The notificationsTick scheduled function
// reads this doc on every tick so toggle/time changes take effect within
// 30 minutes without a redeploy.
const TZ_DEFAULT = 'Africa/Lusaka';

function AutoReminderCard({ id, label, defaultIntro, fields, onToast, cfg, onSaved }) {
  const initial = cfg || {};
  const [enabled, setEnabled] = adUseState(!!initial.enabled);
  const [triggerTimes, setTriggerTimes] = adUseState(initial.triggerTimes || []);
  const [tz, setTz] = adUseState(initial.timezone || TZ_DEFAULT);
  const [extras, setExtras] = adUseState(() => {
    const out = {};
    fields.forEach((f) => { out[f.key] = initial[f.key] != null ? initial[f.key] : f.default; });
    return out;
  });
  const [busy, setBusy] = adUseState(false);
  const [err, setErr] = adUseState(null);

  const addTime = () => setTriggerTimes([...triggerTimes, '13:00']);
  const removeTime = (i) => setTriggerTimes(triggerTimes.filter((_, idx) => idx !== i));
  const setTimeAt = (i, v) => setTriggerTimes(triggerTimes.map((t, idx) => (idx === i ? v : t)));
  const setExtra = (k, v) => setExtras((p) => Object.assign({}, p, { [k]: v }));

  const save = async () => {
    setBusy(true); setErr(null);
    // Validate HH:MM
    for (const t of triggerTimes) {
      if (!/^\d{1,2}:\d{2}$/.test(t)) { setErr(`Invalid time "${t}" — use HH:MM`); setBusy(false); return; }
    }
    try {
      const payload = {
        enabled,
        triggerTimes: triggerTimes.map((t) => {
          const [h, m] = t.split(':').map(Number);
          return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
        }),
        timezone: tz,
      };
      fields.forEach((f) => { payload[f.key] = Number(extras[f.key]) || f.default; });
      await window.YDApp.firestore.collection('config').doc('notifications').set({
        [id]: payload,
        updatedAt: new Date(),
        updatedBy: window.YDApp.auth.currentUser?.email || 'admin',
      }, { merge: true });
      onToast(`${label} saved`);
      onSaved && onSaved(payload);
    } catch (e) {
      setErr(e.message || String(e));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="card" style={{ marginTop: 16 }}>
      <div className="card-bd">
        {err && <div className="modal-err" style={{ marginBottom: 10 }}>{err}</div>}
        <div className="flex ac jb" style={{ marginBottom: 12 }}>
          <div>
            <div style={{ fontSize: 14, fontWeight: 700 }}>{label}</div>
            <div className="meta" style={{ marginTop: 4 }}>{defaultIntro}</div>
          </div>
          <button
            className={'admin-toggle' + (enabled ? ' on' : '')}
            onClick={() => setEnabled(!enabled)}
            disabled={busy}
            aria-pressed={enabled}>
            <span className="admin-toggle-knob"/>
          </button>
        </div>

        <div style={{ display: 'grid', gap: 12, marginTop: 8 }}>
          <div>
            <label className="modal-lbl">Trigger times</label>
            {triggerTimes.length === 0 && (
              <div className="meta" style={{ marginBottom: 6 }}>No times set — add at least one for reminders to fire.</div>
            )}
            {triggerTimes.map((t, i) => (
              <div key={i} className="flex ac g8" style={{ marginBottom: 6 }}>
                <input
                  className="modal-input"
                  type="time"
                  value={t}
                  onChange={(e) => setTimeAt(i, e.target.value)}
                  disabled={busy}
                  style={{ width: 130, margin: 0 }}/>
                <button className="btn btn-sm btn-ghost" onClick={() => removeTime(i)} disabled={busy}>Remove</button>
              </div>
            ))}
            <button className="btn btn-sm" onClick={addTime} disabled={busy}>+ Add time</button>
          </div>

          {fields.map((f) => (
            <div key={f.key}>
              <label className="modal-lbl">{f.label}</label>
              <input
                className="modal-input"
                type="number"
                min={f.min}
                max={f.max}
                value={extras[f.key]}
                onChange={(e) => setExtra(f.key, e.target.value)}
                disabled={busy}
                style={{ width: 140 }}/>
              {f.hint && <div className="meta" style={{ marginTop: 4 }}>{f.hint}</div>}
            </div>
          ))}

          <div>
            <label className="modal-lbl">Timezone</label>
            <input
              className="modal-input"
              value={tz}
              onChange={(e) => setTz(e.target.value)}
              disabled={busy}
              placeholder="Africa/Lusaka"
              style={{ width: 240 }}/>
            <div className="meta" style={{ marginTop: 4 }}>IANA timezone (e.g. Africa/Lusaka, Europe/London).</div>
          </div>
        </div>

        <div className="flex jb ac" style={{ marginTop: 16 }}>
          <div className="meta">
            Tick runs every 30 min. Each trigger fires at most once per day within ±15 min of the configured time.
          </div>
          <button className="btn btn-dark" onClick={save} disabled={busy}>{busy ? 'Saving…' : 'Save'}</button>
        </div>
      </div>
    </div>
  );
}

// ── Notifications: chat-alert kill switch + auto reminders ────
function NotificationsAdmin({ onToast }) {
  const [muted, setMuted] = adUseState(null);
  const [busy, setBusy] = adUseState(false);
  const [err, setErr] = adUseState(null);
  // Config for the auto-reminder cards. Loaded once; each child card
  // writes back via merge so they don't clobber each other.
  const [notifCfg, setNotifCfg] = adUseState(null);

  adUseEffect(() => {
    Promise.all([
      window.YDApp.firestore.collection('config').doc('ingest').get(),
      window.YDApp.firestore.collection('config').doc('notifications').get(),
    ])
      .then(([ingestSnap, notifSnap]) => {
        setMuted(ingestSnap.exists && ingestSnap.get('disableAlerts') === true);
        setNotifCfg(notifSnap.exists ? (notifSnap.data() || {}) : {});
      })
      .catch(e => setErr(e.message || String(e)));
  }, []);

  const toggle = async () => {
    if (muted == null) return;
    setBusy(true);
    setErr(null);
    const next = !muted;
    try {
      await window.YDApp.firestore.collection('config').doc('ingest').set({
        disableAlerts: next,
        updatedAt: new Date(),
        updatedBy: window.YDApp.auth.currentUser?.email || 'admin',
      }, { merge: true });
      setMuted(next);
      onToast(next ? 'Chat alerts muted' : 'Chat alerts unmuted');
    } catch (e) {
      setErr(e.message || String(e));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="admin-section">
      <div className="admin-section-hd">
        <div>
          <h2>Ingest notifications</h2>
          <div className="meta">Mute the Google Chat alerts that fire on every successful KPI / QC ingest. Useful during a bulk re-ingest so the channel doesn't get spammed.</div>
        </div>
      </div>
      <div className="card">
        <div className="card-bd">
          {err && <div className="modal-err" style={{ marginBottom: 14 }}>{err}</div>}
          {muted == null ? <div className="meta">Loading…</div> : (
            <div className="flex ac jb">
              <div>
                <div style={{ fontSize: 14, fontWeight: 600 }}>
                  {muted ? 'Alerts are MUTED' : 'Alerts are LIVE'}
                </div>
                <div className="meta" style={{ marginTop: 4 }}>
                  {muted
                    ? 'No chat cards are being sent. Other operational alerts (missed shifts, request creation) still fire.'
                    : 'Every successful KPI / QC ingest posts a chat card to the configured webhook.'}
                </div>
              </div>
              <button
                className={'admin-toggle' + (muted ? ' on' : '')}
                onClick={toggle}
                disabled={busy}
                aria-pressed={muted}>
                <span className="admin-toggle-knob"/>
              </button>
            </div>
          )}
        </div>
      </div>

      <WebhooksAdmin onToast={onToast}/>

      <div className="admin-section-hd" style={{ marginTop: 24 }}>
        <div>
          <h2>Auto reminders</h2>
          <div className="meta">
            Scheduled Chat reminders. The notificationsTick scheduler ticks every 30 min;
            each enabled reminder fires once per day at any trigger time you list (±15 min match window).
            Posts to each team's Space via the webhooks under <code>config/chat</code>.
          </div>
        </div>
      </div>

      {notifCfg == null ? (
        <div className="meta">Loading reminder settings…</div>
      ) : (
        <>
          <AutoReminderCard
            id="incompleteShift"
            label="Incomplete shift reminders"
            defaultIntro="Pings annotators whose shift has ended but who logged less than the threshold of their committed hours."
            cfg={notifCfg.incompleteShift}
            fields={[{
              key: 'minCompletePct',
              label: 'Minimum completion threshold (%)',
              default: 70,
              min: 0,
              max: 100,
              hint: 'Below this % of committed hours = considered incomplete. 70% means a 4h shift triggers if Quidlo logged <2.8h.',
            }]}
            onToast={onToast}/>
          <AutoReminderCard
            id="clockInReminder"
            label="Auto clock-in reminders"
            defaultIntro="Pings annotators who haven't clocked in by their shift start + grace minutes."
            cfg={notifCfg.clockInReminder}
            fields={[{
              key: 'graceMinutes',
              label: 'Grace minutes after shift start',
              default: 60,
              min: 0,
              max: 480,
              hint: 'Skip pings until the annotator is at least this many minutes late vs their scheduled start.',
            }]}
            onToast={onToast}/>
        </>
      )}
    </div>
  );
}

// ── Tab shell ────────────────────────────────────────────────
function TabAdmin() {
  const [section, setSection] = adUseState('roster');
  const [toast, setToast] = adUseState(null);
  const showToast = (msg) => setToast(msg);

  return (
    <div className="page page-admin">
      <PageHeader
        eyebrow="Admin"
        title="Settings"
        subtitle="Manage roster, pace targets, and ingest notifications"/>

      <div className="admin-nav">
        {[
          { id: 'roster', label: 'Roster' },
          { id: 'targets', label: 'Pace targets' },
          { id: 'notifications', label: 'Notifications' },
        ].map(s => (
          <button
            key={s.id}
            className={section === s.id ? 'active' : ''}
            onClick={() => setSection(s.id)}>
            {s.label}
          </button>
        ))}
      </div>

      {section === 'roster' && <RosterAdmin onToast={showToast}/>}
      {section === 'targets' && <TargetsAdmin onToast={showToast}/>}
      {section === 'notifications' && <NotificationsAdmin onToast={showToast}/>}

      {toast && <AdminToast msg={toast} onClose={() => setToast(null)}/>}
    </div>
  );
}

Object.assign(window, { TabAdmin });
