'use client'

import { useEffect, useMemo, useRef, 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 { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,
} from '@/components/ui/dialog'
import { Textarea } from '@/components/ui/textarea'
import { StatusBadge } from '@/components/app/status-badge'
import { DimensionsEditor } from '@/components/app/dimensions-editor'
import { ChatPanel } from '@/components/app/chat-panel'
import { PermitsGate } from '@/components/app/permits-gate'
import { Mic } from 'lucide-react'
import { PilotAccessChooser } from '@/components/app/pilot-access-chooser'
import type { PilotInviteAccess } from '@/lib/domain/pilot'
import { MyPilotTab, PaperworkPreviewSwitch, useViewerPaperworkAccess, type MyPilotEntry } from '@/components/app/pilot-tools/my-pilot-tab'
import { useExpenses, useInvoices } from '@/lib/demo/invoicing-store'
import { buildInvoicePdf, buildPaperworkPdf, openPdf } from '@/lib/demo/invoice-pdf'
import { brokerCanSeePilotInvoices } from '@/lib/domain/invoicing'
import { DEMO_PILOT_ACCESS, DEMO_PILOT_STATES, demoPilotContacts, demoPilotPaperwork, type DemoPilotContact } from '@/lib/demo/pilot-cars'
import { pilotAccessCoversState, pilotAccessFromEvents } from '@/lib/domain/pilot'
import { CompletionPrompt, PersonalCompletionButton } from '@/components/app/trip-completion'
import { ActivateTripButton } from '@/components/app/trip-activation'
import { ContactPicker } from '@/components/app/contact-picker'
import {
  CartBar, RoutePurchaseControl, useRouteCart, type RouteCart, type RouteCredits,
} from '@/components/app/route-purchase'
import { RequestNewPermitButton } from '@/components/app/permit-reorder'
import {
  hasAnyRouteFormat, requesterText, resolveRequester, RouteFormatChips,
} from '@/components/app/route-formats'
import { INVITE_RULES } from '@/lib/domain/permissions'
import { STATE_NAMES, stateName } from '@/lib/domain/states'
import { ackWarning, getAckedWarnings } from '@/lib/ack'
import { useMediaQuery, XL_QUERY } from '@/lib/use-media-query'
import { StateInfoContent } from '@/components/app/state-notes-dialog'
import { DEMO_ROUTE_TYPES } from '@/lib/demo/route-types'
import {
  canManageStatus, canTransition, resolveStatusForUser,
  shouldPromptCompletion, type TripCompletion,
} from '@/lib/domain/status'
import { formatDateTime, formatFullDate, formatInches, formatWeight, localToday } from '@/lib/format'
import type {
  CarrierContact, ChatMessage, Permit, ServiceRequest, Trip, TripDocument, TripEvent,
  TripParticipant, TripRole, TripWarning,
} from '@/types/db'

const RIGHT_TABS = ['overview', 'documents', 'people', 'my-pilot', 'history'] as const

export interface TripWorkspaceProps {
  /** Which role page owns this render (2026-09-13 separation). */
  pageContext: 'broker' | 'carrier'
  /** Role-specific trip link for the sidebar switcher. */
  tripHref: (t: Trip) => string
  /**
   * Which panel a phone lands on (below xl). Default `chat`. The freight
   * broker page sets `info` (Nash, 2026-09-13: "First, I wanna see the trip
   * information, and then I can start asking questions") and gets a floating
   * microphone button that opens the chat.
   */
  mobileLanding?: 'chat' | 'info'
  trip: Trip
  myTrips: Trip[]
  myRole: TripRole | null
  myUserId: string
  /** Account-level role (session) — gates admin-only controls like the
      permit-handling mode change. */
  myAccountRole?: string
  /** Account sign-up date — drives the 90-day pilot-paperwork window (Nash, 2026-09-13). */
  myAccountCreatedAt?: string
  initialTab?: string
  /** Deep link (driver state sections): opens the chat scoped to this permit. */
  initialPermitId?: string
  participants: TripParticipant[]
  documents: TripDocument[]
  signedUrls: Record<string, string>
  permits: Permit[]
  warnings: TripWarning[]
  chat: ChatMessage[]
  events: TripEvent[]
  requests: ServiceRequest[]
  /**
   * Personal completion for THIS user on THIS trip, plus who else completed it
   * (2026-09-07). Completion is per participant; the trip-wide status action
   * below is kept alongside it.
   */
  myCompletion: TripCompletion
  /** Per-user completion for the sidebar trip switcher, keyed by trip id. */
  completions: Record<string, TripCompletion>
  /** This user's "share my questions" choice on this trip (2026-09-07). */
  shareChat: boolean
  /** Prepaid Express Route credits — real, decrementing (Task 69). */
  routeCredits: RouteCredits
  /** Agent chat languages from the profile (Task 71). */
  chatLanguages: string[]
  primaryLanguage: string
  /** The viewer's saved people, so inviting is a pick not a retype (Task 99). */
  contacts?: { drivers: CarrierContact[]; brokers: CarrierContact[] }
}

/** What the permit cards need beyond TripWorkspaceProps: the per-user, per-trip cart. */
type CardCtx = Pick<TripWorkspaceProps, 'trip' | 'myRole' | 'requests' | 'participants' | 'signedUrls' | 'warnings' | 'routeCredits'> & {
  cart: RouteCart
}

/** Default panel split per the client: 20% trips · 40% chat · 40% trip info. */
const DEFAULT_SPLIT = { left: 20, right: 40 }
const SPLIT_KEY = 'hha-workspace-split'

/**
 * Trip workspace — 3-column AI-tool layout (the design the client approved):
 *   left: trip switcher · center: shared agent chat · right: trip intelligence.
 * Panels are resizable by dragging the dividers (persisted per browser);
 * defaults to the 20/40/40 split. All functionality (chat polling, documents,
 * people, status, requests, history) is preserved.
 */
export function TripWorkspace(props: TripWorkspaceProps) {
  const { trip, myRole, warnings, myCompletion, completions } = props
  // What THIS user sees. Completion is personal; cancelled/draft/waiting stay
  // trip-wide because they describe permit processing, not one person.
  const myStatus = resolveStatusForUser(trip.status, myCompletion)
  const router = useRouter()
  const initialRight = RIGHT_TABS.includes(props.initialTab as (typeof RIGHT_TABS)[number])
    ? props.initialTab
    : 'overview'
  const realPilotParticipants = props.participants.filter((p) => p.role === 'pilot' && p.status !== 'removed')
  // Demo overlay (Nash, 2026-09-12): trips with a New Mexico or Arizona permit
  // show John Cena / Mark Cuban as the hired pilot car when no real pilot is on
  // the trip, so the "My Pilot" tab can be felt on the existing trips.
  const hasDemoPilotState = props.permits.some((pm) => (DEMO_PILOT_STATES as readonly string[]).includes(pm.state_code))
  // A real pilot already covering NM/AZ (per their invitation's access) means
  // no demo pair; an invited pilot with no access yet does not.
  const realAccess = pilotAccessFromEvents([...props.events].reverse())
  const realCoversDemoState = realPilotParticipants.some((p) =>
    DEMO_PILOT_STATES.some((s) => pilotAccessCoversState(realAccess[p.email.toLowerCase()], s)),
  )
  const pilotParticipants: Array<TripParticipant | DemoPilotContact> =
    hasDemoPilotState && !realCoversDemoState ? [...realPilotParticipants, ...demoPilotContacts(trip.id)] : realPilotParticipants
  // Below xl the chat and info panels share the screen via a toggle.
  const [mobileView, setMobileView] = useState<'chat' | 'info'>(
    props.initialTab === 'chat'
      ? 'chat'
      : props.initialTab
        ? 'info'
        : (props.mobileLanding ?? 'chat'),
  )

  // "Ask about this permit" — Section 2 scopes to one exact permit (Task 7).
  // A ?permit= deep link (driver state sections) opens already scoped.
  const [scopedPermit, setScopedPermit] = useState<Permit | null>(
    () => props.permits.find((p) => p.id === props.initialPermitId) ?? null,
  )
  // Internal state-info panel between Section 2 and 3 (Task 12).
  const [stateInfoCode, setStateInfoCode] = useState<string | null>(null)
  // Task 83: the desktop panel and the mobile dialog must never both be
  // mounted. Hiding the dialog's content with `xl:hidden` left its overlay
  // (backdrop blur), the body pointer-events lock and the focus trap active
  // on desktop — "everything becomes blurry, and I can't see the text".
  const isXl = useMediaQuery(XL_QUERY)

  // Phone swipe between chat and trip info (Nash, 2026-09-13: "if I swipe left
  // or right, it will switch between chat and trip info"). Info-landing pages
  // only; a horizontal drag inside a sideways-scrolling strip (suggestion
  // chips, tables) is left alone.
  const swipeStart = useRef<{ x: number; y: number; skip: boolean } | null>(null)
  function onSwipeStart(e: React.TouchEvent) {
    if (props.mobileLanding !== 'info' || isXl) return
    const t = e.touches[0]
    const target = e.target as HTMLElement | null
    swipeStart.current = { x: t.clientX, y: t.clientY, skip: !!target?.closest('.overflow-x-auto, input, textarea, select') }
  }
  function onSwipeEnd(e: React.TouchEvent) {
    const start = swipeStart.current
    swipeStart.current = null
    if (!start || start.skip || props.mobileLanding !== 'info' || isXl) return
    const t = e.changedTouches[0]
    const dx = t.clientX - start.x
    const dy = t.clientY - start.y
    if (Math.abs(dx) < 70 || Math.abs(dx) < Math.abs(dy) * 1.5) return
    // Swipe left → chat (the panel "to the right"); swipe right → trip info.
    setMobileView(dx < 0 ? 'chat' : 'info')
  }
  // Task 84: with zero permits on a waiting trip the Agent has nothing to
  // work from, so Section 2 becomes the way to GET permits instead.
  const permitsGateOpen =
    props.permits.length === 0 && (trip.status === 'draft' || trip.status === 'waiting_for_permits')
  // Route shopping cart — per user, per trip (Task 69).
  const cart = useRouteCart(trip.id, props.myUserId)
  // Per-user warning acknowledgements (Task 10): hides only for THIS user.
  const [acked, setAcked] = useState<Set<string>>(new Set())
  const [ackTick, setAckTick] = useState(0)
  useEffect(() => {
    setAcked(getAckedWarnings(props.myUserId))
  }, [props.myUserId, ackTick])
  const visibleWarnings = warnings.filter((w) => !acked.has(w.id))

  // Resizable split (percent widths for sections 1 and 3; chat takes the rest).
  const containerRef = useRef<HTMLDivElement>(null)
  const [split, setSplit] = useState(DEFAULT_SPLIT)
  useEffect(() => {
    try {
      const saved = localStorage.getItem(SPLIT_KEY)
      if (saved) {
        const parsed = JSON.parse(saved)
        if (typeof parsed?.left === 'number' && typeof parsed?.right === 'number') setSplit(parsed)
      }
    } catch {
      // storage unavailable — keep defaults
    }
  }, [])

  function startDrag(which: 'left' | 'right', e: React.PointerEvent) {
    e.preventDefault()
    const startX = e.clientX
    const startSplit = { ...split }
    const width = containerRef.current?.offsetWidth ?? window.innerWidth
    const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))

    function onMove(ev: PointerEvent) {
      const dPct = ((ev.clientX - startX) / width) * 100
      setSplit((prev) => {
        const next =
          which === 'left'
            ? { ...prev, left: clamp(startSplit.left + dPct, 12, 32) }
            : { ...prev, right: clamp(startSplit.right - dPct, 24, 55) }
        try {
          localStorage.setItem(SPLIT_KEY, JSON.stringify(next))
        } catch {
          // storage unavailable — resize still works for this session
        }
        return next
      })
    }
    function onUp() {
      window.removeEventListener('pointermove', onMove)
      window.removeEventListener('pointerup', onUp)
      document.body.style.cursor = ''
      document.body.style.userSelect = ''
    }
    window.addEventListener('pointermove', onMove)
    window.addEventListener('pointerup', onUp)
    document.body.style.cursor = 'col-resize'
    document.body.style.userSelect = 'none'
  }

  return (
    <div
      ref={containerRef}
      className="flex h-[calc(100vh-3.5rem)] min-h-0 bg-neutral-50"
      onTouchStart={onSwipeStart}
      onTouchEnd={onSwipeEnd}
    >
      {/* ===== Section 1: trip switcher ===== */}
      <TripSidebar
        trips={props.myTrips}
        currentId={trip.id}
        width={split.left}
        completions={completions}
        tripHref={props.tripHref}
      />

      {/* Drag handle 1 */}
      <div
        onPointerDown={(e) => startDrag('left', e)}
        className="hidden w-1 shrink-0 cursor-col-resize bg-neutral-200/60 transition-colors hover:bg-[#f5a623] active:bg-[#f5a623] lg:block"
        title="Drag to resize"
      />

      {/* ===== Section 2: agent chat ===== */}
      {/* While the state-info panel is open it takes its 340px from THIS
          column, so the chat keeps a floor and Section 3 (which may shrink,
          see below) gives up the rest. Without the floor, a wide Section 3
          left the chat a sliver and the panel clipped (Task 65). */}
      <main
        className={`${mobileView === 'info' ? 'hidden' : 'flex'} min-w-0 flex-1 flex-col xl:flex ${
          stateInfoCode ? 'xl:min-w-[20rem]' : ''
        }`}
      >
        <div className="flex items-center justify-between gap-3 border-b bg-white px-4 py-2.5">
          <div className="min-w-0">
            <p className="truncate text-sm font-bold tracking-tight">
              {trip.origin || 'Origin TBD'} → {trip.destination || 'Destination TBD'}
            </p>
            <p className="truncate text-xs text-neutral-500">
              {trip.commodity || 'Commodity TBD'} · {trip.carrier_name || 'Carrier TBD'}
              {trip.unit_number ? ` · Unit ${trip.unit_number}` : ''} ·{' '}
              <span className="font-mono">{trip.ref_code}</span>
            </p>
          </div>
          <div className="flex shrink-0 items-center gap-2">
            <StatusBadge status={myStatus} policy={trip.permit_policy} />
            {props.mobileLanding === 'info' ? (
              // Nash, 2026-09-13: "That trip info button is not good enough. It
              // has to be a little bit bigger… easy for me to find it."
              <button
                type="button"
                onClick={() => setMobileView('info')}
                className="rounded-lg bg-[#0f1b2d] px-4 py-2.5 text-sm font-bold text-white shadow-sm hover:bg-[#16263e] xl:hidden"
              >
                📋 Trip Info
              </button>
            ) : (
              <Button size="sm" variant="outline" className="xl:hidden" onClick={() => setMobileView('info')}>
                Trip Info
              </Button>
            )}
          </div>
        </div>
        {permitsGateOpen ? (
          <PermitsGate
            trip={trip}
            myRole={myRole}
            requests={props.requests}
            onOpenInfo={isXl ? undefined : () => setMobileView('info')}
            orderAction={
              myRole ? (
                <BuyPermitsDialog
                  tripId={trip.id}
                  participants={props.participants.filter(
                    (pt) => pt.status !== 'removed' && pt.user_id !== props.myUserId,
                  )}
                  onDone={() => router.refresh()}
                  triggerLabel="Order permits from our trusted partner Synchron Permits"
                />
              ) : undefined
            }
          />
        ) : (
          <ChatPanel
            {...props}
            shareChat={props.shareChat}
            languages={props.chatLanguages}
            primaryLanguage={props.primaryLanguage}
            scopedPermit={scopedPermit}
            onExitScope={() => setScopedPermit(null)}
          />
        )}
      </main>

      {/* ===== State info panel (Task 12): slides in between Section 2 and 3,
           squeezing the chat; Section 3 stays still. Desktop only — mobile
           gets the same content as a dialog below. ===== */}
      {stateInfoCode && isXl && (
        <StateInfoPanel
          code={stateInfoCode}
          onClose={() => setStateInfoCode(null)}
          className="flex"
        />
      )}
      {!isXl && <StateInfoDialog code={stateInfoCode} onClose={() => setStateInfoCode(null)} />}

      {/* Phone only, and only for pages that land on Trip Info: a floating
          microphone opens the chat (Nash, 2026-09-13: "a floating microphone
          icon on the bottom right that when he clicks, it opens the chat"). */}
      {props.mobileLanding === 'info' && mobileView === 'info' && (
        <button
          type="button"
          onClick={() => setMobileView('chat')}
          className="fixed bottom-5 right-5 z-40 grid h-14 w-14 place-items-center rounded-full bg-[#f5a623] text-[#0f1b2d] shadow-lg ring-4 ring-white transition hover:bg-[#d98b06] xl:hidden"
          title="Ask the Agent"
          aria-label="Open the Agent chat"
        >
          <Mic className="size-6" />
        </button>
      )}

      {/* Drag handle 2 */}
      <div
        onPointerDown={(e) => startDrag('right', e)}
        className="hidden w-1 shrink-0 cursor-col-resize bg-neutral-200/60 transition-colors hover:bg-[#f5a623] active:bg-[#f5a623] xl:block"
        title="Drag to resize"
      />

      {/* ===== Section 3: trip intelligence ===== */}
      <aside
        style={{ ['--panel-w' as string]: `${split.right}%` }}
        className={`${mobileView === 'info' ? 'flex' : 'hidden'} w-full min-w-0 flex-col bg-white xl:flex xl:w-[var(--panel-w)] ${
          // Section 3 "stays still" (Task 12) — except while the state-info
          // panel is open, when it may shrink so the panel and the chat both
          // stay readable (Task 65).
          stateInfoCode ? 'xl:min-w-[18rem] xl:shrink' : 'xl:shrink-0'
        }`}
      >
        <div className="flex items-center justify-between border-b px-4 py-2.5 xl:hidden">
          <p className="text-sm font-bold">Trip information</p>
          {props.mobileLanding === 'info' ? (
            // Nash, 2026-09-13: "change this to… Ask the Agent."
            <button
              type="button"
              onClick={() => setMobileView('chat')}
              className="rounded-lg bg-[#f5a623] px-4 py-2.5 text-sm font-bold text-[#0f1b2d] shadow-sm hover:bg-[#d98b06]"
            >
              🎙 Ask the Agent
            </button>
          ) : (
            <Button size="sm" variant="outline" onClick={() => setMobileView('chat')}>
              ← Back to chat
            </Button>
          )}
        </div>

        {/* Workflow banner (order-intake doc §10): which permit flow is
            active and who the trip is waiting on — shown until the trip is
            active. */}
        {trip.permit_policy && ['draft', 'waiting_for_permits'].includes(trip.status) && (
          <div
            className={`border-b p-4 ${
              trip.permit_policy === 'synchron_required' ? 'bg-blue-50/70' : 'bg-amber-50/70'
            }`}
          >
            {trip.permit_policy === 'synchron_required' ? (
              <>
                <p className="text-sm font-bold text-blue-900">
                  Synchron Permits Processing Requested
                </p>
                <p className="mt-0.5 text-xs leading-relaxed text-blue-800">
                  This trip was submitted to Synchron Permits for permit processing by request of
                  the broker.
                </p>
                <p className="mt-1.5 text-xs text-blue-800">
                  Synchron order status:{' '}
                  <span className="font-semibold capitalize">
                    {(props.requests.find((r) => r.type === 'permit_request')?.status ?? 'pending').replace('_', ' ')}
                  </span>
                </p>
                <p className="mt-1.5 text-xs leading-relaxed text-green-800">
                  Routes are included for permits processed by Synchron Permits and will appear in
                  this workspace with the permits.
                </p>
                <p className="mt-1 text-[11px] text-blue-800/70">
                  Permits will appear here when uploaded by Synchron · Routes will appear here when
                  attached by Synchron
                </p>
              </>
            ) : (
              <>
                <p className="text-sm font-bold text-amber-900">Waiting on Carrier Permits</p>
                <p className="mt-0.5 text-xs leading-relaxed text-amber-800">
                  The carrier dispatcher has been asked to upload existing permits for this trip.
                </p>
                <p className="mt-1 text-[11px] text-amber-800/70">
                  Routes are not included yet. Request a Google Maps route for any uploaded permit
                  when needed.
                </p>
              </>
            )}
            {/* Controlled mode change (§21) — admin only, reason required.
                "Broker Admin / authorized internal support" = backend-later. */}
            {props.myAccountRole === 'admin' && (
              <PermitHandlingChange trip={trip} onDone={() => router.refresh()} />
            )}
          </div>
        )}

        {/* The active shopping cart (Task 69) — Nash: "we should have an
            active shopping cart showing". Renders only while it has items. */}
        <CartBar tripId={trip.id} cart={cart} onPaid={() => router.refresh()} />

        {/* Status + dates + warnings — always visible above the tabs */}
        <div className="space-y-2.5 border-b p-4">
          <div className="flex flex-wrap items-center justify-between gap-2">
            <StatusBadge status={myStatus} policy={trip.permit_policy} />
            <div className="flex flex-wrap items-center gap-2">
              {/* Trip-wide activation — "one push for all members", open to
                  every role on the trip (Task 67). Confirms first. */}
              <ActivateTripButton trip={trip} myRole={myRole} onDone={() => router.refresh()} />
              {/* Personal completion — every participant, including the driver
                  (2026-09-07). "Completes it only for you." */}
              <PersonalCompletionButton
                trip={trip}
                completion={myCompletion}
                onDone={() => router.refresh()}
              />
              {/* Trip-wide status stays broker / dispatcher / admin only. */}
              {myRole && canManageStatus(myRole) && (
                <StatusActions trip={trip} onDone={() => router.refresh()} />
              )}
            </div>
          </div>
          {shouldPromptCompletion(myCompletion) && (
            <CompletionPrompt
              trip={trip}
              completion={myCompletion}
              onDone={() => router.refresh()}
              className="rounded-xl border border-amber-300 bg-amber-50 p-3 text-xs"
            />
          )}
          <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs">
            <span className="font-semibold text-neutral-700">
              Trip # <span className="font-mono">{trip.ref_code}</span>
            </span>
            <span className="text-neutral-500">
              Load #: <span className="italic">pending rate con</span>
            </span>
            {trip.unit_number && (
              <span className="text-neutral-500">
                Unit <span className="font-mono">{trip.unit_number}</span>
              </span>
            )}
            {trip.permit_policy === 'synchron_required' && (
              <span className="text-neutral-500">
                Payment responsible party:{' '}
                <span className="font-semibold capitalize">
                  {trip.payment_responsible_party ?? 'not set'}
                </span>
              </span>
            )}
          </div>
          {/* Compact dates (Task 11) — full details in the tooltip */}
          <p
            className="text-xs text-neutral-500"
            title={`Created ${formatFullDate(trip.created_at)}${trip.pickup_date ? ` · Pickup ${formatFullDate(trip.pickup_date)}` : ''}${trip.delivery_date ? ` · Delivery ${formatFullDate(trip.delivery_date)}` : ''}`}
          >
            {shortRange(trip.pickup_date, trip.delivery_date)}
          </p>
          {visibleWarnings.map((w) => (
            <div
              key={w.id}
              className={`flex items-start gap-2 rounded-lg border p-2.5 text-xs ${
                w.severity === 'danger'
                  ? 'border-red-200 bg-red-50 text-red-800'
                  : w.severity === 'warning'
                    ? 'border-amber-200 bg-amber-50 text-amber-800'
                    : 'border-blue-200 bg-blue-50 text-blue-800'
              }`}
            >
              <span>⚠</span>
              <p className="min-w-0 flex-1 leading-snug">{w.message}</p>
              {/* Per-user acknowledge (Task 10): hides only for THIS user */}
              <button
                onClick={() => {
                  ackWarning(props.myUserId, w.id)
                  setAckTick((t) => t + 1)
                  toast.success('Acknowledged — hidden for you')
                }}
                className="shrink-0 rounded px-1 font-bold text-green-600 opacity-50 transition hover:bg-white/60 hover:opacity-100"
                title="Acknowledge — hides this warning for you only"
              >
                ✓
              </button>
            </div>
          ))}
        </div>

        <Tabs defaultValue={initialRight} className="flex min-h-0 flex-1 flex-col gap-0">
          <TabsList className="w-full shrink-0 justify-start overflow-x-auto rounded-none border-b bg-white px-2">
            <TabsTrigger value="overview">Overview</TabsTrigger>
            <TabsTrigger value="documents">Docs ({props.documents.length})</TabsTrigger>
            <TabsTrigger value="people">People ({props.participants.length})</TabsTrigger>
            {/* "My Pilot" — Nash, 2026-09-12: for the carrier "that tab active
                only if a pilot car is attached"; the broker sees name, phone,
                email and state hired for. Between People and History. */}
            {pilotParticipants.length > 0 && <TabsTrigger value="my-pilot">My Pilot</TabsTrigger>}
            <TabsTrigger value="history">History</TabsTrigger>
          </TabsList>
          <div className="min-h-0 flex-1 overflow-y-auto px-4 pb-6">
            <TabsContent value="overview">
              <OverviewTab
                {...props}
                cart={cart}
                onAskPermit={(p) => {
                  setScopedPermit(p)
                  setMobileView('chat')
                }}
                onStateInfo={(code) => setStateInfoCode(code)}
              />
            </TabsContent>
            <TabsContent value="documents">
              <DocumentsTab
                {...props}
                cart={cart}
                onAskPermit={(p) => {
                  setScopedPermit(p)
                  setMobileView('chat')
                }}
                onStateInfo={(code) => setStateInfoCode(code)}
              />
            </TabsContent>
            <TabsContent value="people"><PeopleTab {...props} /></TabsContent>
            {pilotParticipants.length > 0 && (
              <TabsContent value="my-pilot"><MyPilotWorkspaceTab {...props} pilots={pilotParticipants} /></TabsContent>
            )}
            <TabsContent value="history"><HistoryTab events={props.events} /></TabsContent>
          </div>
        </Tabs>
      </aside>
    </div>
  )
}

