import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import {
  batchJobsStreamUrl,
  batchJobsFetchErrorMessage,
  fetchBatchJobDetail,
  fetchBatchJurisdictions,
  fetchBatchJobsDashboard,
  fetchFailedVideos,
  fetchLaunchLog,
  fetchLaunchStatus,
  launchPipeline,
  stopPipeline,
  mergeBatchIntoDashboard,
  mergeBatchJurisdictions,
  type BatchJob,
  type BatchJurisdictionRun,
  type BatchJobsDashboardPayload,
  type BatchVideoResult,
  type PipelineStage,
  type StageReportRow,
} from '../api/batchJobs'
import DeploymentPanel from '../components/DeploymentPanel'
import { LinkifiedText } from '../utils/linkifiedText'
import {
  formatCompactHours,
  formatCompactNumber,
  formatCompactPair,
  formatFullNumber,
} from '../utils/formatCompact'
import {
  aggregateRunningFileTiming,
  avgSecondsPerFile,
  filterJurisdictionsByState,
  jurisdictionStateCodes,
  remainingVideosForRunningBatches,
  resolveRunningFileClock,
  jurisdictionLastUpdatedIso,
  latestDashboardActivityIso,
  remainingVideosForBatch,
  sortJurisdictions,
  useTickingSeconds,
} from '../utils/batchJobTiming'
import {
  formatAgoCompact,
  formatDateTimeAbsolute,
  formatUpdatedAt,
} from '../utils/dateTime'
import { STATE_CODES } from '../lib/usStates'

type FailedVideoRow = {
  batch_id: string
  batch_step: string
  state_code: string
  jurisdiction_id: string
  jurisdiction_name: string
  video: BatchVideoResult
}

const FAILED_VIDEO_STATUSES = new Set([
  'fail',
  'failed',
  'tombstoned',
  'empty',
  'rate_limit',
  'error',
])

function isFailedVideoStatus(status: string): boolean {
  const s = (status || '').toLowerCase()
  if (!s || s === 'ok' || s === 'pending' || s === 'skipped') return false
  if (FAILED_VIDEO_STATUSES.has(s)) return true
  return s !== 'ok'
}

function failedVideoCount(j: BatchJurisdictionRun): number {
  const fromVideos = (j.videos || []).filter((v) => isFailedVideoStatus(v.status)).length
  const fromStats = Number(j.stats?.fail ?? 0)
  return Math.max(fromVideos, fromStats)
}

// Which runnable step advances each stage (used for the per-stage log drill-down).
const STAGE_STEP: Record<string, string> = {
  discover: 'discover',
  videos: 'catalog',
  transcripts: 'captions',
  analyses: 'analyze',
  reports: 'analyze',
}

function fmtSecs(s?: number | null): string {
  if (s == null || !Number.isFinite(s)) return '—'
  const v = Math.round(s)
  if (v < 60) return `${v}s`
  const m = Math.floor(v / 60)
  const r = v % 60
  return r ? `${m}m ${r}s` : `${m}m`
}

function baseName(p?: string): string {
  if (!p) return '—'
  const parts = p.split('/')
  return parts[parts.length - 1] || p
}

function fmtEta(hours: number): string {
  if (!Number.isFinite(hours) || hours <= 0) return '—'
  if (hours < 1) return `${Math.round(hours * 60)}m`
  if (hours < 48) return `${Math.round(hours)}h`
  return `${(hours / 24).toFixed(1)}d`
}

function mergeDashboardFromStream(
  prev: BatchJobsDashboardPayload | undefined,
  next: BatchJobsDashboardPayload,
): BatchJobsDashboardPayload {
  if (!prev || next.detail !== 'summary') {
    return next
  }
  const prevById = new Map(prev.batches.map((b) => [b.batch_id, b]))
  return {
    ...next,
    batches: next.batches.map((b) => {
      const older = prevById.get(b.batch_id)
      if (older?.jurisdictions?.length) {
        return { ...b, jurisdictions: older.jurisdictions }
      }
      return b
    }),
  }
}

function collectFailedVideos(
  batches: BatchJob[],
  opts?: { batchId?: string; jurisdictionId?: string },
): FailedVideoRow[] {
  const rows: FailedVideoRow[] = []
  for (const batch of batches) {
    if (opts?.batchId && batch.batch_id !== opts.batchId) continue
    for (const j of batch.jurisdictions) {
      if (opts?.jurisdictionId && j.jurisdiction_id !== opts.jurisdictionId) continue
      for (const v of j.videos || []) {
        if (!isFailedVideoStatus(v.status)) continue
        rows.push({
          batch_id: batch.batch_id,
          batch_step: batch.step,
          state_code: j.state_code,
          jurisdiction_id: j.jurisdiction_id,
          jurisdiction_name: j.jurisdiction_name,
          video: v,
        })
      }
    }
  }
  return rows
}

function formatDuration(seconds: unknown): string {
  if (seconds == null || seconds === '') return '—'
  const total = Math.max(0, Math.floor(Number(seconds)))
  if (Number.isNaN(total)) return '—'
  if (total < 60) return `${total}s`
  const m = Math.floor(total / 60)
  const s = total % 60
  if (m < 60) return `${m}m ${s}s`
  const h = Math.floor(m / 60)
  const remM = m % 60
  if (h >= 24) {
    const compact = formatCompactNumber(h)
    return remM > 0 ? `${compact}h ${remM}m` : `${compact}h`
  }
  return remM > 0 ? `${h}h ${remM}m` : `${h}h`
}

function metricCountTitle(n: unknown, label: string): string | undefined {
  const full = formatFullNumber(n)
  const compact = formatCompactNumber(n, '')
  if (!full || full === compact) return undefined
  return `${label}: ${full}`
}

function formatVideoDuration(seconds: unknown): string {
  if (seconds == null || seconds === '') return '—'
  const total = Math.max(0, Math.floor(Number(seconds)))
  if (Number.isNaN(total) || total <= 0) return '—'
  const h = Math.floor(total / 3600)
  const m = Math.floor((total % 3600) / 60)
  const s = total % 60
  if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
  return `${m}:${String(s).padStart(2, '0')}`
}

function statusBadgeClass(status: string): string {
  if (status === 'completed' || status === 'ok') {
    return 'bg-emerald-100 text-emerald-800'
  }
  if (status === 'noop') {
    return 'bg-slate-100 text-slate-700'
  }
  if (status === 'failed' || status === 'fail') {
    return 'bg-red-100 text-red-800'
  }
  if (status === 'running') {
    return 'bg-sky-100 text-sky-800'
  }
  if (status === 'cancelled') {
    return 'bg-slate-200 text-slate-700'
  }
  if (status === 'pending') {
    return 'bg-amber-50 text-amber-900'
  }
  return 'bg-slate-100 text-slate-700'
}

function displayJurisdictionName(j: { jurisdiction_name?: string; jurisdiction_id?: string }): string {
  const raw = (j.jurisdiction_name || j.jurisdiction_id || '').trim()
  return raw.replace(/\s+(city|town|village|borough|municipality)\s*$/i, '').trim() || raw
}

function displayJurisdictionStatus(j: BatchJurisdictionRun): { label: string; badgeStatus: string } {
  const st = j.stats || {}
  if (Number(st.noop) > 0) return { label: 'noop', badgeStatus: 'noop' }
  if (Number(st.dry_run) > 0) return { label: 'dry-run', badgeStatus: 'noop' }
  return { label: j.status || 'pending', badgeStatus: j.status || 'pending' }
}

function SummaryCard({
  label,
  value,
  title,
  onClick,
  active,
  emphasis,
}: {
  label: string
  value: string | number
  title?: string
  onClick?: () => void
  active?: boolean
  emphasis?: 'danger' | 'default'
}) {
  const content = (
    <>
      <div className="text-[10px] font-medium uppercase tracking-wide text-slate-500">{label}</div>
      <div
        className={`mt-0.5 text-xl font-semibold tabular-nums ${
          emphasis === 'danger' ? 'text-red-700' : 'text-slate-900'
        }`}
      >
        {value}
      </div>
    </>
  )

  const className = `rounded-lg border px-3 py-2 text-left shadow-sm transition-colors ${
    active
      ? 'border-red-400 bg-red-50 ring-1 ring-red-300'
      : 'border-slate-200 bg-white hover:border-slate-300'
  } ${onClick ? 'cursor-pointer hover:bg-slate-50' : ''}`

  const tip = title || (onClick ? `View ${label}` : undefined)

  if (onClick) {
    return (
      <button type="button" onClick={onClick} className={className} title={tip}>
        {content}
      </button>
    )
  }

  return (
    <div className={className} title={tip}>
      {content}
    </div>
  )
}

function ProgressBar({ pct }: { pct: number }) {
  const width = Math.min(100, Math.max(0, pct))
  return (
    <div className="h-2 overflow-hidden rounded-full bg-slate-200">
      <div
        className="h-full rounded-full bg-teal-600 transition-all duration-300"
        style={{ width: `${width}%` }}
      />
    </div>
  )
}

