'use client'

import { useMemo, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {
  averageResolutionHours,
  CONFIDENCE_REVIEWS,
  CORRECT_DECISIONS,
  DEVELOPER_ISSUE_CATEGORIES,
  groupRepeatedIssues,
  QA_VISIBILITY,
  SOURCE_ALIGNMENTS,
  SOURCE_HIERARCHY,
  TERMINAL_STATUSES,
  TICKET_DECISIONS,
  TICKET_STATUS_LABELS,
  TRIGGER_LABELS,
  UPDATE_TYPES,
  WRONG_DECISIONS,
} from '@/lib/domain/moderation'
import { getStateInfo, STATE_INFO_TOPICS } from '@/lib/demo/state-info'
import { STATE_NAMES, stateName } from '@/lib/domain/states'
import { formatDateTime, formatInches, formatWeight } from '@/lib/format'
import { BackLink } from '@/components/app/back-link'
import type { DeveloperIssue, Permit, ReviewTicket, TicketEvent, TicketStatus } from '@/types/db'

export interface TicketView {
  ticket: ReviewTicket
  question: string | null
  questionAuthor: string | null
  answer: string
  answerSources: string[] | null
  tripRef: string
  tripLane: string
  carrierName: string
  permit: Permit | null
  permitUrl: string | null
}

export interface ModerationSettings {
  categories: string[]
  state_reviewers: Record<string, string>
}

type View = 'queue' | 'escalated' | 'corrections' | 'states' | 'analytics' | 'knowledge' | 'settings'

/** Left sidebar navigation (§8). */
const NAV: Array<[View, string]> = [
  ['queue', 'Review Queue'],
  ['escalated', 'Escalated'],
  ['corrections', 'Corrections Pending Approval'],
  ['states', 'State Issues'],
  ['analytics', 'Analytics'],
  ['knowledge', 'Knowledge Updates'],
  ['settings', 'Moderator Settings'],
]

const PRIORITY_STYLE: Record<string, string> = {
  high: 'bg-red-50 text-red-700 border-red-200',
  medium: 'bg-amber-50 text-amber-700 border-amber-200',
  low: 'bg-neutral-100 text-neutral-600 border-neutral-200',
}

const normalize = (q: string) =>
  q.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim()

/** One call shape for every moderator action (see the API route's ops). */
async function moderationCall(body: Record<string, unknown>): Promise<boolean> {
  const res = await fetch('/api/admin/moderation', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
  const json = await res.json().catch(() => ({}))
  if (!res.ok) {
    toast.error(json.error ?? 'Action failed')
    return false
  }
  return true
}

/**
 * Moderator Dashboard — a compliance review console, not a comment inbox.
 * Feedback is a review trigger, never truth; only approved corrections ever
 * become knowledge (the admin acts as moderator, state expert, and knowledge
 * manager until the finer roles exist). Moderators never delete questions,
 * answers, or audit history.
 */
export function ModerationConsole({
  views,
  events,
  developerIssues,
  settings,
  adminName,
}: {
  views: TicketView[]
  events: TicketEvent[]
  developerIssues: DeveloperIssue[]
  settings: ModerationSettings
  adminName: string
}) {
  const router = useRouter()
  const [view, setView] = useState<View>('queue')
  const [selectedId, setSelectedId] = useState<string | null>(null)
  const tickets = views.map((v) => v.ticket)

  // ---- summary bar (§8) ----
  const count = (s: TicketStatus) => tickets.filter((t) => t.status === s).length
  const stateCounts = new Map<string, number>()
  for (const t of tickets) if (t.state_code) stateCounts.set(t.state_code, (stateCounts.get(t.state_code) ?? 0) + 1)
  const topState = [...stateCounts.entries()].sort((a, b) => b[1] - a[1])[0]
  const decided = tickets.filter((t) => t.decision && (CORRECT_DECISIONS.has(t.decision) || WRONG_DECISIONS.has(t.decision)))
  const accuracy =
    decided.length > 0
      ? Math.round((decided.filter((t) => CORRECT_DECISIONS.has(t.decision!)).length / decided.length) * 100)
      : null
  const avgHours = averageResolutionHours(tickets)
  const summary: Array<[string, string | number]> = [
    ['New feedback', count('new')],
    ['In review', count('in_review')],
    ['Escalated', count('escalated')],
    ['Approved corrections', count('approved_for_knowledge_update') + count('knowledge_updated')],
    ['Top problem state', topState ? `${topState[0]} (${topState[1]})` : '—'],
    ['Avg resolution', avgHours == null ? '—' : avgHours < 48 ? `${avgHours.toFixed(1)} h` : `${(avgHours / 24).toFixed(1)} d`],
    ['AI accuracy', accuracy == null ? '—' : `${accuracy}%`],
    ['Low confidence', tickets.filter((t) => t.confidence === 'low' || !t.confidence).length],
  ]

  const selected = selectedId ? (views.find((v) => v.ticket.id === selectedId) ?? null) : null
  const open = (id: string) => setSelectedId(id)
  const refresh = () => router.refresh()

  return (
    <div className="mx-auto max-w-7xl space-y-5 px-4 py-8">
      <BackLink />
      <div>
        <h1 className="text-2xl font-bold">Moderator Dashboard</h1>
        <p className="mt-1 text-sm text-neutral-500">
          Review disputed AI answers — feedback is a review trigger, never truth. Every action is
          audited.
        </p>
      </div>

      {/* Summary bar */}
      <div className="grid grid-cols-2 gap-2 text-center sm:grid-cols-4 lg:grid-cols-8">
        {summary.map(([label, value]) => (
          <div key={label} className="rounded-xl border bg-white p-2.5">
            <p className="text-lg font-bold">{value}</p>
            <p className="text-[10px] font-semibold uppercase tracking-wide text-neutral-400">{label}</p>
          </div>
        ))}
      </div>

      <div className="grid gap-5 lg:grid-cols-[210px_1fr]">
        {/* Sidebar (§8) */}
        <nav className="flex gap-1 overflow-x-auto lg:flex-col lg:overflow-visible">
          {NAV.map(([key, label]) => {
            const badge =
              key === 'escalated' ? count('escalated') : key === 'corrections' ? count('correction_proposed') : 0
            return (
              <button
                key={key}
                onClick={() => {
                  setView(key)
                  setSelectedId(null)
                }}
                className={`flex shrink-0 items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm font-semibold transition ${
                  view === key ? 'bg-[#0f1b2d] text-white' : 'text-neutral-600 hover:bg-neutral-100'
                }`}
              >
                {label}
                {badge > 0 && (
                  <span className={`rounded-full px-1.5 text-[10px] ${view === key ? 'bg-white/20' : 'bg-red-50 text-red-700'}`}>
                    {badge}
                  </span>
                )}
              </button>
            )
          })}
          {/* Company verification queue (Task 93) — same admin, one click away. */}
          <Link
            href="/admin/company-review"
            className="flex shrink-0 items-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold text-neutral-600 transition hover:bg-neutral-100"
          >
            Company Review →
          </Link>
        </nav>

        <div className="min-w-0">
          {selected ? (
            <TicketDetail
              view={selected}
              events={events.filter((e) => e.ticket_id === selected.ticket.id)}
              issues={developerIssues.filter((i) => i.ticket_id === selected.ticket.id)}
              allTickets={tickets}
              adminName={adminName}
              onBack={() => setSelectedId(null)}
              onSaved={refresh}
            />
          ) : view === 'queue' || view === 'escalated' || view === 'corrections' ? (
            <QueueView
              views={views}
              preset={view}
              categories={settings.categories}
              adminName={adminName}
              onOpen={open}
              onSaved={refresh}
            />
          ) : view === 'states' ? (
            <StateIssues views={views} settings={settings} onOpen={open} onSaved={refresh} />
          ) : view === 'analytics' ? (
            <Analytics views={views} events={events} />
          ) : view === 'knowledge' ? (
            <KnowledgeUpdates views={views} onOpen={open} onSaved={refresh} />
          ) : (
            <SettingsView settings={settings} onSaved={refresh} />
          )}
        </div>
      </div>
    </div>
  )
}

/* ---------------- Review queue (§8, §9, §23) ---------------- */

function QueueView({
  views,
  preset,
  categories,
  adminName,
  onOpen,
  onSaved,
}: {
  views: TicketView[]
  preset: 'queue' | 'escalated' | 'corrections'
  categories: string[]
  adminName: string
  onOpen: (id: string) => void
  onSaved: () => void
}) {
  const [filters, setFilters] = useState({
    status: '',
    priority: '',
    category: '',
    state: '',
    role: '',
    confidence: '',
    trigger: '',
    assigned: '',
    from: '',
    to: '',
  })
  const [search, setSearch] = useState('')
  const [merging, setMerging] = useState<string | null>(null)

  const tickets = views.map((v) => v.ticket)
  const states = [...new Set(tickets.map((t) => t.state_code).filter(Boolean))] as string[]
  const roles = [...new Set(tickets.map((t) => t.user_role).filter(Boolean))] as string[]
  const assignees = [...new Set(tickets.map((t) => t.assigned_to).filter(Boolean))] as string[]
  const categoryOptions = [...new Set([...categories, ...(tickets.map((t) => t.category).filter(Boolean) as string[])])]

  const filtered = useMemo(
    () =>
      views.filter(({ ticket: t, question, answer, tripRef, carrierName, permit }) => {
        if (preset === 'escalated' && t.status !== 'escalated') return false
        if (preset === 'corrections' && t.status !== 'correction_proposed') return false
        if (filters.status && t.status !== filters.status) return false
        if (filters.priority && t.priority !== filters.priority) return false
        if (filters.category && t.category !== filters.category) return false
        if (filters.state && t.state_code !== filters.state) return false
        if (filters.role && t.user_role !== filters.role) return false
        if (filters.confidence && (t.confidence ?? 'none') !== filters.confidence) return false
        if (filters.trigger && t.trigger !== filters.trigger) return false
        if (filters.assigned === 'me' && t.assigned_to !== adminName) return false
        if (filters.assigned === 'unassigned' && t.assigned_to) return false
        if (filters.assigned && !['me', 'unassigned'].includes(filters.assigned) && t.assigned_to !== filters.assigned) return false
        if (filters.from && t.created_at < filters.from) return false
        if (filters.to && t.created_at > `${filters.to}T23:59:59`) return false
        if (search) {
          const q = search.toLowerCase()
          const hay = [
            question,
            answer,
            t.feedback_reason,
            t.user_comment,
            t.state_code,
            t.category,
            tripRef,
            carrierName,
            permit?.permit_number,
            t.id,
          ]
            .filter(Boolean)
            .join(' ')
            .toLowerCase()
          if (!hay.includes(q)) return false
        }
        return true
      }),
    [views, preset, filters, search, adminName],
  )

  // Repeated-issue detection (§23) — only in the main queue.
  const repeated = preset === 'queue' ? groupRepeatedIssues(tickets) : []

  async function merge(group: (typeof repeated)[number]) {
    const master = [...group.tickets].sort((a, b) => a.created_at.localeCompare(b.created_at))[0]
    setMerging(group.key)
    try {
      const ok = await moderationCall({
        op: 'merge',
        master_id: master.id,
        ticket_ids: group.tickets.map((t) => t.id),
      })
      if (ok) {
        toast.success(`Merged ${group.tickets.length - 1} ticket(s) into master #${master.id.slice(0, 8)}`)
        onSaved()
      }
    } finally {
      setMerging(null)
    }
  }

  const select = (key: keyof typeof filters, options: Array<[string, string]>, allLabel: string) => (
    <select
      key={key}
      value={filters[key]}
      onChange={(e) => setFilters((f) => ({ ...f, [key]: e.target.value }))}
      className="h-9 rounded-lg border px-2 text-sm"
    >
      <option value="">{allLabel}</option>
      {options.map(([v, l]) => (
        <option key={v} value={v}>{l}</option>
      ))}
    </select>
  )

  return (
    <div className="space-y-4">
      {repeated.length > 0 && (
        <div className="space-y-2 rounded-xl border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
          <p className="font-semibold">Possible repeated issue detected</p>
          {repeated.map((g) => (
            <div key={g.key} className="flex flex-wrap items-center justify-between gap-2 text-xs">
              <span>
                {g.tickets.length} tickets about{' '}
                <span className="font-semibold">
                  {g.state_code ? stateName(g.state_code) : 'no state'} · {g.category}
                </span>
              </span>
              <Button size="sm" variant="outline" disabled={merging === g.key} onClick={() => merge(g)}>
                {merging === g.key ? 'Merging…' : 'Merge into master issue'}
              </Button>
            </div>
          ))}
          <p className="text-[11px] text-amber-800/70">
            Merging keeps the earliest ticket as the master and marks the others as duplicates of it;
            publishing the master&apos;s correction closes them all.
          </p>
        </div>
      )}

      {/* Filters + search (§9) */}
      <div className="flex flex-wrap items-center gap-2">
        <Input
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Search question, answer, permit #, trip ID, carrier, state…"
          className="h-9 w-72"
        />
        {preset === 'queue' &&
          select('status', Object.entries(TICKET_STATUS_LABELS), 'All statuses')}
        {select('priority', [['high', 'High'], ['medium', 'Medium'], ['low', 'Low']], 'All priorities')}
        {select('category', categoryOptions.map((c) => [c, c]), 'All topics')}
        {select('state', states.map((s) => [s, stateName(s)]), 'All states')}
        {select('role', roles.map((r) => [r, r]), 'All roles')}
        {select('confidence', [['high', 'High'], ['medium', 'Medium'], ['low', 'Low'], ['none', 'Missing']], 'Any confidence')}
        {select('trigger', Object.entries(TRIGGER_LABELS), 'Any feedback type')}
        {select('assigned', [['me', 'Assigned to me'], ['unassigned', 'Unassigned'], ...assignees.map((a) => [a, a] as [string, string])], 'Any moderator')}
        <Input type="date" value={filters.from} onChange={(e) => setFilters((f) => ({ ...f, from: e.target.value }))} className="h-9 w-36" title="From" />
        <Input type="date" value={filters.to} onChange={(e) => setFilters((f) => ({ ...f, to: e.target.value }))} className="h-9 w-36" title="To" />
        <span className="text-xs text-neutral-400">{filtered.length} of {tickets.length}</span>
      </div>

      {filtered.length === 0 && (
        <div className="rounded-xl border border-dashed p-10 text-center text-sm text-neutral-500">
          {tickets.length === 0
            ? 'No review tickets yet. Tickets appear when someone gives a thumbs-down or reports an AI answer, or when the AI answers with low confidence / no source.'
            : 'No tickets match the filters.'}
        </div>
      )}

      {/* Queue table (§8 columns) */}
      {filtered.length > 0 && (
        <div className="overflow-x-auto rounded-xl border bg-white">
          <table className="w-full text-left text-xs">
            <thead className="bg-neutral-50 text-[10px] uppercase tracking-wide text-neutral-500">
              <tr>
                {['Status', 'Priority', 'State', 'Topic', 'Role', 'Question', 'Confidence', 'Feedback type', 'Created', 'Assigned', ''].map((h) => (
                  <th key={h} className="whitespace-nowrap px-3 py-2 font-semibold">{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {filtered.map(({ ticket: t, question }) => (
                <tr key={t.id} className="border-t hover:bg-neutral-50">
                  <td className="whitespace-nowrap px-3 py-2">
                    <Badge variant="outline">{TICKET_STATUS_LABELS[t.status]}</Badge>
                    {t.duplicate_of && <span className="ml-1 text-[10px] text-neutral-400">dup</span>}
                  </td>
                  <td className="px-3 py-2">
                    {t.priority && (
                      <Badge variant="outline" className={`capitalize ${PRIORITY_STYLE[t.priority]}`}>{t.priority}</Badge>
                    )}
                  </td>
                  <td className="whitespace-nowrap px-3 py-2 font-mono font-bold">{t.state_code ?? '—'}</td>
                  <td className="whitespace-nowrap px-3 py-2">{t.category ?? '—'}</td>
                  <td className="whitespace-nowrap px-3 py-2 capitalize">{t.user_role ?? '—'}</td>
                  <td className="min-w-[220px] max-w-[360px] px-3 py-2">
                    <p className="line-clamp-2 text-neutral-700">{question ?? '(question unavailable)'}</p>
                  </td>
                  <td className="whitespace-nowrap px-3 py-2 capitalize">{t.confidence ?? '—'}</td>
                  <td className="whitespace-nowrap px-3 py-2">
                    {t.trigger.startsWith('thumbs') ? '👎 ' : t.trigger === 'reported' ? '🚩 ' : '🤖 '}
                    {t.feedback_reason ?? TRIGGER_LABELS[t.trigger]?.replace('Auto: ', '') ?? t.trigger}
                  </td>
                  <td className="whitespace-nowrap px-3 py-2 text-neutral-500">{formatDateTime(t.created_at)}</td>
                  <td className="whitespace-nowrap px-3 py-2">{t.assigned_to ?? <span className="text-neutral-400">—</span>}</td>
                  <td className="px-3 py-2">
                    <Button size="sm" variant="outline" onClick={() => onOpen(t.id)}>Open</Button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  )
}

/* ---------------- Ticket detail (§11, §12, §16, §24) ---------------- */

function TicketDetail({
  view,
  events,
  issues,
  allTickets,
  adminName,
  onBack,
  onSaved,
}: {
  view: TicketView
  events: TicketEvent[]
  issues: DeveloperIssue[]
  allTickets: ReviewTicket[]
  adminName: string
  onBack: () => void
  onSaved: () => void
}) {
  const { ticket: t } = view
  const [sourceTab, setSourceTab] = useState<'permit' | 'provision' | 'notes' | 'qa' | 'route'>('permit')
  const [notes, setNotes] = useState(t.moderator_notes ?? '')
  const [assignee, setAssignee] = useState(t.assigned_to ?? '')
  const [closeReason, setCloseReason] = useState('')
  const [closing, setClosing] = useState(false)
  const [devOpen, setDevOpen] = useState(false)
  const [devCategory, setDevCategory] = useState<string>(DEVELOPER_ISSUE_CATEGORIES[0])
  const [devSummary, setDevSummary] = useState('')
  const [correction, setCorrection] = useState({
    correction: t.correction ?? '',
    explanation: t.correction_explanation ?? '',
    source: t.correction_source ?? '',
    state: t.correction_state ?? t.state_code ?? '',
    topic: t.correction_topic ?? t.category ?? '',
  })
  const [flags, setFlags] = useState({
    update_state_notes: t.update_state_notes,
    create_qa: t.create_qa,
    update_prompt_logic: t.update_prompt_logic,
    escalate_developer: t.escalate_developer,
  })
  const [appliesTo, setAppliesTo] = useState(t.applies_to ?? '')
  const [appliesStates, setAppliesStates] = useState(t.applies_states ?? '')
  const [updateType, setUpdateType] = useState(t.update_type ?? '')
  const [qaVisibility, setQaVisibility] = useState(t.qa_visibility ?? '')
  const [saving, setSaving] = useState(false)

  async function patch(body: Record<string, unknown>, okMsg: string, note?: string) {
    setSaving(true)
    try {
      const ok = await moderationCall({ ticket_id: t.id, patch: body, ...(note ? { note } : {}) })
      if (ok) {
        toast.success(okMsg)
        onSaved()
      }
    } finally {
      setSaving(false)
    }
  }

  function decide(decision: string) {
    if (decision === 'Create Developer Issue') {
      setDevOpen(true)
      return
    }
    // Decisions map to sensible statuses; "Create Knowledge Update" moves the
    // ticket into the approval workflow (§16) — nothing publishes by itself.
    const statusFor: Record<string, TicketStatus> = {
      'Correct Answer': 'no_change_needed',
      'No Action Needed': 'no_change_needed',
      Duplicate: 'duplicate',
      'Needs Expert Review': 'escalated',
      'Create Knowledge Update': 'correction_proposed',
    }
    patch({ decision, status: statusFor[decision] ?? 'in_review' }, `Decision recorded: ${decision}`)
  }

  async function createDeveloperIssue() {
    setSaving(true)
    try {
      const ok = await moderationCall({
        op: 'developer_issue',
        ticket_id: t.id,
        error_category: devCategory,
        summary: devSummary,
      })
      if (ok) {
        toast.success('Developer issue created with full technical context')
        setDevOpen(false)
        setDevSummary('')
        onSaved()
      }
    } finally {
      setSaving(false)
    }
  }

  async function publish() {
    setSaving(true)
    try {
      const ok = await moderationCall({ op: 'publish', ticket_id: t.id })
      if (ok) {
        toast.success('Correction published — knowledge updated (future answers use it once the AI backend connects)')
        onSaved()
      }
    } finally {
      setSaving(false)
    }
  }

  const stateInfo = t.state_code ? getStateInfo(t.state_code) : null
  const master = t.duplicate_of ? allTickets.find((x) => x.id === t.duplicate_of) : null
  const duplicates = allTickets.filter((x) => x.duplicate_of === t.id)
  const hasDisclaimer = /decision support|not legal|controlling document|verify|consult/i.test(view.answer)
  const isTerminal = TERMINAL_STATUSES.includes(t.status)
  const correctionProposal = {
    correction: correction.correction,
    correction_explanation: correction.explanation,
    correction_source: correction.source,
    correction_state: correction.state,
    correction_topic: correction.topic,
    ...(appliesTo ? { applies_to: appliesTo } : {}),
    ...(appliesStates ? { applies_states: appliesStates } : {}),
    ...(updateType ? { update_type: updateType } : {}),
    ...(qaVisibility ? { qa_visibility: qaVisibility } : {}),
    ...flags,
  }

  return (
    <div className="space-y-4">
      <button onClick={onBack} className="text-sm font-semibold text-neutral-500 hover:text-neutral-900">
        ← Back to queue
      </button>

      {/* Header (§11 §1) */}
      <Card>
        <CardContent className="pt-6">
          <div className="flex flex-wrap items-center gap-2 text-xs">
            <span className="font-mono text-neutral-400">#{t.id.slice(0, 8)}</span>
            <Badge variant="outline">{TICKET_STATUS_LABELS[t.status]}</Badge>
            {t.priority && (
              <Badge variant="outline" className={`capitalize ${PRIORITY_STYLE[t.priority]}`}>{t.priority}</Badge>
            )}
            {t.state_code && <span className="font-bold">{stateName(t.state_code)}</span>}
            {t.category && <span className="text-neutral-500">{t.category}</span>}
            <span className="text-neutral-400">
              {view.tripRef} · {view.tripLane}
              {view.carrierName ? ` · ${view.carrierName}` : ''}
            </span>
            <span className="ml-auto text-neutral-400">
              AI confidence: <span className="font-semibold capitalize">{t.confidence ?? 'missing'}</span>
            </span>
          </div>
          {(master || duplicates.length > 0) && (
            <p className="mt-2 rounded-lg bg-neutral-50 p-2 text-[11px] text-neutral-600">
              {master
                ? `Merged into master issue #${master.id.slice(0, 8)} (${master.category ?? '—'}).`
                : `Master issue — ${duplicates.length} duplicate ticket${duplicates.length === 1 ? '' : 's'} linked; publishing this correction closes them.`}
            </p>
          )}

          {/* Assignment (§8 assigned moderator, §19) */}
          <div className="mt-3 flex flex-wrap items-end gap-2">
            <div className="flex-1 space-y-1">
              <Label className="text-xs">Assigned moderator</Label>
              <Input value={assignee} onChange={(e) => setAssignee(e.target.value)} placeholder="Unassigned" className="h-8 text-xs" />
            </div>
            <Button size="sm" variant="outline" disabled={saving} onClick={() => patch({ assigned_to: assignee || null }, assignee ? `Assigned to ${assignee}` : 'Unassigned')}>
              Assign
            </Button>
            <Button size="sm" variant="outline" disabled={saving} onClick={() => { setAssignee(adminName); patch({ assigned_to: adminName, ...(t.status === 'new' ? { status: 'in_review' } : {}) }, 'Assigned to you') }}>
              Assign to me
            </Button>
          </div>

          {/* Question (§11 §2) */}
          <div className="mt-3 space-y-2">
            <div className="rounded-lg bg-neutral-50 p-3 text-sm">
              <p className="text-[10px] font-bold uppercase text-neutral-400">
                Question · {view.questionAuthor ?? t.user_role ?? 'user'} · {t.channel}
                {t.language ? ` · ${t.language.toUpperCase()}` : ''}
              </p>
              <p className="mt-1">{view.question ?? '(question unavailable)'}</p>
            </div>
            {/* AI answer (§11 §3) */}
            <div className="rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm">
              <p className="text-[10px] font-bold uppercase text-amber-700">AI answer</p>
              <p className="mt-1 whitespace-pre-wrap">{view.answer}</p>
              <div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 border-t border-amber-200 pt-2 text-[11px] text-amber-800">
                <span>Response time: {t.response_ms != null ? `${(t.response_ms / 1000).toFixed(1)}s` : '—'}</span>
                <span>Sources cited: {view.answerSources?.length ? view.answerSources.join(', ') : 'none'}</span>
                <span>Disclaimer: {hasDisclaimer ? 'yes' : 'no'}</span>
                <span title="Arrives with the AI backend's answer records (§26)">Model / prompt version: {t.source_versions?.model ?? '—'} / {t.source_versions?.prompt ?? '—'}</span>
              </div>
            </div>
            {/* User feedback (§11 §5) */}
            <div className="rounded-lg border p-3 text-xs">
              <p className="text-[10px] font-bold uppercase text-neutral-400">User feedback</p>
              <p className="mt-1">
                <span className="font-semibold">{TRIGGER_LABELS[t.trigger] ?? t.trigger}</span>
                {t.feedback_reason ? ` — ${t.feedback_reason}` : ''} · {formatDateTime(t.created_at)}
              </p>
              {t.user_comment && <p className="mt-1 italic text-neutral-600">“{t.user_comment}”</p>}
            </div>
          </div>
        </CardContent>
      </Card>

      {/* Source evidence (§14) */}
      <Card>
        <CardContent className="pt-6">
          <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Source evidence</h3>
          <p className="mt-1 text-[11px] text-neutral-400">Hierarchy for client-facing answers: {SOURCE_HIERARCHY}</p>
          <div className="mt-2 flex gap-1 overflow-x-auto">
            {(
              [
                ['permit', 'Permit'],
                ['provision', 'State Provision'],
                ['notes', 'State Notes'],
                ['qa', 'Internal Q&A'],
                ['route', 'Route Data'],
              ] as const
            ).map(([key, label]) => (
              <button
                key={key}
                onClick={() => setSourceTab(key)}
                className={`shrink-0 rounded-full px-3 py-1 text-xs font-semibold ${
                  sourceTab === key ? 'bg-[#0f1b2d] text-white' : 'text-neutral-500 hover:text-neutral-900'
                }`}
              >
                {label}
              </button>
            ))}
          </div>
          <div className="mt-3 rounded-lg bg-neutral-50 p-3 text-xs leading-relaxed text-neutral-700">
            {sourceTab === 'permit' &&
              (view.permit ? (
                <div className="space-y-1">
                  <p className="font-semibold">
                    {stateName(view.permit.state_code)}{' '}
                    <span className="font-mono font-normal text-neutral-500">{view.permit.permit_number}</span>
                  </p>
                  <p>
                    W {formatInches(view.permit.permit_width_in)} · H {formatInches(view.permit.permit_height_in)} · L{' '}
                    {formatInches(view.permit.permit_length_in)} · GVW {formatWeight(view.permit.permit_weight_lbs)}
                  </p>
                  <p>Effective {view.permit.effective_date ?? '—'} · Expires {view.permit.expiration_date ?? '—'}</p>
                  <p className="text-neutral-400">Permit version: uploaded {formatDateTime(view.permit.created_at)}</p>
                  {view.permitUrl && (
                    <a href={view.permitUrl} target="_blank" rel="noreferrer" className="font-semibold text-blue-700 hover:underline">
                      Open permit file ↗
                    </a>
                  )}
                </div>
              ) : (
                <p>No permit linked to this answer.</p>
              ))}
            {sourceTab === 'provision' && (
              <p>
                Provisions come from Synchron Permits — the provision file viewer and its version
                ({t.source_versions?.provision ?? 'unknown'}) connect with the backend.
              </p>
            )}
            {sourceTab === 'notes' &&
              (stateInfo ? (
                <div className="space-y-1.5">
                  {STATE_INFO_TOPICS.map((topic) => (
                    <p key={topic}>
                      <span className="font-semibold">{topic}:</span> {stateInfo[topic]}
                    </p>
                  ))}
                  <p className="text-neutral-400">
                    Sample data (version {t.source_versions?.state_notes ?? '—'}) — internal state knowledge connects with the backend.
                  </p>
                </div>
              ) : (
                <p>No state on this ticket.</p>
              ))}
            {sourceTab === 'qa' && <p>Internal Q&A source connects with the knowledge backend.</p>}
            {sourceTab === 'route' && <p>Route data source connects with the route backend.</p>}
          </div>
          <div className="mt-3 grid gap-3 sm:grid-cols-2">
            <div className="space-y-1">
              <Label className="text-xs">Source alignment</Label>
              <select
                value={t.source_alignment ?? ''}
                onChange={(e) => patch({ source_alignment: e.target.value || undefined }, 'Source alignment saved')}
                className="w-full rounded-lg border px-2 py-1.5 text-xs"
              >
                <option value="">— not assessed —</option>
                {SOURCE_ALIGNMENTS.map(([v, l]) => (
                  <option key={v} value={v}>{l}</option>
                ))}
              </select>
            </div>
            <div className="space-y-1">
              <Label className="text-xs">Confidence review (§15)</Label>
              <select
                value={t.confidence_review ?? ''}
                onChange={(e) => patch({ confidence_review: e.target.value || undefined }, 'Confidence review saved')}
                className="w-full rounded-lg border px-2 py-1.5 text-xs"
              >
                <option value="">— not assessed —</option>
                {CONFIDENCE_REVIEWS.map(([v, l]) => (
                  <option key={v} value={v}>{l}</option>
                ))}
              </select>
            </div>
          </div>
        </CardContent>
      </Card>

      {/* Decision + correction workflow (§12, §16, §17, §18) */}
      <Card>
        <CardContent className="pt-6">
          <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">
            Moderator decision{' '}
            {t.decision && <span className="font-normal normal-case text-neutral-400">— current: {t.decision}</span>}
          </h3>
          <div className="mt-2 flex flex-wrap gap-1.5">
            {TICKET_DECISIONS.map((d) => (
              <button
                key={d}
                onClick={() => decide(d)}
                disabled={saving}
                className={`rounded-lg border px-2.5 py-1 text-xs font-semibold transition disabled:opacity-50 ${
                  t.decision === d ? 'border-[#0f1b2d] bg-[#0f1b2d] text-white' : 'hover:border-neutral-400'
                }`}
              >
                {d}
              </button>
            ))}
          </div>

          <div className="mt-4 space-y-3 border-t pt-4">
            <p className="text-[11px] font-bold uppercase text-neutral-400">
              Correction workflow: propose → approve → publish
            </p>
            <Textarea rows={2} value={correction.correction} onChange={(e) => setCorrection((c) => ({ ...c, correction: e.target.value }))} placeholder="Correct answer" className="text-xs" />
            <Textarea rows={2} value={correction.explanation} onChange={(e) => setCorrection((c) => ({ ...c, explanation: e.target.value }))} placeholder="Short explanation" className="text-xs" />
            <div className="grid gap-2 sm:grid-cols-3">
              <Input value={correction.source} onChange={(e) => setCorrection((c) => ({ ...c, source: e.target.value }))} placeholder="Source evidence" className="h-8 text-xs" />
              <Input value={correction.state} onChange={(e) => setCorrection((c) => ({ ...c, state: e.target.value }))} placeholder="State" className="h-8 text-xs" />
              <Input value={correction.topic} onChange={(e) => setCorrection((c) => ({ ...c, topic: e.target.value }))} placeholder="Topic" className="h-8 text-xs" />
            </div>
            <div className="grid gap-2 sm:grid-cols-3">
              <select value={appliesTo} onChange={(e) => setAppliesTo(e.target.value as typeof appliesTo)} className="rounded-lg border px-2 py-1.5 text-xs">
                <option value="">Applies to… (permit scope)</option>
                <option value="this_permit">Only this permit</option>
                <option value="all_permits">All permits</option>
              </select>
              <select value={appliesStates} onChange={(e) => setAppliesStates(e.target.value as typeof appliesStates)} className="rounded-lg border px-2 py-1.5 text-xs">
                <option value="">Applies to… (state scope)</option>
                <option value="one_state">One state</option>
                <option value="multiple_states">Multiple states</option>
              </select>
              <select value={updateType} onChange={(e) => setUpdateType(e.target.value as typeof updateType)} className="rounded-lg border px-2 py-1.5 text-xs" title="Routes the update to the right team (§17)">
                <option value="">Update type…</option>
                {UPDATE_TYPES.map(([v, l]) => (
                  <option key={v} value={v}>{l}</option>
                ))}
              </select>
            </div>
            <div className="flex flex-wrap gap-3 text-xs">
              {(
                [
                  ['update_state_notes', 'Update state notes'],
                  ['create_qa', 'Create Q&A entry'],
                  ['update_prompt_logic', 'Update prompt logic'],
                  ['escalate_developer', 'Escalate to developer'],
                ] as const
              ).map(([key, label]) => (
                <label key={key} className="flex cursor-pointer items-center gap-1.5">
                  <input type="checkbox" checked={flags[key]} onChange={(e) => setFlags((f) => ({ ...f, [key]: e.target.checked }))} className="h-3.5 w-3.5 accent-[#0f1b2d]" />
                  {label}
                </label>
              ))}
            </div>
            {flags.create_qa && (
              <div className="space-y-1">
                <Label className="text-xs">Q&A entry visibility (§18) — who this answer may be used for</Label>
                <select value={qaVisibility} onChange={(e) => setQaVisibility(e.target.value as typeof qaVisibility)} className="w-full rounded-lg border px-2 py-1.5 text-xs">
                  <option value="">— choose —</option>
                  {QA_VISIBILITY.map(([v, l]) => (
                    <option key={v} value={v}>{l}</option>
                  ))}
                </select>
                <p className="text-[11px] text-neutral-400">
                  Processing guidance stays internal; only driver-facing-allowed entries may answer &ldquo;Can I travel tonight?&rdquo;
                </p>
              </div>
            )}
            <div className="flex flex-wrap gap-2">
              <Button size="sm" disabled={saving} onClick={() => patch({ ...correctionProposal, status: 'correction_proposed' }, 'Correction proposed — pending approval')}>
                Propose correction
              </Button>
              {t.status === 'correction_proposed' && (
                <Button size="sm" disabled={saving} onClick={() => patch({ ...correctionProposal, status: 'approved_for_knowledge_update' }, 'Correction approved — ready to publish', 'approved as state expert / knowledge manager')}>
                  Approve correction
                </Button>
              )}
              {t.status === 'approved_for_knowledge_update' && (
                <Button size="sm" disabled={saving} onClick={publish}>
                  Publish to knowledge base
                </Button>
              )}
              <Button size="sm" variant="outline" disabled={saving} onClick={() => patch({ status: 'escalated' }, 'Escalated')}>Escalate</Button>
              <Button size="sm" variant="outline" disabled={saving} onClick={() => patch({ status: 'needs_more_context' }, 'Marked: needs more context')}>Needs more context</Button>
              {!isTerminal && (
                <Button size="sm" variant="outline" disabled={saving} onClick={() => setClosing((c) => !c)}>Close…</Button>
              )}
              {isTerminal && t.status !== 'duplicate' && (
                <Button size="sm" variant="ghost" disabled={saving} onClick={() => patch({ status: 'in_review' }, 'Reopened')}>Reopen</Button>
              )}
            </div>
            {closing && (
              <div className="flex items-end gap-2 rounded-lg bg-neutral-50 p-2">
                <div className="flex-1 space-y-1">
                  <Label className="text-xs">Reason for closing (required, §19)</Label>
                  <Input value={closeReason} onChange={(e) => setCloseReason(e.target.value)} className="h-8 text-xs" placeholder="e.g. user misunderstood the permit — no update needed" />
                </div>
                <Button
                  size="sm"
                  disabled={saving || closeReason.trim().length < 3}
                  onClick={() => patch({ status: 'closed', close_reason: closeReason.trim() }, 'Ticket closed', closeReason.trim()).then(() => setClosing(false))}
                >
                  Close ticket
                </Button>
              </div>
            )}
            <div className="flex items-end gap-2">
              <div className="flex-1 space-y-1">
                <Label className="text-xs">Moderator notes (internal)</Label>
                <Input value={notes} onChange={(e) => setNotes(e.target.value)} className="h-8 text-xs" />
              </div>
              <Button size="sm" variant="outline" disabled={saving} onClick={() => patch({ moderator_notes: notes }, 'Notes saved')}>Save notes</Button>
            </div>
          </div>
        </CardContent>
      </Card>

      {/* Developer issues (§24) */}
      {issues.length > 0 && (
        <Card>
          <CardContent className="pt-6">
            <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Developer issues</h3>
            <div className="mt-2 space-y-2 text-xs">
              {issues.map((i) => (
                <div key={i.id} className="rounded-lg border p-2.5">
                  <div className="flex flex-wrap items-center justify-between gap-2">
                    <p className="font-semibold">
                      {i.error_category}
                      <Badge variant="outline" className="ml-2 capitalize">{i.status}</Badge>
                    </p>
                    {i.status === 'open' && (
                      <Button size="sm" variant="outline" onClick={async () => { if (await moderationCall({ op: 'issue_done', issue_id: i.id })) { toast.success('Issue marked done'); onSaved() } }}>
                        Mark done
                      </Button>
                    )}
                  </div>
                  {i.summary && <p className="mt-1 text-neutral-600">{i.summary}</p>}
                  <p className="mt-1 text-[10px] text-neutral-400">
                    {i.created_by ?? '—'} · {formatDateTime(i.created_at)} · context captured: question, answer, sources, document IDs, confidence, model/prompt version
                  </p>
                </div>
              ))}
            </div>
          </CardContent>
        </Card>
      )}

      {/* Audit trail (§11 §8) */}
      <Card>
        <CardContent className="pt-6">
          <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Audit trail</h3>
          <div className="mt-2 space-y-1.5 text-xs">
            {events.length === 0 && <p className="text-neutral-400">No actions recorded yet.</p>}
            {events.map((e) => (
              <p key={e.id} className="text-neutral-600">
                <span className="font-semibold">{e.action.replace(/_/g, ' ')}</span>
                {e.actor ? ` · ${e.actor}` : ''} · {formatDateTime(e.created_at)}
                {e.detail && Object.keys(e.detail).length > 0 && (
                  <span className="text-neutral-400"> — {JSON.stringify(e.detail)}</span>
                )}
              </p>
            ))}
          </div>
        </CardContent>
      </Card>

      {/* Developer-issue dialog (§24) */}
      <Dialog open={devOpen} onOpenChange={setDevOpen}>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>Create developer issue</DialogTitle>
          </DialogHeader>
          <div className="space-y-3">
            <div className="space-y-1">
              <Label className="text-xs">Error category</Label>
              <select value={devCategory} onChange={(e) => setDevCategory(e.target.value)} className="w-full rounded-lg border px-2 py-1.5 text-sm">
                {DEVELOPER_ISSUE_CATEGORIES.map((c) => (
                  <option key={c} value={c}>{c}</option>
                ))}
              </select>
            </div>
            <div className="space-y-1">
              <Label className="text-xs">Summary for the developer (optional)</Label>
              <Textarea rows={3} value={devSummary} onChange={(e) => setDevSummary(e.target.value)} className="text-sm" />
            </div>
            <p className="text-[11px] text-neutral-400">
              The issue records the user question, AI answer, sources retrieved, document IDs,
              confidence, model/prompt version, and your moderator comments — everything a developer
              needs, in one place.
            </p>
            <div className="flex justify-end gap-2">
              <Button variant="ghost" onClick={() => setDevOpen(false)} disabled={saving}>Cancel</Button>
              <Button onClick={createDeveloperIssue} disabled={saving}>{saving ? 'Creating…' : 'Create issue'}</Button>
            </div>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  )
}

/* ---------------- State issue dashboard (§22) ---------------- */

function StateIssues({
  views,
  settings,
  onOpen,
  onSaved,
}: {
  views: TicketView[]
  settings: ModerationSettings
  onOpen: (id: string) => void
  onSaved: () => void
}) {
  const states = [...new Set([
    ...(views.map((v) => v.ticket.state_code).filter(Boolean) as string[]),
    ...Object.keys(settings.state_reviewers),
  ])].sort()
  const [state, setState] = useState<string>(states[0] ?? '')
  const [reviewer, setReviewer] = useState(settings.state_reviewers[state] ?? '')
  const [saving, setSaving] = useState(false)

  const mine = views.filter((v) => v.ticket.state_code === state)
  const open = mine.filter((v) => !TERMINAL_STATUSES.includes(v.ticket.status))
  const closed = mine.length - open.length
  const topics = countBy(mine.map((v) => v.ticket.category))
  const questions = countBy(mine.map((v) => (v.question ? normalize(v.question) : null)))
  const corrections = mine
    .filter((v) => v.ticket.correction)
    .sort((a, b) => b.ticket.updated_at.localeCompare(a.ticket.updated_at))
    .slice(0, 5)
  const versions = mine.map((v) => v.ticket.source_versions).find(Boolean)

  async function saveReviewer() {
    setSaving(true)
    try {
      const ok = await moderationCall({
        op: 'settings',
        key: 'state_reviewers',
        value: { ...settings.state_reviewers, [state]: reviewer.trim() },
      })
      if (ok) {
        toast.success(`${stateName(state)} expert: ${reviewer.trim() || 'unassigned'}`)
        onSaved()
      }
    } finally {
      setSaving(false)
    }
  }

  if (states.length === 0) {
    return <div className="rounded-xl border border-dashed p-10 text-center text-sm text-neutral-500">No state-tagged tickets yet.</div>
  }

  return (
    <div className="space-y-4">
      <div className="flex flex-wrap items-center gap-2">
        {states.map((s) => (
          <button
            key={s}
            onClick={() => {
              setState(s)
              setReviewer(settings.state_reviewers[s] ?? '')
            }}
            className={`rounded-full border px-3 py-1 text-xs font-semibold ${
              s === state ? 'border-[#0f1b2d] bg-[#0f1b2d] text-white' : 'hover:border-neutral-400'
            }`}
          >
            {stateName(s)} <span className="opacity-60">({views.filter((v) => v.ticket.state_code === s).length})</span>
          </button>
        ))}
      </div>

      <div className="grid grid-cols-3 gap-2 text-center">
        {(
          [
            ['Total tickets', mine.length],
            ['Open', open.length],
            ['Closed', closed],
          ] as const
        ).map(([l, n]) => (
          <div key={l} className="rounded-xl border bg-white p-3">
            <p className="text-xl font-bold">{n}</p>
            <p className="text-[10px] font-semibold uppercase tracking-wide text-neutral-400">{l}</p>
          </div>
        ))}
      </div>

      <div className="grid gap-4 sm:grid-cols-2">
        <Card>
          <CardContent className="pt-6">
            <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">State expert assigned</h3>
            <div className="mt-2 flex items-end gap-2">
              <Input value={reviewer} onChange={(e) => setReviewer(e.target.value)} placeholder="Unassigned" className="h-8 flex-1 text-xs" />
              <Button size="sm" variant="outline" disabled={saving} onClick={saveReviewer}>Save</Button>
            </div>
            <p className="mt-3 text-[11px] text-neutral-400">
              State note version: {versions?.state_notes ?? '—'} · Provision file version: {versions?.provision ?? '—'} (versions arrive with the source backend, §27)
            </p>
          </CardContent>
        </Card>
        <Card>
          <CardContent className="pt-6">
            <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Top disputed topics</h3>
            <Bars rows={topics} />
          </CardContent>
        </Card>
        <Card>
          <CardContent className="pt-6">
            <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Most common questions</h3>
            <ol className="mt-2 space-y-1 text-xs text-neutral-700">
              {questions.length === 0 && <li className="text-neutral-400">None yet.</li>}
              {questions.slice(0, 5).map(([q, n]) => (
                <li key={q}>
                  <span className="font-bold">{n}×</span> {q}
                </li>
              ))}
            </ol>
          </CardContent>
        </Card>
        <Card>
          <CardContent className="pt-6">
            <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Recent corrections</h3>
            <div className="mt-2 space-y-1.5 text-xs">
              {corrections.length === 0 && <p className="text-neutral-400">None yet.</p>}
              {corrections.map((v) => (
                <button key={v.ticket.id} onClick={() => onOpen(v.ticket.id)} className="block w-full rounded-lg border p-2 text-left hover:border-neutral-400">
                  <p className="line-clamp-2">{v.ticket.correction}</p>
                  <p className="mt-0.5 text-[10px] text-neutral-400">{TICKET_STATUS_LABELS[v.ticket.status]} · {formatDateTime(v.ticket.updated_at)}</p>
                </button>
              ))}
            </div>
          </CardContent>
        </Card>
        <Card className="sm:col-span-2">
          <CardContent className="pt-6">
            <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Known unresolved issues</h3>
            <div className="mt-2 space-y-1.5 text-xs">
              {open.length === 0 && <p className="text-neutral-400">Nothing open for {stateName(state)}.</p>}
              {open.map((v) => (
                <button key={v.ticket.id} onClick={() => onOpen(v.ticket.id)} className="flex w-full items-center justify-between gap-2 rounded-lg border p-2 text-left hover:border-neutral-400">
                  <span className="line-clamp-1">
                    <Badge variant="outline" className="mr-1.5">{TICKET_STATUS_LABELS[v.ticket.status]}</Badge>
                    {v.question ?? '(question unavailable)'}
                  </span>
                  <span className="shrink-0 text-neutral-400">{v.ticket.category}</span>
                </button>
              ))}
            </div>
            {mine.some((v) => v.ticket.moderator_notes) && (
              <>
                <h4 className="mt-4 text-[11px] font-bold uppercase text-neutral-400">Moderator comments</h4>
                <ul className="mt-1 space-y-1 text-xs text-neutral-600">
                  {mine.filter((v) => v.ticket.moderator_notes).map((v) => (
                    <li key={v.ticket.id}>“{v.ticket.moderator_notes}”</li>
                  ))}
                </ul>
              </>
            )}
          </CardContent>
        </Card>
      </div>
    </div>
  )
}

/* ---------------- Knowledge updates (§8, §16, §17) ---------------- */

function KnowledgeUpdates({
  views,
  onOpen,
  onSaved,
}: {
  views: TicketView[]
  onOpen: (id: string) => void
  onSaved: () => void
}) {
  const rows = views.filter(
    (v) =>
      ['correction_proposed', 'approved_for_knowledge_update', 'knowledge_updated'].includes(v.ticket.status) ||
      v.ticket.update_type,
  )
  const typeLabel = (v: string | null) => UPDATE_TYPES.find(([k]) => k === v)?.[1] ?? '—'
  const visLabel = (v: string | null) => QA_VISIBILITY.find(([k]) => k === v)?.[1] ?? '—'

  return (
    <div className="space-y-3">
      <p className="text-xs text-neutral-500">
        Proposed → approved → published corrections. Publishing records the knowledge update and
        closes merged duplicates; pushing it into the AI&apos;s controlled sources connects with the
        knowledge backend.
      </p>
      {rows.length === 0 ? (
        <div className="rounded-xl border border-dashed p-10 text-center text-sm text-neutral-500">No corrections in the workflow yet.</div>
      ) : (
        <div className="overflow-x-auto rounded-xl border bg-white">
          <table className="w-full text-left text-xs">
            <thead className="bg-neutral-50 text-[10px] uppercase tracking-wide text-neutral-500">
              <tr>
                {['Status', 'State', 'Topic', 'Update type', 'Q&A visibility', 'Correction', ''].map((h) => (
                  <th key={h} className="whitespace-nowrap px-3 py-2 font-semibold">{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {rows.map(({ ticket: t }) => (
                <tr key={t.id} className="border-t">
                  <td className="whitespace-nowrap px-3 py-2"><Badge variant="outline">{TICKET_STATUS_LABELS[t.status]}</Badge></td>
                  <td className="whitespace-nowrap px-3 py-2 font-mono font-bold">{t.correction_state || t.state_code || '—'}</td>
                  <td className="whitespace-nowrap px-3 py-2">{t.correction_topic || t.category || '—'}</td>
                  <td className="whitespace-nowrap px-3 py-2">{typeLabel(t.update_type)}</td>
                  <td className="whitespace-nowrap px-3 py-2">{t.create_qa ? visLabel(t.qa_visibility) : '—'}</td>
                  <td className="min-w-[240px] px-3 py-2"><p className="line-clamp-2">{t.correction ?? <span className="text-neutral-400">no text yet</span>}</p></td>
                  <td className="whitespace-nowrap px-3 py-2">
                    <div className="flex gap-1.5">
                      <Button size="sm" variant="outline" onClick={() => onOpen(t.id)}>Open</Button>
                      {t.status === 'approved_for_knowledge_update' && (
                        <Button size="sm" onClick={async () => { if (await moderationCall({ op: 'publish', ticket_id: t.id })) { toast.success('Published'); onSaved() } }}>
                          Publish
                        </Button>
                      )}
                    </div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  )
}

/* ---------------- Settings (§3, §22) ---------------- */

function SettingsView({ settings, onSaved }: { settings: ModerationSettings; onSaved: () => void }) {
  const [categories, setCategories] = useState<string[]>(settings.categories)
  const [newCategory, setNewCategory] = useState('')
  const [reviewers, setReviewers] = useState<Record<string, string>>(settings.state_reviewers)
  const [newState, setNewState] = useState('')
  const [saving, setSaving] = useState(false)

  async function save(key: 'categories' | 'state_reviewers', value: unknown, msg: string) {
    setSaving(true)
    try {
      if (await moderationCall({ op: 'settings', key, value })) {
        toast.success(msg)
        onSaved()
      }
    } finally {
      setSaving(false)
    }
  }

  return (
    <div className="grid gap-4 lg:grid-cols-2">
      <Card>
        <CardContent className="pt-6">
          <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Feedback categories</h3>
          <p className="mt-1 text-[11px] text-neutral-400">The topics tickets are filed under (admin-configurable, §3).</p>
          <div className="mt-3 flex flex-wrap gap-1.5">
            {categories.map((c) => (
              <span key={c} className="flex items-center gap-1 rounded-full border bg-neutral-50 px-2.5 py-1 text-xs">
                {c}
                <button onClick={() => setCategories((l) => l.filter((x) => x !== c))} className="text-neutral-400 hover:text-red-600" title="Remove">×</button>
              </span>
            ))}
          </div>
          <div className="mt-3 flex gap-2">
            <Input value={newCategory} onChange={(e) => setNewCategory(e.target.value)} placeholder="New category" className="h-8 text-xs" onKeyDown={(e) => { if (e.key === 'Enter' && newCategory.trim()) { setCategories((l) => [...new Set([...l, newCategory.trim()])]); setNewCategory('') } }} />
            <Button size="sm" variant="outline" onClick={() => { if (newCategory.trim()) { setCategories((l) => [...new Set([...l, newCategory.trim()])]); setNewCategory('') } }}>Add</Button>
          </div>
          <Button size="sm" className="mt-3" disabled={saving} onClick={() => save('categories', categories, 'Categories saved')}>Save categories</Button>
        </CardContent>
      </Card>
      <Card>
        <CardContent className="pt-6">
          <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">State experts</h3>
          <p className="mt-1 text-[11px] text-neutral-400">Who reviews escalations for each state (§22 &ldquo;State expert assigned&rdquo;).</p>
          <div className="mt-3 space-y-1.5">
            {Object.entries(reviewers).map(([s, name]) => (
              <div key={s} className="flex items-center gap-2 text-xs">
                <span className="w-32 truncate font-semibold">{stateName(s)}</span>
                <Input value={name} onChange={(e) => setReviewers((r) => ({ ...r, [s]: e.target.value }))} className="h-8 flex-1 text-xs" />
                <button onClick={() => setReviewers((r) => { const n = { ...r }; delete n[s]; return n })} className="text-neutral-400 hover:text-red-600" title="Remove">×</button>
              </div>
            ))}
          </div>
          <div className="mt-3 flex gap-2">
            <select value={newState} onChange={(e) => setNewState(e.target.value)} className="h-8 rounded-lg border px-2 text-xs">
              <option value="">Add state…</option>
              {Object.entries(STATE_NAMES).filter(([c]) => !reviewers[c]).map(([c, n]) => (
                <option key={c} value={c}>{n}</option>
              ))}
            </select>
            <Button size="sm" variant="outline" onClick={() => { if (newState) { setReviewers((r) => ({ ...r, [newState]: '' })); setNewState('') } }}>Add</Button>
          </div>
          <Button size="sm" className="mt-3" disabled={saving} onClick={() => save('state_reviewers', reviewers, 'State experts saved')}>Save state experts</Button>
        </CardContent>
      </Card>
    </div>
  )
}

/* ---------------- Analytics (§21) ---------------- */

function Analytics({ views, events }: { views: TicketView[]; events: TicketEvent[] }) {
  const tickets = views.map((v) => v.ticket)
  const decided = tickets.filter((t) => t.decision && (CORRECT_DECISIONS.has(t.decision) || WRONG_DECISIONS.has(t.decision)))
  const escalatedEver = new Set(
    events
      .filter((e) => e.action === 'status_changed' && (e.detail as { status?: string } | null)?.status === 'escalated')
      .map((e) => e.ticket_id)
      .concat(tickets.filter((t) => t.status === 'escalated').map((t) => t.id)),
  )
  const avgHours = averageResolutionHours(tickets)
  const byCategoryAccuracy = countBy(decided.map((t) => t.category)).map(([cat, n]) => {
    const correct = decided.filter((t) => t.category === cat && CORRECT_DECISIONS.has(t.decision!)).length
    return [`${cat} — ${Math.round((correct / n) * 100)}% correct`, n] as [string, number]
  })
  const resolvedBy = countBy(
    events
      .filter((e) => e.action === 'status_changed' && TERMINAL_STATUSES.includes(((e.detail as { status?: string } | null)?.status ?? '') as TicketStatus))
      .map((e) => e.actor),
  )
  const highlights: Array<[string, string | number]> = [
    ['Total tickets', tickets.length],
    ['Thumbs down', tickets.filter((t) => t.trigger === 'thumbs_down').length],
    ['Reported', tickets.filter((t) => t.trigger === 'reported').length],
    ['Auto (AI signals)', tickets.filter((t) => t.trigger === 'low_confidence_repeat' || t.trigger === 'no_source').length],
    ['Avg resolution', avgHours == null ? '—' : `${avgHours.toFixed(1)} h`],
    ['Escalation rate', tickets.length ? `${Math.round((escalatedEver.size / tickets.length) * 100)}%` : '—'],
    ['High-confidence wrong', tickets.filter((t) => t.confidence === 'high' && t.decision && WRONG_DECISIONS.has(t.decision)).length],
    ['Low-confidence correct', tickets.filter((t) => t.confidence === 'low' && t.decision && CORRECT_DECISIONS.has(t.decision)).length],
    ['Source conflicts', tickets.filter((t) => t.source_alignment === 'permit_conflicts_provision').length],
    ['Knowledge updates published', tickets.filter((t) => t.status === 'knowledge_updated').length],
    ['OCR / extraction issues', tickets.filter((t) => t.category === 'OCR/Extraction Error').length],
    ['Translation / voice issues', tickets.filter((t) => t.category === 'Translation Error' || t.category === 'Voice Transcription Error').length],
  ]
  const sections: Array<[string, Array<[string, number]>]> = [
    ['Thumbs-down by state', countBy(tickets.map((t) => t.state_code))],
    ['By topic', countBy(tickets.map((t) => t.category))],
    ['By status', countBy(tickets.map((t) => TICKET_STATUS_LABELS[t.status]))],
    ['By priority', countBy(tickets.map((t) => t.priority))],
    ['AI accuracy by category (decided tickets)', byCategoryAccuracy],
    ['Most common correction topics', countBy(tickets.map((t) => t.correction_topic))],
    ['Moderator workload — assigned', countBy(tickets.map((t) => t.assigned_to))],
    ['Moderator workload — resolved', resolvedBy],
    ['Route-related', countBy(tickets.filter((t) => /route/i.test(t.category ?? '')).map((t) => t.category))],
  ]
  const questions = countBy(views.map((v) => (v.question ? normalize(v.question) : null))).slice(0, 5)

  return (
    <div className="space-y-4">
      <div className="grid grid-cols-2 gap-2 text-center sm:grid-cols-3 lg:grid-cols-6">
        {highlights.map(([l, n]) => (
          <div key={l} className="rounded-xl border bg-white p-2.5">
            <p className="text-lg font-bold">{n}</p>
            <p className="text-[10px] font-semibold uppercase tracking-wide text-neutral-400">{l}</p>
          </div>
        ))}
      </div>
      <div className="grid gap-4 sm:grid-cols-2">
        {sections.map(([title, rows]) => (
          <Card key={title}>
            <CardContent className="pt-6">
              <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">{title}</h3>
              <Bars rows={rows} />
            </CardContent>
          </Card>
        ))}
        <Card className="sm:col-span-2">
          <CardContent className="pt-6">
            <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Most common user questions</h3>
            <ol className="mt-2 space-y-1 text-xs text-neutral-700">
              {questions.length === 0 && <li className="text-neutral-400">None yet.</li>}
              {questions.map(([q, n]) => (
                <li key={q}><span className="font-bold">{n}×</span> {q}</li>
              ))}
            </ol>
            <p className="mt-3 text-[11px] text-neutral-400">
              &ldquo;High Confidence on a wrong answer is more dangerous than a wrong answer with Low
              Confidence.&rdquo; Disputes by permit type arrive when permit type is captured with the AI
              backend&apos;s answer records.
            </p>
          </CardContent>
        </Card>
      </div>
    </div>
  )
}

/* ---------------- shared bits ---------------- */

function countBy(values: Array<string | null | undefined>): Array<[string, number]> {
  const map = new Map<string, number>()
  for (const v of values) if (v) map.set(v, (map.get(v) ?? 0) + 1)
  return [...map.entries()].sort((a, b) => b[1] - a[1])
}

function Bars({ rows }: { rows: Array<[string, number]> }) {
  const max = Math.max(1, ...rows.map(([, n]) => n))
  return (
    <div className="mt-3 space-y-1.5">
      {rows.length === 0 && <p className="text-xs text-neutral-400">No data yet.</p>}
      {rows.slice(0, 8).map(([label, n]) => (
        <div key={label} className="flex items-center gap-2 text-xs">
          <span className="w-44 truncate capitalize" title={label}>{label.replace(/_/g, ' ')}</span>
          <div className="h-3 flex-1 rounded bg-neutral-100">
            <div className="h-3 rounded bg-[#0f1b2d]" style={{ width: `${(n / max) * 100}%` }} />
          </div>
          <span className="w-6 text-right font-bold">{n}</span>
        </div>
      ))}
    </div>
  )
}