/* ---------------- Permit-handling mode change (admin, §21) ---------------- */

/**
 * "This should not be casual": pick the mode (payment party re-asked for the
 * Synchron flow, or corrected alone), give a REQUIRED reason, and the change
 * is logged to trip history for everyone to see.
 */
function PermitHandlingChange({ trip, onDone }: { trip: Trip; onDone: () => void }) {
  const [open, setOpen] = useState(false)
  const [mode, setMode] = useState<'synchron_required' | 'upload_allowed'>(
    trip.permit_policy ?? 'upload_allowed',
  )
  const [payment, setPayment] = useState<'broker' | 'carrier' | ''>(
    trip.payment_responsible_party ?? '',
  )
  const [reason, setReason] = useState('')
  const [saving, setSaving] = useState(false)

  async function save() {
    if (reason.trim().length < 3) {
      toast.error('A reason for the change is required.')
      return
    }
    if (mode === 'synchron_required' && !payment) {
      toast.error('Choose who will pay Synchron Permits.')
      return
    }
    setSaving(true)
    try {
      const res = await fetch(`/api/trips/${trip.id}/permit-handling`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          permit_policy: mode,
          ...(mode === 'synchron_required' && payment
            ? { payment_responsible_party: payment }
            : {}),
          reason: reason.trim(),
        }),
      })
      const json = await res.json()
      if (!res.ok) toast.error(json.error ?? 'Could not save the change')
      else {
        toast.success('Permit handling updated — the change is logged in trip history')
        setOpen(false)
        setReason('')
        onDone()
      }
    } finally {
      setSaving(false)
    }
  }

  return (
    <div className="mt-2">
      <button
        onClick={() => setOpen(true)}
        className="text-[11px] font-bold text-neutral-500 underline-offset-2 hover:underline"
      >
        Change permit handling (admin)
      </button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>Change permit handling</DialogTitle>
          </DialogHeader>
          <div className="space-y-4">
            <div className="space-y-2">
              {(
                [
                  ['upload_allowed', 'Carrier Will Upload Permits'],
                  ['synchron_required', 'Request Permits From Synchron Permits'],
                ] as const
              ).map(([value, label]) => (
                <button
                  key={value}
                  type="button"
                  onClick={() => setMode(value)}
                  className={`w-full rounded-lg border-2 p-3 text-left text-sm font-semibold transition ${
                    mode === value ? 'border-[#0f1b2d] bg-[#0f1b2d] text-white' : 'hover:border-neutral-400'
                  }`}
                >
                  {label}
                  {trip.permit_policy === value && (
                    <span className="ml-1.5 text-xs font-normal opacity-70">(current)</span>
                  )}
                </button>
              ))}
            </div>
            {mode === 'synchron_required' && (
              <div className="space-y-1.5">
                <Label>Who will pay Synchron Permits?</Label>
                <div className="grid grid-cols-2 gap-2">
                  {(['broker', 'carrier'] as const).map((p) => (
                    <button
                      key={p}
                      type="button"
                      onClick={() => setPayment(p)}
                      className={`rounded-lg border-2 p-2 text-sm font-semibold capitalize transition ${
                        payment === p ? 'border-[#f5a623] bg-amber-50' : 'hover:border-neutral-400'
                      }`}
                    >
                      {p} pays
                    </button>
                  ))}
                </div>
              </div>
            )}
            <div className="space-y-1.5">
              <Label htmlFor="ph_reason">
                Reason for the change <span className="text-red-600">*</span>
              </Label>
              <Textarea
                id="ph_reason"
                rows={2}
                value={reason}
                onChange={(e) => setReason(e.target.value)}
                placeholder="e.g. broker selected the wrong option at creation"
              />
            </div>
            <p className="text-[11px] text-neutral-400">
              The change updates permissions and route inclusion, and is logged to trip history
              with your name, the old and new values, and this reason. Follow-up emails activate
              with the email backend.
            </p>
            <div className="flex justify-end gap-2">
              <Button variant="ghost" onClick={() => setOpen(false)} disabled={saving}>
                Cancel
              </Button>
              <Button onClick={save} disabled={saving}>
                {saving ? 'Saving…' : 'Save change'}
              </Button>
            </div>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  )
}