function FailedVideosPanel({
  rows,
  scopeLabel,
  subtitle,
  onClose,
  onSelectJurisdiction,
}: {
  rows: FailedVideoRow[]
  scopeLabel: string
  subtitle?: string
  onClose: () => void
  onSelectJurisdiction?: (batchId: string, jurisdictionId: string) => void
}) {
  return (
    <div className="rounded-lg border border-red-200 bg-white shadow-sm">
      <div className="flex flex-wrap items-center justify-between gap-2 border-b border-red-100 bg-red-50/80 px-4 py-3">
        <div>
          <h3 className="text-sm font-semibold text-red-900">Failed videos</h3>
          <p className="text-xs text-red-800/80">
            {scopeLabel} · {rows.length} video{rows.length === 1 ? '' : 's'}
            {subtitle ? ` · ${subtitle}` : ''}
          </p>
        </div>
        <button
          type="button"
          onClick={onClose}
          className="rounded-md border border-red-200 bg-white px-2.5 py-1 text-xs font-medium text-red-800 hover:bg-red-50"
        >
          Close
        </button>
      </div>
      <div className="overflow-auto max-h-[min(420px,55vh)]">
        {rows.length === 0 ? (
          <p className="px-4 py-6 text-center text-sm text-slate-500">
            No per-video failure log yet. Failures are recorded when backfill runs with{' '}
            <code className="rounded bg-slate-100 px-1">--batch-id</code>.
          </p>
        ) : (
          <table className="min-w-full text-sm">
            <thead className="sticky top-0 bg-white text-left text-xs uppercase text-slate-500">
              <tr>
                <th className="px-3 py-2 font-medium">Jurisdiction</th>
                <th className="px-3 py-2 font-medium">Video</th>
                <th className="px-3 py-2 font-medium">Title</th>
                <th className="px-3 py-2 font-medium">Status</th>
                <th className="px-3 py-2 font-medium">Error</th>
              </tr>
            </thead>
            <tbody>
              {rows.map((row) => (
                <tr
                  key={`${row.batch_id}-${row.jurisdiction_id}-${row.video.video_id}`}
                  className="border-t border-slate-100 align-top"
                >
                  <td className="px-3 py-2 text-xs">
                    <button
                      type="button"
                      className="text-left hover:text-teal-800"
                      onClick={() =>
                        onSelectJurisdiction?.(row.batch_id, row.jurisdiction_id)
                      }
                    >
                      <div className="font-medium text-slate-900">
                        {displayJurisdictionName({
                          jurisdiction_name: row.jurisdiction_name,
                          jurisdiction_id: row.jurisdiction_id,
                        })}
                      </div>
                      <div className="font-mono text-[11px] text-slate-500">
                        {row.state_code} · {row.jurisdiction_id}
                      </div>
                      <div className="mt-0.5 font-mono text-[10px] text-slate-400">
                        {row.batch_step}
                      </div>
                    </button>
                  </td>
                  <td className="px-3 py-2 font-mono text-xs whitespace-nowrap">
                    <a
                      href={`https://www.youtube.com/watch?v=${row.video.video_id}`}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-teal-700 hover:underline"
                    >
                      {row.video.video_id}
                    </a>
                  </td>
                  <td className="max-w-[180px] px-3 py-2 text-xs text-slate-700">
                    {row.video.title || '—'}
                  </td>
                  <td className="px-3 py-2 whitespace-nowrap">
                    <span
                      className={`rounded px-1.5 py-0.5 text-xs font-medium ${statusBadgeClass(row.video.status)}`}
                    >
                      {row.video.status}
                    </span>
                    {row.video.transcript_source ? (
                      <div className="mt-0.5 text-[10px] text-slate-500">
                        {row.video.transcript_source}
                      </div>
                    ) : null}
                  </td>
                  <td className="max-w-md px-3 py-2 text-xs text-red-800 whitespace-pre-wrap break-words">
                    {row.video.error ? (
                      <LinkifiedText
                        text={row.video.error}
                        linkClassName="font-medium text-teal-800 underline decoration-teal-500/70 hover:text-teal-950 break-all"
                      />
                    ) : (
                      '—'
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </div>
  )
}

function JurisdictionTable({
  jurisdictions,
  selectedId,
  onSelect,
  onShowFailedVideos,
  emptyMessage,
}: {
  jurisdictions: BatchJurisdictionRun[]
  selectedId: string | null
  onSelect: (id: string) => void
  onShowFailedVideos?: (jurisdictionId: string) => void
  emptyMessage?: string
}) {
  if (!jurisdictions.length) {
    return (
      <p className="py-6 text-center text-sm text-slate-500">
        {emptyMessage ?? (
          <>
            No jurisdictions recorded yet. Run{' '}
            <code className="rounded bg-slate-100 px-1">run_priority_states_last_n.sh captions</code>.
          </>
        )}
      </p>
    )
  }

  return (
    <div className="overflow-auto max-h-[min(420px,50vh)]">
      <table className="min-w-full text-sm">
        <thead className="sticky top-0 bg-slate-50 text-left text-xs uppercase text-slate-500">
          <tr>
            <th className="px-3 py-2 font-medium">State</th>
            <th className="px-3 py-2 font-medium">Jurisdiction</th>
            <th className="px-3 py-2 font-medium">Status</th>
            <th className="px-3 py-2 font-medium">Videos</th>
            <th className="px-3 py-2 font-medium">Files</th>
            <th className="px-3 py-2 font-medium">Time</th>
            <th className="px-3 py-2 font-medium" title="When this jurisdiction was last checked in the batch run">
              Last checked
            </th>
          </tr>
        </thead>
        <tbody>
          {jurisdictions.map((j) => {
            const st = j.stats || {}
            const fc = j.file_counts || {}
            const failCount = failedVideoCount(j)
            const disp = displayJurisdictionStatus(j)
            const selected = selectedId === j.jurisdiction_id
            const lastUpdated = formatUpdatedAt(jurisdictionLastUpdatedIso(j))
            return (
              <tr
                key={j.jurisdiction_id}
                className={`cursor-pointer border-t border-slate-100 hover:bg-teal-50/50 ${
                  selected ? 'bg-teal-50' : ''
                }`}
                onClick={() => onSelect(j.jurisdiction_id)}
              >
                <td className="px-3 py-2 font-mono text-xs">{j.state_code}</td>
                <td className="px-3 py-2">
                  <div className="font-medium text-slate-900">
                    {displayJurisdictionName(j)}
                  </div>
                  <div className="font-mono text-[11px] text-slate-500">{j.jurisdiction_id}</div>
                </td>
                <td className="px-3 py-2">
                  <span
                    className={`inline-block rounded px-1.5 py-0.5 text-xs font-medium ${statusBadgeClass(disp.badgeStatus)}`}
                  >
                    {disp.label}
                    {j.exit_code ? ` (${j.exit_code})` : ''}
                  </span>
                </td>
                <td className="px-3 py-2 text-xs text-slate-600">
                  {j.status === 'pending' ? (
                    <span className="text-slate-400">—</span>
                  ) : Number(st.noop) > 0 ? (
                    <span
                      className="text-slate-500"
                      title="Backfill ran; no videos needed download (already in bronze or on disk)"
                    >
                      all cached
                    </span>
                  ) : (
                    <>
                      ok {st.ok ?? 0} ·{' '}
                  {failCount > 0 && onShowFailedVideos ? (
                    <button
                      type="button"
                      className="font-medium text-red-700 underline decoration-red-300 hover:text-red-900"
                      title="View failed videos for this jurisdiction"
                      onClick={(e) => {
                        e.stopPropagation()
                        onShowFailedVideos(j.jurisdiction_id)
                      }}
                    >
                      fail {failCount}
                    </button>
                  ) : (
                    <>fail {failCount}</>
                  )}{' '}
                      · tomb {st.tombstoned ?? 0}
                    </>
                  )}
                </td>
                <td
                  className="px-3 py-2 text-xs text-slate-600"
                  title="Transcript / analysis / report files under policy cache for this place"
                >
                  {Number(st.noop) > 0 &&
                  !(fc.transcripts_disk ?? fc.transcripts) &&
                  !(fc.analysis_disk ?? fc.analysis) &&
                  !(fc.reports_disk ?? fc.reports) ? (
                    <span className="text-slate-400">—</span>
                  ) : (
                    <>
                      disk T {fc.transcripts_disk ?? fc.transcripts ?? 0} · A{' '}
                      {fc.analysis_disk ?? fc.analysis ?? 0} · R{' '}
                      {fc.reports_disk ?? fc.reports ?? 0}
                    </>
                  )}
                </td>
                <td className="px-3 py-2 text-xs tabular-nums text-slate-600">
                  {formatDuration(j.elapsed_seconds)}
                </td>
                <td
                  className="px-3 py-2 text-xs text-slate-600 whitespace-nowrap"
                  title={lastUpdated.title}
                >
                  {lastUpdated.display}
                </td>
              </tr>
            )
          })}
        </tbody>
      </table>
    </div>
  )
}

function VideoDrillDown({
  jurisdiction,
  onlyFailed = false,
}: {
  jurisdiction: BatchJurisdictionRun
  onlyFailed?: boolean
}) {
  const videos = useMemo(() => {
    const all = jurisdiction.videos || []
    if (!onlyFailed) return all
    return all.filter((v) => isFailedVideoStatus(v.status))
  }, [jurisdiction.videos, onlyFailed])

  return (
    <div
      id="batch-jurisdiction-drilldown"
      className="rounded-lg border border-slate-200 bg-white shadow-sm"
    >
      <div className="border-b border-slate-100 px-4 py-3">
        <h3 className="text-sm font-semibold text-slate-900">
          {onlyFailed ? 'Failed videos · ' : ''}
          {displayJurisdictionName(jurisdiction)}
        </h3>
        <p className="mt-0.5 font-mono text-xs text-slate-500">{jurisdiction.jurisdiction_id}</p>
        {onlyFailed ? (
          <p className="mt-1 text-xs text-red-800">
            {videos.length} failed video{videos.length === 1 ? '' : 's'} in this batch run
          </p>
        ) : null}
      </div>
      <div className="overflow-auto max-h-[min(360px,45vh)]">
        <table className="min-w-full text-sm">
          <thead className="sticky top-0 bg-white text-left text-xs uppercase text-slate-500">
            <tr>
              <th className="px-3 py-2 font-medium">Video</th>
              <th className="px-3 py-2 font-medium">Title</th>
              <th className="px-3 py-2 font-medium">Status</th>
              <th className="px-3 py-2 font-medium">Duration</th>
              <th className="px-3 py-2 font-medium">Source</th>
              <th className="px-3 py-2 font-medium">Error</th>
            </tr>
          </thead>
          <tbody>
            {videos.length === 0 ? (
              <tr>
                <td colSpan={6} className="px-3 py-4 text-center text-xs text-slate-500">
                  {onlyFailed
                    ? 'No failed videos logged for this jurisdiction in this batch run.'
                    : (
                      <>
                        No per-video log for this run (re-run backfill with{' '}
                        <code>--batch-id</code>).
                      </>
                      )}
                </td>
              </tr>
            ) : (
              videos.map((v) => (
                <tr key={v.video_id} className="border-t border-slate-50">
                  <td className="px-3 py-2 font-mono text-xs">
                    <a
                      href={`https://www.youtube.com/watch?v=${v.video_id}`}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-teal-700 hover:underline"
                    >
                      {v.video_id}
                    </a>
                  </td>
                  <td className="max-w-[200px] truncate px-3 py-2 text-xs text-slate-700">
                    {v.title || '—'}
                  </td>
                  <td className="px-3 py-2">
                    <span
                      className={`rounded px-1.5 py-0.5 text-xs font-medium ${statusBadgeClass(v.status)}`}
                    >
                      {v.status}
                    </span>
                  </td>
                  <td className="px-3 py-2 font-mono text-xs tabular-nums text-slate-600 whitespace-nowrap">
                    {formatVideoDuration(v.duration_seconds)}
                  </td>
                  <td className="px-3 py-2 text-xs text-slate-600">{v.transcript_source || '—'}</td>
                  <td className="max-w-md px-3 py-2 text-xs text-slate-600 whitespace-pre-wrap break-words">
                    {v.error ? (
                      <LinkifiedText text={v.error} />
                    ) : (
                      '—'
                    )}
                  </td>
                </tr>
              ))
            )}
          </tbody>
        </table>
      </div>
    </div>
  )
}

function BatchDetailPanel({
  batch,
  selectedJurisdictionId,
  onSelectJurisdiction,
  stateFilter,
  onStateFilterChange,
  showFailedVideos,
  onToggleFailedVideos,
  onShowFailedForJurisdiction,
}: {
  batch: BatchJob
  selectedJurisdictionId: string | null
  onSelectJurisdiction: (id: string | null) => void
  stateFilter: string
  onStateFilterChange: (state: string) => void
  showFailedVideos: boolean
  onToggleFailedVideos: () => void
  onShowFailedForJurisdiction: (jurisdictionId: string) => void
}) {
  const s = batch.summary || {}
  const total = Number(s.total_jurisdictions) || 0
  const processed = Number(s.processed_jurisdictions) || 0
  const pct = total > 0 ? Math.round((100 * processed) / total) : 0
  const cfg = batch.config || {}
  const states = Array.isArray(cfg.states) ? (cfg.states as string[]).join(', ') : '—'

  const selectedJurisdiction = useMemo(
    () =>
      batch.jurisdictions.find((j) => j.jurisdiction_id === selectedJurisdictionId) ?? null,
    [batch.jurisdictions, selectedJurisdictionId],
  )

  const videosFailCount = Number(s.videos_fail) || 0
  const isRunning = batch.status === 'running'
  const fileClock = useMemo(() => resolveRunningFileClock(batch), [batch])
  const currentFileSeconds = useTickingSeconds(
    fileClock?.startedAt,
    isRunning && !!fileClock?.startedAt,
  )
  const avgPerFileSec = avgSecondsPerFile(s)
  const batchRemainingVideos = useMemo(() => remainingVideosForBatch(batch), [batch])

  const stateCodes = useMemo(() => {
    const fromCfg = Array.isArray(cfg.states)
      ? (cfg.states as string[]).map((s) => String(s).toUpperCase().trim()).filter(Boolean)
      : []
    if (fromCfg.length) return [...new Set(fromCfg)].sort()
    return jurisdictionStateCodes(batch.jurisdictions)
  }, [batch.jurisdictions, cfg.states])
  const sortedJurisdictions = useMemo(
    () => sortJurisdictions(batch.jurisdictions),
    [batch.jurisdictions],
  )
  const displayedJurisdictions = useMemo(
    () => filterJurisdictionsByState(sortedJurisdictions, stateFilter),
    [sortedJurisdictions, stateFilter],
  )
  const showJurisdictionTable = !!stateFilter

  return (
    <div className="space-y-4">
      <div>
        <div className="flex flex-wrap items-center gap-2">
          <h2 className="text-lg font-semibold text-slate-900">{batch.step}</h2>
          <span className={`rounded px-2 py-0.5 text-xs font-medium ${statusBadgeClass(batch.status)}`}>
            {batch.status}
          </span>
        </div>
        <p className="mt-1 font-mono text-xs text-slate-500">{batch.batch_id}</p>
        <p className="mt-1 text-xs text-slate-600">
          Started {formatDateTimeAbsolute(batch.started_at)}
          {batch.finished_at
            ? ` · Finished ${formatDateTimeAbsolute(batch.finished_at)}`
            : ''}
        </p>
        {isRunning && fileClock?.videoId ? (
          <p className="mt-1 text-xs text-slate-500">
            Current:{' '}
            <a
              href={`https://www.youtube.com/watch?v=${fileClock.videoId}`}
              target="_blank"
              rel="noopener noreferrer"
              className="font-mono text-teal-700 hover:underline"
            >
              {fileClock.videoId}
            </a>
            {fileClock.title ? ` · ${fileClock.title.slice(0, 72)}` : ''}
          </p>
        ) : null}
      </div>

      <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
        <SummaryCard
          label="Processed"
          value={formatCompactPair(processed, total || '?')}
          title={metricCountTitle(processed, 'Processed jurisdictions')}
        />
        <SummaryCard
          label="Success"
          value={formatCompactNumber(s.success_jurisdictions)}
          title={metricCountTitle(s.success_jurisdictions, 'Success jurisdictions')}
        />
        <SummaryCard
          label="Failed"
          value={formatCompactNumber(s.failed_jurisdictions)}
          title={metricCountTitle(s.failed_jurisdictions, 'Failed jurisdictions')}
        />
        <SummaryCard
          label="Jurisdictions remaining"
          value={formatCompactNumber(s.remaining_jurisdictions)}
          title={metricCountTitle(s.remaining_jurisdictions, 'Remaining jurisdictions')}
        />
        <SummaryCard label="Elapsed" value={formatDuration(s.elapsed_seconds)} />
        <SummaryCard label="ETA" value={formatDuration(s.eta_seconds)} />
        {isRunning ? (
          <>
            <SummaryCard label="Avg / file" value={formatDuration(avgPerFileSec)} />
            <SummaryCard
              label="Current file"
              value={formatDuration(currentFileSeconds)}
            />
            <SummaryCard
              label="Videos remaining"
              value={
                batchRemainingVideos != null
                  ? formatCompactNumber(batchRemainingVideos)
                  : '—'
              }
              title={metricCountTitle(batchRemainingVideos, 'Videos remaining')}
            />
          </>
        ) : null}
        <SummaryCard
          label="Videos OK"
          value={formatCompactNumber(s.videos_ok)}
          title={metricCountTitle(s.videos_ok, 'Videos OK')}
        />
        <SummaryCard
          label="Videos fail"
          value={formatCompactNumber(videosFailCount)}
          title={metricCountTitle(videosFailCount, 'Videos failed')}
          emphasis={videosFailCount > 0 ? 'danger' : 'default'}
          active={showFailedVideos}
          onClick={videosFailCount > 0 ? onToggleFailedVideos : undefined}
        />
      </div>

      <ProgressBar pct={pct} />
      <p className="text-xs text-slate-600">
        States: {states} · N={String(cfg.n ?? '—')} · delay={String(cfg.delay ?? '—')}s · source=
        {String(cfg.transcript_source || '—')}
      </p>

      <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
        <div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-100 px-4 py-2">
          <h3 className="text-sm font-semibold text-slate-800">
            Jurisdictions
            {showJurisdictionTable ? (
              <span className="ml-1 font-normal text-slate-500">
                ({displayedJurisdictions.length} in {stateFilter})
              </span>
            ) : null}
          </h3>
          <div className="flex items-center gap-2">
            <label
              htmlFor="batch-jobs-state-filter"
              className="text-[10px] font-medium uppercase tracking-wide text-slate-500"
            >
              State <span className="text-red-600">*</span>
            </label>
            <select
              id="batch-jobs-state-filter"
              value={stateFilter}
              onChange={(e) => onStateFilterChange(e.target.value)}
              required
              className="min-w-[8rem] rounded-md border border-slate-200 bg-white px-2 py-1 text-xs text-slate-800 shadow-sm"
            >
              <option value="" disabled>
                Select state…
              </option>
              {stateCodes.map((st) => (
                <option key={st} value={st}>
                  {st}
                </option>
              ))}
            </select>
          </div>
        </div>
        {showJurisdictionTable ? (
          <JurisdictionTable
            jurisdictions={displayedJurisdictions}
            selectedId={selectedJurisdictionId}
            onSelect={(id) => onSelectJurisdiction(id)}
            onShowFailedVideos={onShowFailedForJurisdiction}
            emptyMessage={`No jurisdictions in ${stateFilter} for this batch.`}
          />
        ) : (
          <p className="px-4 py-8 text-center text-sm text-slate-500">
            {selectedJurisdictionId
              ? 'Per-jurisdiction details are below. Pick a state to browse the full list for that state.'
              : 'Select a state to browse jurisdictions. The full plan is large; filter by state to keep the table fast.'}
          </p>
        )}
      </div>

      {selectedJurisdiction ? (
        <VideoDrillDown
          jurisdiction={selectedJurisdiction}
          onlyFailed={showFailedVideos}
        />
      ) : null}
    </div>
  )
}

// Priority dev states — mirrors the wrapper-script default (the STATES env in
// youtube_run_priority_states_last_n.sh). "All states" uses STATE_CODES (50 + DC).
const PRIORITY_STATE_CODES = ['AL', 'GA', 'IN', 'MA', 'MT', 'WA', 'WI']

type JobType = 'youtube' | 'deployment'

function JobTypeTabs({
  active,
  onChange,
}: {
  active: JobType
  onChange: (t: JobType) => void
}) {
  const tabs: { key: JobType; label: string }[] = [
    { key: 'youtube', label: 'YouTube pipeline' },
    { key: 'deployment', label: 'Prod deployment' },
  ]
  return (
    <div className="flex gap-1 border-b border-slate-200">
      {tabs.map((t) => (
        <button
          key={t.key}
          type="button"
          onClick={() => onChange(t.key)}
          className={`-mb-px border-b-2 px-3 py-2 text-sm font-medium ${
            active === t.key
              ? 'border-sky-500 text-sky-700'
              : 'border-transparent text-slate-500 hover:text-slate-700'
          }`}
        >
          {t.label}
        </button>
      ))}
    </div>
  )
}

export default function BatchJobStatusPage() {
  const [searchParams, setSearchParams] = useSearchParams()
  const jobType: JobType = searchParams.get('type') === 'deployment' ? 'deployment' : 'youtube'
  const setJobType = useCallback(
    (t: JobType) => {
      const next = new URLSearchParams(searchParams)
      if (t === 'deployment') next.set('type', 'deployment')
      else next.delete('type')
      setSearchParams(next, { replace: true })
    },
    [searchParams, setSearchParams],
  )
  const batchId = searchParams.get('batch') ?? ''
  const jurisdictionId = searchParams.get('jurisdiction') ?? ''
  const stateFilter = (searchParams.get('state') ?? '').toUpperCase()
  const showFailedVideos = searchParams.get('view') === 'failed-videos'
  const failedVideosScopeAll = searchParams.get('failed_scope') === 'all'
  const queryClient = useQueryClient()
  const [streamLive, setStreamLive] = useState(false)
  const [videosLoading, setVideosLoading] = useState(false)

  const { data, isPending, isFetching, isError, error, refetch } = useQuery({
    queryKey: ['batch-jobs-dashboard'],
    queryFn: () => fetchBatchJobsDashboard(false, 'summary'),
    staleTime: 30_000,
    refetchOnWindowFocus: true,
    retry: 1,
  })

  useEffect(() => {
    const url = batchJobsStreamUrl()
    const es = new EventSource(url)

    const applyPayload = (payload: BatchJobsDashboardPayload) => {
      queryClient.setQueryData<BatchJobsDashboardPayload>(['batch-jobs-dashboard'], (prev) =>
        mergeDashboardFromStream(prev, payload),
      )
    }

    es.onopen = () => setStreamLive(true)
    es.addEventListener('summary', (ev) => {
      try {
        applyPayload(JSON.parse(ev.data) as BatchJobsDashboardPayload)
      } catch {
        /* ignore */
      }
    })
    es.onerror = () => {
      setStreamLive(false)
      es.close()
      void refetch()
    }

    return () => {
      setStreamLive(false)
      es.close()
    }
  }, [queryClient, refetch])

  const { data: launchStatus, refetch: refetchLaunch } = useQuery({
    queryKey: ['batch-launch-status'],
    queryFn: fetchLaunchStatus,
    refetchInterval: 20_000,
    retry: false,
  })
  const [launching, setLaunching] = useState(false)
  const [stopping, setStopping] = useState(false)
  const [launchMsg, setLaunchMsg] = useState<string | null>(null)
  // Parallelism = how many jurisdictions analyze at once (the main throughput
  // lever). Persisted so it survives reloads; ceiling is the Gemini quota.
  const [parallelism, setParallelism] = useState<number>(() => {
    const v = Number(localStorage.getItem('batch-parallelism'))
    return v >= 1 && v <= 8 ? v : 6
  })
  const setParallel = useCallback((v: number) => {
    setParallelism(v)
    localStorage.setItem('batch-parallelism', String(v))
  }, [])
  // Which states a full / "ALL"-scope launch targets: the priority dev set
  // (matches the script default) or every US state + DC. Picking a single state
  // from the scope dropdown overrides this. Persisted across reloads.
  const [allScope, setAllScopeState] = useState<'priority' | 'all'>(() => {
    return localStorage.getItem('batch-launch-scope') === 'all' ? 'all' : 'priority'
  })
  const setAllScope = useCallback((v: 'priority' | 'all') => {
    setAllScopeState(v)
    localStorage.setItem('batch-launch-scope', v)
  }, [])
  // How many missing transcripts one ▶ Backfill click fetches. 0 = "All" (sweep
  // every missing transcript). The run is resumable — it re-queries what's still
  // missing — so a bounded size still makes incremental progress across clicks.
  // Persisted across reloads; defaults to 4000.
  const BACKFILL_SIZES = [100, 500, 2000, 4000, 0] as const
  const [backfillSize, setBackfillSizeState] = useState<number>(() => {
    const raw = localStorage.getItem('batch-backfill-size')
    const v = Number(raw)
    return raw != null && (BACKFILL_SIZES as readonly number[]).includes(v) ? v : 4000
  })
  const setBackfillSize = useCallback((v: number) => {
    setBackfillSizeState(v)
    localStorage.setItem('batch-backfill-size', String(v))
  }, [])
  const runLaunch = useCallback(
    async (step: string, states: string[], nOverride?: number) => {
      setLaunching(true)
      setLaunchMsg(null)
      try {
        const res = await launchPipeline({
          step,
          states,
          parallel: parallelism,
          n: nOverride ?? 25,
        })
        setLaunchMsg(res.detail || 'Launched.')
        void refetchLaunch()
        window.setTimeout(() => void refetch(), 2500)
      } catch (e) {
        setLaunchMsg((e as Error).message || 'Launch failed')
      } finally {
        setLaunching(false)
      }
    },
    [refetchLaunch, refetch, parallelism],
  )

  const stopLaunch = useCallback(
    async (step?: string) => {
      const label = step ? `the '${step}' run` : 'all running jobs'
      if (!window.confirm(`Stop ${label}? In-flight work checkpoints and the process exits.`)) {
        return
      }
      setStopping(true)
      setLaunchMsg(null)
      try {
        const res = await stopPipeline(step ? { step } : {})
        setLaunchMsg(res.detail || 'Stop requested.')
        void refetchLaunch()
        window.setTimeout(() => void refetch(), 2500)
      } catch (e) {
        setLaunchMsg((e as Error).message || 'Stop failed')
      } finally {
        setStopping(false)
      }
    },
    [refetchLaunch, refetch],
  )

  // Per-stage drill-down: expand one stage to see its live log + current file.
  const [expandedStage, setExpandedStage] = useState<string | null>(null)
  // The Transcripts stage is advanced by two steps — per-jurisdiction `captions`
  // and the global `backfill` — which write separate launch logs. Show the
  // backfill log while it's the live (running/stalled) step; otherwise the
  // conventional captions log.
  const transcriptsBackfillLive =
    (launchStatus?.running_steps ?? []).includes('backfill') ||
    (launchStatus?.stalled_steps ?? []).includes('backfill')
  const expandedStep = expandedStage
    ? expandedStage === 'transcripts' && transcriptsBackfillLive
      ? 'backfill'
      : STAGE_STEP[expandedStage]
    : null
  const { data: stageLog } = useQuery({
    queryKey: ['launch-log', expandedStep],
    queryFn: () => fetchLaunchLog(expandedStep as string),
    enabled: !!expandedStep,
    refetchInterval: 4000,
    retry: false,
  })

  const selectedBatch = useMemo(() => {
    if (!data?.batches?.length) return null
    if (batchId) {
      return data.batches.find((b) => b.batch_id === batchId) ?? data.batches[0]
    }
    return data.batches[0]
  }, [data, batchId])

  const setBatch = useCallback(
    (id: string) => {
      const next = new URLSearchParams(searchParams)
      next.set('batch', id)
      next.delete('jurisdiction')
      next.delete('state')
      setSearchParams(next, { replace: true })
    },
    [searchParams, setSearchParams],
  )

  const setStateFilter = useCallback(
    (state: string) => {
      const next = new URLSearchParams(searchParams)
      if (selectedBatch?.batch_id) next.set('batch', selectedBatch.batch_id)
      const st = state.trim().toUpperCase()
      if (st) next.set('state', st)
      else next.delete('state')
      if (jurisdictionId && selectedBatch) {
        const visible = filterJurisdictionsByState(
          sortJurisdictions(selectedBatch.jurisdictions),
          st,
        )
        if (!visible.some((j) => j.jurisdiction_id === jurisdictionId)) {
          next.delete('jurisdiction')
        }
      }
      setSearchParams(next, { replace: true })
    },
    [searchParams, setSearchParams, selectedBatch, jurisdictionId],
  )

  const setJurisdiction = useCallback(
    (id: string | null) => {
      const next = new URLSearchParams(searchParams)
      if (selectedBatch?.batch_id) next.set('batch', selectedBatch.batch_id)
      if (id) next.set('jurisdiction', id)
      else next.delete('jurisdiction')
      next.delete('view')
      setSearchParams(next, { replace: true })
    },
    [searchParams, setSearchParams, selectedBatch?.batch_id],
  )

  const setShowFailedVideos = useCallback(
    (
      open: boolean,
      opts?: { batchId?: string; jurisdictionId?: string; allBatches?: boolean },
    ) => {
      const next = new URLSearchParams(searchParams)
      if (opts?.batchId) next.set('batch', opts.batchId)
      if (opts?.jurisdictionId) next.set('jurisdiction', opts.jurisdictionId)
      else if (!open) next.delete('jurisdiction')
      if (open) {
        next.set('view', 'failed-videos')
        if (opts?.allBatches) next.set('failed_scope', 'all')
        else next.delete('failed_scope')
      } else {
        next.delete('view')
        next.delete('failed_scope')
      }
      setSearchParams(next, { replace: true })
    },
    [searchParams, setSearchParams],
  )

  const planStateCodes = useMemo(() => {
    if (!selectedBatch) return [] as string[]
    const fromCfg = Array.isArray(selectedBatch.config?.states)
      ? (selectedBatch.config.states as string[])
          .map((s) => String(s).toUpperCase().trim())
          .filter(Boolean)
      : []
    if (fromCfg.length) return [...new Set(fromCfg)].sort()
    return jurisdictionStateCodes(selectedBatch.jurisdictions)
  }, [selectedBatch])

  const effectiveStateFilter = useMemo(() => {
    if (!selectedBatch || !stateFilter) return ''
    return planStateCodes.includes(stateFilter) ? stateFilter : ''
  }, [selectedBatch, stateFilter, planStateCodes])

  const {
    data: stateJurisdictions,
    isFetching: stateJurisdictionsFetching,
  } = useQuery({
    queryKey: ['batch-jurisdictions', selectedBatch?.batch_id, effectiveStateFilter],
    queryFn: () =>
      fetchBatchJurisdictions(selectedBatch!.batch_id, effectiveStateFilter),
    enabled: !!selectedBatch?.batch_id && !!effectiveStateFilter,
    staleTime: 15_000,
  })

  const selectedBatchForPanel = useMemo(() => {
    if (!selectedBatch) return null
    if (!effectiveStateFilter) return selectedBatch
    if (stateJurisdictions) {
      return mergeBatchJurisdictions(selectedBatch, stateJurisdictions)
    }
    return selectedBatch
  }, [selectedBatch, effectiveStateFilter, stateJurisdictions])

  const visibleJurisdictions = useMemo(() => {
    if (!selectedBatchForPanel || !effectiveStateFilter) return []
    return filterJurisdictionsByState(
      sortJurisdictions(selectedBatchForPanel.jurisdictions),
      effectiveStateFilter,
    )
  }, [selectedBatchForPanel, effectiveStateFilter])

  const effectiveJurisdictionId = useMemo(() => {
    if (!jurisdictionId || !selectedBatch) return null
    if (!effectiveStateFilter) return jurisdictionId
    return visibleJurisdictions.some((j) => j.jurisdiction_id === jurisdictionId)
      ? jurisdictionId
      : null
  }, [jurisdictionId, selectedBatch, effectiveStateFilter, visibleJurisdictions])

  const selectedJurisdictionForVideos = useMemo(() => {
    if (!selectedBatchForPanel || !effectiveJurisdictionId) return null
    return (
      selectedBatchForPanel.jurisdictions.find(
        (j) => j.jurisdiction_id === effectiveJurisdictionId,
      ) ?? null
    )
  }, [selectedBatchForPanel, effectiveJurisdictionId])

  const failedVideosBatchId = failedVideosScopeAll
    ? undefined
    : selectedBatch?.batch_id

  const {
    data: failedVideosPayload,
    isFetching: failedVideosFetching,
  } = useQuery({
    queryKey: ['batch-failed-videos', failedVideosBatchId ?? 'all'],
    queryFn: () => fetchFailedVideos(failedVideosBatchId),
    enabled:
      showFailedVideos &&
      !effectiveJurisdictionId &&
      (data?.totals.videos_fail ?? 0) > 0,
    staleTime: 30_000,
  })

  useEffect(() => {
    if (!selectedBatch?.batch_id) return
    const needsJurisdictionVideos =
      !!effectiveJurisdictionId &&
      !!selectedJurisdictionForVideos &&
      (showFailedVideos
        ? failedVideoCount(selectedJurisdictionForVideos) > 0 &&
          !(selectedJurisdictionForVideos.videos || []).some((v) =>
            isFailedVideoStatus(v.status),
          )
        : (selectedJurisdictionForVideos.videos?.length ?? 0) === 0 &&
          (Number(selectedJurisdictionForVideos.stats?.ok ?? 0) > 0 ||
            failedVideoCount(selectedJurisdictionForVideos) > 0))
    if (!needsJurisdictionVideos) return

    let cancelled = false
    setVideosLoading(true)
    const mode = 'all'
    void fetchBatchJobDetail(selectedBatch.batch_id, mode)
      .then((batch) => {
        if (cancelled) return
        queryClient.setQueryData<BatchJobsDashboardPayload>(
          ['batch-jobs-dashboard'],
          (prev) => mergeBatchIntoDashboard(prev, batch) ?? prev,
        )
      })
      .finally(() => {
        if (!cancelled) setVideosLoading(false)
      })
    return () => {
      cancelled = true
    }
  }, [
    selectedBatch?.batch_id,
    effectiveJurisdictionId,
    showFailedVideos,
    selectedJurisdictionForVideos,
    data?.totals.videos_fail,
    queryClient,
  ])

  const allFailedVideoRows = useMemo((): FailedVideoRow[] => {
    if (effectiveJurisdictionId) {
      return collectFailedVideos(data?.batches ?? [], {
        batchId: selectedBatch?.batch_id,
        jurisdictionId: effectiveJurisdictionId,
      })
    }
    if (failedVideosPayload?.rows?.length) {
      return failedVideosPayload.rows.map((r) => ({
        batch_id: r.batch_id,
        batch_step: r.batch_step,
        state_code: r.state_code,
        jurisdiction_id: r.jurisdiction_id,
        jurisdiction_name: r.jurisdiction_name,
        video: r.video,
      }))
    }
    return collectFailedVideos(data?.batches ?? [], {
      batchId: failedVideosBatchId,
    })
  }, [
    data?.batches,
    selectedBatch?.batch_id,
    effectiveJurisdictionId,
    failedVideosPayload,
    failedVideosBatchId,
  ])

  const failedScopeLabel = useMemo(() => {
    if (effectiveJurisdictionId && selectedBatch) {
      const j = selectedBatch.jurisdictions.find(
        (x) => x.jurisdiction_id === effectiveJurisdictionId,
      )
      return `${selectedBatch.step} · ${j?.jurisdiction_name || effectiveJurisdictionId}`
    }
    if (failedVideosScopeAll) return 'All recent batches'
    if (selectedBatch) return `Batch ${selectedBatch.step} (${selectedBatch.batch_id})`
    return 'All batches'
  }, [effectiveJurisdictionId, selectedBatch, failedVideosScopeAll])

  const failedVideosSubtitle = useMemo(() => {
    if (effectiveJurisdictionId) return undefined
    const total =
      failedVideosPayload?.total_fail_in_summaries ?? data?.totals.videos_fail ?? 0
    const parts: string[] = []
    if (total > allFailedVideoRows.length) {
      parts.push(`${total} in summaries`)
    }
    if (failedVideosPayload?.truncated) {
      parts.push('list capped at 500')
    }
    return parts.length ? parts.join('; ') : undefined
  }, [
    effectiveJurisdictionId,
    failedVideosPayload,
    data?.totals.videos_fail,
    allFailedVideoRows.length,
  ])

  const lastActivityIso = useMemo(() => {
    const fromApi = data?.last_activity_at?.trim()
    if (fromApi) return fromApi
    const fromBatches = latestDashboardActivityIso(data?.batches ?? [])
    if (fromBatches) return fromBatches
    const running = data?.batches?.find((b) => b.status === 'running')
    return running?.updated_at?.trim() || null
  }, [data?.last_activity_at, data?.batches])
  const [agoClockMs, setAgoClockMs] = useState(() => Date.now())
  useEffect(() => {
    const id = window.setInterval(() => setAgoClockMs(Date.now()), 10_000)
    return () => window.clearInterval(id)
  }, [])
  const lastUpdateAgo = useMemo(
    () => (lastActivityIso ? formatAgoCompact(lastActivityIso, agoClockMs) : null),
    [lastActivityIso, agoClockMs],
  )
  const lastUpdateAbsolute = useMemo(
    () => (lastActivityIso ? formatUpdatedAt(lastActivityIso) : null),
    [lastActivityIso],
  )
  // Analyze/report runs don't touch the batch tracker (see "Last batch step"), so
  // these "ago" cards read the most recent policy stamps instead — the real signal
  // that the analyze pipeline is alive, including standalone/parallel runs.
  const lastTranscriptIso = data?.totals.last_transcript_at?.trim() || null
  const lastAnalysisIso = data?.totals.last_analysis_at?.trim() || null
  const lastReportIso = data?.totals.last_report_at?.trim() || null
  const lastTranscriptAgo = useMemo(
    () => (lastTranscriptIso ? formatAgoCompact(lastTranscriptIso, agoClockMs) : null),
    [lastTranscriptIso, agoClockMs],
  )
  const lastTranscriptAbsolute = useMemo(
    () => (lastTranscriptIso ? formatUpdatedAt(lastTranscriptIso) : null),
    [lastTranscriptIso],
  )
  const lastAnalysisAgo = useMemo(
    () => (lastAnalysisIso ? formatAgoCompact(lastAnalysisIso, agoClockMs) : null),
    [lastAnalysisIso, agoClockMs],
  )
  const lastReportAgo = useMemo(
    () => (lastReportIso ? formatAgoCompact(lastReportIso, agoClockMs) : null),
    [lastReportIso, agoClockMs],
  )
  const lastAnalysisAbsolute = useMemo(
    () => (lastAnalysisIso ? formatUpdatedAt(lastAnalysisIso) : null),
    [lastAnalysisIso],
  )
  const lastReportAbsolute = useMemo(
    () => (lastReportIso ? formatUpdatedAt(lastReportIso) : null),
    [lastReportIso],
  )

  const runningTiming = useMemo(
    () => aggregateRunningFileTiming(data?.batches ?? []),
    [data?.batches],
  )
  const remainingVideos = useMemo(
    () => remainingVideosForRunningBatches(data?.batches ?? []),
    [data?.batches],
  )
  const globalCurrentFileSeconds = useTickingSeconds(
    runningTiming.activeVideo?.startedAt,
    (data?.totals.running ?? 0) > 0 && !!runningTiming.activeVideo,
  )

  if (jobType === 'deployment') {
    return (
      <div className="min-h-0 w-full space-y-4">
        <JobTypeTabs active={jobType} onChange={setJobType} />
        <DeploymentPanel />
      </div>
    )
  }

  return (
    <div className="min-h-0 w-full space-y-4">
      <JobTypeTabs active={jobType} onChange={setJobType} />
      <div className="flex flex-wrap items-center justify-between gap-2">
        <p className="text-xs text-slate-600">
          Live from <code>bronze.youtube_batch_job_runs</code> via{' '}
          <code>GET /api/batch-jobs/stream</code>
          {streamLive && (
            <span className="ml-2 inline-flex items-center gap-1 text-emerald-700">
              <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
              connected
            </span>
          )}
        </p>
      </div>

      {isPending && !data && (
        <div className="rounded-lg border border-slate-200 bg-white p-8 text-center text-sm text-slate-500">
          Loading batch summary…
        </div>
      )}

      {isFetching && data && (
        <p className="text-xs text-slate-500">Refreshing batch summary…</p>
      )}

      {isError && (
        <div className="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800">
          Could not load batch jobs: {batchJobsFetchErrorMessage(error)}
          <p className="mt-2 text-xs text-red-700">
            Start or restart the API:{' '}
            <code className="rounded bg-red-100 px-1">
              uvicorn api.main:app --reload --port 8001
            </code>
            . Apply migration 073 if needed. Batch runs sync from{' '}
            <code>run_priority_states_last_n.sh</code>.
            {!streamLive && (
              <>
                {' '}
                <button
                  type="button"
                  className="underline"
                  onClick={() => refetch()}
                >
                  Retry load
                </button>
              </>
            )}
          </p>
        </div>
      )}

      {data && (
        <>
          {(() => {
            const report = data.stage_report ?? { states: [], rows: [] }
            const stateOpts = report.states ?? []
            const scope = stateFilter && stateOpts.includes(stateFilter) ? stateFilter : 'ALL'
            const rowFor = (
              sc: string,
              stage: PipelineStage,
            ): StageReportRow | undefined =>
              report.rows.find((r) => r.scope === sc && r.stage === stage)
            const fmtPct = (done: number, total: number) => {
              if (!total) return '—'
              const r = Math.round((done / total) * 1000) / 10
              return `${Number.isInteger(r) ? r : r.toFixed(1)}%`
            }
            const agoFromIso = (iso?: string) =>
              iso ? formatAgoCompact(iso, agoClockMs) : null
            const openFailed = () => setShowFailedVideos(true, { allBatches: true })
            const setScope = (sc: string) => {
              const next = new URLSearchParams(searchParams)
              if (sc) next.set('state', sc)
              else next.delete('state')
              setSearchParams(next, { replace: true })
            }
            const stageList: PipelineStage[] = [
              'discover',
              'videos',
              'transcripts',
              'analyses',
              'reports',
            ]
            const stageLabel: Record<PipelineStage, string> = {
              discover: 'Discover',
              videos: 'Videos',
              transcripts: 'Transcripts',
              analyses: 'Analyses',
              reports: 'Reports',
            }
            // Discover-stage per-entity split labels (counties vs municipalities).
            const entityShort: Record<string, string> = {
              counties: 'Co',
              municipalities: 'Mun',
            }
            const entityLabel: Record<string, string> = {
              counties: 'Counties',
              municipalities: 'Municipalities (cities/towns)',
            }
            // Which runnable step each stage's "Run" button kicks off.
            const stageStep: Record<PipelineStage, string> = {
              discover: 'discover',
              videos: 'catalog',
              transcripts: 'captions',
              analyses: 'analyze',
              reports: 'analyze',
            }
            const stepDesc: Record<PipelineStage, string> = {
              discover: 'discover channels (scrape sites / search YouTube)',
              videos: 'catalog — list videos on the channel',
              transcripts: 'captions — download transcripts',
              analyses: 'analyze — Gemini policy analysis',
              reports: 'analyze — regenerates reports too',
            }
            type Def = {
              n: number
              stage: PipelineStage
              unit: string
              sub?: string
              pill: boolean
              drill: (() => void) | null
              failTitle: string
            }
            const defs: Def[] = [
              {
                n: 0,
                stage: 'discover',
                unit: 'jurisdictions',
                sub: 'channel discovery — scrape county sites / search YouTube',
                pill: true,
                drill: null,
                failTitle: 'Jurisdictions still missing a YouTube channel',
              },
              {
                n: 1,
                stage: 'videos',
                unit: 'attempted',
                pill: false,
                drill: openFailed,
                failTitle: 'Videos whose caption fetch failed — tap to list',
              },
              {
                n: 2,
                stage: 'transcripts',
                unit: 'videos',
                sub: `${formatCompactNumber(data.totals.files_transcripts_disk ?? 0)} segment rows · deduped to videos`,
                pill: true,
                drill: openFailed,
                failTitle: 'Videos with no usable transcript — tap to list',
              },
              {
                n: 3,
                stage: 'analyses',
                unit: 'videos',
                pill: true,
                drill: null,
                failTitle: 'Analysis errors (bronze policy_analysis_error)',
              },
              {
                n: 4,
                stage: 'reports',
                unit: 'videos',
                pill: true,
                drill: null,
                failTitle: 'Report errors (bronze policy_report_error)',
              },
            ]
            // Fixed-width Failed/Run columns so the progress column lines up across rows.
            const cols =
              'grid grid-cols-[1.4fr_0.8fr_minmax(0,1.7fr)_4.5rem_6.5rem] items-center gap-3'
            const ls = launchStatus
            // ALL view scope → an explicit launch target (priority set or 50 + DC)
            // so the backend can't silently fall back to its priority-only default.
            const launchScopeStates =
              scope === 'ALL'
                ? allScope === 'all'
                  ? STATE_CODES
                  : PRIORITY_STATE_CODES
                : [scope]
            const scopeLaunchLabel =
              scope !== 'ALL' ? scope : allScope === 'all' ? 'all 50 + DC' : 'priority'
            // Each running step maps to the stage(s) it advances. Different steps
            // run concurrently, so we union them for the live "running" markers.
            const stepStages: Record<string, PipelineStage[]> = {
              discover: ['discover'],
              catalog: ['videos'],
              captions: ['videos', 'transcripts'],
              // Global mart-wide sweep — advances only the transcripts stage.
              backfill: ['transcripts'],
              analyze: ['analyses', 'reports'],
              all: ['discover', 'videos', 'transcripts', 'analyses', 'reports'],
              each: ['discover', 'videos', 'transcripts', 'analyses', 'reports'],
            }
            const runningSteps = new Set(ls?.running_steps ?? [])
            const stalledSteps = new Set(ls?.stalled_steps ?? [])
            const runningStages = new Set<PipelineStage>()
            runningSteps.forEach((s) =>
              (stepStages[s] ?? []).forEach((g) => runningStages.add(g)),
            )
            const anyFullRun = runningSteps.has('all') || runningSteps.has('each')
            // A stage's Run is allowed unless its step (or a full-pipeline run) is
            // already running — so e.g. Discover can launch while Analyze runs.
            const canRunStage = (g: PipelineStage) =>
              !!ls?.enabled &&
              !launching &&
              !runningSteps.has(stageStep[g]) &&
              !anyFullRun
            // The global backfill is its own step, so it may run alongside
            // captions; it's only blocked by another backfill or a full run.
            const canRunBackfill =
              !!ls?.enabled &&
              !launching &&
              !runningSteps.has('backfill') &&
              !anyFullRun
            const canRunAll =
              !!ls?.enabled &&
              !launching &&
              runningSteps.size === 0 &&
              data.totals.running === 0
            // Header "last activity" = the freshest per-step stamp for this scope,
            // not the stale batch-step clock.
            const freshestIso = stageList
              .map((s) => rowFor(scope, s)?.last_at || '')
              .reduce((a, b) => (a > b ? a : b), '')
            const freshestAgo = agoFromIso(freshestIso)
            // Mirrors the API stall window (_LAUNCH_STALL_SECONDS /
            // BATCH_JOB_INACTIVITY_SECONDS, default 3600s).
            const STALL_SECONDS = 3600
            const freshestAgeSecs = freshestIso
              ? Math.max(0, Math.floor((agoClockMs - Date.parse(freshestIso)) / 1000))
              : null
            // A batch row stays status='running' after its worker dies or starts
            // skipping already-done work, so totals.running alone overstates
            // liveness. Count row-running as live only with recent recorded
            // progress; otherwise it's a stalled run the reaper hasn't swept yet.
            const rowsStale =
              data.totals.running > 0 &&
              runningSteps.size === 0 &&
              freshestAgeSecs != null &&
              freshestAgeSecs > STALL_SECONDS
            const liveRunningRows = data.totals.running > 0 && !rowsStale
            const isActive = liveRunningRows || runningSteps.size > 0
            const anyStalled = stalledSteps.size > 0 || rowsStale
            const statusText = isActive ? 'Running' : anyStalled ? 'Stalled' : 'Idle'
            const statusDot = isActive
              ? 'bg-emerald-500'
              : anyStalled
                ? 'bg-amber-500'
                : 'bg-slate-400'
            return (
              <>
              <section className="rounded-xl border border-slate-200 bg-white shadow-sm">
                <div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-100 px-4 py-3">
                  <div className="flex flex-wrap items-center gap-1.5 text-sm">
                    <span
                      className={`inline-block h-2.5 w-2.5 rounded-full ${statusDot} ${
                        isActive ? 'animate-pulse' : ''
                      }`}
                    />
                    <span className="font-semibold text-slate-800">{statusText}</span>
                    {freshestAgo ? (
                      <span className="text-slate-500">· last activity {freshestAgo}</span>
                    ) : null}
                    <span className="text-slate-500">
                      {runningSteps.size > 0
                        ? `· running ${[...runningSteps].join(', ')}`
                        : rowsStale
                          ? `· ${formatCompactNumber(data.totals.running)} running, no progress >1h — likely stalled`
                          : stalledSteps.size > 0
                            ? `· ${[...stalledSteps].join(', ')} timed out (no activity 1h)`
                            : `· ${formatCompactNumber(data.totals.running)} running`}
                    </span>
                    {scope !== 'ALL' ? (
                      <span className="rounded bg-slate-100 px-1.5 py-0.5 text-xs font-medium text-slate-600">
                        {scope}
                      </span>
                    ) : null}
                  </div>
                  <div className="flex items-center gap-2">
                    {ls?.enabled && scope === 'ALL' ? (
                      <label
                        className="flex items-center gap-1 text-xs text-slate-500"
                        title="Which states a full / ALL-scope run targets. Priority = the dev set (AL, GA, IN, MA, MT, WA, WI); All = every US state + DC. Pick a single state from the scope dropdown to run just that one. Applies to the next launch."
                      >
                        <span aria-hidden>🗺️</span>
                        <select
                          value={allScope}
                          onChange={(e) => setAllScope(e.target.value as 'priority' | 'all')}
                          className="rounded-md border border-slate-300 bg-white px-1.5 py-1 text-xs font-medium text-slate-700"
                        >
                          <option value="priority">Priority states</option>
                          <option value="all">All states (50 + DC)</option>
                        </select>
                      </label>
                    ) : null}
                    {ls?.enabled ? (
                      <label
                        className="flex items-center gap-1 text-xs text-slate-500"
                        title="Jurisdictions analyzed in parallel — the main throughput lever (ceiling is Gemini quota). Applies to the next launch."
                      >
                        <span aria-hidden>⚡</span>
                        <select
                          value={parallelism}
                          onChange={(e) => setParallel(Number(e.target.value))}
                          className="rounded-md border border-slate-300 bg-white px-1.5 py-1 text-xs font-medium text-slate-700"
                        >
                          {[2, 4, 6, 8].map((p) => (
                            <option key={p} value={p}>
                              {p}× parallel
                            </option>
                          ))}
                        </select>
                      </label>
                    ) : null}
                    {ls?.enabled ? (
                      <button
                        type="button"
                        disabled={!canRunAll}
                        onClick={() => runLaunch('all', launchScopeStates)}
                        title={
                          runningSteps.size > 0
                            ? 'Finish the running step(s) before a full-pipeline run'
                            : `Run the full pipeline (catalog → captions → analyze) for ${scopeLaunchLabel} states`
                        }
                        className="inline-flex items-center gap-1 rounded-md bg-[#354F52] px-2.5 py-1.5 text-xs font-semibold uppercase tracking-wide text-white shadow-sm hover:bg-[#2c4346] disabled:cursor-not-allowed disabled:opacity-50"
                      >
                        {launching ? 'Launching…' : `▶ Run all · ${scopeLaunchLabel}`}
                      </button>
                    ) : null}
                    {ls?.enabled && (ls?.busy || runningSteps.size > 0 || stalledSteps.size > 0) ? (
                      <button
                        type="button"
                        disabled={stopping}
                        onClick={() => void stopLaunch()}
                        title="Stop all running pipeline jobs (SIGTERM — work checkpoints and exits)"
                        className="inline-flex items-center gap-1 rounded-md bg-rose-600 px-2.5 py-1.5 text-xs font-semibold uppercase tracking-wide text-white shadow-sm hover:bg-rose-700 disabled:cursor-not-allowed disabled:opacity-50"
                      >
                        {stopping ? 'Stopping…' : '■ Stop all'}
                      </button>
                    ) : null}
                    {stateOpts.length > 0 ? (
                      <select
                        value={scope === 'ALL' ? '' : scope}
                        onChange={(e) => setScope(e.target.value)}
                        title="Scope the stage numbers (and the detail tables) to one state"
                        className="rounded-md border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50"
                      >
                        <option value="">All states</option>
                        {stateOpts.map((s) => (
                          <option key={s} value={s}>
                            {s}
                          </option>
                        ))}
                      </select>
                    ) : null}
                  </div>
                </div>
                {launchMsg ? (
                  <div className="border-b border-slate-100 bg-slate-50 px-4 py-1.5 text-xs text-slate-600">
                    {launchMsg}
                  </div>
                ) : null}
                <div
                  className={`${cols} px-4 py-2 text-[10px] font-semibold uppercase tracking-wide text-slate-400`}
                >
                  <div>Stage</div>
                  <div>Last activity</div>
                  <div>Progress</div>
                  <div className="text-right">Failed</div>
                  <div className="text-right">Run</div>
                </div>
                {defs.map((st, idx) => {
                  const r = rowFor(scope, st.stage)
                  const done = r?.done ?? 0
                  const total = r?.total ?? 0
                  const failed = r?.failed ?? 0
                  const pct = total ? (done / total) * 100 : 0
                  const ago = agoFromIso(r?.last_at)
                  const running = runningStages.has(st.stage)
                  const expanded = expandedStage === st.stage
                  const tm = data.stage_report?.timing?.[st.stage]
                  // Live pace: avg_seconds is the median gap between *completed*
                  // files, so it freezes during a stall (no new gap is ever
                  // recorded). Once a running stage is overdue for its next
                  // completion, our best estimate of the current pace is at
                  // least how long we've already been waiting — fold the open
                  // trailing gap in so /hr and ETA degrade every clock tick and
                  // recover the moment a file lands.
                  const avgSecs = tm?.avg_seconds && tm.avg_seconds > 0 ? tm.avg_seconds : null
                  const lastAtMs = tm?.last_at ? Date.parse(tm.last_at) : NaN
                  const sinceLastSecs = Number.isFinite(lastAtMs)
                    ? Math.max(0, (agoClockMs - lastAtMs) / 1000)
                    : null
                  const effectiveSecs =
                    avgSecs == null
                      ? null
                      : running && sinceLastSecs != null
                        ? Math.max(avgSecs, sinceLastSecs)
                        : avgSecs
                  const stalled =
                    running &&
                    avgSecs != null &&
                    sinceLastSecs != null &&
                    sinceLastSecs > avgSecs * 3
                  return (
                    <Fragment key={st.stage}>
                    <div className={`${cols} border-t border-slate-100 px-4 py-3`}>
                      <button
                        type="button"
                        onClick={() => setExpandedStage(expanded ? null : st.stage)}
                        className="min-w-0 text-left"
                        title="Show this stage's live log + timing"
                      >
                        <div className="flex items-center gap-1 text-sm font-semibold text-slate-800">
                          <span className="text-slate-400">{expanded ? '▾' : '▸'}</span>
                          {idx + 1} · {stageLabel[st.stage]}
                        </div>
                        {st.sub ? (
                          <div className="mt-0.5 flex items-center gap-1 text-[11px] text-amber-600">
                            <span aria-hidden>⚠️</span>
                            <span className="truncate" title={st.sub}>
                              {st.sub}
                            </span>
                          </div>
                        ) : null}
                      </button>
                      <div className="flex items-center gap-1.5 text-sm">
                        {running ? (
                          <>
                            <span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
                            <span className="font-medium text-emerald-600">running…</span>
                          </>
                        ) : (
                          <>
                            <span className="inline-block h-1.5 w-1.5 rounded-full bg-slate-400" />
                            <span className="text-slate-600">{ago ?? '—'}</span>
                          </>
                        )}
                      </div>
                      <div className="min-w-0">
                        <div className="text-sm tabular-nums text-slate-700">
                          <span className="font-semibold text-slate-900">
                            {formatCompactNumber(done)}
                          </span>{' '}
                          / {formatCompactNumber(total)} {st.unit} · {fmtPct(done, total)}
                        </div>
                        <div className="mt-1.5">
                          <ProgressBar pct={pct} />
                        </div>
                        {st.stage === 'discover' && (r?.breakdown?.length ?? 0) > 0 ? (
                          <div className="mt-1 flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-500">
                            {r!.breakdown!.map((b) => (
                              <span
                                key={b.entity}
                                title={`${entityLabel[b.entity] ?? b.entity}: ${formatCompactNumber(b.done)} of ${formatCompactNumber(b.total)} with a channel`}
                              >
                                <span className="font-medium text-slate-600">
                                  {entityLabel[b.entity] ?? b.entity}
                                </span>{' '}
                                {fmtPct(b.done, b.total)}{' '}
                                <span className="tabular-nums text-slate-400">
                                  {formatCompactNumber(b.done)}/{formatCompactNumber(b.total)}
                                </span>
                              </span>
                            ))}
                          </div>
                        ) : null}
                      </div>
                      <div className="text-right">
                        {failed > 0 ? (
                          st.drill ? (
                            <button
                              type="button"
                              onClick={st.drill}
                              title={st.failTitle}
                              className={
                                st.pill
                                  ? 'inline-flex items-center gap-0.5 rounded-full border border-red-200 bg-red-50 px-2 py-0.5 text-xs font-semibold text-red-700 hover:bg-red-100'
                                  : 'text-sm font-semibold text-red-700 hover:underline'
                              }
                            >
                              {formatCompactNumber(failed)}
                              <span className="text-red-400" aria-hidden>
                                ⌄
                              </span>
                            </button>
                          ) : (
                            <span
                              title={st.failTitle}
                              className="inline-flex items-center rounded-full border border-red-200 bg-red-50 px-2 py-0.5 text-xs font-semibold text-red-700"
                            >
                              {formatCompactNumber(failed)}
                            </span>
                          )
                        ) : (
                          <span className="text-xs text-slate-400">0</span>
                        )}
                      </div>
                      <div className="flex flex-col items-end gap-1">
                        {ls?.enabled ? (
                          <button
                            type="button"
                            disabled={!canRunStage(st.stage)}
                            onClick={() => runLaunch(stageStep[st.stage], launchScopeStates)}
                            title={
                              runningSteps.has(stageStep[st.stage])
                                ? `${stageStep[st.stage]} is already running`
                                : `Run ${stepDesc[st.stage]} · ${scopeLaunchLabel}`
                            }
                            className="whitespace-nowrap rounded-md border border-slate-300 bg-white px-2 py-1 text-[11px] font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
                          >
                            {running ? '· · ·' : '▶ Run'}
                          </button>
                        ) : null}
                        {/* Transcripts only: global mart-wide sweep (also reaches
                            LocalView/union videos captions never visits). The
                            size picker sets how many missing transcripts one
                            click fetches (All = sweep everything). */}
                        {ls?.enabled && st.stage === 'transcripts' ? (
                          <>
                            <button
                              type="button"
                              disabled={!canRunBackfill}
                              onClick={() =>
                                runLaunch(
                                  'backfill',
                                  // The backfill is a flat mart-wide sweep, so
                                  // "All states (50 + DC)" is best expressed as
                                  // an unfiltered national sweep ([] → backend
                                  // omits --states). A literal 50 + DC state
                                  // filter silently drops the CivicSearch videos
                                  // that carry no state_code (~19.5k school +
                                  // ~3k general), which are the priority sources
                                  // here. Priority/single-state scopes still
                                  // filter as before.
                                  scope === 'ALL' && allScope === 'all'
                                    ? []
                                    : launchScopeStates,
                                  backfillSize,
                                )
                              }
                              title={
                                runningSteps.has('backfill')
                                  ? 'backfill is already running'
                                  : `Backfill ${
                                      backfillSize === 0 ? 'all' : backfillSize
                                    } missing transcript(s) across the full event mart (LocalView + YouTube) · ${scopeLaunchLabel}`
                              }
                              className="whitespace-nowrap rounded-md border border-slate-300 bg-white px-2 py-1 text-[11px] font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
                            >
                              {runningSteps.has('backfill') ? '· · ·' : '▶ Backfill'}
                            </button>
                            <select
                              value={backfillSize}
                              onChange={(e) => setBackfillSize(Number(e.target.value))}
                              title="How many missing transcripts one Backfill click fetches (All = sweep everything; runs are resumable)"
                              className="w-full rounded-md border border-slate-300 bg-white px-1 py-0.5 text-[10px] font-medium text-slate-600 hover:bg-slate-50"
                            >
                              <option value={100}>100</option>
                              <option value={500}>500</option>
                              <option value={2000}>2,000</option>
                              <option value={4000}>4,000</option>
                              <option value={0}>All</option>
                            </select>
                          </>
                        ) : null}
                      </div>
                    </div>
                    {expanded ? (
                      <div className="border-t border-slate-100 bg-slate-50 px-4 py-3 text-xs text-slate-600">
                        <div className="flex flex-wrap gap-x-6 gap-y-1">
                          <span>
                            avg <b className="text-slate-800">{fmtSecs(avgSecs)}</b>/file
                            {effectiveSecs ? ` · ${Math.round(3600 / effectiveSecs)}/hr` : ''}
                            {stalled ? (
                              <span
                                className="ml-1 font-semibold text-amber-600"
                                title="No new completions — /hr and ETA now reflect the time since the last file, not the historical pace"
                              >
                                · stalled
                              </span>
                            ) : null}
                          </span>
                          {effectiveSecs && total - done > 0 ? (
                            <span>
                              ETA{' '}
                              <b className={stalled ? 'text-amber-600' : 'text-slate-800'}>
                                {fmtEta(((total - done) * effectiveSecs) / 3600)}
                              </b>{' '}
                              for {formatCompactNumber(total - done)} left
                            </span>
                          ) : null}
                          <span>
                            last file:{' '}
                            <b className="text-slate-800" title={tm?.last_path}>
                              {baseName(tm?.last_path)}
                            </b>
                            {tm?.last_at ? ` · ${agoFromIso(tm.last_at) ?? ''}` : ''}
                          </span>
                          {running && stageLog?.current ? (
                            <span className="min-w-0">
                              current:{' '}
                              <b className="text-emerald-700">{stageLog.current}</b>
                              {stageLog.current_since
                                ? ` · ${agoFromIso(stageLog.current_since) ?? ''} on it`
                                : ''}
                            </span>
                          ) : null}
                        </div>
                        <pre className="mt-2 max-h-56 overflow-auto rounded bg-slate-900 p-2 text-[10px] leading-snug text-slate-100">
                          {stageLog?.lines?.length
                            ? stageLog.lines.join('\n')
                            : 'No log for this step yet — launch it to see live output.'}
                        </pre>
                        {stageLog?.path ? (
                          <div className="mt-1 text-[10px] text-slate-400">log: {stageLog.path}</div>
                        ) : null}
                      </div>
                    ) : null}
                    </Fragment>
                  )
                })}
                <div className="border-t border-slate-100 px-4 py-2 text-[11px] text-slate-400">
                  <span className="text-emerald-500" aria-hidden>
                    ●
                  </span>{' '}
                  retryable{'   '}
                  <span className="text-slate-500" aria-hidden>
                    ▪
                  </span>{' '}
                  terminal · tap a failed count to drill in
                  {ls && !ls.enabled ? ' · launch disabled (set BATCH_JOBS_ALLOW_LAUNCH=1)' : ''}
                </div>
              </section>

              {stateOpts.length > 0 ? (
                <section className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
                  <div className="border-b border-slate-100 px-4 py-2 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
                    State coverage · {stateOpts.length} states · click a row to scope
                  </div>
                  <div className="overflow-x-auto">
                    <table className="w-full text-sm">
                      <thead>
                        <tr className="text-[10px] uppercase tracking-wide text-slate-400">
                          <th className="px-4 py-2 text-left font-semibold">State</th>
                          {stageList.map((s) => (
                            <th key={s} className="px-3 py-2 text-right font-semibold">
                              {stageLabel[s]}
                            </th>
                          ))}
                        </tr>
                      </thead>
                      <tbody>
                        {stateOpts.map((stCode) => (
                          <tr
                            key={stCode}
                            onClick={() => setScope(stCode)}
                            className={`cursor-pointer border-t border-slate-100 hover:bg-slate-50 ${
                              scope === stCode ? 'bg-slate-50' : ''
                            }`}
                          >
                            <td className="px-4 py-2 font-semibold text-slate-800">{stCode}</td>
                            {stageList.map((s) => {
                              const r = rowFor(stCode, s)
                              const done = r?.done ?? 0
                              const total = r?.total ?? 0
                              const bd = s === 'discover' ? r?.breakdown ?? [] : []
                              return (
                                <td key={s} className="px-3 py-2 text-right tabular-nums">
                                  <div className="text-slate-700">{fmtPct(done, total)}</div>
                                  <div className="text-[10px] text-slate-400">
                                    {formatCompactNumber(done)}/{formatCompactNumber(total)}
                                  </div>
                                  {bd.length > 0 ? (
                                    <div className="mt-0.5 space-y-px">
                                      {bd.map((b) => (
                                        <div
                                          key={b.entity}
                                          className="text-[10px] text-slate-400"
                                          title={`${entityLabel[b.entity] ?? b.entity}: ${formatCompactNumber(b.done)} of ${formatCompactNumber(b.total)} with a channel`}
                                        >
                                          <span className="text-slate-500">
                                            {entityShort[b.entity] ?? b.entity}
                                          </span>{' '}
                                          {fmtPct(b.done, b.total)}{' '}
                                          <span className="text-slate-300">
                                            {formatCompactNumber(b.done)}/{formatCompactNumber(b.total)}
                                          </span>
                                        </div>
                                      ))}
                                    </div>
                                  ) : null}
                                </td>
                              )
                            })}
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                </section>
              ) : null}
              </>
            )
          })()}

          <details className="group">
            <summary className="cursor-pointer select-none py-1 text-xs font-medium text-slate-500 hover:text-slate-700">
              All metrics
            </summary>
            <div className="mt-2 grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
            {lastActivityIso && lastUpdateAgo ? (
              <SummaryCard
                label="Last batch step"
                value={lastUpdateAgo}
                title={
                  lastUpdateAbsolute?.title
                    ? `${lastUpdateAbsolute.title} · last catalog/captions batch step (NOT the analyze pipeline — see Last analysis/report)${
                        data.source === 'database' ? ' · from database' : ''
                      }`
                    : 'Last catalog/captions batch step (NOT the analyze pipeline — see Last analysis/report)'
                }
              />
            ) : null}
            {lastTranscriptIso && lastTranscriptAgo ? (
              <SummaryCard
                label="Last transcript"
                value={lastTranscriptAgo}
                title={
                  lastTranscriptAbsolute?.title
                    ? `${lastTranscriptAbsolute.title} · most recent transcript downloaded (transcript_download_at)`
                    : 'Most recent transcript downloaded (transcript_download_at)'
                }
              />
            ) : null}
            {lastAnalysisIso && lastAnalysisAgo ? (
              <SummaryCard
                label="Last analysis"
                value={lastAnalysisAgo}
                title={
                  lastAnalysisAbsolute?.title
                    ? `${lastAnalysisAbsolute.title} · most recent AI analysis written (policy_analysis_at; includes standalone/parallel analyze runs)`
                    : 'Most recent AI analysis written (includes standalone/parallel analyze runs)'
                }
              />
            ) : null}
            {lastReportIso && lastReportAgo ? (
              <SummaryCard
                label="Last report"
                value={lastReportAgo}
                title={
                  lastReportAbsolute?.title
                    ? `${lastReportAbsolute.title} · most recent report generated (policy_report_at; includes standalone/parallel analyze runs)`
                    : 'Most recent report generated (includes standalone/parallel analyze runs)'
                }
              />
            ) : null}
            <SummaryCard
              label="Batches"
              value={formatCompactNumber(data.totals.batches)}
              title={metricCountTitle(data.totals.batches, 'Batches')}
            />
            <SummaryCard
              label="Running"
              value={formatCompactNumber(data.totals.running)}
              title={
                runningTiming.idleRunningBatchCount > 0
                  ? `${formatFullNumber(data.totals.running)} batch(es) with status running; ${runningTiming.idleRunningBatchCount} idle (no active file). Idle runs auto-cancel after 1h without progress.`
                  : metricCountTitle(data.totals.running, 'Running batches')
              }
            />
            <SummaryCard
              label="States started"
              value={formatCompactPair(
                data.totals.states_started ?? 0,
                data.totals.states_planned ?? data.totals.states ?? 0,
              )}
              title="States with at least one jurisdiction running or finished in the batch plan"
            />
            <SummaryCard
              label="States completed"
              value={formatCompactPair(
                data.totals.states_completed ?? 0,
                data.totals.states_planned ?? data.totals.states ?? 0,
              )}
              title="States where every planned jurisdiction row is finished (none pending or running)"
            />
            {data.totals.running > 0 ? (
              <>
                <SummaryCard
                  label="Avg / file"
                  value={formatDuration(runningTiming.avgSecondsPerFile)}
                />
                <SummaryCard
                  label="Current file"
                  value={
                    runningTiming.activeVideo
                      ? formatDuration(globalCurrentFileSeconds)
                      : 'Idle'
                  }
                  title={
                    runningTiming.activeVideo
                      ? [
                          'Time on this in-flight video since it started (can exceed Avg / file if yt-dlp is slow or stuck).',
                          runningTiming.activeVideo.title,
                          runningTiming.activeVideo.videoId,
                          runningTiming.activeVideo.jurisdictionId,
                        ]
                          .filter(Boolean)
                          .join(' · ') || 'In-flight video'
                      : runningTiming.idleRunningBatchCount > 0
                        ? `${runningTiming.idleRunningBatchCount} running batch(es) with no in-flight video (between jurisdictions or waiting).`
                        : 'No in-flight video with a start time'
                  }
                />
                <SummaryCard
                  label="Videos remaining"
                  value={
                    remainingVideos != null ? formatCompactNumber(remainingVideos) : '—'
                  }
                  title={metricCountTitle(remainingVideos, 'Videos remaining')}
                />
              </>
            ) : null}
            <SummaryCard
              label="Jurisdictions done"
              value={formatCompactNumber(data.totals.processed_jurisdictions)}
              title={metricCountTitle(data.totals.processed_jurisdictions, 'Jurisdictions done')}
            />
            <SummaryCard
              label="Jurisdictions failed"
              value={formatCompactNumber(data.totals.failed_jurisdictions)}
              title={metricCountTitle(data.totals.failed_jurisdictions, 'Jurisdictions failed')}
            />
            <SummaryCard
              label="Jurisdictions remaining"
              value={formatCompactNumber(data.totals.remaining_jurisdictions)}
              title={metricCountTitle(
                data.totals.remaining_jurisdictions,
                'Jurisdictions remaining',
              )}
            />
            <SummaryCard
              label="Videos OK"
              value={formatCompactNumber(data.totals.videos_ok)}
              title={metricCountTitle(data.totals.videos_ok, 'Videos OK')}
            />
            <SummaryCard
              label="Videos failed"
              value={formatCompactNumber(data.totals.videos_fail)}
              title={metricCountTitle(data.totals.videos_fail, 'Videos failed')}
              emphasis={data.totals.videos_fail > 0 ? 'danger' : 'default'}
              active={showFailedVideos}
              onClick={
                data.totals.videos_fail > 0
                  ? () =>
                      setShowFailedVideos(!showFailedVideos, {
                        allBatches: !showFailedVideos,
                      })
                  : undefined
              }
            />
            <SummaryCard
              label="Videos attempted"
              value={formatCompactNumber(data.totals.videos_attempted ?? 0)}
              title={
                metricCountTitle(data.totals.videos_attempted, 'Videos attempted') ??
                'Caption fetch outcomes this batch (ok + fail + tombstoned + empty + rate limit)'
              }
            />
            <SummaryCard
              label="Transcripts"
              value={formatCompactNumber(data.totals.files_transcripts_disk ?? 0)}
              title={
                metricCountTitle(data.totals.files_transcripts_disk, 'Transcripts') ??
                'Distinct videos with a downloaded transcript (bronze transcript_download_at, all time) — the denominator for analysis/report progress'
              }
            />
            <SummaryCard
              label="Transcript hours"
              value={formatCompactHours(data.totals.transcript_hours ?? 0)}
              title={
                metricCountTitle(data.totals.transcript_hours, 'Transcript hours') ??
                'Sum of catalog duration_minutes for batch OK videos (from per-video rows or bronze since batch start).'
              }
            />
            <SummaryCard
              label="Bronze download rows"
              value={formatCompactNumber(data.totals.bronze_download_rows ?? 0)}
              title={
                metricCountTitle(data.totals.bronze_download_rows, 'Bronze download rows') ??
                'bronze_event_youtube rows with transcript_download_at (all time)'
              }
            />
            <SummaryCard
              label="Analyses"
              value={formatCompactNumber(data.totals.files_analysis)}
              title={
                metricCountTitle(data.totals.files_analysis, 'Analyses') ??
                'Videos with an AI analysis (bronze policy_analysis_at, all time, live)'
              }
            />
            <SummaryCard
              label="Reports"
              value={formatCompactNumber(data.totals.files_reports)}
              title={
                metricCountTitle(data.totals.files_reports, 'Reports') ??
                'Videos with a generated report (bronze policy_report_at, all time, live)'
              }
            />
            <SummaryCard
              label="Analysis progress"
              value={
                data.totals.files_transcripts_disk > 0
                  ? `${Math.round((data.totals.files_analysis / data.totals.files_transcripts_disk) * 100)}%`
                  : '—'
              }
              title={`${formatCompactNumber(data.totals.files_analysis)} of ${formatCompactNumber(
                data.totals.files_transcripts_disk ?? 0,
              )} transcripts have an AI analysis (live bronze stamps)`}
            />
            <SummaryCard
              label="Reports progress"
              value={
                data.totals.files_transcripts_disk > 0
                  ? `${Math.round((data.totals.files_reports / data.totals.files_transcripts_disk) * 100)}%`
                  : '—'
              }
              title={`${formatCompactNumber(data.totals.files_reports)} of ${formatCompactNumber(
                data.totals.files_transcripts_disk ?? 0,
              )} transcripts have a generated report (live bronze stamps; same denominator as Analysis progress, so the two read as one funnel — not reports÷analyses)`}
            />
            <SummaryCard
              label="Analysis (24h)"
              value={formatCompactNumber(data.totals.files_analysis_recent ?? 0)}
              title={
                metricCountTitle(data.totals.files_analysis_recent, 'AI analyses') ??
                'AI summary analyses written in the last 24 hours (by file mtime)'
              }
            />
            <SummaryCard
              label="Reports (24h)"
              value={formatCompactNumber(data.totals.files_reports_recent ?? 0)}
              title={
                metricCountTitle(data.totals.files_reports_recent, 'Reports') ??
                'Reports generated in the last 24 hours (by file mtime)'
              }
            />
            </div>
          </details>

          {showFailedVideos && !effectiveJurisdictionId && (
            <FailedVideosPanel
              rows={allFailedVideoRows}
              scopeLabel={failedScopeLabel}
              subtitle={
                failedVideosFetching
                  ? 'loading…'
                  : failedVideosSubtitle
              }
              onClose={() => setShowFailedVideos(false)}
              onSelectJurisdiction={(bid, jid) => {
                setShowFailedVideos(true, { batchId: bid, jurisdictionId: jid })
              }}
            />
          )}

          <div className="grid min-h-0 gap-4 lg:grid-cols-[minmax(220px,280px)_1fr]">
            {videosLoading || stateJurisdictionsFetching ? (
              <p className="text-xs text-slate-500 lg:col-span-2">
                {videosLoading
                  ? 'Loading per-video details…'
                  : 'Loading jurisdictions for selected state…'}
              </p>
            ) : null}
            <aside className="rounded-lg border border-slate-200 bg-white shadow-sm">
              <div className="border-b border-slate-100 px-3 py-2 text-sm font-semibold text-slate-800">
                Batches
              </div>
              <ul className="max-h-[min(520px,60vh)] overflow-auto p-2">
                {data.batches.length === 0 ? (
                  <li className="px-2 py-4 text-center text-xs text-slate-500">No batches yet</li>
                ) : (
                  data.batches.map((b) => {
                    const sum = b.summary || {}
                    const t = Number(sum.total_jurisdictions) || 0
                    const p = Number(sum.processed_jurisdictions) || 0
                    const pct = t > 0 ? Math.round((100 * p) / t) : 0
                    const active = selectedBatch?.batch_id === b.batch_id
                    return (
                      <li key={b.batch_id} className="mb-1">
                        <button
                          type="button"
                          onClick={() => setBatch(b.batch_id)}
                          className={`w-full rounded-md border px-2 py-2 text-left text-sm transition-colors ${
                            active
                              ? 'border-teal-500 bg-teal-50'
                              : 'border-transparent hover:bg-slate-50'
                          }`}
                        >
                          <div className="flex items-center gap-1">
                            <span className="font-medium text-slate-900">{b.step}</span>
                            <span
                              className={`rounded px-1 text-[10px] font-medium ${statusBadgeClass(b.status)}`}
                            >
                              {b.status}
                            </span>
                          </div>
                          <div className="mt-0.5 truncate font-mono text-[10px] text-slate-500">
                            {b.batch_id}
                          </div>
                          <div className="mt-1 text-[11px] text-slate-600">
                            {p}/{t || '?'} jurisdictions · {pct}%
                          </div>
                        </button>
                      </li>
                    )
                  })
                )}
              </ul>
            </aside>

            <section className="min-w-0">
              {selectedBatchForPanel ? (
                <BatchDetailPanel
                  batch={selectedBatchForPanel}
                  selectedJurisdictionId={effectiveJurisdictionId}
                  onSelectJurisdiction={setJurisdiction}
                  stateFilter={effectiveStateFilter}
                  onStateFilterChange={setStateFilter}
                  showFailedVideos={showFailedVideos}
                  onToggleFailedVideos={() =>
                      setShowFailedVideos(!showFailedVideos, {
                        batchId: selectedBatchForPanel.batch_id,
                        allBatches: false,
                      })
                    }
                  onShowFailedForJurisdiction={(jid) => {
                    setShowFailedVideos(true, {
                      batchId: selectedBatchForPanel.batch_id,
                      jurisdictionId: jid,
                    })
                    requestAnimationFrame(() => {
                      document
                        .getElementById('batch-jurisdiction-drilldown')
                        ?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
                    })
                  }}
                />
              ) : (
                <div className="rounded-lg border border-slate-200 bg-white p-8 text-center text-sm text-slate-500">
                  Select a batch
                </div>
              )}
            </section>
          </div>
        </>
      )}
    </div>
  )
}
