/* Dispatch — QA Board (kanban)
 * Loaded by index.html after lib.jsx.
 *
 * Replicates the original app's BoardView: a five-column kanban over the live
 * QA pool (every open ticket from enabled repos), keyed by `qaStage`. Drag a
 * card across columns to set its stage via DispatchAPI.setTicketQaStage — which
 * optimistically updates window.__DISPATCH_LIVE__ and fires `dispatch:data-loaded`,
 * so the card jumps columns immediately (and reverts if the API call fails). */

// Same five stages and order as the original BoardView.
const QA_COLS = [
  { id: 'needs', title: 'Needs Test', dot: 'bg-amber-500' },
  { id: 'assigned', title: 'Assigned', dot: 'bg-blue-500' },
  { id: 'progress', title: 'In Progress', dot: 'bg-teal-500' },
  { id: 'pass', title: 'Passed', dot: 'bg-emerald-500' },
  { id: 'fail', title: 'Failed', dot: 'bg-red-500' },
];
const qbStageOf = (t) => t.qaStage || t.status;

// Deterministic avatar tint from a login (mirrors the original's hue hash).
function qbHue(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 QbAvatar({ ticket }) {
  const login = ticket.assigneeLogin;
  const label = ticket.assigneeName || login;
  if (!label) {
    return <div className="flex h-6 w-6 items-center justify-center rounded-full bg-muted text-[10px] font-semibold text-muted-foreground" title="Unassigned">?</div>;
  }
  const initials = (label.split(/[\s_\-.]+/).map(s => s[0]).filter(Boolean).slice(0, 2).join('') || '?').toUpperCase();
  return (
    <div className="flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-semibold text-white" title={label}
      style={{ backgroundColor: `hsl(${qbHue(login || label)} 55% 45%)` }}>{initials}</div>
  );
}

function QbCard({ ticket, busy, onDragStart, onDragEnd, onOpen }) {
  const sp = ticket.stepProgress;
  const tot = sp ? sp.pass + sp.fail + sp.pending : 0;
  const done = sp ? sp.pass + sp.fail : 0;
  const repo = (ticket.repoName || '').split('-')[0];
  const num = ticket.num || ticket.sourceIssueNumber;
  return (
    <div draggable onDragStart={onDragStart} onDragEnd={onDragEnd} onClick={() => onOpen && onOpen(ticket)}
      className={cn('cursor-pointer rounded-lg border bg-card p-3 shadow-sm transition hover:border-primary/40', busy && 'opacity-50')}>
      <div className="flex items-center justify-between gap-2">
        <span className="truncate font-mono text-[11px] text-muted-foreground">{repo}{repo && num ? ' ' : ''}{num ? `#${num}` : ''}</span>
        <Badge variant={prioVariant(ticket.priority)} className="capitalize">{ticket.priority || '—'}</Badge>
      </div>
      <div className="mt-1.5 line-clamp-2 text-sm font-medium leading-snug">{ticket.title}</div>
      <div className="mt-2.5 flex items-center justify-between gap-2">
        {sp ? (
          <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
            <span className="font-mono">{done}/{tot}</span>
            <div className="h-1 w-14 overflow-hidden rounded-full bg-muted"><div className="h-full rounded-full bg-primary" style={{ width: tot ? `${(done / tot) * 100}%` : 0 }} /></div>
          </div>
        ) : (
          <span className="font-mono text-[10px] text-muted-foreground">{ticket.age || relAge(ticket.updatedAt || ticket.createdAt)}</span>
        )}
        <QbAvatar ticket={ticket} />
      </div>
    </div>
  );
}

function QaBoard() {
  const [live, setLive] = useState(() => (window.__DISPATCH_LIVE__ && window.__DISPATCH_LIVE__.tickets) || null);
  const [dragId, setDragId] = useState(null);
  const [dropCol, setDropCol] = useState(null);
  const [busyId, setBusyId] = useState(null);
  const [error, setError] = useState(null);
  const [overviewOpen, setOverviewOpen] = useOverviewOpen();
  const [openItem, setOpenItem] = useState(null);
  const [pages, setPages] = useState({}); // per-column page (10 cards/page)
  const [project, setProject] = useState('all');

  useEffect(() => {
    const read = () => setLive((window.__DISPATCH_LIVE__ && window.__DISPATCH_LIVE__.tickets) || null);
    read();
    window.addEventListener('dispatch:data-loaded', read);
    return () => window.removeEventListener('dispatch:data-loaded', read);
  }, []);
  // Reset per-column pages when the project filter changes so no column is
  // left pointing past the end of its (now shorter) list.
  useEffect(() => { setPages({}); }, [project]);

  const tickets = Array.isArray(live) ? live : [];
  // Project filter: options come from the repos whose tickets actually land on
  // the board (i.e. have a QA stage). `mtProjectOf` is the shared project label.
  const boardTickets = tickets.filter(t => QA_COLS.some(c => c.id === qbStageOf(t)));
  const projects = [...new Set(boardTickets.map(mtProjectOf))].sort();
  const scoped = project === 'all' ? tickets : tickets.filter(t => mtProjectOf(t) === project);

  async function onDrop(colId) {
    const id = dragId;
    setDragId(null); setDropCol(null);
    if (!id) return;
    const t = tickets.find(x => x.id === id);
    if (!t || qbStageOf(t) === colId) return;
    setBusyId(id); setError(null);
    try {
      await api.setTicketQaStage(id, colId);
    } catch (e) {
      setError(`Stage move failed: ${e.message || e}`);
    } finally {
      setBusyId(null);
    }
  }

  return (
    <div className="space-y-4">
      {overviewOpen && (
        <Card className="border-primary/20 bg-primary/5">
          <CardContent className="pt-6">
            <h2 className="text-2xl font-semibold tracking-tight">QA Board</h2>
            <p className="mt-1 text-sm text-muted-foreground">Every open ticket across enabled repos, by QA stage. Drag a card between columns to set its <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">qa:*</code> stage on GitHub.</p>
          </CardContent>
        </Card>
      )}

      {overviewOpen && (
        <div className="grid gap-4 sm:grid-cols-3 lg:grid-cols-5">
          {QA_COLS.map(c => (
            <Card key={c.id}><CardContent className="pt-5">
              <div className="flex items-center gap-2 text-sm text-muted-foreground"><span className={cn('h-2 w-2 rounded-full', c.dot)} />{c.title}</div>
              <div className="mt-1 text-3xl font-semibold tracking-tight">{scoped.filter(t => qbStageOf(t) === c.id).length}</div>
            </CardContent></Card>
          ))}
        </div>
      )}

      {error && <div className="rounded-md border border-destructive/40 bg-destructive/10 px-4 py-2 text-sm text-destructive">{error}</div>}

      {projects.length > 0 && (
        <div className="flex flex-wrap items-center justify-end gap-2">
          <span className="text-sm text-muted-foreground">Project</span>
          <Select className="w-52" value={project} onChange={e => setProject(e.target.value)}>
            <option value="all">All projects ({projects.length})</option>
            {projects.map(p => <option key={p} value={p}>{p}</option>)}
          </Select>
        </div>
      )}

      <div className="flex gap-4 overflow-x-auto pb-2">
        {QA_COLS.map(col => {
          const colItems = scoped.filter(t => qbStageOf(t) === col.id);
          const active = dropCol === col.id;
          const pageCount = Math.max(1, Math.ceil(colItems.length / PER_PAGE));
          const page = Math.min(pages[col.id] || 1, pageCount);
          const paged = colItems.slice((page - 1) * PER_PAGE, page * PER_PAGE);
          const setColPage = (p) => setPages(prev => ({ ...prev, [col.id]: p }));
          return (
            <div key={col.id}
              onDragOver={(e) => { e.preventDefault(); if (dropCol !== col.id) setDropCol(col.id); }}
              onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) setDropCol(null); }}
              onDrop={() => onDrop(col.id)}
              className={cn('flex w-72 shrink-0 flex-col rounded-xl border bg-muted/40 transition-colors', active && 'border-primary bg-primary/5 ring-1 ring-primary')}>
              <div className="flex items-center justify-between border-b px-3 py-2.5">
                <div className="flex items-center gap-2 text-sm font-semibold"><span className={cn('h-2 w-2 rounded-full', col.dot)} />{col.title}</div>
                <Badge variant="secondary">{colItems.length}</Badge>
              </div>
              <div className="flex min-h-[120px] flex-1 flex-col gap-2 p-2">
                {paged.map(t => (
                  <QbCard key={t.id} ticket={t} busy={busyId === t.id}
                    onDragStart={() => setDragId(t.id)}
                    onDragEnd={() => { setDragId(null); setDropCol(null); }}
                    onOpen={setOpenItem} />
                ))}
                {colItems.length === 0 && (
                  <div className="flex flex-1 items-center justify-center rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
                    {tickets.length === 0 ? 'No tickets loaded (sign in for live data).' : 'Drop tickets here'}
                  </div>
                )}
              </div>
              {colItems.length > PER_PAGE && (
                <div className="flex items-center justify-between border-t px-3 py-2 text-xs text-muted-foreground">
                  <button className="disabled:opacity-40" disabled={page <= 1} onClick={() => setColPage(page - 1)}>‹ Prev</button>
                  <span>{page}/{pageCount}</span>
                  <button className="disabled:opacity-40" disabled={page >= pageCount} onClick={() => setColPage(page + 1)}>Next ›</button>
                </div>
              )}
            </div>
          );
        })}
      </div>
      {openItem && <TicketDetailModal item={openItem} onClose={() => setOpenItem(null)} />}
    </div>
  );
}