/* ---------------- Trip switcher (Section 1) ---------------- */

function TripSidebar({
  trips,
  currentId,
  width,
  completions,
  tripHref,
}: {
  trips: Trip[]
  currentId: string
  width: number
  tripHref: (t: Trip) => string
  /** Per-user completion, so a trip this user finished sinks to the bottom. */
  completions: Record<string, TripCompletion>
}) {
  const [query, setQuery] = useState('')

  const ordered = useMemo(() => {
    const q = query.trim().toLowerCase()
    const filtered = q
      ? trips.filter((t) =>
          [t.origin, t.destination, t.commodity, t.carrier_name, t.unit_number, t.ref_code]
            .filter(Boolean)
            .join(' ')
            .toLowerCase()
            .includes(q),
        )
      : trips
    // Active first, pending next, history last — using the status THIS user
    // sees, so a personally-completed trip drops to the bottom for them only.
    const rank = (t: Trip) => {
      const st = resolveStatusForUser(t.status, completions[t.id])
      return st === 'active' ? 0 : st === 'draft' || st === 'waiting_for_permits' ? 1 : 2
    }
    return [...filtered].sort((a, b) => rank(a) - rank(b))
  }, [trips, query, completions])

  return (
    <aside
      style={{ ['--panel-w' as string]: `${width}%` }}
      className="hidden w-[var(--panel-w)] min-w-[180px] shrink-0 flex-col border-r bg-white lg:flex"
    >
      <div className="space-y-3 border-b p-4">
        <Button asChild className="w-full">
          <Link href="/trips/new">+ New Trip</Link>
        </Button>
        <Input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search carrier, lane, commodity…"
        />
      </div>
      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto p-3">
        {ordered.map((t) => (
          <Link
            key={t.id}
            href={tripHref(t)}
            className={`block rounded-xl border p-3 transition ${
              t.id === currentId
                ? 'border-[#f5a623]/60 bg-[#f5a623]/5 shadow-sm'
                : 'border-transparent hover:border-neutral-200 hover:bg-neutral-50'
            }`}
          >
            <p className="truncate text-sm font-bold">
              {t.origin || 'Origin TBD'} → {t.destination || 'Destination TBD'}
            </p>
            <p className="mt-0.5 truncate text-xs text-neutral-500">
              {t.commodity || 'Commodity TBD'} · {t.carrier_name || 'Carrier TBD'}
              {t.unit_number ? ` · ${t.unit_number}` : ''}
            </p>
            <div className="mt-2">
              <StatusBadge
                status={resolveStatusForUser(t.status, completions[t.id])}
                policy={t.permit_policy}
              />
            </div>
          </Link>
        ))}
        {ordered.length === 0 && (
          <p className="p-4 text-center text-xs text-neutral-500">No trips match your search.</p>
        )}
      </div>
      <div className="border-t p-3">
        <Link
          href="/history"
          className="block rounded-lg py-2 text-center text-xs font-medium text-neutral-500 hover:bg-neutral-50 hover:text-neutral-900"
        >
          Search trip history →
        </Link>
      </div>
    </aside>
  )
}

