'use client'

import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { Mic } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import {
  Dialog, DialogContent, DialogHeader, DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { stateName } from '@/lib/domain/states'
import { CHAT_LANGUAGES, DEFAULT_LANGUAGE, resolveLanguagePrefs } from '@/lib/domain/languages'
import { persistLanguagePrefs, useLocalLanguagePrefs } from '@/lib/domain/language-prefs-local'
import { DEMO_AI_CREDITS_REMAINING } from '@/lib/demo/credits'
import { FEEDBACK_REASONS } from '@/lib/domain/moderation'
import { getDemoTranslation } from '@/lib/demo/chat-translations'
import { formatDateTime } from '@/lib/format'
import type { ChatMessage, Trip, TripParticipant, TripRole } from '@/types/db'

/**
 * The only permit fields the chat needs — the state selector and the
 * permit-scoped banner. Both `Permit` (workspace) and `PermitLite` (driver
 * dashboard) satisfy this, so neither caller has to cast.
 */
export type ChatPermit = { id: string; state_code: string; permit_number: string | null }

/**
 * The shared agent chat.
 *
 * Extracted from the trip workspace on 2026-09-07 so the driver gets the SAME
 * chat inline on his Agent tab rather than being bounced into the desktop
 * workspace. Nash: "When I click on Ask Your Agent… I should have the chat
 * view for the driver dashboard, how it looks on the driver."
 *
 * Message logic (polling, optimistic send, merge, feedback, review tickets) is
 * unchanged — only the presentation adapts, via `compact`, to the driver's
 * narrow 420px column.
 */
export type ChatPanelProps = {
  trip: Trip
  chat: ChatMessage[]
  permits: ChatPermit[]
  myRole: TripRole | null
  myUserId: string
  participants: TripParticipant[]
  scopedPermit: ChatPermit | null
  onExitScope: () => void
  /** Driver mobile column: tighter padding, no min-height stretch. */
  compact?: boolean
  /**
   * Override the composer's starter questions. The driver surface passes the
   * four questions the client approved for drivers; clicking one fills the
   * composer in place rather than navigating away.
   */
  suggestions?: string[]
  /**
   * This user's per-trip sharing choice ("let broker and dispatch see my
   * questions and answers"). Sharing is the default.
   */
  shareChat?: boolean
  /**
   * The user's enabled chat languages and default, from the profile (Task 71).
   * Nash: "one language, English, first, and then a button to add… It should
   * stick to your profile… maybe you want to set one of them to be primary."
   */
  languages?: string[]
  primaryLanguage?: string
  /**
   * Replica mode (pilot car previews, 2026-09-12). Nash: "the pilot driver
   * has the same ability [as the carrier driver]… voice first… change the
   * languages and all the same tools… the only limitation… he has access to
   * ask only the states that are shared with him." With `offline` set there
   * is no polling and no network: questions are answered locally by
   * `answer`, sharing and feedback are kept in component state, and the
   * "Whole trip" option is offered only when `wholeTrip` is true (full trip
   * access). Everything else — state selector, languages, voice, sharing
   * choice, starter questions — is the same component the carrier driver uses.
   */
  offline?: {
    answer: (question: string, stateCode: string | null, language: string) => string
    wholeTrip: boolean
  }
}

const CHAT_POLL_MS = 4000

const SUGGESTED_QUESTIONS = [
  'Can I drive at night?',
  'Do I need escorts?',
  'What are the curfews on this trip?',
  'Is every permit valid today?',
  'Do the permit dimensions match the load?',
]

/** The four launch agents (per the meeting: English, Spanish, Russian, Romanian). */
const AGENTS = CHAT_LANGUAGES

export function ChatPanel({
  trip,
  chat,
  permits,
  myRole,
  myUserId,
  participants,
  scopedPermit,
  onExitScope,
  compact = false,
  suggestions = SUGGESTED_QUESTIONS,
  shareChat = true,
  languages: initialLanguages,
  primaryLanguage: initialPrimary,
  offline,
}: ChatPanelProps) {
  // Enabled languages + default come from the profile; a new user has English
  // only. Every chat opens in the primary language (Task 71).
  const serverPrefs = resolveLanguagePrefs({
    chat_languages: initialLanguages ?? [DEFAULT_LANGUAGE],
    primary_language: initialPrimary,
  })
  // A browser-side copy exists only while the profile cannot store the choice
  // yet; it wins so the interface shows what the user did.
  const localPrefs = useLocalLanguagePrefs(myUserId)
  const [savedPrefs, setSavedPrefs] = useState<typeof serverPrefs | null>(null)
  const prefs = savedPrefs ?? localPrefs ?? serverPrefs
  const [language, setLanguage] = useState(prefs.primary)
  const [managing, setManaging] = useState(false)
  const [savingLangs, setSavingLangs] = useState(false)

  async function saveLanguages(next: { languages: string[]; primary: string }) {
    setSavingLangs(true)
    try {
      // Always reflected in the interface immediately — kept locally when the
      // profile cannot store it yet (Nash: "show what happens if API would
      // have worked").
      const { prefs: saved } = await persistLanguagePrefs(myUserId, next)
      setSavedPrefs(saved)
      if (!saved.languages.includes(language)) setLanguage(saved.primary)
      return true
    } finally {
      setSavingLangs(false)
    }
  }
  const labelOf = (code: string) => AGENTS.find((a) => a.code === code)?.label ?? code
  const nameOf = (code: string) => AGENTS.find((a) => a.code === code)?.name ?? code
  const [messages, setMessages] = useState<ChatMessage[]>(chat)
  const [text, setText] = useState('')
  const [stateFocus, setStateFocus] = useState(() =>
    offline && !offline.wholeTrip ? (permits.find((p) => p.state_code)?.state_code ?? '') : '',
  )
  const [pending, setPending] = useState(false)
  const scrollRef = useRef<HTMLDivElement>(null)
  const messagesRef = useRef(messages)
  messagesRef.current = messages

  const states = Array.from(new Set(permits.map((p) => p.state_code).filter(Boolean)))

  /** Merge server messages: dedupe by id, drop optimistic copies once real rows arrive. */
  function merge(prev: ChatMessage[], incoming: ChatMessage[]): ChatMessage[] {
    if (incoming.length === 0) return prev
    const known = new Set(prev.map((m) => m.id))
    const fresh = incoming.filter((m) => !known.has(m.id))
    if (fresh.length === 0) return prev
    const freshMine = new Set(
      fresh.filter((m) => !m.is_ai && m.user_id === myUserId).map((m) => m.content),
    )
    const kept = prev.filter((m) => !(m.id.startsWith('tmp-') && freshMine.has(m.content)))
    return [...kept, ...fresh].sort((a, b) => a.created_at.localeCompare(b.created_at))
  }

  // Live updates: poll for messages from other participants / the AI.
  useEffect(() => {
    if (offline) return
    let stopped = false
    const timer = setInterval(async () => {
      if (document.hidden) return
      const real = messagesRef.current.filter((m) => !m.id.startsWith('tmp-'))
      const after = real.length ? real[real.length - 1].created_at : ''
      try {
        const res = await fetch(
          `/api/trips/${trip.id}/chat${after ? `?after=${encodeURIComponent(after)}` : ''}`,
        )
        if (!res.ok) return
        const json = await res.json()
        if (!stopped && Array.isArray(json.messages)) {
          setMessages((prev) => merge(prev, json.messages))
        }
      } catch {
        // transient network error — try again on the next tick
      }
    }, CHAT_POLL_MS)
    return () => {
      stopped = true
      clearInterval(timer)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [trip.id])

  // Keep the newest message in view.
  useEffect(() => {
    const el = scrollRef.current
    if (el) el.scrollTop = el.scrollHeight
  }, [messages.length])

  // Section 2 is an AGENT chat, not a party chat (Task 9) — every send asks
  // the Agent. When scoped to a permit (Task 7), the question carries it.
  async function send() {
    if (!text.trim() || pending) return
    const body = text.trim()
    setText('')
    setPending(true)
    const scopeState = scopedPermit?.state_code ?? stateFocus
    // optimistic message
    setMessages((m) => [
      ...m,
      {
        id: `tmp-${Date.now()}`,
        trip_id: trip.id,
        user_id: myUserId,
        author_label: 'You',
        author_role: myRole ?? '',
        is_ai: false,
        content: body,
        state_code: scopeState || null,
        permit_id: scopedPermit?.id ?? null,
        confidence: null,
        sources: null,
        feedback: null,
        // The optimistic copy mirrors the sharing choice so it does not flash
        // as public before the real row arrives.
        is_private: !share,
        private_for_user_id: share ? null : myUserId,
        answer_to_message_id: null,
        created_at: new Date().toISOString(),
      },
    ])
    if (offline) {
      const reply = offline.answer(body, scopeState || null, language)
      setMessages((m) => [
        ...m,
        {
          id: `tmp-ai-${Date.now()}`,
          trip_id: trip.id,
          user_id: null,
          author_label: 'Agent',
          author_role: 'agent',
          is_ai: true,
          content: reply,
          state_code: scopeState || null,
          permit_id: scopedPermit?.id ?? null,
          confidence: null,
          sources: null,
          feedback: null,
          is_private: !share,
          private_for_user_id: share ? null : myUserId,
          answer_to_message_id: null,
          created_at: new Date().toISOString(),
        },
      ])
      setPending(false)
      return
    }
    try {
      const res = await fetch(`/api/trips/${trip.id}/chat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          question: body,
          state_code: scopeState,
          ...(scopedPermit ? { permit_id: scopedPermit.id } : {}),
          ask_ai: true,
          language,
        }),
      })
      const json = await res.json()
      if (!res.ok) toast.error(json.error ?? 'Message failed')
      else if (Array.isArray(json.messages)) {
        setMessages((m) => merge(m, json.messages))
      }
    } finally {
      setPending(false)
    }
  }

  async function feedback(id: string, value: 1 | -1) {
    if (offline) {
      setMessages((ms) => ms.map((x) => (x.id === id ? { ...x, feedback: value } : x)))
      toast.success(value === 1 ? 'Thanks — marked helpful' : 'Thanks — flagged for review')
      return
    }
    const res = await fetch(`/api/trips/${trip.id}/chat/feedback`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message_id: id, feedback: value }),
    })
    if (!res.ok) {
      toast.error('Could not record feedback')
      return
    }
    setMessages((m) => m.map((msg) => (msg.id === id ? { ...msg, feedback: value } : msg)))
    toast.success('Feedback recorded')
  }

  // Thumbs down / report → one quick follow-up question (never a long form),
  // which creates a review ticket for the Moderator Dashboard.
  const [review, setReview] = useState<{ id: string; report: boolean } | null>(null)
  const [reviewReason, setReviewReason] = useState('')
  const [reviewComment, setReviewComment] = useState('')
  const [reviewSending, setReviewSending] = useState(false)

  async function submitReview() {
    if (!review) return
    setReviewSending(true)
    try {
      const res = await fetch(`/api/trips/${trip.id}/chat/feedback`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message_id: review.id,
          ...(review.report ? { report: true } : { feedback: -1 }),
          reason: reviewReason,
          comment: reviewComment,
        }),
      })
      if (!res.ok) {
        toast.error('Could not record feedback')
        return
      }
      if (!review.report) {
        setMessages((m) =>
          m.map((msg) => (msg.id === review.id ? { ...msg, feedback: -1 } : msg)),
        )
      }
      toast.success('Feedback received')
      setReview(null)
      setReviewReason('')
      setReviewComment('')
    } finally {
      setReviewSending(false)
    }
  }

  /* -------- Sharing my questions with the others on this trip -------- */
  const [share, setShare] = useState(shareChat)
  // Turning sharing off asks about the questions already sent.
  const [askHidePast, setAskHidePast] = useState(false)
  const [savingShare, setSavingShare] = useState(false)

  async function setSharing(next: boolean, hidePast: boolean) {
    if (offline) {
      setShare(next)
      setAskHidePast(false)
      if (hidePast) setMessages((m) => m.map((x) => (x.user_id === myUserId || x.is_ai ? { ...x, is_private: true, private_for_user_id: myUserId } : x)))
      toast.success(next ? 'Sharing is back on — your next questions are visible to everyone on this assignment' : hidePast ? 'Hidden — your earlier questions and answers are private too' : 'Your next questions stay private to you')
      return
    }
    setSavingShare(true)
    try {
      const res = await fetch(`/api/trips/${trip.id}/chat/privacy`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ share: next, hide_past: hidePast }),
      })
      const json = await res.json().catch(() => ({}))
      if (!res.ok) {
        toast.error(json.error ?? 'Could not save your choice')
        return
      }
      setShare(next)
      setAskHidePast(false)
      toast.success(
        next
          ? 'Sharing is back on — your next questions are visible to everyone on this trip'
          : hidePast
            ? 'Hidden — your earlier questions and answers are private too'
            : 'Your next questions stay private to you',
      )
      // Re-read through the privacy filter so what is on screen matches what
      // the other participants can actually see.
      const refreshed = await fetch(`/api/trips/${trip.id}/chat`)
      if (refreshed.ok) {
        const data = await refreshed.json()
        if (Array.isArray(data.messages)) setMessages(data.messages)
      }
    } finally {
      setSavingShare(false)
    }
  }

  const online = participants.filter((p) => p.status !== 'removed')
  const isPending = trip.status === 'draft' || trip.status === 'waiting_for_permits'

  return (
    <div
      className={
        compact
          ? // Driver column: sized to the phone viewport minus the trip header
            // and the bottom nav, so the nav stays reachable (Task 74). Nash:
            // "it takes me a long time to scroll down and get to the buttons to
            // switch between the trips and agent… a lot of dead space."
            'flex h-[calc(100dvh-15rem)] min-h-[24rem] max-h-[38rem] flex-col overflow-hidden rounded-2xl border border-line bg-white'
          : 'flex min-h-0 flex-1 flex-col'
      }
    >
      {/* Agent (language) selector — answers come in the selected language once the AI connects.
          The AI Credits indicator stays small and quiet (never a running
          meter); it only gets loud when the balance is low or out. Viewing
          old answers or navigating NEVER consumes credits. */}
      <div
        className={`flex items-center gap-2 overflow-x-auto border-b bg-white py-2 ${
          compact ? 'px-2.5' : 'px-4'
        }`}
      >
        <span
          className="order-last ml-auto shrink-0 text-[11px] text-neutral-400"
          title="AI Credits pay only for new AI answers — never for navigating, viewing permits, or re-reading old answers. Balance updates with the metering backend."
        >
          AI Credits: {DEMO_AI_CREDITS_REMAINING}
        </span>
        {/* Only the languages this user enabled — English alone for a new
            user — plus one button to add more (Task 71). */}
        {prefs.languages.map((code) => (
          <button
            key={code}
            type="button"
            onClick={() => setLanguage(code)}
            className={`shrink-0 rounded-full border px-3 py-1.5 text-xs font-semibold transition ${
              language === code
                ? 'border-[#0f1b2d] bg-[#0f1b2d] text-white'
                : 'text-neutral-500 hover:border-neutral-400'
            }`}
            title={
              code === prefs.primary
                ? 'Your default language — the agent answers in this language'
                : 'The agent answers in this language'
            }
          >
            {labelOf(code)}
            {code === prefs.primary && prefs.languages.length > 1 ? ' ·★' : ''}
          </button>
        ))}
        <button
          type="button"
          onClick={() => setManaging((m) => !m)}
          className={`shrink-0 rounded-full border border-dashed px-3 py-1.5 text-xs font-semibold transition ${
            managing ? 'border-[#0f1b2d] text-[#0f1b2d]' : 'text-neutral-500 hover:border-neutral-400'
          }`}
          title="Add a language, remove one, or choose your default"
        >
          {prefs.languages.length < AGENTS.length ? '+ Add language' : 'Languages'}
        </button>
      </div>

      {managing && (
        <div className={`border-b bg-neutral-50 py-2.5 text-[11px] ${compact ? 'px-2.5' : 'px-4'}`}>
        {/* Task 85 — Nash: "I need the button to apply, and when I click apply,
            this should disappear… on the right side of the additional
            languages." Every change is already saved the moment it is made,
            so Apply only closes the panel. */}
        <div className="flex flex-wrap items-start gap-3">
        <div className="min-w-0 flex-1 space-y-2">
          {prefs.languages.length < AGENTS.length && (
            <div className="flex flex-wrap items-center gap-1.5">
              <span className="font-semibold text-neutral-600">Add:</span>
              {AGENTS.filter((a) => !prefs.languages.includes(a.code)).map((a) => (
                <button
                  key={a.code}
                  type="button"
                  disabled={savingLangs}
                  onClick={async () => {
                    const ok = await saveLanguages({
                      languages: [...prefs.languages, a.code],
                      primary: prefs.primary,
                    })
                    if (ok) {
                      setLanguage(a.code)
                      toast.success(`${a.name} added to your languages`)
                    }
                  }}
                  className="rounded-full border bg-white px-2.5 py-1 font-semibold text-neutral-700 hover:border-[#0f1b2d] disabled:opacity-60"
                >
                  {a.label}
                </button>
              ))}
            </div>
          )}
          <div className="space-y-1">
            {prefs.languages.map((code) => (
              <div key={code} className="flex flex-wrap items-center gap-2">
                <span className="w-28 font-semibold text-neutral-700">{labelOf(code)}</span>
                <label className="flex cursor-pointer items-center gap-1 text-neutral-600">
                  <input
                    type="radio"
                    name="primary-language"
                    checked={code === prefs.primary}
                    disabled={savingLangs}
                    onChange={async () => {
                      const ok = await saveLanguages({ languages: prefs.languages, primary: code })
                      if (ok) {
                        setLanguage(code)
                        toast.success(`${nameOf(code)} is now your default language`)
                      }
                    }}
                    className="accent-[#0f1b2d]"
                  />
                  Default
                </label>
                {/* Never zero languages — the last one cannot be removed. */}
                {prefs.languages.length > 1 && (
                  <button
                    type="button"
                    disabled={savingLangs}
                    onClick={async () => {
                      const remaining = prefs.languages.filter((c) => c !== code)
                      const ok = await saveLanguages({
                        languages: remaining,
                        primary: code === prefs.primary ? remaining[0] : prefs.primary,
                      })
                      if (ok) toast.success(`${nameOf(code)} removed`)
                    }}
                    className="text-neutral-400 hover:text-red-600 disabled:opacity-60"
                    title={`Remove ${nameOf(code)}`}
                  >
                    Remove
                  </button>
                )}
              </div>
            ))}
          </div>
          <p className="text-neutral-500">
            Saved to your profile — the same languages and default apply on every trip.
          </p>
        </div>
        <Button
          type="button"
          onClick={() => setManaging(false)}
          className={`ml-auto shrink-0 self-start bg-[#0f1b2d] font-bold text-white hover:bg-[#1c2f4a] ${
            compact ? 'min-h-11 px-5 text-sm' : ''
          }`}
          title="Done with languages — close this panel"
        >
          Apply
        </Button>
        </div>
        </div>
      )}

      {/* Sharing toggle — "on top", per the 2026-09-07 meeting. Deliberately
          plain language: Nash rejected calling it an incognito mode ("it's
          nothing cool. It's just share or not share"). */}
      <div
        className={`flex flex-wrap items-center gap-2 border-b bg-white py-2 ${
          compact ? 'px-2.5' : 'px-4'
        }`}
      >
        <label className="flex cursor-pointer items-center gap-2 text-[11px] text-neutral-600">
          <input
            type="checkbox"
            checked={share}
            disabled={savingShare}
            onChange={(e) => {
              if (e.target.checked) setSharing(true, false)
              // Turning it off: ask about the questions already sent first.
              else setAskHidePast(true)
            }}
            className="h-3.5 w-3.5 cursor-pointer accent-[#0f1b2d]"
          />
          <span>
            Let broker and dispatch see my questions and answers
            {!share && <span className="ml-1 font-semibold text-amber-700">· Off</span>}
          </span>
        </label>
      </div>

      {askHidePast && (
        <div className={`border-b bg-amber-50 py-2.5 ${compact ? 'px-2.5' : 'px-4'}`}>
          <p className="text-[11px] leading-relaxed text-amber-900">
            Also hide your earlier questions on this trip?
          </p>
          <div className="mt-2 flex flex-wrap gap-2">
            <Button size="sm" disabled={savingShare} onClick={() => setSharing(false, true)}>
              Yes, hide them
            </Button>
            <Button
              size="sm"
              variant="outline"
              disabled={savingShare}
              onClick={() => setSharing(false, false)}
            >
              No, leave them
            </Button>
            <Button
              size="sm"
              variant="ghost"
              disabled={savingShare}
              onClick={() => setAskHidePast(false)}
            >
              Cancel
            </Button>
          </div>
        </div>
      )}

      {/* Permit-scoped mode banner (Task 7): Section 2 "flips" to one permit */}
      {scopedPermit && (
        <div className="flex items-center justify-between gap-3 border-b border-[#f5a623]/40 bg-amber-50 px-4 py-2 duration-300 animate-in fade-in slide-in-from-top-2">
          <p className="min-w-0 truncate text-xs font-semibold text-amber-900">
            🎯 Asking about: {stateName(scopedPermit.state_code)}
            {scopedPermit.permit_number && (
              <span className="ml-1.5 font-mono text-amber-700">{scopedPermit.permit_number}</span>
            )}
            <span className="ml-2 font-normal text-amber-700">
              — answers focus on this permit only
            </span>
          </p>
          <button
            onClick={onExitScope}
            className="shrink-0 rounded-md px-2 py-0.5 text-xs font-bold text-amber-900 hover:bg-amber-100"
          >
            ✕ Back to trip chat
          </button>
        </div>
      )}

      <div
        ref={scrollRef}
        className={`min-h-0 flex-1 space-y-4 overflow-y-auto ${compact ? 'p-3' : 'p-5'}`}
      >
        {/* Desktop Section 2 never shows the chat at zero permits any more
            (PermitsGate, Task 84); the driver's Agent pane still does, so the
            waiting note stays for `compact` only. */}
        {compact && isPending && permits.length === 0 && (
          <div className="mx-auto max-w-md rounded-2xl border border-dashed border-amber-300 bg-amber-50 p-6 text-center">
            <p className="text-2xl">⏳</p>
            <h3 className="mt-2 font-bold text-amber-800">This trip is waiting on permits</h3>
            <p className="mt-2 text-sm leading-relaxed text-neutral-600">
              Upload permits in the Docs tab or invite the dispatcher to continue. Once permits are
              uploaded, the Agent can answer permit questions.
            </p>
          </div>
        )}
        {messages.length === 0 && !isPending && (
          <div className="mx-auto mt-16 max-w-md text-center text-sm text-neutral-500">
            <p className="text-2xl">💬</p>
            <p className="mt-3">
              Ask the HeavyHaul Agent about permits, curfews, escorts, or trip details.
              {share
                ? ` Everyone on this trip (${online.length} ${
                    online.length === 1 ? 'participant' : 'participants'
                  }) sees this conversation.`
                : ' Sharing is off, so your questions and the answers stay private to you on this trip.'}
            </p>
          </div>
        )}
            {messages.map((m) => {
              const mine = !m.is_ai && m.user_id === myUserId
              return (
              <div key={m.id} className={`flex ${mine ? 'justify-end' : ''}`}>
                <div
                  className={`max-w-[85%] rounded-2xl p-3 text-sm ${
                    m.is_ai
                      ? 'border border-amber-200 bg-amber-50'
                      : mine
                        ? 'bg-neutral-900 text-white'
                        : 'border bg-white'
                  }`}
                >
                  <p className={`mb-1 text-xs font-semibold ${mine ? 'text-neutral-300' : 'text-neutral-500'}`}>
                    {m.is_ai ? '🤖 ' : ''}
                    {mine ? 'You' : m.author_label}
                    {m.author_role && !m.is_ai ? ` · ${m.author_role}` : ''}
                    {m.state_code ? ` · ${m.state_code}` : ''} · {formatDateTime(m.created_at)}
                  </p>
                  <p className="leading-relaxed whitespace-pre-wrap">
                    {getDemoTranslation(m.id, language, m.content)}
                  </p>
                  {m.is_ai && (
                    <div className="mt-2 flex flex-wrap items-center gap-2 border-t pt-2 text-xs text-neutral-500">
                      {m.confidence && (
                        <Badge variant="outline" className="capitalize">Confidence: {m.confidence}</Badge>
                      )}
                      {m.sources && m.sources.length > 0 && <span>Sources: {m.sources.join(', ')}</span>}
                      {!m.id.startsWith('tmp-') && (
                        <span className="ml-auto flex items-center gap-2">
                          <button
                            onClick={() => {
                              setReview({ id: m.id, report: true })
                              setReviewReason('')
                              setReviewComment('')
                            }}
                            className="text-[10px] font-semibold text-neutral-400 hover:text-neutral-700"
                            title="Report this answer / ask for human review"
                          >
                            Report
                          </button>
                          {/* Task 86 — real tap targets (Nash: "easy to click"):
                              32px on desktop, 44px on the driver's phone. */}
                          <Button
                            type="button"
                            variant="ghost"
                            size="icon"
                            onClick={() => feedback(m.id, 1)}
                            className={`${compact ? 'size-11 text-xl' : 'text-base'} ${
                              m.feedback === 1 ? 'opacity-100' : 'opacity-50 hover:opacity-100'
                            }`}
                            title="Good answer"
                            aria-label="Good answer"
                          >
                            👍
                          </Button>
                          <Button
                            type="button"
                            variant="ghost"
                            size="icon"
                            onClick={() => {
                              setReview({ id: m.id, report: false })
                              setReviewReason('')
                              setReviewComment('')
                            }}
                            className={`${compact ? 'size-11 text-xl' : 'text-base'} ${
                              m.feedback === -1 ? 'opacity-100' : 'opacity-50 hover:opacity-100'
                            }`}
                            title="Bad answer"
                            aria-label="Bad answer"
                          >
                            👎
                          </Button>
                        </span>
                      )}
                    </div>
                  )}
                </div>
              </div>
              )
            })}
        {pending && <p className="text-center text-xs text-neutral-400">Sending…</p>}
      </div>

      {/* Thumbs-down / Report follow-up (§28) as a POP-UP (Task 86). Nash:
          "it should be a pop-up which has a bigger font, the question, bigger
          buttons for me to click the reason, and a bigger submit button. This
          way it fills up the screen." Same reasons, same ticket pipeline as
          before — only the container and sizes changed. Desktop and driver. */}
      <Dialog
        open={!!review}
        onOpenChange={(open) => {
          if (!open && !reviewSending) setReview(null)
        }}
      >
        <DialogContent
          showCloseButton={false}
          className="flex max-h-[90dvh] w-full max-w-[calc(100%-1rem)] flex-col gap-0 overflow-y-auto p-5 sm:max-w-md"
        >
          <DialogHeader>
            <DialogTitle className="text-lg font-bold leading-snug">
              {review?.report
                ? 'Report this answer — what looks wrong?'
                : 'What was wrong with this answer?'}
            </DialogTitle>
          </DialogHeader>
          <div className="mt-4 grid gap-2">
            {FEEDBACK_REASONS.map((r) => (
              <button
                key={r}
                type="button"
                onClick={() => setReviewReason(r)}
                className={`min-h-12 w-full rounded-xl border px-4 text-left text-base font-semibold transition ${
                  reviewReason === r
                    ? 'border-[#0f1b2d] bg-[#0f1b2d] text-white'
                    : 'bg-white text-neutral-800 hover:border-neutral-400'
                }`}
              >
                {r}
              </button>
            ))}
          </div>
          <Input
            value={reviewComment}
            onChange={(e) => setReviewComment(e.target.value)}
            placeholder="Tell us what you expected or what looks wrong (optional)"
            className="mt-4 h-11 text-base"
          />
          <Button
            type="button"
            onClick={submitReview}
            disabled={reviewSending}
            className="mt-4 min-h-13 w-full bg-[#f5a623] text-base font-bold text-[#0f1b2d] hover:bg-[#d98b06]"
          >
            {reviewSending ? 'Sending…' : 'Submit'}
          </Button>
          <Button
            type="button"
            variant="ghost"
            onClick={() => setReview(null)}
            disabled={reviewSending}
            className="mt-2 min-h-11 w-full text-base"
          >
            Cancel
          </Button>
        </DialogContent>
      </Dialog>

      {/* Composer */}
      <div className={`border-t bg-white ${compact ? 'p-3' : 'p-4'}`}>
        {messages.length < 4 && myRole && (
          <div className="mb-3 flex gap-2 overflow-x-auto pb-1">
            {suggestions.map((q) => (
              <button
                key={q}
                type="button"
                onClick={() => setText(q)}
                className="shrink-0 rounded-full border bg-neutral-50 px-3 py-1.5 text-xs text-neutral-600 transition hover:border-[#f5a623] hover:text-neutral-900"
              >
                {q}
              </button>
            ))}
          </div>
        )}
        {/* Task 87 — driver: voice first. Nash: "the main source of
            interaction with the AI agent will be via voice… the icon for the
            microphone should be bigger, and it should be above — prioritized
            instead of typing." Speech-to-text itself is the AI backend
            (TODO(backend)); the control is the same stub, only bigger and first. */}
        {compact && (
          <div className="mb-3 flex flex-col items-center">
            <Button
              type="button"
              disabled={!myRole}
              onClick={() => toast.info('Voice input connects with the AI backend — coming soon.')}
              className="h-16 w-16 rounded-full bg-[#f5a623] text-[#0f1b2d] shadow-md hover:bg-[#d98b06]"
              title="Tap to talk"
              aria-label="Tap to talk"
            >
              <Mic className="size-7" />
            </Button>
            <span className="mt-1.5 text-xs font-bold text-neutral-700">Tap to talk</span>
          </div>
        )}
        <form
          onSubmit={(e) => {
            e.preventDefault()
            send()
          }}
          className={compact ? 'flex flex-nowrap gap-2' : 'flex flex-wrap gap-2'}
        >
          {/* Nash: "the selection of the state should be ALWAYS [available],
              even if you go to a specific permit to ask." While scoped, the
              selector shows the permit's state; picking a different state (or
              Whole trip) exits the permit scope and continues there. */}
          {states.length > 0 && (
            <select
              value={scopedPermit ? (scopedPermit.state_code ?? '') : stateFocus}
              onChange={(e) => {
                if (scopedPermit) onExitScope()
                setStateFocus(e.target.value)
              }}
              className={`rounded-lg border px-2 text-sm ${compact ? 'h-11 shrink-0' : 'py-2'}`}
              title="Focus on one state or the whole trip"
            >
              {(!offline || offline.wholeTrip) && <option value="">Whole trip</option>}
              {states.map((s) => (
                <option key={s} value={s}>{s}</option>
              ))}
            </select>
          )}
          <Input
            value={text}
            onChange={(e) => setText(e.target.value)}
            placeholder={
              scopedPermit
                ? `Ask about the ${stateName(scopedPermit.state_code)} permit…`
                : 'Ask the Agent: Can I drive at night? Do I need escorts? …'
            }
            disabled={!myRole}
            className={compact ? 'h-11 min-w-0 flex-1 text-base' : 'min-w-[12rem] flex-1'}
          />
          {!compact && (
            <Button
              type="button"
              variant="outline"
              size="icon"
              disabled={!myRole}
              onClick={() => toast.info('Voice input connects with the AI backend — coming soon.')}
              title="Ask by voice"
            >
              🎙️
            </Button>
          )}
          <Button
            type="submit"
            disabled={pending || !myRole}
            className={`bg-[#f5a623] font-bold text-[#0f1b2d] hover:bg-[#d98b06] ${
              compact ? 'h-11 shrink-0 px-4 text-base' : ''
            }`}
            title={
              share
                ? 'Ask the HeavyHaul Agent — the question and answer are visible to everyone on this trip'
                : 'Ask the HeavyHaul Agent — sharing is off, so this question and its answer stay private to you'
            }
          >
            {compact ? 'Ask' : 'Ask Agent'}
          </Button>
        </form>
        <p className="mt-2 text-[11px] text-neutral-400">
          The official permit and provisions remain the controlling documents. Answers are decision
          support, not legal authority.
        </p>
      </div>
    </div>
  )
}
