'use client';

/**
 * Jobs panel for the Forge web UI. Mirrors the extension's JobsTab —
 * list jobs, expand a job to see recent runs, expand a run to see
 * per-item dispatches. Click a dispatch's "open →" to deep-link into
 * the resulting Pipeline run (?view=pipelines&pipelineId=...).
 *
 * Read + run + toggle + delete + reset_dedup. Creation lives in the
 * extension (form is connector-aware); web is read-mostly.
 */

import { useEffect, useState, useCallback } from 'react';

interface Job {
  id: string;
  name: string;
  enabled: boolean;
  schedule_interval_minutes: number;
  source_connector: string;
  source_tool: string;
  dispatch_type: 'pipeline' | 'chat';
  last_run_at: string | null;
  next_run_at: string | null;
}

interface JobRun {
  id: string;
  job_id: string;
  started_at: string;
  finished_at: string | null;
  status: 'running' | 'ok' | 'error';
  items_seen: number;
  items_new: number;
  items_dispatched: number;
  error: string | null;
  trigger: 'schedule' | 'manual';
  notes?: string | null;
  log?: string | null;
}

interface JobDispatch {
  id: string;
  item_key: string;
  item_preview: string | null;
  dispatch_type: 'pipeline' | 'chat';
  dispatch_target_id: string | null;
  status: 'dispatched' | 'error';
  error: string | null;
  created_at: string;
}

interface Props {
  /** Switch the dashboard to the pipelines view for a specific run. */
  onViewPipeline?: (pipelineId: string) => void;
}

/**
 * Dispatch the Dashboard nav event to jump to the Logs view with a
 * pre-filled search string. Used by the "view logs" buttons here.
 */
function navigateLogs(search: string) {
  window.dispatchEvent(new CustomEvent('forge:navigate', { detail: { view: 'logs', logSearch: search } }));
}

