/* Dispatch — shared library
 * React hook aliases, DispatchAPI, cn, shadcn-style primitives, Pagination, sort helpers, and formatting helpers.
 * 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. */
const { useState, useEffect, useRef } = React;
const api = window.DispatchAPI;

// ── cn + shadcn/ui-style primitives ──────────────────────────────────────
// Hand-written to match shadcn's component styling (Tailwind class recipes),
// since the real Radix-based packages need a build the no-build app lacks.
const cn = (...a) => a.filter(Boolean).join(' ');

function Button({ variant = 'default', size = 'default', className, ...props }) {
  const variants = {
    default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
    secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
    outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
    ghost: 'hover:bg-accent hover:text-accent-foreground',
    destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
    link: 'text-primary underline-offset-4 hover:underline',
  };
  // Uniform button size across the app (compact). default / sm / lg all render
  // the same so buttons look consistent regardless of which size a caller used;
  // `icon` matches the height as a square.
  const COMPACT = 'h-8 rounded-md px-3 text-xs';
  const sizes = { default: COMPACT, sm: COMPACT, lg: COMPACT, icon: 'h-8 w-8' };
  return <button className={cn('inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', variants[variant], sizes[size], className)} {...props} />;
}
const Card = ({ className, ...p }) => <div className={cn('rounded-xl border bg-card text-card-foreground shadow', className)} {...p} />;
const CardHeader = ({ className, ...p }) => <div className={cn('flex flex-col space-y-1.5 p-5', className)} {...p} />;
const CardTitle = ({ className, ...p }) => <h3 className={cn('font-semibold leading-none tracking-tight', className)} {...p} />;
const CardContent = ({ className, ...p }) => <div className={cn('p-5 pt-0', className)} {...p} />;
const Label = ({ className, ...p }) => <label className={cn('text-sm font-medium leading-none', className)} {...p} />;
const Input = ({ className, ...p }) => <input className={cn('flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50', className)} {...p} />;
const Textarea = ({ className, ...p }) => <textarea className={cn('flex min-h-[70px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring', className)} {...p} />;
// Custom dropdown (shadcn Select-style) — a drop-in for a native <select>:
// same value / onChange / <option> children API, but fully Tailwind-styled so
// the open menu follows the theme (incl. dark mode). onChange fires a synthetic
// { target: { value } } so existing call sites work unchanged. className sets
// the control's width.
function Select({ className, value, onChange, disabled, children }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  const options = React.Children.toArray(children)
    .filter(c => c && c.type === 'option')
    .map(c => ({ value: c.props.value, label: c.props.children }));
  const current = options.find(o => String(o.value) === String(value ?? ''));
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
  }, [open]);
  function pick(v) { setOpen(false); onChange && onChange({ target: { value: v } }); }
  return (
    <div ref={ref} className={cn('relative', className)}>
      <button type="button" disabled={disabled} onClick={() => setOpen(o => !o)}
        className="flex h-9 w-full items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50">
        <span className="truncate">{current ? current.label : ''}</span>
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0 opacity-60"><path d="M6 9l6 6 6-6" /></svg>
      </button>
      {open && (
        <div className="absolute left-0 top-full z-50 mt-1 max-h-60 w-full min-w-[10rem] overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
          {options.map(o => (
            <button key={String(o.value)} type="button" onClick={() => pick(o.value)}
              className={cn('flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground', String(o.value) === String(value ?? '') && 'bg-accent/60')}>
              {o.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}
function Badge({ variant = 'default', className, ...props }) {
  const variants = {
    default: 'border-transparent bg-primary text-primary-foreground',
    secondary: 'border-transparent bg-secondary text-secondary-foreground',
    destructive: 'border-transparent bg-destructive text-destructive-foreground',
    warning: 'border-transparent bg-amber-500 text-white',
    outline: 'text-foreground',
  };
  return <span className={cn('inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold', variants[variant], className)} {...props} />;
}
// shadcn "Sheet" — a right-side drawer.
function Sheet({ open, onClose, title, children }) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';
    return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div>
      <div className="fixed inset-0 z-40 bg-black/60" onClick={onClose} />
      <div className="fixed inset-y-0 right-0 z-50 flex h-full w-full max-w-md flex-col border-l bg-background shadow-lg">
        <div className="flex items-center justify-between border-b p-4">
          <h2 className="text-base font-semibold">{title}</h2>
          <Button variant="ghost" size="icon" onClick={onClose} aria-label="Close">✕</Button>
        </div>
        <div className="flex-1 overflow-y-auto p-4">{children}</div>
      </div>
    </div>
  );
}

// shadcn "Dialog" — a centered modal. Same API as Sheet (open/onClose/title/
// children) so callers can swap between the two.
function Modal({ open, onClose, title, className, footer, children }) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';
    return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div>
      <div className="fixed inset-0 z-40 bg-black/60" onClick={onClose} />
      <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
        <div className={cn('flex max-h-[85vh] w-full max-w-2xl flex-col rounded-lg border bg-background shadow-lg', className)}
          role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
          <div className="flex items-center justify-between border-b p-4">
            <h2 className="text-base font-semibold">{title}</h2>
            <Button variant="ghost" size="icon" onClick={onClose} aria-label="Close">✕</Button>
          </div>
          <div className="flex-1 overflow-y-auto p-4">{children}</div>
          {footer && <div className="flex flex-wrap items-center justify-end gap-2 border-t p-4">{footer}</div>}
        </div>
      </div>
    </div>
  );
}

// Shared ticket-detail modal for GitHub-synced feed items (Hub, lane pages,
// My Tasks, QA Board). Mirrors the Help Desk ticket modal's layout — a labeled
// metadata grid + description — and always renders a footer. Help Desk tickets
// keep their own richer TicketDrawer.
function TicketDetailModal({ item, onClose }) {
  if (!item) return null;
  const Field = ({ label, children }) => (
    <div className="space-y-1"><div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">{label}</div>{children}</div>
  );
  const footer = (
    <>
      {item.sourceUrl && <a href={item.sourceUrl} target="_blank" rel="noreferrer"><Button variant="outline" size="sm">View in GitHub</Button></a>}
      <Button size="sm" onClick={onClose}>Close</Button>
    </>
  );
  return (
    <Modal open onClose={onClose} title={item.title} footer={footer}>
      <div className="space-y-4">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Ticket"><span className="font-mono text-sm">{item.num || '—'}</span></Field>
          <Field label="Lane">{typeof LaneBadge === 'function' ? <LaneBadge lane={item.lane} /> : <span className="text-sm">{titleCase(item.lane)}</span>}</Field>
          <Field label="Status"><Badge variant="outline">{titleCase(item.status)}</Badge></Field>
          <Field label="Priority"><Badge variant={prioVariant(item.priority)}>{item.priority}</Badge></Field>
          <Field label="Requester"><span className="text-sm">{item.requester || item.owner || '—'}</span></Field>
          <Field label="Assignee"><span className="text-sm">{item.assigneeName || item.assigneeLogin || '—'}</span></Field>
          {item.qaStage && <Field label="QA stage"><span className="text-sm">{titleCase(item.qaStage)}</span></Field>}
          <Field label="Updated"><span className="text-sm">{item.age || '—'}</span></Field>
        </div>
        {item.description && (
          <div><div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Description</div>
            <p className="whitespace-pre-wrap text-sm">{item.description}</p></div>
        )}
        <p className="text-xs text-muted-foreground">Comments and full activity for lane items live on the linked GitHub issue.</p>
      </div>
    </Modal>
  );
}

// ── helpers ───────────────────────────────────────────────────────────────
const STATUS_FALLBACK = { new: 'New', in_progress: 'In Progress', waiting_for_publish: 'Waiting for Publish', resolved: 'Resolved', closed: 'Closed' };
const titleCase = (r) => r == null || r === '' ? '—' : String(r).toLowerCase().split(/[_\s-]+/).filter(Boolean).map(w => w[0].toUpperCase() + w.slice(1)).join(' ');
const statusLabel = (n, statuses) => (statuses || []).find(s => s.name === n)?.label || STATUS_FALLBACK[n] || titleCase(n);
const prioVariant = (p) => (p === 'critical' || p === 'high') ? 'destructive' : p === 'medium' ? 'warning' : 'secondary';
function relAge(iso) {
  if (!iso) return '';
  const ms = Date.now() - new Date(iso).getTime();
  if (Number.isNaN(ms)) return '';
  const m = Math.floor(ms / 60000);
  if (m < 1) return 'just now';
  if (m < 60) return `${m}m`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h`;
  const d = Math.floor(h / 24);
  return d < 30 ? `${d}d` : `${Math.floor(d / 30)}mo`;
}

// ── Submit form ─────────────────────────────────────────────────────────
// Tailwind pagination — shadcn data-table style (Previous / Page X of Y / Next).
// Demo convention: 10 records per page for every grid/list.
const PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
const PER_PAGE = 10;
function Pagination({ page, pageCount, onPage }) {
  if (pageCount <= 1) return null;
  return (
    <div className="flex items-center gap-2">
      <span className="text-xs text-muted-foreground">Page {page} of {pageCount}</span>
      <Button variant="outline" size="sm" disabled={page <= 1} onClick={() => onPage(page - 1)}>Previous</Button>
      <Button variant="outline" size="sm" disabled={page >= pageCount} onClick={() => onPage(page + 1)}>Next</Button>
    </div>
  );
}

// ── Sortable columns ─────────────────────────────────────────────────────
// Reusable across all demo tables. useSort holds { key, dir }; clicking a
// header toggles asc/desc. sortRows sorts with a table-specific value accessor.
function useSort(defaultKey = null, defaultDir = 'asc') {
  const [sort, setSort] = useState({ key: defaultKey, dir: defaultDir });
  const toggle = (key) => setSort(s => (s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' }));
  return [sort, toggle];
}
function sortRows(rows, sort, valueOf) {
  if (!sort || !sort.key) return rows;
  const out = [...rows].sort((a, b) => {
    const av = valueOf(a, sort.key), bv = valueOf(b, sort.key);
    if (av == null && bv == null) return 0;
    if (av == null) return 1;
    if (bv == null) return -1;
    if (typeof av === 'number' && typeof bv === 'number') return av - bv;
    return String(av).localeCompare(String(bv), undefined, { numeric: true, sensitivity: 'base' });
  });
  return sort.dir === 'desc' ? out.reverse() : out;
}
// Sortable <th>. Inherits padding from the row's [&>th] utilities.
function SortHeader({ label, sortKey, sort, onSort, align }) {
  const active = sort.key === sortKey;
  return (
    <th className={align === 'right' ? 'text-right' : undefined}>
      <button type="button" onClick={() => onSort(sortKey)}
        className={cn('inline-flex items-center gap-1 hover:text-foreground', align === 'right' && 'flex-row-reverse')}>
        {label}<span className="w-3 text-xs">{active ? (sort.dir === 'asc' ? '↑' : '↓') : ''}</span>
      </button>
    </th>
  );
}

// Chevron toggle for collapsible sections (hero + KPI cards) to maximize space.
function CollapseToggle({ open, onClick }) {
  return (
    <button type="button" onClick={onClick} aria-expanded={open} aria-label={open ? 'Collapse' : 'Expand'}
      className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground">
      {open ? 'Hide' : 'Show'}
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
        className={cn('transition-transform', !open && '-rotate-90')}><path d="M6 9l6 6 6-6" /></svg>
    </button>
  );
}

// Toast notification — Tailwind Plus "simple" overlay design: a top-right
// white card with a status icon, title, message, and a close button.
// variant ∈ success | error | warning (drives the icon + its color).
function Notification({ variant = 'success', title, message, onClose }) {
  const icons = {
    success: <path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />,
    error: <path strokeLinecap="round" strokeLinejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />,
    warning: <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />,
  };
  const color = variant === 'success' ? 'text-emerald-500' : variant === 'warning' ? 'text-amber-500' : 'text-destructive';
  return (
    <div aria-live="assertive" className="pointer-events-none fixed inset-0 z-50 flex items-start justify-end px-4 py-6 sm:p-6">
      <div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg border bg-card shadow-lg">
        <div className="p-4">
          <div className="flex items-start">
            <div className="shrink-0">
              <svg className={cn('h-6 w-6', color)} fill="none" viewBox="0 0 24 24" strokeWidth="1.6" stroke="currentColor" aria-hidden="true">{icons[variant] || icons.success}</svg>
            </div>
            <div className="ml-3 w-0 flex-1 pt-0.5">
              <p className="text-sm font-medium text-foreground">{title}</p>
              {message && <p className="mt-1 text-sm text-muted-foreground">{message}</p>}
            </div>
            <div className="ml-4 flex shrink-0">
              <button type="button" onClick={onClose} className="inline-flex rounded-md text-muted-foreground hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring">
                <span className="sr-only">Close</span>
                <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" /></svg>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// Shared "Hide/Show overview" preference, persisted to localStorage so the
// collapsed state survives reloads (one setting across all pages, like the
// dark-mode toggle). Drop-in for useState(true).
const OVERVIEW_KEY = 'dispatch-demo-overview';
function useOverviewOpen() {
  const [open, setOpen] = useState(() => {
    try { return localStorage.getItem(OVERVIEW_KEY) !== 'closed'; } catch { return true; }
  });
  // Broadcast changes so every instance (each page + the header toggle) stays
  // in sync — the Hide/Show control lives in the top bar but is read per-page.
  useEffect(() => {
    const onSync = (e) => setOpen(!!e.detail);
    window.addEventListener('dispatch:overview-changed', onSync);
    return () => window.removeEventListener('dispatch:overview-changed', onSync);
  }, []);
  const set = (v) => {
    const next = typeof v === 'function' ? v(open) : v;
    try { localStorage.setItem(OVERVIEW_KEY, next ? 'open' : 'closed'); } catch {}
    // Fires our own listener too, updating this instance.
    window.dispatchEvent(new CustomEvent('dispatch:overview-changed', { detail: next }));
  };
  return [open, set];
}

