/* Dispatch — Reporting & Admin
 * Loaded by index.html after hub.jsx (uses LANE_META) and lib.jsx.
 *
 * Faithful Tailwind port of the OLD Reporting module (old-views_aux.jsx):
 *   - Release Readiness — per-repo "ready cards" from live repo stats
 *   - Team Workload — per-tester cards from the live QA team
 *   - Developer Activity — GitHub contributions (commits/PRs/reviews) via the
 *     contributions report API, with a range picker + sparklines
 *   - Repository Scope — toggle repos in/out of the live pipeline
 * All read window.__DISPATCH_LIVE__ (repos / qaTeam / tickets / lanes). */

const laneLabel = (l) => (LANE_META[l] && LANE_META[l].label) || titleCase(l);
const rpIsOpen = (t) => t.status !== 'resolved' && t.status !== 'closed' && t.qaStage !== 'pass';

// Fixed status palette (works in light + dark), matching the old meter colors.
const RP_BAR = { pass: 'bg-emerald-500', progress: 'bg-teal-500', assigned: 'bg-blue-500', fail: 'bg-red-500', blocked: 'bg-slate-400', pending: 'bg-amber-500' };

function rpHue(s) { let h = 0; for (let k = 0; k < (s || '').length; k++) h = (h * 31 + s.charCodeAt(k)) | 0; return Math.abs(h) % 360; }
function rpInitials(name, login) {
  const src = (name || login || '?').split(/[\s_\-.]+/).map(x => x[0]).filter(Boolean).slice(0, 2).join('');
  return (src || '?').toUpperCase();
}
function RpAvatar({ name, login, avatarUrl, size = 40 }) {
  if (avatarUrl) return <img src={avatarUrl} alt="" style={{ width: size, height: size }} className="rounded-full" />;
  return (
    <div className="flex items-center justify-center rounded-full font-semibold text-white"
      style={{ width: size, height: size, fontSize: size * 0.36, backgroundColor: `hsl(${rpHue(login || name)} 55% 50%)` }}>
      {rpInitials(name, login)}
    </div>
  );
}

// Generic CSV download.
function downloadCsv(filename, header, rows) {
  const csv = [header, ...rows].map(r => r.map(c => `"${String(c ?? '').replace(/"/g, '""')}"`).join(',')).join('\r\n');
  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click(); a.remove();
  URL.revokeObjectURL(url);
}

// ── Release Readiness — per-repo ready cards ──────────────────────────────
function repoReadiness(r) {
  const s = r.stats || {};
  const pass = (s.resolved ?? 0) + (s.passed ?? 0) + (s.closed ?? 0);
  const fail = s.failed ?? 0;
  const progress = s.in_progress ?? 0;
  const pending = (s.new ?? 0) + (s.blocked ?? 0);
  const total = pass + fail + progress + pending || 1;
  return { pass, fail, progress, pending, total, readyPct: Math.round((pass / total) * 100) };
}