/* ---------------- State info (Task 12) ---------------- */

/**
 * Internal state knowledge. Desktop: a hidden section that carousels in from
 * the right between Section 2 and Section 3, squeezing the chat (Section 3
 * stays still). Topics run across the top (not Synchron's left-nav style).
 * Content is sample data until the internal state-data backend connects.
 */
// StateInfoContent moved to @/components/app/state-notes-dialog (2026-09-12) so
// the driver screens show the same state notes. Same markup, one source.

function StateInfoPanel({
  code,
  onClose,
  className = '',
}: {
  code: string
  onClose: () => void
  className?: string
}) {
  return (
    <aside
      className={`w-[340px] min-w-[340px] shrink-0 flex-col overflow-hidden border-l bg-white shadow-[-8px_0_24px_-12px_rgba(0,0,0,0.15)] duration-300 animate-in slide-in-from-right ${className}`}
    >
      <div className="flex items-center justify-between border-b px-4 py-2.5">
        <p className="text-sm font-bold">
          {stateName(code)} <span className="text-xs font-normal text-neutral-400">state info</span>
        </p>
        <Button size="sm" variant="ghost" onClick={onClose} title="Close">
          ✕
        </Button>
      </div>
      <StateInfoContent code={code} />
    </aside>
  )
}

/** Mobile version of the state info: same content as a popup. Mounted only
    below xl (Task 83) — never alongside the desktop panel. */
function StateInfoDialog({ code, onClose }: { code: string | null; onClose: () => void }) {
  return (
    <Dialog open={!!code} onOpenChange={(open) => !open && onClose()}>
      <DialogContent className="flex max-h-[85vh] flex-col overflow-hidden p-0">
        <DialogHeader className="border-b px-4 py-3">
          <DialogTitle>{code ? stateName(code) : ''} — state info</DialogTitle>
        </DialogHeader>
        {code && <StateInfoContent code={code} />}
      </DialogContent>
    </Dialog>
  )
}

/* ---------------- Status actions ---------------- */

function StatusActions({ trip, onDone }: { trip: Trip; onDone: () => void }) {
  const [pending, setPending] = useState(false)

  async function change(to: string) {
    setPending(true)
    try {
      const res = await fetch(`/api/trips/${trip.id}/status`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ to }),
      })
      const json = await res.json()
      if (!res.ok) toast.error(json.error ?? 'Status change failed')
      else {
        toast.success(`Trip marked ${to.replace(/_/g, ' ')}`)
        onDone()
      }
    } finally {
      setPending(false)
    }
  }

  const actions: Array<{ to: string; label: string; variant?: 'outline' | 'destructive' }> = []
  // "for everyone" distinguishes these from the per-user "Complete for me"
  // button beside them (both kept, client decision 2026-09-07).
  if (canTransition(trip.status, 'completed'))
    actions.push({ to: 'completed', label: 'Complete for everyone' })
  if (canTransition(trip.status, 'active') && trip.status === 'completed')
    actions.push({ to: 'active', label: 'Reopen', variant: 'outline' })
  if (canTransition(trip.status, 'cancelled'))
    actions.push({ to: 'cancelled', label: 'Cancel trip', variant: 'destructive' })

  // Nash: never complete/cancel in one click — always confirm first.
  const [confirming, setConfirming] = useState<{ to: string; label: string } | null>(null)

  if (actions.length === 0) return null
  return (
    <div className="flex gap-2">
      {actions.map((a) => (
        <Button
          key={a.to}
          size="sm"
          variant={a.variant ?? 'default'}
          disabled={pending}
          onClick={() => setConfirming({ to: a.to, label: a.label })}
        >
          {a.label}
        </Button>
      ))}
      <Dialog open={!!confirming} onOpenChange={(open) => !open && setConfirming(null)}>
        <DialogContent className="sm:max-w-sm">
          <DialogHeader>
            <DialogTitle>
              {confirming?.to === 'completed'
                ? 'Mark this trip completed for everyone?'
                : confirming?.to === 'cancelled'
                  ? 'Cancel this trip?'
                  : 'Reopen this trip?'}
            </DialogTitle>
          </DialogHeader>
          <p className="text-sm text-neutral-600">
            {confirming?.to === 'completed'
              ? 'This completes the trip for every participant, not just you. It moves to History and you can reopen it later. To finish it only for yourself, use “Complete for me”.'
              : confirming?.to === 'cancelled'
                ? 'Cancelling is final for this trip. Everyone on the trip will see it as cancelled.'
                : 'The trip returns to Active and shows on the dashboards again.'}
          </p>
          <div className="flex justify-end gap-2">
            <Button variant="ghost" onClick={() => setConfirming(null)} disabled={pending}>
              Go back
            </Button>
            <Button
              variant={confirming?.to === 'cancelled' ? 'destructive' : 'default'}
              disabled={pending}
              onClick={async () => {
                if (!confirming) return
                await change(confirming.to)
                setConfirming(null)
              }}
            >
              {pending ? 'Working…' : `Yes, ${confirming?.label.toLowerCase()}`}
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  )
}

/* ---------------- Overview ---------------- */

