/* Dispatch — Help Desk tab
 * Submit form, chart, Reports/Assignees panels, ticket drawer, and the HelpDesk view.
 * Loaded by index.html as a <script type="text/babel"> module. Shared symbols
 * (React hook aliases, api, cn, UI primitives, helpers) live in lib.jsx
 * and are declared once there; other modules use them without redeclaring. */
// Disk-mode dev returns relative `/uploads/...` paths; resolve them against the
// API origin so the image loads from the API server, not the Pages origin. S3
// URLs come back absolute and pass through unchanged.
function resolveAttachmentUrl(filePath) {
  if (!filePath) return '';
  if (/^https?:\/\//i.test(filePath)) return filePath;
  if (filePath.startsWith('/')) {
    try { return (api.getApiUrl?.() || '').replace(/\/$/, '') + filePath; } catch { return filePath; }
  }
  return filePath;
}

// Up to 5 image attachments, 5 MB each — matches the server-side limits.
const HD_MAX_FILES = 5;
const HD_MAX_FILE_BYTES = 5 * 1024 * 1024;
const HD_ALLOWED_MIMES = new Set(['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']);

function SubmitForm({ metadata, applications, neededBy, defaultsEmail, onSubmit, bare }) {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [category, setCategory] = useState('general');
  const [priority, setPriority] = useState('medium');
  const [applicationId, setApplicationId] = useState('');
  const [neededById, setNeededById] = useState('');
  const [steps, setSteps] = useState([]);          // replication steps (array of strings)
  const [files, setFiles] = useState([]);           // [{ file, preview }]
  const [fileError, setFileError] = useState(null);
  const [notify, setNotify] = useState(!!defaultsEmail);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const formRef = useRef(null);
  const cats = metadata?.categories ?? [];
  const prios = metadata?.priorities ?? [{ name: 'low', label: 'Low' }, { name: 'medium', label: 'Medium' }, { name: 'high', label: 'High' }, { name: 'critical', label: 'Critical' }];
  const canSubmit = title.trim().length > 2 && description.trim().length > 2 && !busy;

  const addStep = () => setSteps(s => [...s, '']);
  const setStep = (i, val) => setSteps(s => s.map((x, idx) => (idx === i ? val : x)));
  const removeStep = (i) => setSteps(s => s.filter((_, idx) => idx !== i));

  function appendFiles(incoming) {
    setFileError(null);
    const accepted = [];
    for (const f of incoming) {
      if (!HD_ALLOWED_MIMES.has(f.type)) { setFileError(`${f.name}: only image files are allowed.`); continue; }
      if (f.size > HD_MAX_FILE_BYTES) { setFileError(`${f.name}: over 5 MB.`); continue; }
      accepted.push({ file: f, preview: URL.createObjectURL(f) });
    }
    setFiles(prev => {
      if (prev.length + accepted.length > HD_MAX_FILES) setFileError(`Maximum ${HD_MAX_FILES} images. Some were dropped.`);
      return [...prev, ...accepted].slice(0, HD_MAX_FILES);
    });
  }
  function removeFile(idx) {
    setFiles(prev => { const next = [...prev]; const removed = next.splice(idx, 1)[0]; if (removed?.preview) URL.revokeObjectURL(removed.preview); return next; });
  }

  // Paste a screenshot (Ctrl+V) while focused in the form to attach it.
  useEffect(() => {
    const el = formRef.current;
    if (!el) return;
    const onPaste = (e) => {
      const imgs = Array.from(e.clipboardData?.items ?? []).filter(it => it.kind === 'file' && it.type.startsWith('image/')).map(it => it.getAsFile()).filter(Boolean);
      if (imgs.length) { e.preventDefault(); appendFiles(imgs); }
    };
    el.addEventListener('paste', onPaste);
    return () => el.removeEventListener('paste', onPaste);
  }, []);

  const onDragOver = (e) => e.preventDefault();
  const onDrop = (e) => { e.preventDefault(); const d = Array.from(e.dataTransfer?.files ?? []).filter(f => f.type.startsWith('image/')); if (d.length) appendFiles(d); };

  async function submit(e) {
    e.preventDefault();
    if (!canSubmit) return;
    setBusy(true); setError(null);
    try {
      const payload = { title: title.trim(), description: description.trim(), category, priority, notifySubmitter: notify };
      if (applicationId) payload.applicationId = Number(applicationId);
      if (neededById) payload.neededById = Number(neededById);
      const cleanSteps = steps.map(s => s.trim()).filter(Boolean);
      if (cleanSteps.length) payload.replicationSteps = cleanSteps;
      await onSubmit(payload, files.map(f => f.file));
      setTitle(''); setDescription(''); setApplicationId(''); setNeededById(''); setSteps([]);
      for (const f of files) if (f.preview) URL.revokeObjectURL(f.preview);
      setFiles([]); setFileError(null);
    } catch (err) { setError(err.message || 'Submit failed'); }
    finally { setBusy(false); }
  }

  const optional = <span className="font-normal text-muted-foreground">(optional)</span>;
  const formEl = (
    <form ref={formRef} onSubmit={submit} onDragOver={onDragOver} onDrop={onDrop} className="space-y-4">
      {error && <div className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">{error}</div>}
      <div className="space-y-1.5" data-tour="hd-form-title">
        <Label>What do you need?</Label>
        <Input value={title} onChange={e => setTitle(e.target.value)} placeholder="e.g. Need new monitor cable, locked out of Salesforce…" />
      </div>
      <div className="space-y-1.5">
        <Label>More detail</Label>
        <Textarea value={description} onChange={e => setDescription(e.target.value)} placeholder="What you've tried, error messages, when it started…" />
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1.5">
          <Label>Category</Label>
          <Select value={category} onChange={e => setCategory(e.target.value)}>{cats.map(c => <option key={c.name} value={c.name}>{c.label}</option>)}</Select>
        </div>
        <div className="space-y-1.5">
          <Label>Urgency</Label>
          <Select value={priority} onChange={e => setPriority(e.target.value)}>{prios.map(p => <option key={p.name} value={p.name}>{p.label}</option>)}</Select>
        </div>
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1.5">
          <Label>Application {optional}</Label>
          <Select value={applicationId} onChange={e => setApplicationId(e.target.value)}>
            <option value="">— none —</option>
            {(applications ?? []).map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
          </Select>
        </div>
        <div className="space-y-1.5">
          <Label>Needed by {optional}</Label>
          <Select value={neededById} onChange={e => setNeededById(e.target.value)}>
            <option value="">— no deadline —</option>
            {(neededBy ?? []).map(n => <option key={n.id} value={n.id}>{n.duration}</option>)}
          </Select>
        </div>
      </div>
      <div className="space-y-1.5">
        <div className="flex items-center justify-between">
          <Label>Replication steps {optional}</Label>
          <Button type="button" variant="ghost" size="sm" onClick={addStep}>+ Add step</Button>
        </div>
        {steps.length === 0 && <p className="text-xs text-muted-foreground">For bugs, list the steps to reproduce — one per line.</p>}
        {steps.map((s, i) => (
          <div key={i} className="flex items-center gap-2">
            <Input value={s} onChange={e => setStep(i, e.target.value)} placeholder={`Step ${i + 1}`} />
            <Button type="button" variant="ghost" size="icon" aria-label="Remove step" onClick={() => removeStep(i)}>✕</Button>
          </div>
        ))}
      </div>
      <div className="space-y-1.5" data-tour="hd-form-attach">
        <Label>Screenshots {optional}</Label>
        <label className="flex cursor-pointer flex-col items-center justify-center rounded-md border border-dashed px-3 py-4 text-center text-xs text-muted-foreground hover:border-primary/50 hover:bg-muted/40">
          <span>Drop images here, paste with <kbd className="rounded border bg-muted px-1">Ctrl+V</kbd>, or <span className="font-medium text-foreground">browse</span></span>
          <span className="mt-0.5">max {HD_MAX_FILES}, 5 MB each</span>
          <input type="file" accept="image/*" multiple className="hidden" onChange={e => { appendFiles(Array.from(e.target.files ?? [])); e.target.value = ''; }} />
        </label>
        {fileError && <p className="text-xs text-destructive">{fileError}</p>}
        {files.length > 0 && (
          <div className="flex flex-wrap gap-2 pt-1">
            {files.map((f, i) => (
              <div key={i} className="relative h-16 w-16 overflow-hidden rounded-md border">
                <img src={f.preview} alt="" className="h-full w-full object-cover" />
                <button type="button" onClick={() => removeFile(i)} aria-label="Remove image"
                  className="absolute right-0 top-0 flex h-5 w-5 items-center justify-center bg-black/60 text-xs text-white">✕</button>
              </div>
            ))}
          </div>
        )}
      </div>
      {defaultsEmail && (
        <label className="flex items-center gap-2 text-sm">
          <input type="checkbox" className="h-4 w-4 rounded border-input accent-primary" checked={notify} onChange={e => setNotify(e.target.checked)} />
          Notify me in Slack
        </label>
      )}
      <Button type="submit" className="w-full" disabled={!canSubmit} data-tour="hd-form-submit">{busy ? 'Submitting…' : 'Submit ticket'}</Button>
    </form>
  );
  if (bare) return formEl;
  return (
    <Card>
      <CardHeader><CardTitle>New help desk request</CardTitle></CardHeader>
      <CardContent>{formEl}</CardContent>
    </Card>
  );
}

// ── Ticket drawer ─────────────────────────────────────────────────────────
function TicketDrawer({ ticketId, isTriager, metadata, assignees, onClose, onChanged }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const [comment, setComment] = useState('');
  const [ghOpen, setGhOpen] = useState(false);
  const [ghRepo, setGhRepo] = useState('');
  const [ghProject, setGhProject] = useState('');
  const [ghBusy, setGhBusy] = useState(false);
  const [ghError, setGhError] = useState(null);

  // Repos to offer in "Send to GitHub": enabled ones (the queue) when any are
  // enabled, otherwise all known repos. Value is the owner/name path.
  const allRepos = (window.__DISPATCH_LIVE__?.repos ?? []).map(r => ({ path: r.path || r.name })).filter(r => r.path);
  const ghRepos = allRepos.filter(r => (window.__DISPATCH_LIVE__?.repos ?? []).some(x => (x.path || x.name) === r.path && x.enabled));
  const repos = ghRepos.length ? ghRepos : allRepos;

  async function load() {
    setLoading(true);
    try { setData(await api.fetchHelpdeskTicket(ticketId)); setError(null); }
    catch (e) { setError(e.message || 'Failed to load'); }
    finally { setLoading(false); }
  }
  useEffect(() => { load(); }, [ticketId]);

  const ticket = data?.ticket;
  const comments = data?.comments ?? [];
  const attachments = data?.attachments ?? [];
  const replicationSteps = data?.replicationSteps ?? [];
  const statuses = metadata?.statuses ?? Object.entries(STATUS_FALLBACK).map(([name, label]) => ({ name, label }));

  async function setField(field, value) {
    setBusy(true);
    try { await api.updateHelpdeskTicket(ticketId, { [field]: value }); await load(); onChanged && onChanged(); }
    catch (e) { setError(e.message || 'Update failed'); }
    finally { setBusy(false); }
  }
  async function submitComment(e) {
    e.preventDefault();
    if (!comment.trim()) return;
    setBusy(true);
    try {
      const author = api.getCurrentUserName() || api.getCurrentUserEmail() || undefined;
      await api.addHelpdeskComment(ticketId, comment.trim(), author);
      setComment(''); await load();
    } catch (e2) { setError(e2.message || 'Comment failed'); }
    finally { setBusy(false); }
  }
  async function setArchived(archived) {
    setBusy(true); setError(null);
    try { await (archived ? api.archiveHelpdeskTicket(ticketId) : api.unarchiveHelpdeskTicket(ticketId)); await load(); onChanged && onChanged(); }
    catch (e) { setError(e.message || (archived ? 'Archive failed' : 'Unarchive failed')); }
    finally { setBusy(false); }
  }
  async function sendToGithub(e) {
    e.preventDefault();
    if (!ghRepo) return;
    setGhBusy(true); setGhError(null);
    try {
      const proj = ghProject.trim() ? Number(ghProject.trim()) : undefined;
      await api.sendHelpdeskTicketToGithub(ticketId, ghRepo, proj);
      setGhOpen(false); setGhProject(''); await load(); onChanged && onChanged();
    } catch (err) { setGhError(err.message || 'Failed to send to GitHub'); }
    finally { setGhBusy(false); }
  }
  function EditSelect({ field, current, options, allowEmpty, emptyLabel }) {
    const known = options.some(o => o.value === (current || ''));
    return (
      <Select value={current || ''} disabled={busy} onChange={e => setField(field, e.target.value || (allowEmpty ? null : e.target.value))}>
        {allowEmpty && <option value="">{emptyLabel || 'None'}</option>}
        {!known && current && <option value={current}>{titleCase(current)} (current)</option>}
        {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
      </Select>
    );
  }

  const statusOpts = statuses.map(s => ({ value: s.name, label: s.label || titleCase(s.name) }));
  const prioOpts = (metadata?.priorities ?? []).map(p => ({ value: p.name, label: p.label }));
  const catOpts = (metadata?.categories ?? []).map(c => ({ value: c.name, label: c.label }));
  const assigneeOpts = (assignees ?? []).map(a => ({ value: a.email, label: a.name }));

  return (
    <Modal open onClose={onClose} title={loading ? 'Loading…' : (ticket?.title || 'Ticket')}
      footer={
        <>
          {ticket && <Button type="button" size="sm" disabled={busy || !comment.trim()} onClick={submitComment}>Add comment</Button>}
          {ticket && (ticket.githubIssueUrl
            ? <a href={ticket.githubIssueUrl} target="_blank" rel="noreferrer"><Button type="button" size="sm">View in GitHub #{ticket.githubIssueNumber}</Button></a>
            : isTriager && <Button type="button" size="sm" disabled={busy} onClick={() => { setGhError(null); setGhRepo(repos[0]?.path || ''); setGhOpen(o => !o); }}>Send to GitHub</Button>)}
          {ticket && isTriager && (ticket.archived
            ? <Button type="button" size="sm" disabled={busy} onClick={() => setArchived(false)}>Unarchive</Button>
            : <Button type="button" size="sm" disabled={busy} onClick={() => setArchived(true)}>Archive</Button>)}
          {ticket && ticket.archived && <Badge variant="secondary">Archived</Badge>}
          <Button size="sm" variant="outline" onClick={onClose}>Close</Button>
        </>
      }>
      {error && <div className="mb-3 rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">{error}</div>}
      {loading && <div className="text-sm text-muted-foreground">Loading ticket…</div>}
      {!loading && ticket && (
        <div className="space-y-4">
          <div className="grid grid-cols-2 gap-3">
            <div className="space-y-1"><div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Status</div>
              {isTriager ? <EditSelect field="status" current={ticket.status} options={statusOpts} /> : <div className="text-sm font-medium">{statusLabel(ticket.status, statuses)}</div>}</div>
            <div className="space-y-1"><div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Priority</div>
              {isTriager ? <EditSelect field="priority" current={ticket.priority} options={prioOpts} /> : <Badge variant={prioVariant(ticket.priority)}>{ticket.priority}</Badge>}</div>
            <div className="space-y-1"><div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Category</div>
              {isTriager ? <EditSelect field="category" current={ticket.category || 'general'} options={catOpts} /> : <div className="text-sm font-medium">{ticket.category || 'general'}</div>}</div>
            <div className="space-y-1"><div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Assignee</div>
              {isTriager ? <EditSelect field="assignee" current={ticket.assignee} options={assigneeOpts} allowEmpty emptyLabel="Unassigned" /> : <div className="text-sm font-medium">{ticket.assignee || 'Unassigned'}</div>}</div>
          </div>
          <div>
            <div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Reported by</div>
            <div className="text-sm">{ticket.reporter} · {relAge(ticket.createdAt)} ago</div>
          </div>
          {ticket.neededBy && (
            <div>
              <div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Needed by</div>
              <div className="text-sm font-medium">{ticket.neededBy}</div>
            </div>
          )}
          <div>
            <div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Description</div>
            <p className="whitespace-pre-wrap text-sm">{ticket.description}</p>
          </div>
          {replicationSteps.length > 0 && (
            <div>
              <div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Replication steps</div>
              <ol className="list-decimal space-y-0.5 pl-5 text-sm">
                {replicationSteps.map(s => <li key={s.id}>{s.stepDetails}</li>)}
              </ol>
            </div>
          )}
          <div>
            <div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Attachments{attachments.length > 0 && ` · ${attachments.length}`}</div>
            {attachments.length === 0
              ? <div className="text-sm italic text-muted-foreground">No attachments.</div>
              : (
                <div className="flex flex-wrap gap-2">
                  {attachments.map(att => (
                    <a key={att.id} href={resolveAttachmentUrl(att.filePath)} target="_blank" rel="noreferrer"
                      className="block h-20 w-20 overflow-hidden rounded-md border hover:border-primary" title={att.fileName}>
                      <img src={resolveAttachmentUrl(att.filePath)} alt={att.fileName || 'attachment'} className="h-full w-full object-cover" loading="lazy" />
                    </a>
                  ))}
                </div>
              )}
          </div>
          <hr />
          <div>
            <div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Comments · {comments.length}</div>
            {comments.length === 0 && <div className="mb-3 text-sm italic text-muted-foreground">No comments yet.</div>}
            <div className="space-y-2">
              {comments.map(c => (
                <div key={c.id} className="rounded-md bg-muted p-2">
                  <div className="flex justify-between"><strong className="text-xs">{c.author}</strong><span className="text-xs text-muted-foreground">{relAge(c.createdAt)}</span></div>
                  <div className="whitespace-pre-wrap text-sm">{c.text}</div>
                </div>
              ))}
            </div>
            <div className="mt-3">
              <Textarea value={comment} onChange={e => setComment(e.target.value)} placeholder="Add a comment…" />
            </div>
            {isTriager && ghOpen && !ticket.githubIssueUrl && (
              <form onSubmit={sendToGithub} className="mt-3 space-y-3 rounded-md border p-3">
                <div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Send to GitHub</div>
                <div className="space-y-1.5">
                  <Label>Repository</Label>
                  <Select value={ghRepo} onChange={e => setGhRepo(e.target.value)}>
                    {repos.length === 0 && <option value="">No repos enabled — go to Repo Scope</option>}
                    {repos.map(r => <option key={r.path} value={r.path}>{r.path}</option>)}
                  </Select>
                </div>
                <div className="space-y-1.5">
                  <Label>Project number <span className="font-normal text-muted-foreground">(optional)</span></Label>
                  <Input value={ghProject} onChange={e => setGhProject(e.target.value)} placeholder="e.g. 2 — leave empty to skip" inputMode="numeric" />
                  <p className="text-xs text-muted-foreground">Adds the new issue to that GitHub Project V2 board.</p>
                </div>
                <div className="rounded-md bg-primary/5 px-3 py-2 text-xs text-muted-foreground">
                  <span className="font-medium text-foreground">Issue title:</span> {ticket.title}<br />
                  <span className="font-medium text-foreground">Labels:</span> {ticket.priority}, {ticket.category || 'general'}
                </div>
                {ghError && <div className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">{ghError}</div>}
                <div className="flex gap-2">
                  <Button type="submit" size="sm" disabled={ghBusy || !ghRepo}>{ghBusy ? 'Sending…' : 'Send to GitHub'}</Button>
                  <Button type="button" size="sm" variant="ghost" onClick={() => setGhOpen(false)}>Cancel</Button>
                </div>
              </form>
            )}
          </div>
        </div>
      )}
    </Modal>
  );
}

// ── Chart card (ticket volume over time) ─────────────────────────────────
function buildDailySeries(rows, days) {
  const map = new Map((rows || []).map(r => [r.day, r.count]));
  const labels = [], values = [];
  const today = new Date();
  for (let i = days - 1; i >= 0; i--) {
    const dt = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i);
    const key = `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
    labels.push(dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));
    values.push(map.get(key) || 0);
  }
  return { labels, values };
}
function ChartCard({ overTime }) {
  const canvasRef = useRef(null);
  const chartRef = useRef(null);
  const [days, setDays] = useState(90);
  useEffect(() => {
    if (!window.Chart || !canvasRef.current) return;
    const { labels, values } = buildDailySeries(overTime, days);
    chartRef.current = new window.Chart(canvasRef.current, {
      type: 'line',
      data: { labels, datasets: [{
        data: values, fill: true, tension: 0.4, pointRadius: 0, borderWidth: 2,
        borderColor: 'hsl(274 44% 40%)',
        backgroundColor: (ctx) => {
          const { chart } = ctx; const { ctx: c, chartArea } = chart;
          if (!chartArea) return 'rgba(108,57,148,0.1)';
          const g = c.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
          g.addColorStop(0, 'rgba(108,57,148,0.35)'); g.addColorStop(1, 'rgba(108,57,148,0.02)');
          return g;
        },
      }] },
      options: {
        responsive: true, maintainAspectRatio: false, animation: false,
        plugins: { legend: { display: false }, tooltip: { intersect: false, mode: 'index' } },
        scales: {
          x: { grid: { display: false }, ticks: { maxTicksLimit: 8, font: { size: 10 }, color: 'hsl(240 3.8% 46.1%)' } },
          y: { beginAtZero: true, ticks: { precision: 0, font: { size: 10 }, color: 'hsl(240 3.8% 46.1%)' }, grid: { color: 'rgba(0,0,0,0.05)' } },
        },
      },
    });
    return () => { chartRef.current && chartRef.current.destroy(); };
  }, [overTime, days]);
  const ranges = [{ d: 90, label: 'Last 3 months' }, { d: 30, label: 'Last 30 days' }, { d: 7, label: 'Last 7 days' }];
  return (
    <Card>
      <CardHeader className="flex-row items-center justify-between space-y-0">
        <div><CardTitle>Ticket volume</CardTitle><p className="mt-1 text-sm text-muted-foreground">New tickets over the last {days} days</p></div>
        <div className="inline-flex rounded-lg border p-0.5">
          {ranges.map(r => (
            <button key={r.d} onClick={() => setDays(r.d)} className={cn('rounded-md px-2.5 py-1 text-xs font-medium', days === r.d ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground')}>{r.label}</button>
          ))}
        </div>
      </CardHeader>
      <CardContent>
        <div className="h-[240px]">
          {window.Chart ? <canvas ref={canvasRef} /> : <div className="flex h-full items-center justify-center text-sm text-muted-foreground">Chart library unavailable.</div>}
        </div>
      </CardContent>
    </Card>
  );
}

// ── Reports panel: KPIs + chart + breakdowns ─────────────────────────────
function Breakdown({ title, rows, total }) {
  return (
    <Card>
      <CardHeader className="pb-2"><CardTitle className="text-sm">{title}</CardTitle></CardHeader>
      <CardContent className="space-y-2">
        {(!rows || rows.length === 0) && <div className="text-sm text-muted-foreground">No data.</div>}
        {(rows || []).map(r => (
          <div key={r.name}>
            <div className="mb-1 flex justify-between text-sm"><span>{titleCase(r.name)}</span><span className="text-muted-foreground">{r.count}</span></div>
            <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted"><div className="h-full bg-primary" style={{ width: `${total ? Math.round((r.count / total) * 100) : 0}%` }} /></div>
          </div>
        ))}
      </CardContent>
    </Card>
  );
}
function ReportsPanel({ reports }) {
  const byStatus = reports?.byStatus || [];
  const total = byStatus.reduce((a, s) => a + s.count, 0);
  return (
    <div className="space-y-6">
      <ChartCard overTime={reports?.overTime} />
      <div className="grid gap-4 md:grid-cols-3">
        <Breakdown title="By status" rows={byStatus} total={total} />
        <Breakdown title="By priority" rows={reports?.byPriority} total={total} />
        <Breakdown title="By category" rows={reports?.byCategory} total={total} />
      </div>
    </div>
  );
}

// ── Assignees panel: roster + workload ───────────────────────────────────
function AssigneesPanel() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [page, setPage] = useState(1);
  const [sort, toggleSort] = useSort('open', 'desc');
  useEffect(() => {
    let cancelled = false;
    api.fetchHelpdeskAssigneeWorkload().then(r => { if (!cancelled) setData(r); }).catch(e => { if (!cancelled) setError(e.message || 'Failed to load'); });
    return () => { cancelled = true; };
  }, []);
  if (error) return <div className="rounded-md border border-amber-300 bg-amber-50 px-4 py-2 text-sm text-amber-800">{error}</div>;
  if (!data) return <div className="text-sm text-muted-foreground">Loading assignees…</div>;
  const rows = sortRows([...(data.assignees || [])], sort, (a, k) => a[k] ?? '');
  const un = data.unassigned || { open: 0, total: 0 };
  const avail = (open, active) => !active ? { l: 'Inactive', v: 'secondary' } : open >= 6 ? { l: 'At capacity', v: 'destructive' } : open >= 3 ? { l: 'Busy', v: 'warning' } : { l: 'Available', v: 'default' };
  const pageCount = Math.max(1, Math.ceil(rows.length / PER_PAGE));
  const paged = rows.slice((page - 1) * PER_PAGE, page * PER_PAGE);
  return (
    <Card>
      <div className="overflow-x-auto">
        <table className="w-full text-sm">
          <thead className="border-b text-muted-foreground">
            <tr className="[&>th]:px-4 [&>th]:py-2.5 [&>th]:text-left [&>th]:font-medium">
              <SortHeader label="Name" sortKey="name" sort={sort} onSort={toggleSort} />
              <SortHeader label="Email" sortKey="email" sort={sort} onSort={toggleSort} />
              <SortHeader label="Category" sortKey="category" sort={sort} onSort={toggleSort} />
              <th>Availability</th>
              <SortHeader label="Open" sortKey="open" sort={sort} onSort={toggleSort} align="right" />
              <SortHeader label="Total" sortKey="total" sort={sort} onSort={toggleSort} align="right" />
            </tr>
          </thead>
          <tbody className="divide-y">
            {paged.map(a => { const av = avail(a.open, a.isActive); return (
              <tr key={a.id}>
                <td className="px-4 py-2.5 font-medium">{a.name}</td>
                <td className="px-4 py-2.5 text-muted-foreground">{a.email}</td>
                <td className="px-4 py-2.5">{a.category || '—'}</td>
                <td className="px-4 py-2.5"><Badge variant={av.v}>{av.l}</Badge></td>
                <td className="px-4 py-2.5 text-right">{a.open}</td>
                <td className="px-4 py-2.5 text-right">{a.total}</td>
              </tr>
            ); })}
            <tr className="bg-muted/40">
              <td className="px-4 py-2.5 italic" colSpan="4">Unassigned</td>
              <td className="px-4 py-2.5 text-right">{un.open}</td>
              <td className="px-4 py-2.5 text-right">{un.total}</td>
            </tr>
          </tbody>
        </table>
      </div>
      <div className="flex flex-wrap items-center justify-between gap-2 border-t px-4 py-2">
        <span className="text-xs text-muted-foreground">{rows.length} assignees</span>
        <Pagination page={page} pageCount={pageCount} onPage={setPage} />
      </div>
    </Card>
  );
}

// Guided tour, ported from the old Help Desk "? Guide" flow. Spotlights each
// [data-tour=…] target step-by-step (centered when a step has no target).
// Steps with `modal: true` open the New-ticket form and spotlight fields inside
// it; the rest highlight the queue chrome. The tour opens/closes the modal as
// it enters/leaves those steps (via onModalChange).
const HD_TOUR_STEPS = [
  { title: 'Welcome to the Help Desk', body: 'A quick tour of how to file a request and track it. Use Next to step through, or press Esc to leave any time.' },
  { target: '[data-tour="hd-submit"]', title: 'File a request', body: 'This is where you file a request — hardware, software access, a password reset, anything that isn’t a code issue. Let’s open the form.' },
  { modal: true, target: '[data-tour="hd-form-title"]', title: 'Describe what you need', body: 'One line summarizing the request or problem — e.g. “Locked out of Salesforce” — then add detail below.' },
  { modal: true, target: '[data-tour="hd-form-attach"]', title: 'Attach screenshots', body: 'Optional — drag images in, paste with Ctrl+V, or browse. Up to 5 images, 5 MB each.' },
  { modal: true, target: '[data-tour="hd-form-submit"]', title: 'Set urgency & submit', body: 'Pick a category and urgency (and a deadline if it matters), then Submit ticket.' },
  { target: '[data-tour="hd-queue"]', title: 'Track your tickets', body: 'Every request lands here. Click a ticket to open it, follow the thread, and add comments.' },
  { target: '[data-tour="hd-filters"]', title: 'Find anything fast', body: 'Search by text or a GitHub # (e.g. #123), or filter by status, priority, category, and assignee. Toggle “Show archived” to see closed-out tickets.' },
  { target: '[data-tour="hd-tabs"]', title: 'Assignees & Reports', body: 'Switch to Assignees to see the team and their workload, or Reports for a live breakdown of the queue.' },
  { title: 'That’s everything', body: 'Triagers get extra controls inside each ticket — change status, assign an owner, send it to GitHub, and archive. Click ? Guide anytime to replay this tour.' },
];

function HelpDeskTour({ steps, onClose, onModalChange }) {
  const [i, setI] = useState(0);
  const [rect, setRect] = useState(null);
  const popRef = useRef(null);
  const step = steps[i];
  const isFirst = i === 0, isLast = i === steps.length - 1;
  const goNext = () => isLast ? onClose() : setI(n => Math.min(n + 1, steps.length - 1));
  const goBack = () => setI(n => Math.max(0, n - 1));

  // Open/close the New-ticket modal as we enter/leave modal steps.
  useEffect(() => { if (onModalChange) onModalChange(!!step.modal); }, [i, step.modal]);
  // Ensure the modal is closed when the tour ends.
  useEffect(() => () => { if (onModalChange) onModalChange(false); }, []);

  useEffect(() => {
    // Re-query on each measure so targets inside the just-opened modal are
    // found once React has mounted them (after the rAFs below).
    const measure = () => {
      const el = step.target ? document.querySelector(step.target) : null;
      if (!el) { setRect(null); return; }
      const r = el.getBoundingClientRect();
      setRect({ top: r.top, left: r.left, width: r.width, height: r.height });
    };
    const el0 = step.target ? document.querySelector(step.target) : null;
    if (el0) el0.scrollIntoView({ block: 'center', behavior: 'smooth' });
    const raf = requestAnimationFrame(() => requestAnimationFrame(measure));
    window.addEventListener('resize', measure); window.addEventListener('scroll', measure, true);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', measure); window.removeEventListener('scroll', measure, true); };
  }, [i, step.target, step.modal]);

  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') { e.preventDefault(); onClose(); }
      else if (e.key === 'ArrowRight') { e.preventDefault(); goNext(); }
      else if (e.key === 'ArrowLeft') { e.preventDefault(); goBack(); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  });

  const PAD = 8, POP_W = 340;
  const spot = rect ? { top: rect.top - PAD, left: rect.left - PAD, width: rect.width + PAD * 2, height: rect.height + PAD * 2 } : null;
  let popStyle;
  if (spot) {
    const vw = window.innerWidth, vh = window.innerHeight;
    const popH = popRef.current?.offsetHeight ?? 190;
    let top = spot.top + spot.height + 12;
    if (top + popH > vh - 12) { const above = spot.top - popH - 12; top = above >= 12 ? above : Math.max(12, (vh - popH) / 2); }
    let left = Math.min(spot.left, vw - POP_W - 12); if (left < 12) left = 12;
    popStyle = { position: 'fixed', top, left, width: POP_W };
  } else {
    popStyle = { position: 'fixed', top: '50%', left: '50%', width: POP_W, transform: 'translate(-50%, -50%)' };
  }

  return ReactDOM.createPortal(
    <div className="fixed inset-0 z-[100]" role="dialog" aria-modal="true" aria-label="Help Desk tour">
      <div className="fixed inset-0" style={spot ? { background: 'transparent' } : { background: 'rgba(0,0,0,0.6)' }} onClick={e => e.stopPropagation()} />
      {spot && <div className="pointer-events-none fixed rounded-lg ring-2 ring-primary transition-all" style={{ ...spot, boxShadow: '0 0 0 9999px rgba(0,0,0,0.6)' }} />}
      <div ref={popRef} style={popStyle} className="rounded-lg border bg-popover p-4 text-popover-foreground shadow-lg">
        <div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Step {i + 1} of {steps.length}</div>
        <h3 className="mt-1 text-base font-semibold">{step.title}</h3>
        <p className="mt-1 text-sm text-muted-foreground">{step.body}</p>
        <div className="mt-4 flex items-center justify-between">
          <button className="text-xs text-muted-foreground hover:text-foreground" onClick={onClose}>Skip tour</button>
          <div className="flex gap-2">
            {!isFirst && <Button size="sm" variant="outline" onClick={goBack}>Back</Button>}
            <Button size="sm" onClick={goNext}>{isLast ? 'Done' : 'Next'}</Button>
          </div>
        </div>
      </div>
    </div>,
    document.body
  );
}

// ── Help Desk view: top tabs (Help Desk / Assignees / Reports) ───────────
function HelpDesk() {
  const [me, setMe] = useState(null);
  const [metadata, setMetadata] = useState(null);
  const [assignees, setAssignees] = useState([]);
  const [applications, setApplications] = useState([]);
  const [neededByOptions, setNeededByOptions] = useState([]);
  const [reports, setReports] = useState(null);
  const [tickets, setTickets] = useState([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(true);
  const [loadError, setLoadError] = useState(null);
  const [tab, setTab] = useState('helpdesk');
  const [search, setSearch] = useState('');
  const [status, setStatus] = useState('all');
  const [priority, setPriority] = useState('all');
  const [category, setCategory] = useState('all');
  const [assignee, setAssignee] = useState('all');
  const [openId, setOpenId] = useState(null);
  const [submitOpen, setSubmitOpen] = useState(false);
  const [archived, setArchived] = useState(false);
  const [toast, setToast] = useState(null);
  const debounced = useRef('');

  function refreshReports() { api.fetchHelpdeskReports().then(setReports).catch(() => {}); }

  async function reload() {
    setLoading(true);
    try {
      const params = { archived: archived ? 'true' : 'false', limit: 50 };
      if (status !== 'all') params.status = status;
      if (priority !== 'all') params.priority = priority;
      if (category !== 'all') params.category = category;
      if (assignee !== 'all') params.assignee = assignee;
      if (debounced.current) params.search = debounced.current;
      const res = await api.fetchHelpdeskTickets(params);
      setTickets(res.items); setTotal(res.total); setLoadError(null);
    } catch (e) { setLoadError(e.message || 'Failed to load tickets'); }
    finally { setLoading(false); }
  }

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const [meta, meRes, ass, apps, needed, rep] = await Promise.all([
          api.fetchHelpdeskMetadata(),
          api.fetchHelpdeskMe().catch(() => ({ email: '', name: '', isTriager: false })),
          api.fetchHelpdeskAssignees().catch(() => []),
          api.fetchHelpdeskApplications().catch(() => []),
          api.fetchHelpdeskNeededBy().catch(() => []),
          api.fetchHelpdeskReports().catch(() => null),
        ]);
        if (cancelled) return;
        setMetadata(meta); setMe(meRes);
        setAssignees(Array.isArray(ass) ? ass : []);
        setApplications(Array.isArray(apps) ? apps : []);
        setNeededByOptions(Array.isArray(needed) ? needed : []);
        setReports(rep);
      } catch (e) { if (!cancelled) setLoadError(e.message || 'Help Desk API unreachable'); }
    })();
    return () => { cancelled = true; };
  }, []);

  useEffect(() => {
    const t = setTimeout(() => { debounced.current = search.trim(); reload(); }, 250);
    return () => clearTimeout(t);
  }, [search, status, priority, category, assignee, archived]);

  async function handleSubmit(input, files) {
    const reporter = api.getCurrentUserName() || api.getCurrentUserEmail() || 'Anonymous';
    await api.createHelpdeskTicket({ ...input, reporter }, files);
    setToast('Ticket submitted'); setSubmitOpen(false);
    await reload(); refreshReports();
    // Also refresh the shared live feed so My Tasks / Up for Grabs and the Hub
    // pick up the new ticket without a full page reload.
    api.refreshHelpdeskLive();
    setTimeout(() => setToast(null), 2500);
  }

  const statuses = metadata?.statuses ?? Object.entries(STATUS_FALLBACK).map(([name, label]) => ({ name, label }));
  const myEmail = me?.email || api.getCurrentUserEmail();
  const TABS = [
    { id: 'helpdesk', label: 'Help Desk' },
    { id: 'assignees', label: 'Assignees' },
    { id: 'reports', label: 'Reports' },
  ];
  const byStatus = reports?.byStatus || [];
  const kpis = [
    { label: 'Total tickets', value: byStatus.reduce((a, s) => a + s.count, 0) },
    { label: 'Open', value: byStatus.filter(s => s.name === 'new' || s.name === 'in_progress').reduce((a, s) => a + s.count, 0) },
    { label: 'Resolved', value: reports?.resolution?.resolvedCount ?? 0 },
    { label: 'Avg days to resolve', value: reports?.resolution?.avgDays == null ? '—' : Number(reports.resolution.avgDays).toFixed(1) },
  ];

  // Sortable columns + client-side pagination (10 per page).
  const [page, setPage] = useState(1);
  const [sort, toggleSort] = useSort('createdAt', 'desc');
  useEffect(() => { setPage(1); }, [tickets, sort]);
  const hdSortVal = (t, k) => k === 'priority' ? (PRIORITY_RANK[t.priority] ?? 9)
    : k === 'createdAt' ? new Date(t.createdAt).getTime() : (t[k] ?? '');
  const sorted = sortRows(tickets, sort, hdSortVal);
  const pageCount = Math.max(1, Math.ceil(sorted.length / PER_PAGE));
  const paged = sorted.slice((page - 1) * PER_PAGE, page * PER_PAGE);

  const [overviewOpen, setOverviewOpen] = useOverviewOpen();
  const [tourOpen, setTourOpen] = useState(false);

  return (
    <div className="space-y-4">
      {loadError && <div className="rounded-md border border-amber-300 bg-amber-50 px-4 py-2 text-sm text-amber-800">{loadError}</div>}
      {toast && <div className="rounded-md border border-green-300 bg-green-50 px-4 py-2 text-sm text-green-800">{toast}</div>}

      {/* Overview (description card + KPI cards) — the whole block collapses to
          maximize table space; the toggle lives on the tabs row below. */}
      {overviewOpen && (
        <>
          <Card className="border-primary/20 bg-primary/5">
            <CardContent className="pt-6">
              <h2 className="text-2xl font-semibold tracking-tight">Submit a ticket. Track an existing one.</h2>
              <p className="mt-1 text-sm text-muted-foreground">Hardware, password resets, software access — anything that isn't a code issue. Triagers can promote a ticket to GitHub once it's clearly a dev task.</p>
            </CardContent>
          </Card>
          <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
            {kpis.map(k => (
              <Card key={k.label}><CardContent className="pt-5">
                <div className="text-sm text-muted-foreground">{k.label}</div>
                <div className="mt-1 text-3xl font-semibold tracking-tight">{k.value}</div>
              </CardContent></Card>
            ))}
          </div>
        </>
      )}

      {/* Top-level tabs + overview collapse toggle. */}
      <div className="flex flex-wrap items-center gap-2">
        <div data-tour="hd-tabs" 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}{t.id === 'helpdesk' && total > 0 && <span className="rounded bg-foreground/10 px-1.5 text-xs">{total}</span>}
            </button>
          ))}
        </div>
        <div className="ml-auto flex items-center gap-2">
          {tab === 'helpdesk' && (
            <>
              <Button size="sm" onClick={() => setTourOpen(true)} title="How to use the Help Desk">? Guide</Button>
              <Button data-tour="hd-submit" size="sm" onClick={() => setSubmitOpen(true)}>+ New ticket</Button>
              <Button size="sm" variant="outline" disabled={loading} onClick={() => { reload(); refreshReports(); }} title="Reload the ticket list">{loading ? 'Refreshing…' : '↻ Refresh list'}</Button>
            </>
          )}
        </div>
      </div>

      {tab === 'reports' && <ReportsPanel reports={reports} />}
      {tab === 'assignees' && <AssigneesPanel />}
      {tab === 'helpdesk' && (
        <Card>
          {/* Toolbar: search + status/priority/category/assignee filters, New ticket at the end. */}
          <div data-tour="hd-filters" className="flex flex-wrap items-center gap-2 border-b p-3">
            <Input className="w-56" placeholder="Search tickets or #123 (GitHub #)…" value={search} onChange={e => setSearch(e.target.value)} />
            <Select className="w-36" value={status} onChange={e => setStatus(e.target.value)}>
              <option value="all">All statuses</option>
              {statuses.map(s => <option key={s.name} value={s.name}>{s.label || titleCase(s.name)}</option>)}
            </Select>
            <Select className="w-36" value={priority} onChange={e => setPriority(e.target.value)}>
              <option value="all">All priorities</option>
              {['low', 'medium', 'high', 'critical'].map(p => <option key={p} value={p}>{titleCase(p)}</option>)}
            </Select>
            <Select className="w-36" value={category} onChange={e => setCategory(e.target.value)}>
              <option value="all">All categories</option>
              {(metadata?.categories ?? []).map(c => <option key={c.name} value={c.name}>{c.label}</option>)}
            </Select>
            <Select className="w-40" value={assignee} onChange={e => setAssignee(e.target.value)}>
              <option value="all">Any assignee</option>
              <option value="unassigned">Unassigned</option>
              {assignees.map(a => <option key={a.id} value={a.email}>{a.name}</option>)}
            </Select>
            <label className="flex items-center gap-1.5 text-sm text-muted-foreground">
              <input type="checkbox" className="h-4 w-4 accent-primary" checked={archived} onChange={e => setArchived(e.target.checked)} />
              Show archived
            </label>
          </div>
          <div data-tour="hd-queue" className="overflow-x-auto">
            <table className="w-full text-sm">
              <thead className="border-b text-muted-foreground">
                <tr className="[&>th]:px-4 [&>th]:py-2.5 [&>th]:text-left [&>th]:font-medium">
                  <SortHeader label="Ticket" sortKey="title" sort={sort} onSort={toggleSort} />
                  <SortHeader label="Status" sortKey="status" sort={sort} onSort={toggleSort} />
                  <SortHeader label="Priority" sortKey="priority" sort={sort} onSort={toggleSort} />
                  <SortHeader label="Category" sortKey="category" sort={sort} onSort={toggleSort} />
                  <SortHeader label="Reporter" sortKey="reporter" sort={sort} onSort={toggleSort} />
                  <SortHeader label="Assignee" sortKey="assignee" sort={sort} onSort={toggleSort} />
                  <th>GitHub</th>
                  <SortHeader label="Age" sortKey="createdAt" sort={sort} onSort={toggleSort} align="right" />
                </tr>
              </thead>
              <tbody className="divide-y">
                {loading && tickets.length === 0 && <tr><td colSpan="8" className="px-4 py-6 text-center text-muted-foreground">Loading…</td></tr>}
                {!loading && tickets.length === 0 && <tr><td colSpan="8" className="px-4 py-6 text-center text-muted-foreground">No tickets match.</td></tr>}
                {paged.map(t => (
                  <tr key={t.id} onClick={() => setOpenId(t.id)} className="cursor-pointer transition-colors hover:bg-muted/50">
                    <td className="max-w-[280px] px-4 py-2.5">
                      <div className="truncate font-medium">{t.title}</div>
                    </td>
                    <td className="px-4 py-2.5"><Badge variant="outline">{statusLabel(t.status, statuses)}</Badge></td>
                    <td className="px-4 py-2.5"><Badge variant={prioVariant(t.priority)}>{t.priority}</Badge></td>
                    <td className="px-4 py-2.5 text-muted-foreground">{t.category || 'general'}</td>
                    <td className="max-w-[150px] truncate px-4 py-2.5 text-muted-foreground">{t.reporter}</td>
                    <td className="max-w-[150px] truncate px-4 py-2.5 text-muted-foreground">{t.assignee || '—'}</td>
                    <td className="px-4 py-2.5">
                      <input type="checkbox" readOnly checked={!!t.githubIssueUrl} className="h-4 w-4 accent-primary"
                        title={t.githubIssueUrl ? `Sent to GitHub #${t.githubIssueNumber}` : 'Not sent to GitHub'} />
                    </td>
                    <td className="px-4 py-2.5 text-right text-muted-foreground">{relAge(t.createdAt)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          <div className="flex flex-wrap items-center justify-between gap-2 border-t px-4 py-2">
            <span className="text-xs text-muted-foreground">{tickets.length} shown · {total} total</span>
            <Pagination page={page} pageCount={pageCount} onPage={setPage} />
          </div>
        </Card>
      )}

      {openId && <TicketDrawer ticketId={openId} isTriager={!!me?.isTriager} metadata={metadata} assignees={assignees} onClose={() => setOpenId(null)} onChanged={() => { reload(); refreshReports(); api.refreshHelpdeskLive(); }} />}
      <Modal open={submitOpen} onClose={() => setSubmitOpen(false)} title="New help desk request">
        <SubmitForm bare metadata={metadata} applications={applications} neededBy={neededByOptions} defaultsEmail={myEmail} onSubmit={handleSubmit} />
      </Modal>
      {tourOpen && <HelpDeskTour steps={HD_TOUR_STEPS} onClose={() => setTourOpen(false)} onModalChange={setSubmitOpen} />}
    </div>
  );
}