function ReadinessPanel({ repos }) {
  const scoped = repos.filter(r => r.scope || r.enabled);

  if (scoped.length === 0) {
    return <Card><CardContent className="pt-6 text-center text-sm text-muted-foreground">No repos in scope. Enable some in the <strong>Repository Scope</strong> tab (sign in for live data).</CardContent></Card>;
  }

  return (
    <div className="space-y-4">
      <p className="text-sm text-muted-foreground">Ship-readiness by repo — release-ready % from live ticket counts.</p>
      <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
        {scoped.map(r => {
          const d = repoReadiness(r);
          const pctColor = d.readyPct >= 80 ? 'text-emerald-600 dark:text-emerald-400' : d.readyPct >= 50 ? 'text-teal-600 dark:text-teal-400' : 'text-amber-600 dark:text-amber-400';
          return (
            <Card key={r.id} className="flex flex-col">
              <CardContent className="space-y-3 pt-5">
                <div>
                  <h3 className="font-semibold leading-tight">{r.name}</h3>
                  <div className="truncate font-mono text-xs text-muted-foreground">{r.path}</div>
                </div>
                <div className="space-y-1">
                  <div className="flex items-center justify-between text-xs">
                    <span className="text-muted-foreground">Release-ready</span>
                    <span className={cn('font-semibold', pctColor)}>{d.readyPct}%</span>
                  </div>
                  <div className="flex h-2 overflow-hidden rounded-full bg-muted">
                    <div className={RP_BAR.pass} style={{ width: `${(d.pass / d.total) * 100}%` }} />
                    <div className={RP_BAR.progress} style={{ width: `${(d.progress / d.total) * 100}%` }} />
                    <div className={RP_BAR.fail} style={{ width: `${(d.fail / d.total) * 100}%` }} />
                  </div>
                  <div className="flex justify-between text-[10px] text-muted-foreground"><span>0</span><span>50%</span><span>100%</span></div>
                </div>
                <div className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
                  <span className="inline-flex items-center gap-1"><span className={cn('h-2 w-2 rounded-full', RP_BAR.pass)} />{d.pass} resolved</span>
                  <span className="inline-flex items-center gap-1"><span className={cn('h-2 w-2 rounded-full', RP_BAR.progress)} />{d.progress} in progress</span>
                  <span className="inline-flex items-center gap-1"><span className={cn('h-2 w-2 rounded-full', RP_BAR.fail)} />{d.fail} failed</span>
                  <span className="inline-flex items-center gap-1"><span className={cn('h-2 w-2 rounded-full', RP_BAR.pending)} />{d.pending} pending</span>
                </div>
                {d.fail > 0 && (
                  <div className="rounded-md bg-red-500/10 px-2.5 py-1.5 text-xs font-medium text-red-600 dark:text-red-400">
                    ⚠ {d.fail} blocker{d.fail === 1 ? '' : 's'} on the cut-off path
                  </div>
                )}
              </CardContent>
            </Card>
          );
        })}
      </div>
    </div>
  );
}

// ── Team Workload — per-tester cards ──────────────────────────────────────
function memberLoad(counts) {
  const c = counts || {};
  return {
    pass: (c.resolved ?? 0) + (c.passed ?? 0) + (c.closed ?? 0),
    progress: c.in_progress ?? 0,
    assigned: c.new ?? 0,
    fail: c.failed ?? 0,
    blocked: c.blocked ?? 0,
    total: c.total ?? ((c.resolved ?? 0) + (c.passed ?? 0) + (c.closed ?? 0) + (c.in_progress ?? 0) + (c.new ?? 0) + (c.failed ?? 0) + (c.blocked ?? 0)),
  };
}

function WorkloadPanel({ qaTeam, tickets }) {
  // Prefer the live QA team (server-aggregated counts). Fall back to deriving
  // per-assignee counts from the ticket feed when no team is configured.
  let cards;
  if (qaTeam && qaTeam.length) {
    cards = qaTeam.map(m => ({
      key: m.id, name: m.name || m.githubLogin, login: m.githubLogin, avatarUrl: m.avatarUrl,
      focus: m.focus, capacity: m.capacity || 'open', load: memberLoad(m.counts),
    }));
  } else {
    const by = {};
    tickets.filter(t => t.assigneeLogin || t.assigneeName).forEach(t => {
      const key = t.assigneeLogin || t.assigneeName;
      const c = by[key] || (by[key] = { name: t.assigneeName || t.assigneeLogin, login: t.assigneeLogin, counts: {} });
      const bucket = t.qaStage === 'fail' ? 'failed' : (t.status === 'resolved' || t.status === 'closed' || t.qaStage === 'pass') ? 'resolved'
        : t.status === 'in_progress' ? 'in_progress' : t.status === 'blocked' ? 'blocked' : 'new';
      c.counts[bucket] = (c.counts[bucket] || 0) + 1;
      c.counts.total = (c.counts.total || 0) + 1;
    });
    cards = Object.entries(by).map(([key, v]) => ({ key, name: v.name, login: v.login, capacity: 'open', load: memberLoad(v.counts) }))
      .sort((a, b) => b.load.total - a.load.total);
  }

  if (cards.length === 0) {
    return <Card><CardContent className="pt-6 text-center text-sm text-muted-foreground">No QA team or assigned tickets yet (sign in for live data).</CardContent></Card>;
  }

  const capChip = (c) => c === 'open' ? 'default' : c === 'busy' ? 'warning' : 'outline';
  const rows = [
    ['pass', 'Resolved'], ['progress', 'In progress'], ['assigned', 'New / unstarted'], ['fail', 'Failed'], ['blocked', 'Blocked'],
  ];
  return (
    <div className="space-y-4">
      <p className="text-sm text-muted-foreground">Per-tester load across all enabled repos.</p>
      <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
        {cards.map(m => {
          const l = m.load; const denom = l.total || 1;
          return (
            <Card key={m.key}>
              <CardContent className="space-y-3 pt-5">
                <div className="flex items-center gap-3">
                  <RpAvatar name={m.name} login={m.login} avatarUrl={m.avatarUrl} />
                  <div className="min-w-0 flex-1">
                    <h3 className="truncate font-semibold leading-tight">{m.name}</h3>
                    <div className="truncate text-xs text-muted-foreground">{m.focus || '—'}</div>
                  </div>
                  <Badge variant={capChip(m.capacity)} className="capitalize">{m.capacity}</Badge>
                </div>
                <div className="flex h-2 overflow-hidden rounded-full bg-muted">
                  <div className={RP_BAR.pass} style={{ width: `${(l.pass / denom) * 100}%` }} />
                  <div className={RP_BAR.progress} style={{ width: `${(l.progress / denom) * 100}%` }} />
                  <div className={RP_BAR.assigned} style={{ width: `${(l.assigned / denom) * 100}%` }} />
                  <div className={RP_BAR.fail} style={{ width: `${(l.fail / denom) * 100}%` }} />
                  <div className={RP_BAR.blocked} style={{ width: `${(l.blocked / denom) * 100}%` }} />
                </div>
                <div className="space-y-1 text-sm">
                  {rows.map(([k, label]) => (
                    <div key={k} className="flex items-center justify-between">
                      <span className="inline-flex items-center gap-2 text-muted-foreground"><span className={cn('h-2 w-2 rounded-full', RP_BAR[k])} />{label}</span>
                      <strong>{l[k]}</strong>
                    </div>
                  ))}
                </div>
              </CardContent>
            </Card>
          );
        })}
      </div>
    </div>
  );
}