function OverviewTab({
  trip,
  permits,
  requests,
  myRole,
  myUserId,
  warnings,
  signedUrls,
  participants,
  routeCredits,
  cart,
  onAskPermit,
  onStateInfo,
}: TripWorkspaceProps & { cart: RouteCart; onAskPermit: (p: Permit) => void; onStateInfo: (code: string) => void }) {
  const router = useRouter()
  return (
    <div className="grid gap-4 pt-4">
      <Card>
        <CardContent className="pt-6">
          <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Load</h3>
          <dl className="mt-3 grid grid-cols-2 gap-3 text-sm">
            <Field label="Commodity" value={trip.commodity || '—'} />
            <Field label="Carrier" value={trip.carrier_name || '—'} />
          </dl>
          <DimensionsEditor
            title="Load Dimensions"
            tripId={trip.id}
            prefix="load"
            values={{
              length: trip.load_length_in,
              width: trip.load_width_in,
              height: trip.load_height_in,
              weight: trip.load_weight_lbs,
            }}
            note="The commodity itself, pulled from the rate con — correct it here if it was read wrong."
            canEdit={!!myRole && ['broker', 'dispatcher', 'admin'].includes(myRole)}
            onSaved={() => router.refresh()}
          />
          <DimensionsEditor
            title="Overall Dimensions"
            tripId={trip.id}
            prefix="overall"
            values={{
              length: trip.overall_length_in,
              width: trip.overall_width_in,
              height: trip.overall_height_in,
              weight: trip.overall_weight_lbs,
            }}
            note="Truck + trailer combined. Every permit is cross-checked against these dimensions — changes are recorded in the trip history."
            canEdit={!!myRole && ['broker', 'dispatcher', 'driver', 'admin'].includes(myRole)}
            onSaved={() => router.refresh()}
          />
          {trip.notes && (
            <p className="mt-3 rounded-lg bg-neutral-50 p-3 text-sm text-neutral-600">{trip.notes}</p>
          )}
        </CardContent>
      </Card>

      <Card>
        <CardContent className="pt-6">
          <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">
            Permits ({permits.length})
          </h3>
          {permits.length === 0 ? (
            <p className="mt-3 rounded-lg border border-dashed p-4 text-sm text-neutral-500">
              No permits yet. Upload them in the Documents tab
              {trip.permit_source === 'synchron' ? ' — a Synchron permit request is on file.' : '.'}
            </p>
          ) : (
            <div className="mt-3 space-y-2.5">
              {permits.map((p) => (
                <TripPermitCard
                  key={p.id}
                  p={p}
                  ctx={{ trip, myRole, requests, participants, signedUrls, warnings, routeCredits, cart }}
                  onAskPermit={onAskPermit}
                  onStateInfo={onStateInfo}
                />
              ))}
            </div>
          )}

          {requests.length > 0 && (
            <div className="mt-4 border-t pt-3">
              <h4 className="text-xs font-bold uppercase tracking-wide text-neutral-500">
                Requests to Synchron Permits
              </h4>
              <ul className="mt-2 space-y-1.5 text-sm">
                {requests.map((r) => (
                  <li key={r.id} className="flex items-center justify-between gap-2">
                    <span className="min-w-0">
                      {r.type === 'permit_request' ? 'Permit request' : 'Route request'}
                      {r.state_code ? ` · ${r.state_code}` : ''}
                      <span className="ml-1 text-xs text-neutral-500">by {r.requester_label}</span>
                      {/* Nash: "we do need a timestamp, a date and time" */}
                      <span className="block text-[11px] text-neutral-400">
                        {formatDateTime(r.created_at)}
                      </span>
                    </span>
                    <Badge variant="outline" className="capitalize">{r.status.replace('_', ' ')}</Badge>
                  </li>
                ))}
              </ul>
            </div>
          )}

          {/* Task 14: visible entry point at the BOTTOM of the Overview */}
          {myRole && (
            <div className="mt-4 border-t pt-4">
              <BuyPermitsDialog
                tripId={trip.id}
                participants={participants.filter(
                  (pt) => pt.status !== 'removed' && pt.user_id !== myUserId,
                )}
                onDone={() => router.refresh()}
              />
              <p className="mt-2 text-center text-[11px] text-neutral-400">
                Permit orders go to our trusted partner Synchron Permits.
              </p>
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  )
}

/**
 * A single permit card with manual correction: if the AI misread the permit,
 * the broker/carrier side edits the fields and the permit's warnings are
 * recomputed against the trip.
 */
/**
 * Shared assembly for the full-featured permit card — used by the Overview
 * AND the Docs tab (Nash: "the same powers in the Docs section"). Computes
 * check-dims, mismatch, route request + purchaser, file link, and demo route
 * type, then renders PermitCard.
 */
function TripPermitCard({
  p,
  ctx,
  onAskPermit,
  onStateInfo,
}: {
  p: Permit
  ctx: CardCtx
  onAskPermit: (p: Permit) => void
  onStateInfo: (code: string) => void
}) {
  const router = useRouter()
  const { trip, myRole, requests, participants, signedUrls, warnings, routeCredits, cart } = ctx
  // An expired permit that somebody already re-ordered (Task 75).
  const reorder = requests.find(
    (r) => r.type === 'permit_request' && r.replaces_permit_id === p.id && r.status !== 'cancelled',
  )
  // Demo-only: express/extended per state is normally assigned by the backend.
  const isDemoTrip = trip.ref_code === 'HH-48843549'
  // Permits are checked against OVERALL dims (fallback: load dims) — same rule
  // as the warnings engine; used to paint the offending value red on the card.
  const checkDims = {
    width: trip.overall_width_in ?? trip.load_width_in,
    height: trip.overall_height_in ?? trip.load_height_in,
    length: trip.overall_length_in ?? trip.load_length_in,
    weight: trip.overall_weight_lbs ?? trip.load_weight_lbs,
  }
  const routeRequest = requests.find(
    (r) => r.type === 'route_request' && r.state_code === p.state_code && r.status !== 'cancelled',
  )
  // "Who made it… which member of the group" (Task 70) — name AND role,
  // resolved from the participants list.
  const buyer = routeRequest ? resolveRequester(routeRequest, participants) : undefined
  return (
    <PermitCard
      permit={p}
      tripId={trip.id}
      // Synchron-processed trips include routes — no separate route purchase.
      canBuyRoute={!!myRole && trip.permit_policy !== 'synchron_required'}
      routeIncluded={trip.permit_policy === 'synchron_required'}
      routeRequest={routeRequest}
      purchasedBy={buyer}
      permitUrl={p.document_id ? signedUrls[p.document_id] : undefined}
      hasMismatch={warnings.some((w) => w.permit_id === p.id && w.kind === 'dimension_mismatch')}
      checkDims={checkDims}
      routeType={isDemoTrip ? DEMO_ROUTE_TYPES[p.state_code] : undefined}
      myRole={myRole}
      participants={participants}
      routeCredits={routeCredits}
      cart={cart}
      reorder={reorder}
      onAsk={() => onAskPermit(p)}
      onStateInfo={() => p.state_code && onStateInfo(p.state_code)}
      onSaved={() => router.refresh()}
    />
  )
}

/** Compact date range, timezone-safe for date-only strings: "Sep 1 – Sep 6, '26" */
function shortRange(effective: string | null, expiration: string | null): string {
  const fmt = (s: string) =>
    new Date(s + 'T12:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
  const yr = (s: string) => `’${s.slice(2, 4)}`
  if (effective && expiration) return `${fmt(effective)} – ${fmt(expiration)}, ${yr(expiration)}`
  if (expiration) return `Expires ${fmt(expiration)}, ${yr(expiration)}`
  if (effective) return `From ${fmt(effective)}, ${yr(effective)}`
  return 'Dates pending'
}

const ROUTE_STATUS_LABELS: Record<string, string> = {
  requested: 'Route requested',
  in_progress: 'Route in progress',
  fulfilled: '✓ Route ready',
}

function PermitCard({
  permit: p,
  tripId,
  canBuyRoute,
  routeIncluded = false,
  routeRequest,
  purchasedBy,
  permitUrl,
  hasMismatch = false,
  checkDims,
  routeType,
  myRole,
  participants,
  routeCredits,
  cart,
  reorder,
  onAsk,
  onStateInfo,
  onSaved,
}: {
  permit: Permit
  tripId: string
  canBuyRoute: boolean
  /** Synchron flow: the route comes WITH the permit — show a note, no buy button. */
  routeIncluded?: boolean
  routeRequest?: ServiceRequest
  /** Who ordered the route — Nash: "who made that request… which member of the group". */
  purchasedBy?: { name: string; role: string | null }
  permitUrl?: string
  hasMismatch?: boolean
  checkDims?: { width: number | null; height: number | null; length: number | null; weight: number | null }
  routeType?: 'express' | 'extended'
  myRole: TripRole | null
  participants: TripParticipant[]
  routeCredits: RouteCredits
  cart: RouteCart
  /** A permit request already re-ordering this expired permit (Task 75). */
  reorder?: ServiceRequest
  onAsk: () => void
  onStateInfo: () => void
  onSaved: () => void
}) {
  // Nash, 2026-09-13: "when the route is already purchased and the route is
  // ready… let's display by default… if I wanna hide, I can click on the
  // [chip] and it will hide them." Open on load; the chip toggles.
  const [showParts, setShowParts] = useState(true)
  // Expired: today (local time) is past the expiration date → reddish, not harsh.
  const isExpired = !!p.expiration_date && p.expiration_date < localToday()

  // Which permit value is smaller than what the trip needs (paints it red).
  const bad = (permitValue: number | null, needed: number | null | undefined) =>
    hasMismatch && permitValue != null && needed != null && permitValue < needed

  return (
    <div
      className={`group/permit rounded-lg border p-2.5 ${
        isExpired || hasMismatch ? 'border-red-200 bg-red-50/40' : 'bg-neutral-50'
      }`}
    >
      {/* Mobile QA 2026-09-13: wrap so the route-credit chip drops under the
          title on a phone instead of squeezing the state name to "Oh…". */}
      <div className="flex flex-wrap items-center justify-between gap-2">
        <p className="min-w-0 flex-1 basis-[12rem] truncate text-sm font-semibold">
          <button
            type="button"
            onClick={onStateInfo}
            className="rounded-sm decoration-neutral-300 underline-offset-2 hover:underline"
            title={`Learn more about ${stateName(p.state_code)} — travel info, escorts, limits`}
          >
            {stateName(p.state_code)}
          </button>
          {p.permit_number && (
            <span className="ml-1.5 font-mono text-[11px] text-neutral-400">{p.permit_number}</span>
          )}
          <span
            className={`ml-1.5 text-[11px] font-normal ${
              isExpired ? 'font-semibold text-red-600' : 'text-neutral-500'
            }`}
          >
            {shortRange(p.effective_date, p.expiration_date)}
            {isExpired && ' · Expired'}
          </span>
        </p>
        <span className="flex shrink-0 items-center gap-1.5">
          {routeRequest?.status === 'fulfilled' && hasAnyRouteFormat(routeRequest) ? (
            // Expands the row of delivered formats below — GPX, Hummer GPS and
            // the Google Maps part(s) (Task 66). The name + role of the buyer
            // sits on the chip itself (Task 70): "This is public."
            <button
              onClick={() => setShowParts((s) => !s)}
              className="max-w-[16rem] truncate rounded-full bg-green-50 px-2 py-0.5 text-[10px] font-bold text-green-700 ring-1 ring-inset ring-green-200 hover:bg-green-100"
              title={`${
                (routeRequest.route_links?.length ?? 0) > 1
                  ? `Google Maps in ${routeRequest.route_links!.length} parts`
                  : 'Show or hide the delivered route formats'
              }${purchasedBy ? ` — purchased by ${requesterText(purchasedBy)}` : ''}`}
            >
              ✓ Route ready
              {(routeRequest.route_links?.length ?? 0) > 1 ? ` · ${routeRequest.route_links!.length} parts` : ''}
              {purchasedBy ? ` · ${purchasedBy.name}${purchasedBy.role ? `, ${purchasedBy.role}` : ''}` : ''}
            </button>
          ) : routeRequest ? (
            <span
              className={`max-w-[16rem] truncate rounded-full px-2 py-0.5 text-[10px] font-bold ring-1 ring-inset ${
                routeRequest.status === 'fulfilled'
                  ? 'bg-green-50 text-green-700 ring-green-200'
                  : 'bg-blue-50 text-blue-700 ring-blue-200'
              }`}
              title={purchasedBy ? `Requested by ${requesterText(purchasedBy)}` : undefined}
            >
              {ROUTE_STATUS_LABELS[routeRequest.status] ?? routeRequest.status}
              {purchasedBy ? ` · by ${purchasedBy.name}${purchasedBy.role ? `, ${purchasedBy.role}` : ''}` : ''}
            </span>
          ) : routeIncluded && p.state_code ? (
            <span
              className="rounded-full bg-green-50 px-2 py-0.5 text-[10px] font-bold text-green-700 ring-1 ring-inset ring-green-200"
              title="Routes are included for permits processed by Synchron Permits — no separate purchase required. The route will be attached to this workspace."
            >
              Route included
            </span>
          ) : (
            canBuyRoute &&
            p.state_code && (
              // Prepaid credit → use it from the button; money due → cart (Task 69).
              <RoutePurchaseControl
                tripId={tripId}
                stateCode={p.state_code}
                routeType={routeType}
                credits={routeCredits}
                cart={cart}
                onOrdered={onSaved}
              />
            )
          )}
        </span>
      </div>
      <div className="mt-1.5 flex items-center gap-3 text-[11px] text-neutral-600">
        {(
          [
            ['W', p.permit_width_in, checkDims?.width, formatInches],
            ['H', p.permit_height_in, checkDims?.height, formatInches],
            ['L', p.permit_length_in, checkDims?.length, formatInches],
            ['GVW', p.permit_weight_lbs, checkDims?.weight, formatWeight],
          ] as const
        ).map(([label, value, needed, fmt]) => (
          <span key={label} className={bad(value, needed) ? 'font-bold text-red-600' : ''}>
            <span className={`font-bold ${bad(value, needed) ? 'text-red-400' : 'text-neutral-400'}`}>
              {label}{' '}
            </span>
            {fmt(value)}
            {bad(value, needed) && ' ⚠'}
          </span>
        ))}
        {p.extraction_status !== 'processed' && (
          <span className="ml-auto text-[10px] font-semibold text-amber-700">
            {{ pending: 'Reading…', failed: 'Read failed', skipped: 'Manual' }[p.extraction_status] ??
              p.extraction_status}
          </span>
        )}
      </div>
      {/* The delivered route in all three formats (Tasks 6, 66): GPX · Hummer
          GPS · Google Maps (one chip, or Part 1..N — each opens its own tab). */}
      {showParts && routeRequest?.status === 'fulfilled' && hasAnyRouteFormat(routeRequest) && (
        <div className="mt-1.5 flex flex-wrap items-center gap-1.5">
          <RouteFormatChips
            request={routeRequest}
            chipClassName="rounded-md bg-white px-2 py-0.5 text-[10px] font-bold text-info ring-1 ring-neutral-200 hover:ring-info"
          />
        </div>
      )}
      <div className="mt-1.5 flex items-center gap-3 text-[11px]">
        <button
          type="button"
          onClick={onAsk}
          className="font-semibold text-[#0f1b2d] hover:underline"
          title="Focus the Agent chat on this exact permit"
        >
          💬 Ask about this permit
        </button>
        {permitUrl ? (
          <a
            href={permitUrl}
            target="_blank"
            rel="noreferrer"
            className="font-semibold text-info hover:underline"
          >
            View permit
          </a>
        ) : (
          <button
            type="button"
            onClick={() => toast.info('No permit file is linked to this state yet.')}
            className="font-semibold text-neutral-400"
          >
            View permit
          </button>
        )}
        <button
          type="button"
          onClick={() =>
            toast.info('Provisions come from Synchron Permits — available once the connection is live.')
          }
          className="font-semibold text-neutral-400 hover:text-neutral-600"
          title="Provisions arrive with the Synchron connection"
        >
          View provisions
        </button>
        {/* Nash, 2026-09-12 (broker trip view, section 3 · Docs): "he sees
            view permit, view provisions, but he needs to also have the button
            to view state notes." Same panel the state name opens; the same
            three links the driver screens have. */}
        {p.state_code && (
          <button
            type="button"
            onClick={onStateInfo}
            className="font-semibold text-info hover:underline"
            title={`Internal state notes for ${stateName(p.state_code)}`}
          >
            View state notes
          </button>
        )}
        {/* Expired → re-order from Synchron, any member (Task 75). */}
        {isExpired && p.state_code && (
          <span className="ml-auto">
            <RequestNewPermitButton
              tripId={tripId}
              permit={p}
              myRole={myRole}
              existing={reorder ?? null}
              participants={participants}
              onDone={onSaved}
            />
          </span>
        )}
      </div>
    </div>
  )
}

function Field({ label, value }: { label: string; value: string }) {
  return (
    <div className="rounded-lg bg-neutral-50 p-2.5">
      <dt className="text-[10px] font-bold uppercase tracking-wide text-neutral-400">{label}</dt>
      <dd className="mt-0.5 font-medium">{value}</dd>
    </div>
  )
}

/**
 * "Buy more permits" (Task 14): order a NEW permit for a state from inside the
 * trip. Supporting documents (route surveys etc.) can be attached, and the
 * requester chooses which OTHER participants get informed — or keeps it
 * private. The email itself (recipient: Synchron Permits, CC: the chosen
 * people, trip data attached) is sent by the backend later; everything is
 * recorded now.
 */
function BuyPermitsDialog({
  tripId,
  participants,
  onDone,
  triggerLabel = 'Buy more permits',
}: {
  tripId: string
  participants: TripParticipant[]
  onDone: () => void
  /** Task 84: Section 2's zero-permit Synchron view opens the same dialog
      under the wording Nash chose ("our existing order permit from trusted partner"). */
  triggerLabel?: string
}) {
  const [open, setOpen] = useState(false)
  const [pending, setPending] = useState(false)
  const [files, setFiles] = useState<File[]>([])
  const [notify, setNotify] = useState<Set<string>>(new Set())
  // Nash: pick states "as you have it on hashtags" — type, suggest, add a chip.
  const [pickedStates, setPickedStates] = useState<string[]>([])
  const [stateQuery, setStateQuery] = useState('')

  const suggestions =
    stateQuery.trim() === ''
      ? []
      : Object.entries(STATE_NAMES)
          .filter(
            ([code, name]) =>
              !pickedStates.includes(code) &&
              (name.toLowerCase().startsWith(stateQuery.trim().toLowerCase()) ||
                code.toLowerCase().startsWith(stateQuery.trim().toLowerCase())),
          )
          .slice(0, 6)

  function pickState(code: string) {
    setPickedStates((prev) => (prev.includes(code) ? prev : [...prev, code]))
    setStateQuery('')
  }

  async function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    if (pickedStates.length === 0) {
      toast.error('Add at least one state — start typing and pick from the list.')
      return
    }
    setPending(true)
    try {
      const fd = new FormData(e.currentTarget)

      // Supporting documents land in the trip's Docs first.
      const uploadedNames: string[] = []
      for (const file of files) {
        const upload = new FormData()
        upload.set('file', file)
        upload.set('kind', 'other')
        const res = await fetch(`/api/trips/${tripId}/documents`, { method: 'POST', body: upload })
        if (res.ok) uploadedNames.push(file.name)
        else toast.error(`Could not upload ${file.name}`)
      }

      const notes = [String(fd.get('notes') ?? '').trim(), uploadedNames.length > 0 ? `Supporting docs: ${uploadedNames.join(', ')}` : '']
        .filter(Boolean)
        .join('\n')

      // One request per state — each state tracks its own status in the
      // "Requests to Synchron Permits" history.
      let failed = 0
      for (const state of pickedStates) {
        const res = await fetch(`/api/trips/${tripId}/requests`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            type: 'permit_request',
            state_code: state,
            notes,
            notify: [...notify],
          }),
        })
        if (!res.ok) {
          failed++
          const json = await res.json().catch(() => ({}))
          toast.error(json.error ?? `Request for ${stateName(state)} failed`)
        }
      }
      if (failed < pickedStates.length) {
        toast.success(
          `Permit order${pickedStates.length - failed === 1 ? '' : 's'} for ${pickedStates
            .map((s) => stateName(s))
            .join(', ')} sent to our trusted partner Synchron Permits`,
        )
        setOpen(false)
        setFiles([])
        setNotify(new Set())
        setPickedStates([])
        setStateQuery('')
        onDone()
      }
    } finally {
      setPending(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button className="w-full bg-[#0f1b2d] font-bold hover:bg-[#1c2f4a]">
          {triggerLabel}
        </Button>
      </DialogTrigger>
      <DialogContent className="max-h-[85vh] overflow-y-auto">
        <DialogHeader>
          <DialogTitle>Order permits from our trusted partner Synchron Permits</DialogTitle>
        </DialogHeader>
        <form onSubmit={submit} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="state_query">
              States <span className="text-red-600">*</span>
            </Label>
            {pickedStates.length > 0 && (
              <div className="flex flex-wrap gap-1.5">
                {pickedStates.map((code) => (
                  <span
                    key={code}
                    className="flex items-center gap-1 rounded-full bg-[#0f1b2d] px-2.5 py-1 text-xs font-semibold text-white"
                  >
                    {stateName(code)}
                    <button
                      type="button"
                      onClick={() => setPickedStates((prev) => prev.filter((c) => c !== code))}
                      className="ml-0.5 rounded-full px-0.5 leading-none hover:text-[#f5a623]"
                      title={`Remove ${stateName(code)}`}
                    >
                      ×
                    </button>
                  </span>
                ))}
              </div>
            )}
            <div className="relative">
              <Input
                id="state_query"
                value={stateQuery}
                onChange={(e) => setStateQuery(e.target.value)}
                onKeyDown={(e) => {
                  // Tab or Enter fills the top suggestion and readies the next
                  // one — "he can click tab on the keyboard and it fulfills
                  // that name and gives me the next one."
                  if ((e.key === 'Tab' || e.key === 'Enter') && suggestions.length > 0) {
                    e.preventDefault()
                    pickState(suggestions[0][0])
                  }
                }}
                placeholder="Start typing a state — e.g. Ari… or AZ"
                autoComplete="off"
              />
              {suggestions.length > 0 && (
                <div className="absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-lg border bg-white shadow-lg">
                  {suggestions.map(([code, name], i) => (
                    <button
                      key={code}
                      type="button"
                      onClick={() => pickState(code)}
                      className={`block w-full px-3 py-2 text-left text-sm hover:bg-neutral-50 ${
                        i === 0 ? 'bg-neutral-50 font-semibold' : ''
                      }`}
                    >
                      {name} <span className="text-xs text-neutral-400">{code}</span>
                    </button>
                  ))}
                </div>
              )}
            </div>
            <p className="text-[11px] text-neutral-400">
              Type and pick from the list (or press Tab) — add as many states as you need.
            </p>
          </div>
          <div className="space-y-2">
            <Label htmlFor="req_notes">Special comments</Label>
            <Textarea id="req_notes" name="notes" rows={3} placeholder="Anything we should know?" />
          </div>
          <div className="space-y-2">
            <Label htmlFor="req_files">Supporting documents (optional)</Label>
            <Input
              id="req_files"
              type="file"
              multiple
              accept=".pdf,.jpg,.jpeg,.png,.docx"
              onChange={(e) => setFiles([...(e.target.files ?? [])])}
            />
            {files.length > 0 && (
              <p className="text-xs text-neutral-500">
                {files.map((f) => f.name).join(' · ')}
              </p>
            )}
            <p className="text-[11px] text-neutral-400">
              Route surveys or other files the permit team needs — they attach to this trip.
            </p>
          </div>
          {participants.length > 0 && (
            <div className="space-y-2">
              <Label>Who should be informed?</Label>
              <div className="space-y-1.5">
                {participants.map((pt) => (
                  <label key={pt.id} className="flex cursor-pointer items-center gap-2 text-sm">
                    <input
                      type="checkbox"
                      checked={notify.has(pt.id)}
                      onChange={(e) =>
                        setNotify((prev) => {
                          const next = new Set(prev)
                          if (e.target.checked) next.add(pt.id)
                          else next.delete(pt.id)
                          return next
                        })
                      }
                      className="h-4 w-4 accent-[#0f1b2d]"
                    />
                    <span>
                      {pt.name || pt.email}
                      <span className="ml-1 text-xs capitalize text-neutral-500">· {pt.role}</span>
                    </span>
                  </label>
                ))}
              </div>
              <p className="text-[11px] text-neutral-400">
                Checked people are informed about this order. Leave everyone unchecked to keep it
                between you and Synchron Permits.
              </p>
            </div>
          )}
          <p className="text-xs text-neutral-500">
            The order goes to our trusted partner Synchron Permits with this trip&apos;s details,
            and its status shows on the trip. Payment for permit processing is collected directly
            by Synchron Permits.
          </p>
          <Button type="submit" className="w-full" disabled={pending}>
            {pending ? 'Sending…' : 'Send permit order'}
          </Button>
        </form>
      </DialogContent>
    </Dialog>
  )
}

/* ---------------- Documents ---------------- */

function DocumentsTab({
  trip,
  documents,
  signedUrls,
  myRole,
  permits,
  requests,
  participants,
  warnings,
  routeCredits,
  cart,
  onAskPermit,
  onStateInfo,
}: TripWorkspaceProps & { cart: RouteCart; onAskPermit: (p: Permit) => void; onStateInfo: (code: string) => void }) {
  const router = useRouter()
  const fileRef = useRef<HTMLInputElement>(null)
  const [kind, setKind] = useState(
    trip.permit_policy === 'synchron_required' && myRole !== 'admin' ? 'other' : 'permit',
  )
  // Nash: "you have to have the ability to upload multiple files" — progress
  // text while a batch runs, null when idle.
  const [uploading, setUploading] = useState<string | null>(null)

  async function upload(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const files = [...(fileRef.current?.files ?? [])]
    if (files.length === 0) return
    try {
      let ok = 0
      let lastNote: string | null = null
      for (let i = 0; i < files.length; i++) {
        setUploading(files.length > 1 ? `Uploading ${i + 1} of ${files.length}…` : 'Uploading…')
        const fd = new FormData()
        fd.set('file', files[i])
        fd.set('kind', kind)
        const res = await fetch(`/api/trips/${trip.id}/documents`, { method: 'POST', body: fd })
        const json = await res.json()
        if (!res.ok) toast.error(`${files[i].name}: ${json.error ?? 'upload failed'}`)
        else {
          ok++
          if (json.note) lastNote = json.note
        }
      }
      if (ok > 0) {
        toast.success(ok === 1 ? (lastNote ?? 'Document uploaded') : `${ok} documents uploaded`)
        if (fileRef.current) fileRef.current.value = ''
        router.refresh()
      }
    } finally {
      setUploading(null)
    }
  }

  // Task 13: users upload ONLY rate cons, permits, and supporting docs.
  // Provisions come from Synchron; routes are never uploaded.
  // Synchron flow lockdown (order-intake doc §6/§10): broker and carrier
  // cannot upload permits manually — Synchron uploads them through API.
  const permitUploadLocked = trip.permit_policy === 'synchron_required' && myRole !== 'admin'
  const uploadKinds: Record<string, string> = {
    rate_confirmation: 'Rate confirmation',
    ...(permitUploadLocked ? {} : { permit: 'Permit' }),
    other: 'Supporting doc',
  }
  const kindLabels: Record<string, string> = {
    ...uploadKinds,
    provision: 'Provisions',
    route: 'Route',
  }
  const groupOrder = ['rate_confirmation', 'permit', 'other'] as const
  // Permit files that no permit record points at yet (still extracting) keep
  // a plain row so an upload never looks lost. Every permit RECORD gets the
  // full card below — see the Permits group (2026-09-07, Task 64).
  const linkedDocIds = new Set(permits.map((pm) => pm.document_id).filter(Boolean))
  const unlinkedPermitDocs = documents.filter((d) => d.kind === 'permit' && !linkedDocIds.has(d.id))
  const grouped = groupOrder
    .map((k) => ({
      kind: k,
      label: k === 'other' ? 'Supporting docs' : kindLabels[k] + (k === 'permit' ? 's' : ''),
      docs:
        k === 'permit'
          ? unlinkedPermitDocs
          : documents.filter((d) => (k === 'other' ? !['rate_confirmation', 'permit'].includes(d.kind) : d.kind === k)),
      // Nash: "the amount of permits we have on overview should be equal to
      // the amount of permits that we see in the section docs." One Synchron
      // order PDF carries many state permits, so the Docs tab must list one
      // card per PERMIT, not one per file.
      count: k === 'permit' ? permits.length + unlinkedPermitDocs.length : 0,
    }))
    .filter((g) => g.docs.length > 0 || g.count > 0)

  return (
    <div className="space-y-4 pt-4">
      {myRole && myRole !== 'shipper' && (
        <Card>
          <CardContent className="pt-6">
            <form onSubmit={upload} className="flex flex-wrap items-end gap-3">
              <div className="min-w-40 space-y-2">
                <Label>Document type</Label>
                <select
                  value={kind}
                  onChange={(e) => setKind(e.target.value)}
                  className="w-full rounded-lg border px-3 py-2 text-sm"
                >
                  {Object.entries(uploadKinds).map(([v, l]) => (
                    <option key={v} value={v}>{l}</option>
                  ))}
                </select>
              </div>
              <div className="min-w-56 flex-1 space-y-2">
                <Label>{kind === 'rate_confirmation' ? 'File' : 'Files'}</Label>
                {/* Multiple files for permits & supporting docs (Nash: "he
                    wants to upload 8 permits"); one rate con per trip. */}
                <Input
                  ref={fileRef}
                  type="file"
                  required
                  multiple={kind !== 'rate_confirmation'}
                  accept=".pdf,.jpg,.jpeg,.png,.docx"
                />
              </div>
              <Button type="submit" disabled={!!uploading}>
                {uploading ?? 'Upload'}
              </Button>
            </form>
            {kind === 'permit' && (
              <p className="mt-2 text-xs text-neutral-500">
                Permit uploads are read automatically — dimensions, dates, and warnings appear on
                the Overview tab. The first processed permit activates the trip.
              </p>
            )}
            {permitUploadLocked && (
              <p className="mt-2 text-xs text-neutral-500">
                Permits for this trip are processed and uploaded by Synchron Permits — manual
                permit upload is disabled.
              </p>
            )}
          </CardContent>
        </Card>
      )}

      {documents.length === 0 && permits.length === 0 ? (
        <div className="rounded-xl border border-dashed p-10 text-center text-sm text-neutral-500">
          No documents yet.
        </div>
      ) : (
        <div className="space-y-4">
          {grouped.map((g) => (
            <div key={g.kind}>
              <h4 className="mb-2 text-xs font-bold uppercase tracking-wide text-neutral-500">
                {g.label} ({g.kind === 'permit' ? g.count : g.docs.length})
              </h4>
              <div className="space-y-2">
                {/* Nash: "the same powers in the Docs section" — every permit
                    record renders the SAME card as the Overview (dims, alerts,
                    route, ask), and the count matches the Overview even when
                    several permits share one uploaded file. */}
                {g.kind === 'permit' &&
                  permits.map((pm) => (
                    <TripPermitCard
                      key={pm.id}
                      p={pm}
                      ctx={{ trip, myRole, requests, participants, signedUrls, warnings, routeCredits, cart }}
                      onAskPermit={onAskPermit}
                      onStateInfo={onStateInfo}
                    />
                  ))}
                {g.docs.map((d) => {
                  return (
                    <div key={d.id} className="flex items-center justify-between rounded-xl border bg-white p-3">
                      <div className="min-w-0">
                        <p className="truncate text-sm font-semibold">{d.file_name}</p>
                        <p className="text-xs text-neutral-500">
                          {d.uploader_label} · {formatDateTime(d.created_at)}
                        </p>
                      </div>
                      {signedUrls[d.id] && (
                        <a
                          href={signedUrls[d.id]}
                          target="_blank"
                          rel="noreferrer"
                          className="shrink-0 text-sm font-semibold text-blue-700 hover:underline"
                        >
                          View
                        </a>
                      )}
                    </div>
                  )
                })}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

/* ---------------- People ---------------- */

function PeopleTab({ trip, participants, myRole, contacts, permits }: TripWorkspaceProps) {
  const router = useRouter()
  const invitable = myRole ? INVITE_RULES[myRole] : []

  return (
    <div className="space-y-4 pt-4">
      {invitable.length > 0 && (
        <InviteForm
          tripId={trip.id}
          roles={invitable}
          contacts={contacts}
          permits={permits}
          alreadyOn={participants.map((p) => p.email)}
          onDone={() => router.refresh()}
        />
      )}
      <div className="space-y-2">
        {participants.map((p) => (
          <div key={p.id} className="flex items-center justify-between rounded-xl border bg-white p-3">
            <div className="flex items-center gap-3">
              <span className="grid h-9 w-9 place-items-center rounded-full bg-neutral-100 text-xs font-bold">
                {(p.name || p.email).slice(0, 2).toUpperCase()}
              </span>
              <div>
                <p className="text-sm font-semibold">{p.name || p.email}</p>
                <p className="text-xs text-neutral-500">{p.email}</p>
              </div>
            </div>
            <div className="flex flex-col items-end gap-1">
              <Badge variant="secondary" className="capitalize">{p.role}</Badge>
              <span className={`text-[11px] font-semibold ${p.status === 'active' ? 'text-green-700' : 'text-amber-700'}`}>
                {p.status === 'active' ? '● Joined' : '○ Invited'}
              </span>
            </div>
          </div>
        ))}
      </div>
      <p className="text-xs leading-relaxed text-neutral-500">
        Access is per trip: each person here has a role on this trip only. Roles decide who can
        invite whom and what they can manage.
      </p>
    </div>
  )
}

function InviteForm({
  tripId, roles, onDone, contacts, permits = [], alreadyOn = [],
}: {
  tripId: string
  roles: TripRole[]
  onDone: () => void
  contacts?: { drivers: CarrierContact[]; brokers: CarrierContact[] }
  /** States and permits the pilot access chooser can offer (2026-09-12). */
  permits?: Permit[]
  alreadyOn?: string[]
}) {
  const [pending, setPending] = useState(false)
  const [inviteUrl, setInviteUrl] = useState<string | null>(null)
  const formRef = useRef<HTMLFormElement>(null)
  // Which role is selected — the pilot access chooser appears for `pilot`
  // (Nash, 2026-09-12: "as he does that we need to ask what type of access
  // that pilot will have"). The select stays a named field for FormData.
  const [role, setRole] = useState<TripRole>(roles[0])
  const [pilotAccess, setPilotAccess] = useState<PilotInviteAccess>({ type: 'decide_later' })

  /**
   * Fill the form from a saved contact (Task 99). The inputs are uncontrolled
   * (the submit reads FormData), so the values are set on the elements —
   * everything stays editable and the existing validated submit is unchanged.
   */
  function fillFrom(c: { name: string; email: string; phone: string; phone_ext: string; role: string }) {
    const form = formRef.current
    if (!form) return
    const setField = (field: string, value: string) => {
      const el = form.elements.namedItem(field) as HTMLInputElement | HTMLSelectElement | null
      if (el) el.value = value
    }
    setField('name', c.name)
    setField('email', c.email)
    setField('phone', c.phone)
    setField('phone_ext', c.phone_ext)
    // Only adopt the contact's role when this inviter may use it.
    if ((roles as string[]).includes(c.role)) {
      setField('role', c.role)
      setRole(c.role as TripRole)
    }
  }

  async function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setPending(true)
    setInviteUrl(null)
    try {
      const fd = new FormData(e.currentTarget)
      const res = await fetch(`/api/trips/${tripId}/invite`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email: fd.get('email'),
          name: fd.get('name'),
          phone: fd.get('phone'),
          phone_ext: fd.get('phone_ext'),
          role: fd.get('role'),
          pilot_access: fd.get('role') === 'pilot' ? pilotAccess : undefined,
        }),
      })
      const json = await res.json()
      if (!res.ok) toast.error(json.error ?? 'Invitation failed')
      else {
        toast.success('Invitation created')
        setInviteUrl(json.invite_url)
        formRef.current?.reset()
        setRole(roles[0])
        setPilotAccess({ type: 'decide_later' })
        onDone()
      }
    } finally {
      setPending(false)
    }
  }

  return (
    <Card>
      <CardContent className="pt-6">
        <h3 className="text-sm font-bold uppercase tracking-wide text-neutral-500">Invite someone</h3>
        {/* Nash: "I should have a way to choose from the previous contacts."
            Renders nothing when none are saved. */}
        {contacts && (
          <ContactPicker
            drivers={contacts.drivers}
            brokers={contacts.brokers}
            alreadyAdded={alreadyOn}
            onPick={fillFrom}
            className="mt-3"
          />
        )}
        <form ref={formRef} onSubmit={submit} className="mt-3 grid gap-3 sm:grid-cols-2">
          <div className="space-y-1.5">
            <Label htmlFor="inv_name">Name</Label>
            <Input id="inv_name" name="name" required />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="inv_email">Email</Label>
            <Input id="inv_email" name="email" type="email" required />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="inv_phone">
              Phone <span className="text-red-600">*</span>
            </Label>
            {/* Phone required (it finds existing users); extension optional.
                Both accept formatted text like "555-123-4567" / "x204". */}
            <div className="flex gap-2">
              <Input id="inv_phone" name="phone" type="tel" required className="flex-1" />
              <Input
                id="inv_phone_ext"
                name="phone_ext"
                placeholder="Ext."
                title="Extension (optional)"
                className="w-20"
              />
            </div>
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="inv_role">Role on this trip</Label>
            <select
              id="inv_role"
              name="role"
              value={role}
              onChange={(e) => setRole(e.target.value as TripRole)}
              className="w-full rounded-lg border px-3 py-2 text-sm"
            >
              {roles.map((r) => (
                <option key={r} value={r} className="capitalize">{r}</option>
              ))}
            </select>
          </div>
          {role === 'pilot' && (
            <PilotAccessChooser permits={permits} value={pilotAccess} onChange={setPilotAccess} />
          )}
          <Button type="submit" className="sm:col-span-2" disabled={pending}>
            {pending ? 'Inviting…' : 'Send invitation'}
          </Button>
        </form>
        {inviteUrl && (
          <div className="mt-3 rounded-lg bg-green-50 p-3 text-xs text-green-800">
            Invitation link (also usable directly):{' '}
            <button
              className="font-mono font-semibold underline"
              onClick={() => {
                navigator.clipboard.writeText(inviteUrl)
                toast.success('Link copied')
              }}
            >
              copy link
            </button>
          </div>
        )}
      </CardContent>
    </Card>
  )
}

/* ---------------- My Pilot (carrier / broker view) ---------------- */

/**
 * Real participants + the access recorded on each pilot's invitation (trip
 * History, `participant_invited` → `pilot_access`). Invoices come from the
 * pilot replica store for the design review (admin) — Nash: "we can actually
 * go ahead and show it how it looks" — and are an empty state for customers
 * until the invoicing backend exists.
 */
function MyPilotWorkspaceTab({ pilots, participants, events, myRole, myUserId, myAccountRole, myAccountCreatedAt, trip }: TripWorkspaceProps & { pilots: Array<TripParticipant | DemoPilotContact> }) {
  const { invoices } = useInvoices()
  const { expenses } = useExpenses()
  const paperworkAccess = useViewerPaperworkAccess(myAccountCreatedAt)
  const viewer: 'carrier' | 'broker' = myRole === 'broker' ? 'broker' : 'carrier'
  const accessFor = (email: string): string => {
    const ev = [...events]
      .reverse()
      .find((e) => e.action === 'participant_invited' && e.detail?.email === email && typeof e.detail?.pilot_access === 'string')
    return (ev?.detail?.pilot_access as string | undefined) ?? 'Decide later'
  }
  const nameOf = (id: string | null) => participants.find((p) => p.user_id === id)?.name ?? null
  const entries: MyPilotEntry[] = pilots.map((p) => {
    const demo = 'demo' in p && p.demo
    const access = demo ? DEMO_PILOT_ACCESS : accessFor(p.email)
    const inviter = participants.find((x) => x.user_id === p.invited_by)
    return {
      name: p.name || p.email,
      kind: demo ? (p as DemoPilotContact).kind : 'Pilot driver',
      phone: p.phone ?? '—',
      email: p.email,
      access,
      states: access.startsWith('States: ') ? access.slice(8).split(', ') : [],
      status: p.status === 'active' ? 'Joined' : 'Invited',
      invitedBy: demo ? 'Demo pilot car (design preview)' : inviter ? `${inviter.name || inviter.email} (${inviter.role})` : nameOf(p.invited_by) ?? undefined,
      // Broker: contact details only for a pilot the broker invited (Nash, 2026-09-13).
      contactHidden: viewer === 'broker' && (demo || p.invited_by !== myUserId),
      // Paperwork (Nash, 2026-09-13): the demo pair carries demo documents; a
      // real pilot participant has none on file until pilot accounts exist.
      documents: demo ? demoPilotPaperwork((p as DemoPilotContact).kind) : undefined,
    }
  })
  // Broker: only when the broker invited the pilot, or the pilot invited the broker.
  const me = participants.find((p) => p.user_id === myUserId)
  const brokerAllowed = brokerCanSeePilotInvoices({
    pilotInvitedByBroker: pilots.some((p) => p.invited_by === myUserId),
    brokerInvitedByPilot: !!me && pilots.some((p) => p.user_id && p.user_id === me.invited_by),
  })
  const showInvoices = viewer === 'carrier' || brokerAllowed
  const isInternal = myAccountRole === 'admin'
  return (
    <>
      {isInternal && <div className="pt-4"><PaperworkPreviewSwitch signedUpAt={myAccountCreatedAt} /></div>}
      <MyPilotTab
        viewer={viewer}
        pilots={entries}
        invoices={isInternal ? invoices : []}
        showInvoices={showInvoices}
        invoicesNote={isInternal ? 'Design preview — invoices from the pilot replica store are shown here for review; real pilot invoices arrive with the invoicing backend.' : undefined}
        onViewInvoice={async (inv) => openPdf(await buildInvoicePdf(inv, expenses), `${inv.invoiceNumber}.pdf`, 'view')}
        paperwork={paperworkAccess.access}
        paperworkPlan={paperworkAccess.plan}
        onViewDocument={async (entry, d) =>
          openPdf(
            await buildPaperworkPdf({ label: d.label, owner: entry.name, ownerKind: entry.kind, status: d.status, expirationDate: d.expirationDate, uploadedAt: d.uploadedAt, tripRef: trip.ref_code }),
            `${entry.name.replace(/\s+/g, '-')}-${d.key}.pdf`,
            'view',
          )}
      />
    </>
  )
}

/* ---------------- Chat (Section 2, center panel) ---------------- */

/* ChatPanel moved to @/components/app/chat-panel (2026-09-07) so the driver
   experience can render the same agent chat inline on mobile. */

/* ---------------- History ---------------- */

function HistoryTab({ events }: { events: TripEvent[] }) {
  const labels: Record<string, string> = {
    trip_created_via_intake: 'Trip created via broker intake',
    trip_created: 'Trip created',
    dispatcher_invited: 'Dispatcher invited',
    participant_invited: 'Participant invited',
    invitation_accepted: 'Invitation accepted',
    document_uploaded: 'Document uploaded',
    status_changed: 'Status changed',
    service_requested: 'Service requested',
    dimensions_updated: 'Overall dimensions updated',
    unit_updated: 'Unit profile updated',
    payment_responsibility_selected: 'Payment responsibility selected',
    permit_handling_changed: 'Permit handling mode changed',
    payment_responsibility_changed: 'Payment responsibility changed',
    permit_updated: 'Permit corrected manually',
    warning_dismissed: 'Warning accepted & dismissed',
  }
  return (
    <div className="space-y-2 pt-4">
      {events.length === 0 && (
        <p className="rounded-xl border border-dashed p-8 text-center text-sm text-neutral-500">
          No activity recorded yet.
        </p>
      )}
      {events.map((e) => (
        <div key={e.id} className="flex items-start justify-between rounded-xl border bg-white p-3 text-sm">
          <div>
            <p className="font-semibold">{labels[e.action] ?? e.action}</p>
            <p className="text-xs text-neutral-500">
              {e.actor_label}
              {e.detail ? ` · ${summarize(e.detail)}` : ''}
            </p>
          </div>
          <span className="shrink-0 text-xs text-neutral-400">{formatDateTime(e.created_at)}</span>
        </div>
      ))}
    </div>
  )
}

function summarize(detail: Record<string, unknown>): string {
  return Object.entries(detail)
    .filter(([k]) => !['source_ip'].includes(k))
    .map(([k, v]) => `${k.replace(/_/g, ' ')}: ${String(v)}`)
    .join(' · ')
}
