'use client';

import { useEffect, useState, useCallback } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import { PageHeader } from '@bytelyst/dashboard-components';
import { Button } from '@/components/ui/Primitives';
import { useAuth } from '@/lib/auth-context';
import {
  getJob,
  getJobRuns,
  getJobEvents,
  getJobArtifacts,
  getArtifactDownloadUrl,
  getJobDag,
  getJobExplain,
  patchJob,
  operatorAction,
  updateDraft,
  submitDraft,
  requestReview,
  submitReview,
  reconcilePrState,
  subscribeJobEvents,
  FLEET_ENGINES,
  type OperatorAction,
  type FleetEngine,
  type FleetJob,
  type FleetRun,
  type FleetEvent,
  type FleetArtifact,
  type DagNode,
  type JobExplain,
} from '@/lib/fleet-client';

export default function FleetJobDetailPage() {
  const { token, user } = useAuth();
  const params = useParams();
  const jobId = params.id as string;

  const [job, setJob] = useState<FleetJob | null>(null);
  const [runs, setRuns] = useState<FleetRun[]>([]);
  const [events, setEvents] = useState<FleetEvent[]>([]);
  const [artifacts, setArtifacts] = useState<FleetArtifact[]>([]);
  const [dag, setDag] = useState<DagNode | null>(null);
  const [explain, setExplain] = useState<JobExplain | null>(null);
  const [loading, setLoading] = useState(true);
  const [shipping, setShipping] = useState(false);
  const [acting, setActing] = useState<OperatorAction | null>(null);
  const [reviewing, setReviewing] = useState(false);
  const [reconcilingPr, setReconcilingPr] = useState(false);
  const [eventFilter, setEventFilter] = useState<string>('');
  const [streamMode, setStreamMode] = useState<'connecting' | 'live' | 'polling'>('connecting');

  const refresh = useCallback(async () => {
    try {
      const [j, r, e, a, d, x] = await Promise.all([
        getJob(jobId),
        getJobRuns(jobId),
        getJobEvents(jobId),
        getJobArtifacts(jobId),
        getJobDag(jobId),
        getJobExplain(jobId),
      ]);
      setJob(j);
      setRuns(r.runs);
      // Merge (not replace) so a slow snapshot can't clobber newer streamed
      // events — the event log is append-only, keyed by monotonic `seq`.
      setEvents(prev => {
        const bySeq = new Map(prev.map(x => [x.seq, x]));
        for (const ev of e.events) bySeq.set(ev.seq, ev);
        return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
      });
      setArtifacts(a.artifacts);
      setDag(d?.dag ?? null);
      setExplain(x);
    } catch {
      /* degrade */
    } finally {
      setLoading(false);
    }
  }, [jobId]);

  useEffect(() => {
    if (!token || !jobId) return;
    refresh();
  }, [token, jobId, refresh]);

  // Live event stream: subscribe via SSE once authenticated; append new events
  // (deduped by seq). Fall back to polling if streaming is unavailable, and
  // surface that degraded state to the operator (live vs polling badge).
  useEffect(() => {
    if (!token || !jobId) return;
    let pollTimer: ReturnType<typeof setInterval> | undefined;
    setStreamMode('connecting');

    const appendEvent = (e: FleetEvent) => {
      setEvents(prev => (prev.some(x => x.seq === e.seq) ? prev : [...prev, e]));
      setStreamMode('live');
    };

    const startPolling = () => {
      if (pollTimer) return;
      setStreamMode('polling');
      pollTimer = setInterval(() => {
        getJobEvents(jobId)
          .then(r => setEvents(r.events))
          .catch(() => {});
      }, 4000);
    };

    const sub = subscribeJobEvents(
      jobId,
      { onEvent: appendEvent, onError: startPolling },
      { lastEventId: -1 }
    );

    return () => {
      sub.close();
      if (pollTimer) clearInterval(pollTimer);
    };
  }, [token, jobId]);

  const handleShip = async () => {
    if (!job) return;
    setShipping(true);
    try {
      await patchJob(jobId, { leaseEpoch: job.leaseEpoch, stage: 'shipped' });
      await refresh(); // pull updated job + runs (PR may now be merged)
    } catch {
      /* show error in production */
    } finally {
      setShipping(false);
    }
  };

  const handleAction = async (action: OperatorAction) => {
    if (!job) return;
    setActing(action);
    try {
      const updated = await operatorAction(jobId, action);
      setJob(updated);
      await refresh();
    } catch {
      /* show error in production */
    } finally {
      setActing(null);
    }
  };

  const handleRequestReview = async () => {
    if (!job) return;
    setReviewing(true);
    try {
      const updated = await requestReview(jobId);
      setJob(updated);
      await refresh();
    } catch {
      /* show error in production */
    } finally {
      setReviewing(false);
    }
  };

  const handleReconcilePr = async () => {
    setReconcilingPr(true);
    try {
      await reconcilePrState(jobId);
      await refresh(); // pull the (possibly now merged) run state
    } catch {
      /* show error in production */
    } finally {
      setReconcilingPr(false);
    }
  };

  const handleReview = async (decision: 'approve' | 'reject') => {
    if (!job || !user) return;
    setReviewing(true);
    try {
      const updated = await submitReview(jobId, { reviewer: user.email, decision });
      setJob(updated);
      await refresh();
    } catch {
      /* show error in production */
    } finally {
      setReviewing(false);
    }
  };

  if (loading) {
    return (
      <div className="p-6">
        <PageHeader title="Job Detail" />
        <p className="text-muted-foreground mt-4">Loading...</p>
      </div>
    );
  }

  if (!job) {
    return (
      <div className="p-6">
        <PageHeader title="Job Not Found" />
        <p className="text-muted-foreground mt-4">The requested job does not exist.</p>
        <Link href="/dashboard/fleet/jobs" className="text-sm underline mt-2 inline-block">
          ← Back to jobs
        </Link>
      </div>
    );
  }

  return (
    <div className="p-6 space-y-8">
      <div className="flex items-center justify-between">
        <PageHeader title={job.idempotencyKey} />
        <div className="flex items-center gap-2">
          {job.stage !== 'shipped' && job.stage !== 'failed' && (
            <Button onClick={handleShip} disabled={shipping} aria-label="Ship this job">
              {shipping
                ? 'Shipping...'
                : runs.some(r => r.prUrl && r.prState !== 'merged')
                  ? 'Ship & merge PR ✓'
                  : 'Ship ✓'}
            </Button>
          )}
          {job.stage === 'building' && (
            <Button
              variant="secondary"
              onClick={handleRequestReview}
              disabled={reviewing}
              aria-label="Send this job to review"
            >
              {reviewing ? 'Sending...' : 'Send to review'}
            </Button>
          )}
          {job.stage !== 'shipped' && (
            <Button
              variant="secondary"
              onClick={() => handleAction('requeue')}
              disabled={acting !== null}
              aria-label="Requeue this job"
            >
              {acting === 'requeue' ? 'Requeuing...' : 'Requeue'}
            </Button>
          )}
          {(job.stage === 'dead_letter' || job.stage === 'failed') && (
            <Button
              onClick={() => handleAction('redrive')}
              disabled={acting !== null}
              aria-label="Re-drive this job with a fresh retry budget"
              title="Requeue with a fresh retry budget (resets the attempt counter)"
            >
              {acting === 'redrive' ? 'Re-driving...' : 'Re-drive ↻'}
            </Button>
          )}
          {job.stage !== 'shipped' && job.stage !== 'failed' && job.stage !== 'dead_letter' && (
            <>
              <Button
                variant="destructive"
                onClick={() => handleAction('cancel')}
                disabled={acting !== null}
                aria-label="Cancel this job"
              >
                {acting === 'cancel' ? 'Cancelling...' : 'Cancel'}
              </Button>
              <Button
                variant="destructive"
                onClick={() => handleAction('reject')}
                disabled={acting !== null}
                aria-label="Reject this job to dead letter"
              >
                {acting === 'reject' ? 'Rejecting...' : 'Reject'}
              </Button>
            </>
          )}
        </div>
      </div>

      {/* Job metadata */}
      <section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
        <MetaCard label="Stage" value={job.stage} />
        <MetaCard label="Priority" value={job.priority} />
        <MetaCard label="Kind" value={job.kind} />
        <MetaCard label="Attempts" value={String(job.attempts)} />
      </section>

      {/* Prompt (the job body) + PR/target config */}
      <PromptCard job={job} onChanged={refresh} />

      {/* Pull request (surfaced from whichever run opened it) */}
      {(() => {
        const prRun = runs.find(r => r.prUrl);
        if (prRun?.prUrl) {
          return (
            <section
              className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/20 p-3"
              aria-label="Pull request"
            >
              <span className="text-sm font-semibold">Pull Request</span>
              <a
                href={prRun.prUrl}
                target="_blank"
                rel="noopener noreferrer"
                className="text-sm text-primary hover:underline break-all"
              >
                {prRun.prUrl} ↗
              </a>
              {prRun.prState && (
                <span
                  className={`rounded-full px-2 py-0.5 text-xs font-medium ${
                    prRun.prState === 'merged'
                      ? 'bg-green-600/15 text-green-700 dark:text-green-400'
                      : 'bg-muted text-muted-foreground'
                  }`}
                >
                  {prRun.prState}
                </span>
              )}
              {prRun.prState !== 'merged' && (
                <Button
                  variant="secondary"
                  onClick={handleReconcilePr}
                  disabled={reconcilingPr}
                  aria-label="Check GitHub for the PR's current merge state"
                  title="Detect a PR merged directly in GitHub and update its state here"
                >
                  {reconcilingPr ? 'Checking...' : 'Refresh PR status'}
                </Button>
              )}
            </section>
          );
        }
        // Parked in review with no PR — explain why + what to do.
        if (job.repo && (job.stage === 'review' || job.stage === 'testing')) {
          return (
            <section
              className="rounded-lg border border-amber-500/40 bg-amber-500/5 p-3 text-sm"
              aria-label="No pull request"
            >
              <span className="font-semibold">No pull request yet.</span>{' '}
              <span className="text-muted-foreground">
                This run didn’t open a PR — it ran without a <code>verify</code> step, or the
                factory wasn’t in PR mode. Set a verify command and requeue to produce one, or
                <strong> Ship</strong> to promote as-is.
              </span>
            </section>
          );
        }
        return null;
      })()}

      {/* Review gate (multi-reviewer human gate) */}
      {job.stage === 'review' && (
        <ReviewGateCard
          job={job}
          reviewing={reviewing}
          onApprove={() => handleReview('approve')}
          onReject={() => handleReview('reject')}
        />
      )}

      {/* DAG subtree (if present) */}
      {dag && dag.children.length > 0 && (
        <section>
          <h2 className="text-lg font-semibold mb-2">DAG Subtree</h2>
          <DagTree node={dag} />
        </section>
      )}

      {/* Routing explainability (§7) */}
      {explain && <ExplainPanel explain={explain} />}

      {/* Event timeline */}
      <section>
        <h2 className="text-lg font-semibold mb-2 flex items-center gap-2">
          Event Timeline
          {streamMode === 'live' && (
            <span
              className="inline-flex items-center gap-1 text-xs font-medium text-green-600"
              data-testid="live-indicator"
            >
              <span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
              Live
            </span>
          )}
          {streamMode === 'polling' && (
            <span
              className="inline-flex items-center gap-1 text-xs font-medium text-amber-600"
              data-testid="polling-indicator"
              title="Live stream unavailable — refreshing every 4s instead."
            >
              <span className="h-2 w-2 rounded-full bg-amber-500" />
              Polling
            </span>
          )}
          {events.length > 0 && (
            <select
              value={eventFilter}
              onChange={e => setEventFilter(e.target.value)}
              aria-label="Filter timeline by event type"
              className="ml-auto rounded border bg-background px-2 py-1 text-xs font-normal"
            >
              <option value="">All events</option>
              {[...new Set(events.map(e => e.type))].sort().map(t => (
                <option key={t} value={t}>
                  {t}
                </option>
              ))}
            </select>
          )}
        </h2>
        {events.length === 0 ? (
          <p className="text-muted-foreground text-sm">No events recorded.</p>
        ) : eventFilter ? (
          <ul className="space-y-2">
            {events
              .filter(e => e.type === eventFilter)
              .map(e => (
                <li
                  key={e.id}
                  className="flex items-start gap-3 text-sm border-l-2 border-muted pl-3 py-1"
                >
                  <span className="text-muted-foreground text-xs whitespace-nowrap">
                    {new Date(e.at).toLocaleTimeString()}
                  </span>
                  <span className="font-medium">{e.type}</span>
                  {e.actor && <span className="text-muted-foreground">by {e.actor}</span>}
                </li>
              ))}
          </ul>
        ) : (
          <ul className="space-y-2">
            {groupTimelineEvents(events).map(g =>
              g.kind === 'single' ? (
                <li
                  key={g.event.id}
                  className="flex items-start gap-3 text-sm border-l-2 border-muted pl-3 py-1"
                >
                  <span className="text-muted-foreground text-xs whitespace-nowrap">
                    {new Date(g.event.at).toLocaleTimeString()}
                  </span>
                  <span className="font-medium">{g.event.type}</span>
                  {g.event.actor && (
                    <span className="text-muted-foreground">by {g.event.actor}</span>
                  )}
                </li>
              ) : (
                <li
                  key={g.key}
                  className="flex items-start gap-3 text-sm border-l-2 border-dashed border-muted pl-3 py-1 text-muted-foreground"
                  title={`${g.count} ${g.type} events from ${new Date(g.first).toLocaleTimeString()} to ${new Date(g.last).toLocaleTimeString()}`}
                >
                  <span className="text-xs whitespace-nowrap">
                    {new Date(g.first).toLocaleTimeString()}
                  </span>
                  <span>
                    {g.type} <span className="tabular-nums">×{g.count}</span>
                    <span className="text-xs"> · over {fmtDuration(g.last - g.first)}</span>
                  </span>
                </li>
              )
            )}
          </ul>
        )}
      </section>

      {/* Runs */}
      <section>
        <div className="flex items-center justify-between mb-2">
          <h2 className="text-lg font-semibold">Runs</h2>
          {runs.length > 0 &&
            (() => {
              const t = runTotals(runs);
              return (
                <div className="flex flex-wrap gap-2" aria-label="Job totals">
                  <Stat
                    label="Total cost"
                    value={
                      t.costUsd > 0
                        ? `${t.estimated ? '~' : ''}${fmtUsd(t.costUsd)}${t.estimated ? ' approx' : ''}`
                        : '—'
                    }
                  />
                  <Stat label="Tokens in" value={t.tokensIn > 0 ? fmtNum(t.tokensIn) : '—'} />
                  <Stat label="Tokens out" value={t.tokensOut > 0 ? fmtNum(t.tokensOut) : '—'} />
                  <Stat
                    label="Total time"
                    value={t.durationMs > 0 ? fmtDuration(t.durationMs) : '—'}
                  />
                </div>
              );
            })()}
        </div>
        {runs.length === 0 ? (
          <p className="text-muted-foreground text-sm">No runs yet.</p>
        ) : (
          <div className="overflow-x-auto">
            <table className="w-full text-sm" aria-label="Job runs">
              <thead>
                <tr className="border-b text-left text-muted-foreground">
                  <th className="pb-2 pr-4">Attempt</th>
                  <th className="pb-2 pr-4">Engine</th>
                  <th className="pb-2 pr-4">Factory</th>
                  <th className="pb-2 pr-4">Result</th>
                  <th className="pb-2 pr-4">Started</th>
                  <th className="pb-2 pr-4">Duration</th>
                  <th className="pb-2 pr-4">Model</th>
                  <th className="pb-2 pr-4 text-right">Tokens (in/out)</th>
                  <th className="pb-2 pr-4 text-right">Cost</th>
                  <th className="pb-2">PR</th>
                </tr>
              </thead>
              <tbody>
                {runs.map(r => {
                  const ins = r.insights ?? {};
                  const dur = r.endedAt ? Date.parse(r.endedAt) - Date.parse(r.startedAt) : null;
                  const tin = ins.tokensIn;
                  const tout = ins.tokensOut;
                  const tcached = ins.tokensCached;
                  return (
                    <tr key={r.id} className="border-b last:border-0">
                      <td className="py-2 pr-4">#{r.attempt}</td>
                      <td className="py-2 pr-4 font-mono text-xs">
                        {ins.engine ?? (r.engine && r.engine !== 'unknown' ? r.engine : '—')}
                        {ins.sessionId && (
                          <div className="mt-0.5">
                            {ins.sessionUrl ? (
                              <a
                                href={ins.sessionUrl}
                                target="_blank"
                                rel="noopener noreferrer"
                                className="text-primary hover:underline"
                                title={`Open agent session ${ins.sessionId}`}
                              >
                                open session ↗
                              </a>
                            ) : (
                              <span
                                className="text-muted-foreground"
                                title={`Resume locally: devin --resume ${ins.sessionId}`}
                              >
                                session: {ins.sessionId}
                              </span>
                            )}
                          </div>
                        )}
                      </td>
                      <td className="py-2 pr-4 font-mono text-xs">{r.factoryId ?? '—'}</td>
                      <td className="py-2 pr-4">{r.result ?? 'running'}</td>
                      <td className="py-2 pr-4 text-xs text-muted-foreground">
                        {new Date(r.startedAt).toLocaleString()}
                      </td>
                      <td className="py-2 pr-4 text-xs">
                        {dur != null && dur >= 0 ? fmtDuration(dur) : '—'}
                      </td>
                      <td className="py-2 pr-4 font-mono text-xs">{ins.model ?? '—'}</td>
                      <td className="py-2 pr-4 text-right font-mono text-xs">
                        {tin != null || tout != null ? (
                          <>
                            {fmtNum(tin ?? 0)} / {fmtNum(tout ?? 0)}
                            {tcached ? (
                              <span className="text-muted-foreground">
                                {' '}
                                (+{fmtNum(tcached)} cached)
                              </span>
                            ) : null}
                          </>
                        ) : (
                          '—'
                        )}
                      </td>
                      <td className="py-2 text-right font-mono text-xs">
                        {ins.costUsd != null ? (
                          <>
                            {ins.estimated ? '~' : ''}
                            {fmtUsd(ins.costUsd)}
                            {ins.estimated ? (
                              <span className="text-muted-foreground"> approx</span>
                            ) : null}
                          </>
                        ) : (
                          '—'
                        )}
                      </td>
                      <td className="py-2 text-xs">
                        {r.prUrl ? (
                          <span className="inline-flex items-center gap-1">
                            <a
                              href={r.prUrl}
                              target="_blank"
                              rel="noopener noreferrer"
                              className="text-primary hover:underline"
                              aria-label="Open pull request"
                            >
                              PR ↗
                            </a>
                            {r.prState && (
                              <span
                                className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${
                                  r.prState === 'merged'
                                    ? 'bg-green-600/15 text-green-700 dark:text-green-400'
                                    : 'bg-muted text-muted-foreground'
                                }`}
                              >
                                {r.prState}
                              </span>
                            )}
                          </span>
                        ) : (
                          '—'
                        )}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </section>

      {/* Artifacts */}
      <section>
        <h2 className="text-lg font-semibold mb-2">Artifacts</h2>
        {artifacts.length === 0 ? (
          <p className="text-muted-foreground text-sm">No artifacts.</p>
        ) : (
          <ul className="space-y-1">
            {artifacts.map(a => (
              <li key={a.id} className="text-sm flex items-center gap-2">
                <span className="rounded bg-muted px-1.5 py-0.5 text-xs">{a.kind}</span>
                <span className="font-mono text-xs">{a.contentType}</span>
                <span className="text-muted-foreground text-xs">
                  ({(a.sizeBytes / 1024).toFixed(1)} KB)
                </span>
                <button
                  type="button"
                  className="text-xs underline text-muted-foreground hover:text-foreground"
                  onClick={async () => {
                    const url = await getArtifactDownloadUrl(a.id);
                    if (url) window.open(url, '_blank', 'noopener');
                  }}
                >
                  {a.kind === 'log' ? 'view log' : 'download'}
                </button>
              </li>
            ))}
          </ul>
        )}
      </section>

      <Link href="/dashboard/fleet/jobs" className="text-sm underline inline-block">
        ← Back to jobs
      </Link>
    </div>
  );
}

function MetaCard({ label, value }: { label: string; value: string }) {
  return (
    <div className="rounded-lg border p-3">
      <p className="text-xs text-muted-foreground">{label}</p>
      <p className="text-sm font-medium capitalize">{value}</p>
    </div>
  );
}

/**
 * The job's prompt (verbatim `bodyMd`) + its PR/target config. The prompt is only
 * mutable before a factory picks the job up (stage `draft`/`queued`/`blocked`):
 * it can be edited + saved in place, and a `draft` can be submitted from here.
 * Once a factory claims it (assigned+) the prompt is locked and renders read-only.
 */
function PromptCard({ job, onChanged }: { job: FleetJob; onChanged: () => void | Promise<void> }) {
  const editable = job.stage === 'draft' || job.stage === 'queued' || job.stage === 'blocked';
  const [editing, setEditing] = useState(false);
  const [draftBody, setDraftBody] = useState(job.bodyMd ?? '');
  const [editEngine, setEditEngine] = useState<FleetEngine>(job.engine ?? 'devin');
  const [editRepo, setEditRepo] = useState(job.repo ?? '');
  const [editBranch, setEditBranch] = useState(job.baseBranch ?? '');
  const [editVerify, setEditVerify] = useState(job.verify ?? '');
  const [editAutoMerge, setEditAutoMerge] = useState(!!job.autoMerge);
  const [busy, setBusy] = useState<null | 'save' | 'submit'>(null);
  const [err, setErr] = useState<string | null>(null);

  const beginEdit = () => {
    setDraftBody(job.bodyMd ?? '');
    setEditEngine(job.engine ?? 'devin');
    setEditRepo(job.repo ?? '');
    setEditBranch(job.baseBranch ?? '');
    setEditVerify(job.verify ?? '');
    setEditAutoMerge(!!job.autoMerge);
    setErr(null);
    setEditing(true);
  };

  const save = async () => {
    if (!draftBody.trim()) {
      setErr('Prompt cannot be empty.');
      return;
    }
    setBusy('save');
    setErr(null);
    try {
      const repo = editRepo.trim();
      await updateDraft(job.id, {
        bodyMd: draftBody.trim(),
        engine: editEngine,
        ...(repo
          ? {
              repo,
              baseBranch: editBranch.trim() || 'main',
              autoMerge: editAutoMerge,
              ...(editVerify.trim() ? { verify: editVerify.trim() } : {}),
            }
          : {}),
      });
      setEditing(false);
      await onChanged();
    } catch (e) {
      setErr(e instanceof Error ? e.message : 'Save failed.');
    } finally {
      setBusy(null);
    }
  };

  const submit = async () => {
    setBusy('submit');
    setErr(null);
    try {
      await submitDraft(job.id);
      await onChanged();
    } catch (e) {
      setErr(e instanceof Error ? e.message : 'Submit failed.');
    } finally {
      setBusy(null);
    }
  };

  const cfg: Array<[string, string | undefined]> = [
    ['Engine', job.engine],
    ['Repo', job.repo],
    ['Base branch', job.repo ? (job.baseBranch ?? 'main') : undefined],
    ['Verify', job.verify],
    ['Auto-merge', job.repo ? (job.autoMerge ? 'yes' : 'no') : undefined],
    ['Capabilities', job.capabilities?.length ? job.capabilities.join(', ') : undefined],
    ['Deps', job.deps?.length ? job.deps.join(', ') : undefined],
    ['Idempotency key', job.idempotencyKey],
  ];
  const shown = cfg.filter(([, v]) => v != null && v !== '');

  return (
    <section className="space-y-3" aria-label="Prompt and target">
      <div className="flex items-center gap-2">
        <h2 className="text-lg font-semibold">Prompt</h2>
        <span
          className={`rounded-full px-2 py-0.5 text-xs font-medium ${
            editable
              ? 'bg-blue-500/15 text-blue-700 dark:text-blue-300'
              : 'bg-muted text-muted-foreground'
          }`}
          title={
            editable
              ? 'Not yet picked up — the prompt can still be edited.'
              : 'The job has been picked up — its prompt is locked.'
          }
        >
          {editable
            ? job.stage === 'draft'
              ? 'draft — editable'
              : 'editable until picked up'
            : 'read-only — picked up'}
        </span>
        <div className="ml-auto flex items-center gap-2">
          {editable && !editing && (
            <Button variant="secondary" onClick={beginEdit} aria-label="Edit prompt and config">
              Edit
            </Button>
          )}
          {job.stage === 'draft' && !editing && (
            <Button onClick={submit} disabled={busy !== null} aria-label="Submit this draft">
              {busy === 'submit' ? 'Submitting…' : 'Submit'}
            </Button>
          )}
        </div>
      </div>

      {editing ? (
        <div className="space-y-2">
          <textarea
            value={draftBody}
            onChange={e => setDraftBody(e.target.value)}
            rows={10}
            className="w-full rounded-lg border bg-background p-3 text-sm font-mono"
            aria-label="Edit job prompt"
          />
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="text-xs text-muted-foreground">
              Engine
              <select
                value={editEngine}
                onChange={e => setEditEngine(e.target.value as FleetEngine)}
                className="mt-1 w-full rounded border bg-background px-2 py-1 text-sm"
              >
                {FLEET_ENGINES.map(e => (
                  <option key={e} value={e}>
                    {e}
                  </option>
                ))}
              </select>
            </label>
            <label className="text-xs text-muted-foreground">
              Repo (optional — opens a PR)
              <input
                value={editRepo}
                onChange={e => setEditRepo(e.target.value)}
                placeholder="e.g. learning_ai_notes"
                className="mt-1 w-full rounded border bg-background px-2 py-1 text-sm font-mono"
              />
            </label>
            <label className="text-xs text-muted-foreground">
              Base branch
              <input
                value={editBranch}
                onChange={e => setEditBranch(e.target.value)}
                placeholder="main"
                disabled={!editRepo.trim()}
                className="mt-1 w-full rounded border bg-background px-2 py-1 text-sm font-mono disabled:opacity-50"
              />
            </label>
            <label className="text-xs text-muted-foreground">
              Verify command (needed to open a PR)
              <input
                value={editVerify}
                onChange={e => setEditVerify(e.target.value)}
                placeholder="e.g. pnpm run verify"
                disabled={!editRepo.trim()}
                className="mt-1 w-full rounded border bg-background px-2 py-1 text-sm font-mono disabled:opacity-50"
              />
            </label>
            <label className="flex items-center gap-2 self-end text-sm">
              <input
                type="checkbox"
                checked={editAutoMerge}
                onChange={e => setEditAutoMerge(e.target.checked)}
                disabled={!editRepo.trim()}
              />
              Auto-merge PR
            </label>
          </div>
          <div className="flex items-center gap-2">
            <Button onClick={save} disabled={busy !== null} aria-label="Save prompt">
              {busy === 'save' ? 'Saving…' : 'Save'}
            </Button>
            <Button
              variant="secondary"
              onClick={() => {
                setEditing(false);
                setErr(null);
              }}
              disabled={busy !== null}
              aria-label="Cancel editing"
            >
              Cancel
            </Button>
          </div>
        </div>
      ) : (
        <pre
          className="max-h-96 overflow-auto whitespace-pre-wrap rounded-lg border bg-muted/30 p-3 text-sm font-mono"
          aria-label="Job prompt body"
        >
          {job.bodyMd?.trim() ? job.bodyMd : 'No prompt body.'}
        </pre>
      )}

      {err && (
        <p className="text-sm text-destructive" role="status">
          {err}
        </p>
      )}

      {shown.length > 0 && (
        <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
          {shown.map(([k, v]) => (
            <div key={k} className="rounded-lg border p-3">
              <p className="text-xs text-muted-foreground">{k}</p>
              <p className="text-sm font-medium break-words font-mono">{v}</p>
            </div>
          ))}
        </div>
      )}
    </section>
  );
}

/** Compact stat chip for the per-job cost/token/time totals. */
function Stat({ label, value }: { label: string; value: string }) {
  return (
    <span className="rounded-md border px-2 py-1 text-xs">
      <span className="text-muted-foreground">{label}: </span>
      <span className="font-medium font-mono">{value}</span>
    </span>
  );
}

type TimelineGroup =
  | { kind: 'single'; event: FleetEvent }
  | { kind: 'collapsed'; key: string; type: string; count: number; first: number; last: number };

/** High-frequency event types collapsed into one summary row so a long-running
 *  job's timeline isn't buried under hundreds of heartbeats. */
const COLLAPSIBLE_EVENT_TYPES = new Set(['lease_renewed']);

/** Fold consecutive runs of a collapsible event type (e.g. lease_renewed) into a
 *  single "type ×N · over Xm" row; everything else renders verbatim, in order. */
function groupTimelineEvents(events: FleetEvent[]): TimelineGroup[] {
  const out: TimelineGroup[] = [];
  let run: FleetEvent[] = [];
  const flush = () => {
    if (run.length === 0) return;
    if (run.length === 1) {
      out.push({ kind: 'single', event: run[0]! });
    } else {
      const first = run[0]!;
      const last = run[run.length - 1]!;
      out.push({
        kind: 'collapsed',
        key: `grp-${first.id}-${last.id}`,
        type: first.type,
        count: run.length,
        first: Date.parse(first.at),
        last: Date.parse(last.at),
      });
    }
    run = [];
  };
  for (const e of events) {
    if (COLLAPSIBLE_EVENT_TYPES.has(e.type)) {
      if (run.length > 0 && run[0]!.type === e.type) run.push(e);
      else {
        flush();
        run = [e];
      }
    } else {
      flush();
      out.push({ kind: 'single', event: e });
    }
  }
  flush();
  return out;
}

/** Sum cost / tokens / wall-time across a job's runs. */
function runTotals(runs: FleetRun[]) {
  let costUsd = 0;
  let tokensIn = 0;
  let tokensOut = 0;
  let durationMs = 0;
  let estimated = false;
  for (const r of runs) {
    const ins = r.insights ?? {};
    costUsd += ins.costUsd ?? 0;
    if (ins.estimated) estimated = true;
    tokensIn += ins.tokensIn ?? 0;
    tokensOut += ins.tokensOut ?? 0;
    if (r.endedAt) {
      const d = Date.parse(r.endedAt) - Date.parse(r.startedAt);
      if (Number.isFinite(d) && d > 0) durationMs += d;
    }
  }
  return { costUsd, tokensIn, tokensOut, durationMs, estimated };
}

function fmtUsd(n: number): string {
  return n < 0.01 && n > 0 ? `$${n.toFixed(4)}` : `$${n.toFixed(2)}`;
}

function fmtNum(n: number): string {
  return n.toLocaleString();
}

function fmtDuration(ms: number): string {
  if (ms < 1000) return `${ms}ms`;
  const s = Math.round(ms / 1000);
  if (s < 60) return `${s}s`;
  const m = Math.floor(s / 60);
  const rem = s % 60;
  if (m < 60) return rem ? `${m}m ${rem}s` : `${m}m`;
  const h = Math.floor(m / 60);
  return `${h}h ${m % 60}m`;
}

function ReviewGateCard({
  job,
  reviewing,
  onApprove,
  onReject,
}: {
  job: FleetJob;
  reviewing: boolean;
  onApprove: () => void;
  onReject: () => void;
}) {
  const decisions = job.reviewDecisions ?? [];
  const required = job.reviewPolicy?.requiredApprovals ?? 1;
  const reviewers = job.reviewPolicy?.reviewers ?? [];
  const approvals = new Set(decisions.filter(d => d.decision === 'approve').map(d => d.reviewer))
    .size;

  return (
    <section
      className="rounded-lg border border-purple-500/40 bg-purple-500/5 p-4 space-y-3"
      aria-label="Review gate"
      data-testid="review-gate"
    >
      <div className="flex items-center justify-between">
        <h2 className="text-lg font-semibold">Review gate</h2>
        <span className="text-sm tabular-nums" data-testid="review-progress">
          {approvals} / {required} approvals
        </span>
      </div>
      {reviewers.length > 0 && (
        <p className="text-xs text-muted-foreground">Reviewers: {reviewers.join(', ')}</p>
      )}
      {decisions.length > 0 && (
        <ul className="space-y-1 text-sm" aria-label="Review decisions">
          {decisions.map(d => (
            <li key={d.reviewer} className="flex items-center gap-2">
              <span
                className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
                  d.decision === 'approve'
                    ? 'bg-green-500/20 text-green-700 dark:text-green-400'
                    : 'bg-red-500/20 text-red-700 dark:text-red-400'
                }`}
              >
                {d.decision}
              </span>
              <span className="font-mono text-xs">{d.reviewer}</span>
              {d.note && <span className="text-xs text-muted-foreground">— {d.note}</span>}
            </li>
          ))}
        </ul>
      )}
      <div className="flex gap-2">
        <Button onClick={onApprove} disabled={reviewing} aria-label="Approve this job">
          {reviewing ? 'Submitting...' : 'Approve'}
        </Button>
        <Button
          variant="destructive"
          onClick={onReject}
          disabled={reviewing}
          aria-label="Reject this job in review"
        >
          {reviewing ? 'Submitting...' : 'Request changes'}
        </Button>
      </div>
    </section>
  );
}

function DagTree({ node, depth = 0 }: { node: DagNode; depth?: number }) {
  return (
    <div className={`${depth > 0 ? 'ml-4 border-l pl-3' : ''}`}>
      <div className="flex items-center gap-2 py-1">
        <span className="font-mono text-xs">{node.idempotencyKey}</span>
        <span className="rounded bg-muted px-1.5 py-0.5 text-xs">{node.stage}</span>
        {node.kind === 'composite' && (
          <span className="text-xs text-muted-foreground">(composite)</span>
        )}
      </div>
      {node.children.map(child => (
        <DagTree key={child.id} node={child} depth={depth + 1} />
      ))}
    </div>
  );
}

const SCORE_TERMS: { key: keyof JobExplain['factories'][number]['breakdown']; label: string }[] = [
  { key: 'capabilityFit', label: 'Capability' },
  { key: 'affinity', label: 'Affinity' },
  { key: 'load', label: 'Load' },
  { key: 'costFit', label: 'Cost fit' },
  { key: 'health', label: 'Health' },
  { key: 'starvation', label: 'Starvation' },
];

function ExplainPanel({ explain }: { explain: JobExplain }) {
  return (
    <section>
      <h2 className="text-lg font-semibold mb-1">Routing Explainability</h2>
      <p className="text-muted-foreground text-xs mb-3">
        Why this job routes where it does — the §7 weighted score per factory.{' '}
        {explain.bestFactoryId ? (
          <>
            Best factory: <span className="font-mono">{explain.bestFactoryId}</span>.
          </>
        ) : (
          <>No eligible factory right now.</>
        )}
        {!explain.depsSatisfied && (
          <span className="text-amber-600"> Blocked on deps: {explain.unmetDeps.join(', ')}.</span>
        )}
      </p>
      {explain.factories.length === 0 ? (
        <p className="text-muted-foreground text-sm">No factories have reported in.</p>
      ) : (
        <table className="w-full text-sm" aria-label="Routing score breakdown">
          <thead>
            <tr className="border-b text-left text-muted-foreground">
              <th className="pb-2 pr-4">Factory</th>
              <th className="pb-2 pr-4">Score</th>
              {SCORE_TERMS.map(t => (
                <th key={t.key} className="pb-2 pr-4 text-right">
                  {t.label}
                </th>
              ))}
              <th className="pb-2">Eligible</th>
            </tr>
          </thead>
          <tbody>
            {explain.factories.map(f => (
              <tr key={f.factoryId} className="border-b last:border-0">
                <td className="py-2 pr-4 font-mono text-xs">{f.factoryId}</td>
                <td className="py-2 pr-4 font-medium">{f.score.toFixed(2)}</td>
                {SCORE_TERMS.map(t => (
                  <td key={t.key} className="py-2 pr-4 text-right font-mono text-xs">
                    {f.breakdown[t.key].toFixed(2)}
                  </td>
                ))}
                <td className="py-2">
                  {f.eligible ? (
                    <span className="text-green-600">✓</span>
                  ) : (
                    <span className="text-muted-foreground" title={f.ineligibleReasons.join('; ')}>
                      ✗
                    </span>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </section>
  );
}