export default function JobsView({ onViewPipeline }: Props) {
  const [jobs, setJobs] = useState<Job[] | null>(null);
  const [err, setErr] = useState('');
  const [expandedJobId, setExpandedJobId] = useState<string>('');
  const [runsByJob, setRunsByJob] = useState<Record<string, JobRun[]>>({});

  const refresh = useCallback(async () => {
    try {
      const r = await fetch('/api/jobs');
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      const j = await r.json();
      setJobs(j.jobs || []);
      setErr('');
    } catch (e) {
      setErr(e instanceof Error ? e.message : String(e));
      setJobs([]);
    }
  }, []);

  useEffect(() => { void refresh(); }, [refresh]);

  // While a Job row is expanded, poll its state every 3s so the user
  // sees Dispatched-Pipelines + busy badges update without manual
  // refresh. Stops when the row is collapsed.
  useEffect(() => {
    if (!expandedJobId) return;
    const id = expandedJobId;
    const interval = setInterval(() => {
      void refresh();
      void loadRuns(id);
    }, 3000);
    return () => clearInterval(interval);
  }, [expandedJobId, refresh]);

  async function loadRuns(jobId: string) {
    try {
      const r = await fetch(`/api/jobs/${encodeURIComponent(jobId)}/runs?limit=20`);
      const j = await r.json();
      setRunsByJob((prev) => ({ ...prev, [jobId]: j.runs || [] }));
    } catch {}
  }

  function expand(id: string) {
    if (expandedJobId === id) { setExpandedJobId(''); return; }
    setExpandedJobId(id);
    if (!runsByJob[id]) void loadRuns(id);
  }

  async function fireNow(id: string, opts?: { resetDedup?: boolean }) {
    try {
      const qs = opts?.resetDedup ? '?reset_dedup=1' : '';
      const r = await fetch(`/api/jobs/${encodeURIComponent(id)}/run${qs}`, { method: 'POST' });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        setErr(j.error || `Fire failed (${r.status})`);
        return;
      }
      // executeRun is async — give it a moment to write the
      // pipeline_runs row, then refetch state. Without this the UI
      // would not see job.busy turn true, and Stop drain / Cancel
      // all wouldn't appear until the next manual refresh.
      setTimeout(() => { void refresh(); void loadRuns(id); }, 300);
    } catch (e) { setErr(`Fire failed: ${e instanceof Error ? e.message : String(e)}`); }
  }

  async function cancelDrain(id: string, opts?: { cancelInflight?: boolean }) {
    if (!confirm(opts?.cancelInflight
        ? 'Cancel the drain AND stop any in-flight pipelines this Job dispatched?'
        : 'Stop the sequential drain? In-flight pipelines keep running.')) return;
    const qs = opts?.cancelInflight ? '?cancel_inflight=1' : '';
    const r = await fetch(`/api/jobs/${id}/cancel${qs}`, { method: 'POST' });
    if (!r.ok) { setErr(`Cancel failed: ${await r.text()}`); return; }
    void refresh();
  }

  async function toggleConcurrencyMode(j: Job) {
    const next = (j as any).concurrency_mode === 'parallel' ? 'sequential' : 'parallel';
    const r = await fetch(`/api/jobs/${j.id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ concurrency_mode: next }),
    });
    if (!r.ok) { setErr(`Failed to switch mode: ${await r.text()}`); return; }
    void refresh();
  }

  async function toggleOnFailure(j: Job) {
    const next = (j as any).on_failure === 'stop' ? 'continue' : 'stop';
    const r = await fetch(`/api/jobs/${j.id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ on_failure: next }),
    });
    if (!r.ok) { setErr(`Failed to switch on_failure: ${await r.text()}`); return; }
    void refresh();
  }

  async function toggle(j: Job) {
    try {
      await fetch(`/api/jobs/${encodeURIComponent(j.id)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ enabled: !j.enabled }),
      });
      await refresh();
    } catch (e) { alert(`Toggle failed: ${e instanceof Error ? e.message : String(e)}`); }
  }

  async function remove(id: string) {
    if (!confirm('Delete this job? (cascades runs + dedup)')) return;
    try {
      await fetch(`/api/jobs/${encodeURIComponent(id)}`, { method: 'DELETE' });
      await refresh();
    } catch (e) { alert(`Delete failed: ${e instanceof Error ? e.message : String(e)}`); }
  }

  async function resetDedup(id: string) {
    if (!confirm('Wipe dedup state? Next tick will re-process every item.')) return;
    try {
      const r = await fetch(`/api/jobs/${encodeURIComponent(id)}/reset_dedup`, { method: 'POST' });
      const j = await r.json();
      alert(`Removed ${j.removed} dedup keys`);
    } catch (e) { alert(`Reset failed: ${e instanceof Error ? e.message : String(e)}`); }
  }

  return (
    <div className="flex-1 flex flex-col min-h-0 overflow-auto p-4">
      <div className="flex items-center justify-between mb-3">
        <div>
          <h2 className="text-sm font-semibold text-[var(--text-primary)]">Jobs</h2>
          <p className="text-[11px] text-[var(--text-secondary)] mt-0.5">
            Scheduled connector polls — each new item fans out to a Pipeline run or a Chat session.
            Create + edit via the Forge extension (Jobs tab).
          </p>
        </div>
        <div className="flex items-center gap-2">
          <button
            onClick={() => navigateLogs('[jobs]')}
            className="text-[11px] px-2 py-1 border border-[var(--border)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
            title="Tail forge.log filtered to [jobs] lines"
          >
            View all job logs →
          </button>
          <button
            onClick={() => void refresh()}
            className="text-[11px] px-2 py-1 border border-[var(--border)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
          >
            Refresh
          </button>
        </div>
      </div>

      {err && <div className="text-[11px] text-[var(--red)] bg-[var(--red)]/10 rounded p-2 mb-3">{err}</div>}

      {jobs == null ? (
        <div className="text-[11px] text-[var(--text-secondary)] text-center py-8">Loading…</div>
      ) : jobs.length === 0 ? (
        <div className="text-[11px] text-[var(--text-secondary)] text-center py-8">
          (no jobs · open the Forge extension → Jobs tab → + New job)
        </div>
      ) : (
        <div className="space-y-2">
          {jobs.map((j) => (
            <JobRow
              key={j.id}
              job={j}
              expanded={expandedJobId === j.id}
              runs={runsByJob[j.id]}
              onClick={() => expand(j.id)}
              onFire={() => fireNow(j.id)}
              onForceFire={() => fireNow(j.id, { resetDedup: true })}
              onToggle={() => toggle(j)}
              onDelete={() => remove(j.id)}
              onReset={() => resetDedup(j.id)}
              onToggleMode={() => toggleConcurrencyMode(j)}
              onToggleOnFailure={() => toggleOnFailure(j)}
              onCancelDrain={(cancelInflight) => cancelDrain(j.id, { cancelInflight })}
              onViewPipeline={onViewPipeline}
            />
          ))}
        </div>
      )}
    </div>
  );
}

interface DispatchedPipeline {
  dispatch_id: string;
  job_run_id: string;
  item_key: string;
  item_preview: string | null;
  pipeline_id: string;
  pipeline_status: string;
  workflow_name: string | null;
  dispatched_at: string;
}

function JobRow({ job, expanded, runs, onClick, onFire, onForceFire, onToggle, onDelete, onReset, onToggleMode, onToggleOnFailure, onCancelDrain, onViewPipeline }: {
  job: Job; expanded: boolean; runs?: JobRun[];
  onClick: () => void; onFire: () => void; onForceFire: () => void; onToggle: () => void; onDelete: () => void; onReset: () => void;
  onToggleMode: () => void;
  onToggleOnFailure: () => void;
  onCancelDrain: (cancelInflight: boolean) => void;
  onViewPipeline?: (id: string) => void;
}) {
  // Dispatched-pipelines mini list — loaded lazily on first expand. Tells
  // the user "Job X spawned these pipelines, here's their state" without
  // having to navigate to PipelineView per row.
  const [pipes, setPipes] = useState<DispatchedPipeline[] | null>(null);
  const [pipesLoading, setPipesLoading] = useState(false);
  useEffect(() => {
    if (!expanded) return;
    let cancelled = false;
    setPipesLoading(true);
    fetch(`/api/jobs/${job.id}/dispatched-pipelines?limit=15`)
      .then((r) => r.json())
      .then((j) => { if (!cancelled) setPipes(j.pipelines || []); })
      .catch(() => { if (!cancelled) setPipes([]); })
      .finally(() => { if (!cancelled) setPipesLoading(false); });
    return () => { cancelled = true; };
  }, [expanded, job.id, job.last_run_at]);

  const hasScheduledDrain = !!job.next_run_at;
  const inflightCount = pipes ? pipes.filter((p) => p.pipeline_status === 'running' || p.pipeline_status === 'pending').length : 0;

  return (
    <div className="rounded border border-[var(--border)] bg-[var(--bg-secondary)]">
      <div onClick={onClick} className="px-3 py-2 cursor-pointer flex items-baseline gap-2">
        <span className="text-[10px] text-[var(--text-secondary)] w-3">{expanded ? '▾' : '▸'}</span>
        <div className="flex-1 min-w-0">
          <div className="flex items-baseline gap-2">
            <span className="text-[12px] font-semibold text-[var(--text-primary)] truncate">{job.name}</span>
            <span className={`text-[9px] px-1.5 py-0.5 rounded ${job.enabled ? 'bg-[var(--green)]/15 text-[var(--green)]' : 'bg-[var(--text-secondary)]/15 text-[var(--text-secondary)]'}`}>
              {job.enabled ? 'enabled' : 'disabled'}
            </span>
            {(job as any).busy && (
              <span
                className="text-[9px] px-1.5 py-0.5 rounded bg-[var(--accent)]/20 text-[var(--accent)]"
                title={(job as any).busy_reason}
              >running</span>
            )}
            {/* Show pacing mode — 'sequential' is the default (1 pipeline at a time);
                'parallel' fans out up to max_per_tick at once and is louder visually so
                the user knows fan-out can happen. */}
            <span
              className={`text-[9px] px-1.5 py-0.5 rounded ${
                (job as any).concurrency_mode === 'parallel'
                  ? 'bg-[var(--yellow)]/20 text-[var(--yellow)]'
                  : 'bg-[var(--text-secondary)]/15 text-[var(--text-secondary)]'
              }`}
              title={(job as any).concurrency_mode === 'parallel'
                ? 'parallel — each tick dispatches up to max_per_tick pipelines concurrently'
                : 'sequential — one pipeline at a time; next tick waits until prior finishes'}
            >
              {(job as any).concurrency_mode === 'parallel' ? 'parallel' : 'sequential'}
            </span>
            {(job as any).on_failure === 'stop' && (
              <span
                className="text-[9px] px-1.5 py-0.5 rounded bg-[var(--red)]/20 text-[var(--red)]"
                title="on_failure: stop — any item's pipeline failure halts the drain. Force-run to resume."
              >stop on fail</span>
            )}
          </div>
          <div className="text-[10px] text-[var(--text-secondary)] font-mono mt-0.5 truncate">
            {job.source_connector}.{job.source_tool} → {job.dispatch_type} · every {job.schedule_interval_minutes}m
          </div>
          <div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
            {job.last_run_at ? `last ${job.last_run_at}` : 'never run'} · {job.next_run_at ? `next ${job.next_run_at}` : 'next: pending'}
          </div>
        </div>
      </div>
      {expanded && (
        <div className="border-t border-[var(--border)] px-3 py-2 bg-[var(--bg-tertiary)]">
          <div className="flex items-center gap-2 mb-2">
            <button
              onClick={(e) => { e.stopPropagation(); if (!(job as any).busy) onFire(); }}
              disabled={!!(job as any).busy}
              className={`text-[10px] px-2 py-0.5 border border-[var(--border)] rounded ${(job as any).busy ? 'opacity-50 cursor-not-allowed' : 'hover:bg-[var(--bg-secondary)]'}`}
              title={(job as any).busy ? `Disabled: ${(job as any).busy_reason}` : 'Trigger one tick now'}
            >Run now</button>
            <button
              onClick={(e) => { e.stopPropagation(); if (!(job as any).busy) onForceFire(); }}
              disabled={!!(job as any).busy}
              className={`text-[10px] px-2 py-0.5 border border-[var(--border)] rounded ${(job as any).busy ? 'opacity-50 cursor-not-allowed' : 'hover:bg-[var(--bg-secondary)]'}`}
              title={(job as any).busy ? `Disabled: ${(job as any).busy_reason}` : 'Reset dedup + run — re-dispatch every item this tick'}
            >Force run</button>
            <button onClick={(e) => { e.stopPropagation(); onToggle(); }} className="text-[10px] px-2 py-0.5 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]">{job.enabled ? 'Disable' : 'Enable'}</button>
            <button onClick={(e) => { e.stopPropagation(); onReset(); }} className="text-[10px] px-2 py-0.5 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]" title="Wipe dedup state">Reset dedup</button>
            <button
              onClick={(e) => { e.stopPropagation(); onToggleMode(); }}
              className="text-[10px] px-2 py-0.5 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]"
              title={(job as any).concurrency_mode === 'parallel'
                ? 'Switch to sequential — one pipeline at a time'
                : 'Switch to parallel — fan out up to max_per_tick per tick'}
            >
              {(job as any).concurrency_mode === 'parallel' ? '→ Sequential' : '→ Parallel'}
            </button>
            <button
              onClick={(e) => { e.stopPropagation(); onToggleOnFailure(); }}
              className="text-[10px] px-2 py-0.5 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]"
              title={(job as any).on_failure === 'stop'
                ? 'on_failure=stop: halt drain on any failure. Click to switch to continue (default — skip failed item, proceed).'
                : 'on_failure=continue (default): keep dispatching even if items fail. Click to switch to stop (halt on first failure).'}
            >
              {(job as any).on_failure === 'stop' ? '→ Continue on fail' : '→ Stop on fail'}
            </button>
            <button onClick={(e) => { e.stopPropagation(); navigateLogs(`[jobs] ${job.id}`); }} className="text-[10px] px-2 py-0.5 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]" title="Tail forge.log filtered to this job's id">View logs</button>
            {/* Cancel — visible only when something is in flight or a drain is queued. Hidden otherwise to keep the action bar lean. */}
            {(hasScheduledDrain || inflightCount > 0) && (
              <>
                <button
                  onClick={(e) => { e.stopPropagation(); onCancelDrain(false); }}
                  className="text-[10px] px-2 py-0.5 border border-[var(--yellow)]/50 text-[var(--yellow)] rounded hover:bg-[var(--yellow)]/10"
                  title="Stop the sequential drain — clears next_run_at so no more items dispatch automatically. In-flight pipelines KEEP running."
                >Stop drain</button>
                {inflightCount > 0 && (
                  <button
                    onClick={(e) => { e.stopPropagation(); onCancelDrain(true); }}
                    className="text-[10px] px-2 py-0.5 border border-[var(--red)]/50 text-[var(--red)] rounded hover:bg-[var(--red)]/10"
                    title={`Stop drain AND cancel ${inflightCount} in-flight pipeline(s) this Job dispatched`}
                  >Cancel all ({inflightCount})</button>
                )}
              </>
            )}
            <span className="flex-1" />
            <button onClick={(e) => { e.stopPropagation(); onDelete(); }} className="text-[10px] px-2 py-0.5 border border-[var(--red)]/50 text-[var(--red)] rounded hover:bg-[var(--red)]/10">Delete</button>
          </div>

          {/* Dispatched pipelines panel — what this Job spawned, current state. */}
          {(pipes && pipes.length > 0) && (() => {
            // Aggregate counts by status for the summary header.
            const c = { running: 0, pending: 0, done: 0, failed: 0, cancelled: 0, unknown: 0 } as Record<string, number>;
            for (const p of pipes) { c[p.pipeline_status] = (c[p.pipeline_status] || 0) + 1; }
            // Best-effort "items waiting to be dispatched" hint from the
            // most recent run's deferred count. Sequential mode rolls
            // these over to the next tick — they are NOT in
            // dispatched-pipelines yet.
            const latestDeferred = (() => {
              if (!runs || runs.length === 0) return 0;
              const m = String(runs[0].notes || '').match(/(\d+)\s*item\(s\)\s*deferred/i);
              return m ? Number(m[1]) : 0;
            })();
            return (
            <div className="mb-2">
              <div className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-1 flex items-baseline gap-2 flex-wrap">
                <span>Dispatched pipelines</span>
                <span className="text-[10px] font-normal normal-case text-[var(--text-primary)]">
                  {pipes.length} total
                  {c.running   ? <> · <span className="text-[var(--accent)]">{c.running} running</span></>   : null}
                  {c.pending   ? <> · <span className="text-[var(--accent)]">{c.pending} pending</span></>   : null}
                  {c.done      ? <> · <span className="text-[var(--green)]">{c.done} done</span></>          : null}
                  {c.failed    ? <> · <span className="text-[var(--red)]">{c.failed} failed</span></>        : null}
                  {c.cancelled ? <> · <span className="text-[var(--text-secondary)]">{c.cancelled} cancelled</span></>: null}
                </span>
                {latestDeferred > 0 && (
                  <span className="text-[10px] font-normal normal-case text-[var(--yellow)]">
                    · {latestDeferred} waiting in queue (deferred)
                  </span>
                )}
                {pipesLoading && <span className="text-[var(--text-secondary)]">· loading…</span>}
              </div>
              <div className="space-y-0.5">
                {/* Trim to "what's actionable": all running/pending + the
                    one most-recent terminal entry. Full history would
                    bloat the panel and isn't useful here — go to the
                    Pipeline view for history. */}
                {(() => {
                  const active = pipes.filter((p) => p.pipeline_status === 'running' || p.pipeline_status === 'pending');
                  const lastTerminal = pipes.find((p) =>
                    p.pipeline_status !== 'running' && p.pipeline_status !== 'pending');
                  const display = lastTerminal ? [...active, lastTerminal] : active;
                  return display;
                })().map((p) => (
                  <div
                    key={p.dispatch_id}
                    className="flex items-baseline gap-2 text-[10px] font-mono py-0.5 px-1 rounded hover:bg-[var(--bg-secondary)]/50 cursor-pointer"
                    onClick={(e) => { e.stopPropagation(); if (onViewPipeline) onViewPipeline(p.pipeline_id); }}
                    title={`Click to open Pipeline view for ${p.pipeline_id}`}
                  >
                    <span className={
                      p.pipeline_status === 'running'   ? 'text-[var(--accent)]'
                    : p.pipeline_status === 'pending'   ? 'text-[var(--accent)]'
                    : p.pipeline_status === 'done'      ? 'text-[var(--green)]'
                    : p.pipeline_status === 'failed'    ? 'text-[var(--red)]'
                    : p.pipeline_status === 'cancelled' ? 'text-[var(--text-secondary)]'
                    :                                     'text-[var(--text-secondary)]'
                    }>
                      {p.pipeline_status === 'running'   ? '🔄'
                     : p.pipeline_status === 'pending'   ? '⏳'
                     : p.pipeline_status === 'done'      ? '✅'
                     : p.pipeline_status === 'failed'    ? '❌'
                     : p.pipeline_status === 'cancelled' ? '⏹'
                     :                                     '·'}
                    </span>
                    <span className="text-[var(--text-secondary)] w-20 truncate">{p.pipeline_id.slice(0, 8)}</span>
                    <span className="truncate flex-1 text-[var(--text-primary)]">{p.item_key}</span>
                    <span className="text-[var(--text-secondary)]">{p.pipeline_status}</span>
                  </div>
                ))}
              </div>
            </div>
            );
          })()}
          <div className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-1">Recent runs</div>
          {runs == null ? (
            <div className="text-[10px] text-[var(--text-secondary)]">Loading…</div>
          ) : runs.length === 0 ? (
            <div className="text-[10px] text-[var(--text-secondary)]">(no runs yet)</div>
          ) : (
            <div className="space-y-0.5">
              {runs.map((r) => (
                <RunRow key={r.id} jobId={job.id} run={r} onViewPipeline={onViewPipeline} />
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function RunRow({ jobId, run, onViewPipeline }: { jobId: string; run: JobRun; onViewPipeline?: (id: string) => void }) {
  const [expanded, setExpanded] = useState(false);
  const [dispatches, setDispatches] = useState<JobDispatch[] | null>(null);
  const [loading, setLoading] = useState(false);
  const [showLog, setShowLog] = useState(false);

  async function load() {
    if (dispatches != null || loading) return;
    setLoading(true);
    try {
      const r = await fetch(`/api/jobs/${encodeURIComponent(jobId)}/runs/${encodeURIComponent(run.id)}`);
      const j = await r.json();
      setDispatches(j.dispatches || []);
    } catch {} finally { setLoading(false); }
  }

  function onToggle() {
    const next = !expanded;
    setExpanded(next);
    if (next) void load();
  }

  async function copyLog() {
    if (!run.log) return;
    try { await navigator.clipboard.writeText(run.log); } catch {}
  }

  const dur = run.finished_at && run.started_at
    ? `${Math.round((new Date(run.finished_at).getTime() - new Date(run.started_at).getTime()) / 100) / 10}s`
    : '…';
  const color = run.status === 'ok' ? 'var(--green)' : run.status === 'error' ? 'var(--red)' : 'var(--text-secondary)';

  return (
    <div className="text-[10.5px]">
      <div onClick={onToggle} className="cursor-pointer flex items-baseline gap-1.5 py-0.5">
        <span className="text-[9px] text-[var(--text-secondary)] w-3">{expanded ? '▾' : '▸'}</span>
        <span className="font-mono text-[var(--text-secondary)]">{run.started_at}</span>
        <span style={{ color }}>{run.status}</span>
        <span className="text-[var(--text-secondary)]">seen={run.items_seen} new={run.items_new} dispatched={run.items_dispatched}</span>
        <span className="text-[var(--text-secondary)]">{dur}</span>
        {run.trigger === 'manual' && <span className="text-[9px] px-1 rounded bg-[var(--text-secondary)]/15 text-[var(--text-secondary)]">manual</span>}
      </div>
      {run.error && <div className="font-mono text-[10px] text-[var(--red)] pl-5 py-0.5">{run.error.slice(0, 300)}</div>}
      {run.notes && <div className="text-[10px] text-[var(--text-secondary)] pl-5 py-0.5 italic">{run.notes}</div>}
      {expanded && (
        <div className="pl-5 pr-1 pb-1 pt-0.5 space-y-0.5 border-l border-[var(--border)] ml-1.5">
          <div className="flex items-center gap-2 py-0.5">
            <span className="text-[10px] text-[var(--text-secondary)] uppercase">Dispatches</span>
            <span className="flex-1" />
            {run.log && (
              <>
                <button
                  onClick={() => setShowLog((v) => !v)}
                  className="text-[10px] px-1.5 py-0 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]"
                >
                  {showLog ? 'hide log' : `show log (${run.log.split('\n').length} lines)`}
                </button>
                {showLog && (
                  <button onClick={copyLog} className="text-[10px] px-1.5 py-0 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]">copy</button>
                )}
              </>
            )}
          </div>
          {showLog && run.log && (
            <pre className="text-[10px] font-mono whitespace-pre-wrap leading-snug p-2 bg-black/30 text-[var(--text-secondary)] border border-[var(--border)] rounded max-h-80 overflow-auto">
              {run.log}
            </pre>
          )}
          {loading ? (
            <div className="text-[10px] text-[var(--text-secondary)]">Loading dispatches…</div>
          ) : dispatches == null || dispatches.length === 0 ? (
            <div className="text-[10px] text-[var(--text-secondary)]">(no dispatches)</div>
          ) : (
            dispatches.map((d) => <DispatchRow key={d.id} d={d} onViewPipeline={onViewPipeline} />)
          )}
        </div>
      )}
    </div>
  );
}

function DispatchRow({ d, onViewPipeline }: { d: JobDispatch; onViewPipeline?: (id: string) => void }) {
  function open() {
    if (!d.dispatch_target_id) return;
    if (d.dispatch_type === 'pipeline' && onViewPipeline) {
      onViewPipeline(d.dispatch_target_id);
    } else if (d.dispatch_type === 'chat') {
      // No web chat surface yet — copy session id for use in the extension.
      navigator.clipboard?.writeText(d.dispatch_target_id).catch(() => {});
    }
  }

  const statusColor = d.status === 'dispatched' ? 'var(--green)' : 'var(--red)';

  return (
    <div className="text-[10px] py-0.5">
      <div className="flex items-center gap-1.5">
        <span className="font-mono text-[var(--text-secondary)] w-16 shrink-0 truncate">{d.item_key}</span>
        <span className="flex-1 min-w-0 truncate text-[var(--text-primary)]">{d.item_preview || '(no preview)'}</span>
        <span style={{ color: statusColor }} className="text-[9px]">{d.status}</span>
        {d.dispatch_target_id && (
          <button
            onClick={open}
            className="text-[9px] px-1 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]"
            title={d.dispatch_type === 'pipeline' ? 'Open pipeline run' : 'Copy chat session id'}
          >
            {d.dispatch_type === 'pipeline' ? 'open →' : 'copy id'}
          </button>
        )}
      </div>
      {d.error && <div className="font-mono text-[9.5px] text-[var(--red)] pl-1">{d.error.slice(0, 200)}</div>}
    </div>
  );
}