// ── Repository Scope — toggle repos in/out of the pipeline ────────────────

// Map the classified sync status into a toast notification (or null for the
// quiet states). Covers the three the user cares about — success, expired
// GitHub token, timeout — plus a generic failure fallback.
function syncNoteFrom(s) {
  if (!s) return null;
  switch (s.kind) {
    case 'success':
      return { variant: 'success', title: 'Sync completed',
        message: `Last GitHub sync finished ${relAge(s.at)} · +${s.itemsAdded ?? 0} new, ${s.itemsUpdated ?? 0} updated.` };
    case 'expired_token':
      return { variant: 'error', title: 'GitHub sync token expired',
        message: 'The sync was rejected (“bad credentials”). Rotate the SYNC_GITHUB_TOKEN secret to resume syncing.' };
    case 'timeout':
      return { variant: 'warning', title: 'Sync timed out',
        message: 'The last GitHub sync didn’t finish in time. It will retry on the next scheduled run.' };
    case 'error':
      return { variant: 'error', title: 'Sync failed', message: s.error || 'The last GitHub sync failed.' };
    default:
      return null; // never / running → no toast
  }
}

function RepoScopePanel({ repos, lanes }) {
  const [search, setSearch] = useState('');
  const [busyId, setBusyId] = useState(null);
  const [syncBusy, setSyncBusy] = useState(false);
  const [msg, setMsg] = useState(null);
  const [syncNote, setSyncNote] = useState(null);

  // Surface the latest sync outcome as a toast on open.
  useEffect(() => {
    let cancelled = false;
    api.fetchSyncStatus().then(s => { if (!cancelled) setSyncNote(syncNoteFrom(s)); }).catch(() => {});
    return () => { cancelled = true; };
  }, []);
  // Auto-dismiss the success toast; leave failures up until closed.
  useEffect(() => {
    if (syncNote && syncNote.variant === 'success') {
      const t = setTimeout(() => setSyncNote(null), 6000);
      return () => clearTimeout(t);
    }
  }, [syncNote]);

  const filtered = repos.filter(r => !search
    || (r.name || '').toLowerCase().includes(search.toLowerCase())
    || (r.path || '').toLowerCase().includes(search.toLowerCase()));
  const groups = [
    { key: 'enabled', label: 'In scope — feeding tickets into Dispatch', list: filtered.filter(r => r.enabled) },
    { key: 'disabled', label: 'Available — toggle on to start syncing', list: filtered.filter(r => !r.enabled) },
  ];

  async function toggle(r) {
    setMsg(null); setBusyId(r.id);
    try { await api.setRepoEnabled(r.id, !r.enabled); }
    catch (e) { setMsg({ err: true, text: `Toggle failed: ${e.message}` }); }
    finally { setBusyId(null); }
  }
  async function changeLane(r, laneId) {
    setMsg(null); setBusyId(r.id);
    try { await api.setRepoDefaultLane(r.id, laneId); }
    catch (e) { setMsg({ err: true, text: `Lane change failed: ${e.message}` }); }
    finally { setBusyId(null); }
  }
  async function runSync() {
    setMsg(null); setSyncBusy(true);
    try {
      const s = await api.triggerSync();
      setMsg({ text: `Synced ${s.itemsAdded ?? 0} new + ${s.itemsUpdated ?? 0} updated across ${s.reposProcessed ?? 0} repos. Reload to see changes.` });
      setSyncNote({ variant: 'success', title: 'Sync completed',
        message: `+${s.itemsAdded ?? 0} new, ${s.itemsUpdated ?? 0} updated across ${s.reposProcessed ?? 0} repos.` });
    } catch (e) {
      setMsg({ err: true, text: `Sync failed: ${e.message}` });
      // Re-read the classified status so the toast can name the cause
      // (expired token / timeout) rather than a raw message.
      api.fetchSyncStatus()
        .then(st => setSyncNote(syncNoteFrom(st) || { variant: 'error', title: 'Sync failed', message: e.message }))
        .catch(() => setSyncNote({ variant: 'error', title: 'Sync failed', message: e.message }));
    }
    finally { setSyncBusy(false); }
  }

  if (repos.length === 0) {
    return <Card><CardContent className="pt-6 text-center text-sm text-muted-foreground">No repositories loaded (sign in for live data).</CardContent></Card>;
  }

  return (
    <div className="space-y-4">
      {syncNote && <Notification variant={syncNote.variant} title={syncNote.title} message={syncNote.message} onClose={() => setSyncNote(null)} />}
      <div className="flex flex-wrap items-center justify-between gap-2">
        <p className="text-sm text-muted-foreground">Toggle repos in/out of the live ticket pipeline. Changes take effect immediately.</p>
        <div className="flex items-center gap-2">
          <Input className="w-48" placeholder="Filter repos…" value={search} onChange={e => setSearch(e.target.value)} />
          <Button size="sm" disabled={syncBusy} onClick={runSync}>{syncBusy ? 'Syncing…' : 'Sync now'}</Button>
        </div>
      </div>
      {msg && <div className={cn('rounded-md px-4 py-2 text-sm', msg.err ? 'bg-destructive/10 text-destructive' : 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400')}>{msg.text}</div>}

      <Card className="flex items-center gap-3 p-4">
        <div className="flex h-9 w-9 items-center justify-center rounded-md bg-foreground text-background text-xs font-bold">GH</div>
        <div className="text-sm">
          <div className="font-semibold">Connected to ucpm-dev</div>
          <div className="text-xs text-muted-foreground">{repos.length} repositories · {repos.filter(r => r.enabled).length} feeding Dispatch</div>
        </div>
      </Card>

      {groups.map(g => g.list.length === 0 ? null : (
        <div key={g.key} className="space-y-2">
          <div className="flex items-baseline justify-between">
            <h3 className="text-sm font-semibold">{g.label}</h3>
            <span className="text-xs text-muted-foreground">{g.list.length} repo{g.list.length === 1 ? '' : 's'}</span>
          </div>
          <Card className="divide-y">
            {g.list.map(r => {
              const busy = busyId === r.id;
              return (
                <div key={r.id} className={cn('flex flex-wrap items-center gap-3 p-3', busy && 'opacity-60')}>
                  <div className="min-w-0 flex-1">
                    <div className="font-medium">{r.name}</div>
                    <div className="truncate font-mono text-xs text-muted-foreground">{r.path}</div>
                  </div>
                  <div className="flex items-center gap-2 text-xs text-muted-foreground">
                    <span>{r.open ?? 0} open</span><span>·</span>
                    <span className="font-medium text-teal-600 dark:text-teal-400">{r.ready ?? 0} resolved</span><span>·</span>
                    <span>synced {r.lastSync || '—'}</span>
                  </div>
                  {lanes.length > 0 && (
                    <Select className="w-32" value={r.defaultLaneId == null ? '' : String(r.defaultLaneId)} disabled={busy}
                      onChange={e => changeLane(r, e.target.value === '' ? null : Number(e.target.value))}>
                      <option value="">— lane —</option>
                      {lanes.map(l => <option key={l.id} value={l.id}>{l.label || l.name}</option>)}
                    </Select>
                  )}
                  <label className="inline-flex cursor-pointer items-center">
                    <input type="checkbox" className="peer sr-only" checked={!!r.enabled} disabled={busy} onChange={() => toggle(r)} />
                    <span className="relative h-5 w-9 rounded-full bg-muted transition-colors after:absolute after:left-0.5 after:top-0.5 after:h-4 after:w-4 after:rounded-full after:bg-white after:shadow after:transition-transform peer-checked:bg-primary peer-checked:after:translate-x-4" />
                  </label>
                </div>
              );
            })}
          </Card>
        </div>
      ))}
    </div>
  );
}

// ── Developer Activity — GitHub contributions ─────────────────────────────
const rpIsoDay = (d) => { const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; };
function rpRolling(days) { const to = new Date(); const from = new Date(); from.setDate(from.getDate() - (days - 1)); return { from: rpIsoDay(from), to: rpIsoDay(to) }; }
function rpThisMonth() { const n = new Date(); return { from: rpIsoDay(new Date(n.getFullYear(), n.getMonth(), 1)), to: rpIsoDay(n) }; }
function rpLastMonth() { const n = new Date(); return { from: rpIsoDay(new Date(n.getFullYear(), n.getMonth() - 1, 1)), to: rpIsoDay(new Date(n.getFullYear(), n.getMonth(), 0)) }; }
const CONTRIB_PRESETS = [
  { id: 'last7', label: 'Last 7 days', range: () => rpRolling(7) },
  { id: 'last14', label: 'Last 14 days', range: () => rpRolling(14) },
  { id: 'last30', label: 'Last 30 days', range: () => rpRolling(30) },
  { id: 'last90', label: 'Last 90 days', range: () => rpRolling(90) },
  { id: 'thisMonth', label: 'This month', range: rpThisMonth },
  { id: 'lastMonth', label: 'Last month', range: rpLastMonth },
];
function windowDaysForRange({ from, to }) {
  const ms = new Date(to + 'T00:00:00Z').getTime() - new Date(from + 'T00:00:00Z').getTime();
  return Math.max(14, Math.ceil(ms / 86400000) + 1);
}

function ContributionSparkline({ byDay, from, to }) {
  const map = new Map((byDay || []).map(d => [d.day, d]));
  const days = [];
  const start = new Date(from + 'T00:00:00Z'), end = new Date(to + 'T00:00:00Z');
  for (let t = start.getTime(); t <= end.getTime(); t += 86400000) {
    const key = rpIsoDay(new Date(t)); const m = map.get(key) || {};
    days.push((m.commits || 0) + (m.prsOpened || 0) + (m.prsMerged || 0) + (m.reviewsGiven || 0) + (m.commentsMade || 0));
  }
  const max = Math.max(1, ...days);
  const capped = days.length > 60 ? days.slice(days.length - 60) : days;
  return (
    <div className="flex h-7 w-28 items-end gap-px" title={`${days.reduce((s, v) => s + v, 0)} events / ${days.length} days`}>
      {capped.map((v, i) => <div key={i} className={cn('flex-1 rounded-sm', v === 0 ? 'bg-muted' : 'bg-primary/80')} style={{ height: `${Math.max(2, (v / max) * 28)}px` }} />)}
    </div>
  );
}

const CONTRIB_STATS = [['commits', 'commits'], ['prsMerged', 'merged'], ['prsOpened', 'opened'], ['reviewsGiven', 'reviews'], ['commentsMade', 'comments']];
function ContributorCard({ dev, range, expanded, onToggle }) {
  const t = dev.totals || {};
  const totalActivity = (t.commits || 0) + (t.prsOpened || 0) + (t.prsMerged || 0) + (t.reviewsGiven || 0) + (t.commentsMade || 0);
  return (
    <Card>
      <div className="flex cursor-pointer items-center gap-3 p-3" onClick={onToggle}>
        <RpAvatar name={dev.name} login={dev.login} avatarUrl={dev.avatarUrl} size={36} />
        <div className="min-w-0 flex-1">
          <div className="flex flex-wrap items-baseline gap-x-2">
            <strong className="text-sm">{dev.name}</strong>
            <span className="font-mono text-xs text-muted-foreground">@{dev.login}</span>
            {!dev.assigneeId && <Badge variant="outline" className="text-[10px]">not in team</Badge>}
            {dev.assigneeId && !dev.isActive && <Badge variant="outline" className="text-[10px]">inactive</Badge>}
          </div>
          <div className="mt-0.5 text-xs text-muted-foreground">{(dev.byRepo || []).length} repo{(dev.byRepo || []).length === 1 ? '' : 's'} · {totalActivity} event{totalActivity === 1 ? '' : 's'}</div>
        </div>
        <div className="hidden gap-4 sm:flex">
          {CONTRIB_STATS.map(([k, label]) => (
            <div key={k} className="min-w-[42px] text-center">
              <div className="text-base font-bold leading-none">{t[k] ?? 0}</div>
              <div className="mt-1 text-[10px] text-muted-foreground">{label}</div>
            </div>
          ))}
        </div>
        <ContributionSparkline byDay={dev.byDay} from={range.from} to={range.to} />
        <span className="text-sm text-muted-foreground">{expanded ? '▾' : '▸'}</span>
      </div>
      {expanded && (dev.byRepo || []).length > 0 && (
        <div className="border-t px-3 py-3">
          <div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Per repo</div>
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead className="text-muted-foreground"><tr className="[&>th]:px-2 [&>th]:py-1 [&>th]:text-right [&>th:first-child]:text-left"><th>Repo</th><th>Commits</th><th>+/−</th><th>PR open</th><th>PR merge</th><th>Reviews</th><th>Comments</th></tr></thead>
              <tbody className="divide-y">
                {dev.byRepo.map(r => (
                  <tr key={r.repoId} className="[&>td]:px-2 [&>td]:py-1 [&>td]:text-right [&>td:first-child]:text-left">
                    <td className="font-mono">{r.repoName}</td><td>{r.commits}</td>
                    <td className="text-muted-foreground"><span className="text-emerald-600 dark:text-emerald-400">+{r.additions}</span> <span className="text-red-600 dark:text-red-400">−{r.deletions}</span></td>
                    <td>{r.prsOpened}</td><td>{r.prsMerged}</td><td>{r.reviewsGiven}</td><td>{r.commentsMade}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}
    </Card>
  );
}

function ContributionsPanel() {
  const [presetId, setPresetId] = useState('last30');
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [refreshing, setRefreshing] = useState(false);
  const [expanded, setExpanded] = useState(null);
  const preset = CONTRIB_PRESETS.find(p => p.id === presetId) || CONTRIB_PRESETS[2];
  const range = preset.range();

  function load() {
    setLoading(true); setError(null);
    api.fetchContributionsReport(range)
      .then(r => setData(r))
      .catch(e => setError(e.message || 'Failed to load report'))
      .finally(() => setLoading(false));
  }
  useEffect(load, [range.from, range.to]);

  async function refresh() {
    setRefreshing(true); setError(null);
    try { await api.refreshContributions({ windowDays: windowDaysForRange(range) }); load(); }
    catch (e) { setError(e.message || 'Refresh failed'); }
    finally { setRefreshing(false); }
  }

  const devs = (data && data.developers) || [];
  const lastSync = data && data.lastSyncedAt;
  return (
    <div className="space-y-4">
      <div className="flex flex-wrap items-center gap-2">
        <span className="text-sm text-muted-foreground">Range</span>
        <Select className="w-40" value={presetId} onChange={e => setPresetId(e.target.value)}>
          {CONTRIB_PRESETS.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
        </Select>
        <span className="text-xs text-muted-foreground">{range.from} → {range.to}</span>
        <div className="ml-auto flex items-center gap-2">
          <span className="text-xs text-muted-foreground">{lastSync ? `Synced ${api.relativeTime(lastSync)} ago` : 'Never synced'}</span>
          <Button size="sm" variant="outline" disabled={refreshing} onClick={refresh}>{refreshing ? 'Refreshing…' : 'Refresh from GitHub'}</Button>
        </div>
      </div>
      {error && <div className="rounded-md border border-amber-300 bg-amber-50 px-4 py-2 text-sm text-amber-800">{error}</div>}
      {loading && devs.length === 0 && <Card><CardContent className="pt-6 text-center text-sm text-muted-foreground">Loading…</CardContent></Card>}
      {!loading && devs.length === 0 && !error && (
        <Card><CardContent className="pt-6 text-center text-sm text-muted-foreground">{lastSync ? 'No activity in this window — try a wider range.' : 'No data yet — click "Refresh from GitHub".'}</CardContent></Card>
      )}
      <div className="space-y-2.5">
        {devs.map(d => <ContributorCard key={d.login} dev={d} range={range} expanded={expanded === d.login} onToggle={() => setExpanded(expanded === d.login ? null : d.login)} />)}
      </div>
    </div>
  );
}

// ── Reporting shell ───────────────────────────────────────────────────────
function Reporting() {
  const readLive = () => ({
    tickets: (window.__DISPATCH_LIVE__ && window.__DISPATCH_LIVE__.tickets) || [],
    repos: (window.__DISPATCH_LIVE__ && window.__DISPATCH_LIVE__.repos) || [],
    qaTeam: (window.__DISPATCH_LIVE__ && window.__DISPATCH_LIVE__.qaTeam) || [],
    lanes: (window.__DISPATCH_LIVE__ && window.__DISPATCH_LIVE__.lanes) || [],
  });
  const [live, setLive] = useState(readLive);
  const [tab, setTab] = useState('readiness');
  const [overviewOpen, setOverviewOpen] = useOverviewOpen();
  useEffect(() => {
    const read = () => setLive(readLive());
    read();
    window.addEventListener('dispatch:data-loaded', read);
    return () => window.removeEventListener('dispatch:data-loaded', read);
  }, []);

  const repos = Array.isArray(live.repos) ? live.repos : [];
  const isLive = (Array.isArray(live.tickets) && live.tickets.length > 0) || repos.length > 0;
  const inScope = repos.filter(r => r.enabled).length;

  function exportReadiness() {
    const scoped = repos.filter(r => r.scope || r.enabled);
    downloadCsv('release-readiness.csv',
      ['Repo', 'Path', 'Release-ready %', 'Resolved', 'In progress', 'Failed', 'Pending'],
      scoped.map(r => { const d = repoReadiness(r); return [r.name, r.path, d.readyPct, d.pass, d.progress, d.fail, d.pending]; }));
  }

  const tabs = [
    { id: 'readiness', label: 'Release Readiness' },
    { id: 'workload', label: 'Team Workload' },
    { id: 'devs', label: 'Developer Activity' },
    { id: 'repos', label: 'Repository Scope' },
  ];

  return (
    <div className="space-y-4">
      {overviewOpen && (
        <Card className="border-primary/20 bg-primary/5">
          <CardContent className="flex flex-wrap items-start justify-between gap-3 pt-6">
            <div>
              <div className="text-[11px] font-semibold uppercase tracking-wider text-primary">Reporting &amp; Admin</div>
              <h2 className="text-2xl font-semibold tracking-tight">Reporting</h2>
              <p className="mt-1 text-sm text-muted-foreground">Sprint health, team capacity, developer activity, and repository scope — in one place.</p>
            </div>
            <div className="flex items-center gap-2">
              <Badge variant={isLive ? 'default' : 'outline'}>{isLive ? '● Live' : '○ Mock'}</Badge>
              <Badge variant="outline">{inScope} of {repos.length} repos in scope</Badge>
            </div>
          </CardContent>
        </Card>
      )}

      <div className="flex flex-wrap items-center gap-2">
        <div className="inline-flex flex-wrap gap-1 rounded-lg bg-muted p-1">
          {tabs.map(t => (
            <button key={t.id} onClick={() => setTab(t.id)}
              className={cn('inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors', tab === t.id ? 'bg-background text-foreground shadow' : 'text-muted-foreground hover:text-foreground')}>
              {t.label}
            </button>
          ))}
        </div>
        <div className="ml-auto flex items-center gap-2">
          {tab === 'readiness' && <Button size="sm" onClick={exportReadiness}>Export report</Button>}
        </div>
      </div>

      {tab === 'readiness' && <ReadinessPanel repos={repos} />}
      {tab === 'workload' && <WorkloadPanel qaTeam={live.qaTeam} tickets={live.tickets} />}
      {tab === 'repos' && <RepoScopePanel repos={repos} lanes={live.lanes} />}
      {tab === 'devs' && <ContributionsPanel />}
    </div>
  );
}
