'use client'

import { useEffect, useMemo, useRef, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { permitRouteRequest, permitRouteLabel, routeIncludedWithPermit, includedRouteStatusLabel } from '@/lib/domain/permit-routes'
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, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
} 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 { BuyPermitsDialog } from '@/components/app/buy-permits-dialog'
import { PermitFeeLine, PermitFeeSummary } from '@/components/app/permit-cost'
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, isDemoPilotState, type DemoPilotContact } from '@/lib/demo/pilot-cars'
import { canManagePilotAccess, canViewPilotPaperwork, paperworkRequestsFromEvents, parsePilotAccessLine, 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 { RoutePurchaseHistory } from '@/components/app/route-purchase-history'
import { RequestNewPermitButton } from '@/components/app/permit-reorder'
import {
  hasAnyRouteFormat, requesterText, resolveRequester, RouteFormatChips,
} from '@/components/app/route-formats'
import { INVITE_RULES, canSeeParticipantPhone } from '@/lib/domain/permissions'
import { canRemoveParticipant } from '@/lib/domain/participants'
import { provenanceLine } from '@/lib/domain/auto-participants'
import { stateName } from '@/lib/domain/states'
import { ackWarning, getAckedWarnings } from '@/lib/ack'
import { useMediaQuery, XL_QUERY } from '@/lib/use-media-query'
import { StateInfoDialog, StateInfoView } from '@/components/app/state-info-view'
import { DEMO_ROUTE_TYPES } from '@/lib/demo/route-types'
import {
  canManageStatus, canTransition, resolveStatusForUser,
  shouldPromptCompletion, type TripCompletion,
} from '@/lib/domain/status'
import { cityState, formatDateTime, formatFullDate, formatInches, formatWeight, localToday } from '@/lib/format'
import type {
  CarrierContact, ChatMessage, Permit, ServiceRequest, Trip, TripDocument, TripEvent,
  TripParticipant, TripRole, TripWarning,
} from '@/types/db'
import type { InviteContact } from '@/lib/domain/contacts'
import { canAssignCarrier, carrierIdLine, type CarrierResolution, type CarrierSummary } from '@/lib/domain/carrier-assignment'

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
  /**
   * Back from Synchron's card page (2026-09-29, D4): `?draft=` reopens the
   * Buy More Permits dialog on its saved draft, `?setup=` is the card session
   * that finishes the order. Carrier page only.
   */
  resumeDraftId?: string
  resumeSetupRef?: string
  participants: Array<TripParticipant & { triggering_user_name?: string | null; triggering_company_name?: string | null }>
  /**
   * The viewer's active mode (decision D17, 2026-09-30): a Company Admin
   * acting in the company's mode may remove a participant that company's
   * Auto-Participant rule added. Omitted = no company powers.
   */
  viewerContext?: { activeCompanyId: string | null; activeIsCompanyAdmin: boolean }
  /** The carrier performing this trip — name, MC, DOT — and how it was resolved (2026-09-19). */
  carrier?: { resolution: CarrierResolution; adminOptions: CarrierSummary[] }
  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[]
    /** Everyone this person may invite from — saved + shared a trip (2026-09-18). */
    invite?: InviteContact[]
  }
}

/** What the permit cards need beyond TripWorkspaceProps: the per-user, per-trip cart. */
type CardCtx = Pick<TripWorkspaceProps, 'trip' | 'myRole' | 'requests' | 'participants' | 'signedUrls' | 'warnings' | 'routeCredits' | 'pageContext'> & {
  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) => isDemoPilotState(pm.state_code, trip.source_system))
  // 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).
  // Server-side, one cart per user per trip; Carrier Dispatch only this phase (D6).
  const cart = useRouteCart(trip.id, props.myUserId, { enabled: props.pageContext === 'carrier' })
  // 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 ===== */}
      {/* State information covers this column at its full width (Nash,
          2026-09-25: "it's going to overlap on top of the agent chat section…
          it's just going to be as wide as section two"), so nothing else on the
          page gives up room. It used to slide in beside the chat and take 340px
          from it, which left both too narrow to read (Task 65). */}
      <main className={`${mobileView === 'info' ? 'hidden' : 'flex'} relative min-w-0 flex-1 flex-col xl:flex`}>
        <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}
                  tripRef={trip.ref_code}
                  pageContext={props.pageContext}
                  documents={props.documents}
                  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)}
          />
        )}

        {/* The chat keeps its state underneath and comes back on close. */}
        {stateInfoCode && isXl && (
          <StateInfoPanel code={stateInfoCode} onClose={() => setStateInfoCode(null)} className="absolute inset-0 z-20 flex" />
        )}
      </main>

      {/* Below xl the same content is a dialog — never mounted alongside the
          desktop overlay (Task 83). */}
      {!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).
          '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>

        {/* No permit handling chosen yet (Nash, 2026-09-19): the broker's
            New Trip page creates the trip from the rate confirmation alone;
            the choice is made here, whenever they are ready. */}
        {!trip.permit_policy && myRole === 'broker' && !['completed', 'cancelled'].includes(trip.status) && (
          <div className="border-b bg-neutral-50 p-4">
            <p className="text-sm font-bold text-neutral-800">Permit handling not chosen yet</p>
            <p className="mt-0.5 text-xs leading-relaxed text-neutral-600">
              Decide whether the carrier uploads existing permits or you request them from Synchron Permits. Optional — you can also just keep this trip for your own records.
            </p>
            <PermitHandlingChange trip={trip} onDone={() => router.refresh()} firstChoice />
          </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>
                {trip.synchron_order_token && (
                  <p className="mt-1 text-xs text-blue-800">Synchron order #{trip.synchron_order_token}</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. */}
        {props.pageContext === 'carrier' && (
          <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, firstChoice = false }: { trip: Trip; onDone: () => void; /** The broker's first choice on a trip created without one — no reason needed. */ firstChoice?: boolean }) {
  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 (!firstChoice && 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={firstChoice
          ? 'rounded-lg bg-[#0f1b2d] px-3 py-1.5 text-xs font-bold text-white hover:bg-[#1c2d47]'
          : 'text-[11px] font-bold text-neutral-500 underline-offset-2 hover:underline'}
      >
        {firstChoice ? 'Choose permit handling' : 'Change permit handling (admin)'}
      </button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>{firstChoice ? 'How should permits be handled for this trip?' : '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>
            )}
            {!firstChoice && (
              <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'
            }`}
          >
            {/* Nash, 2026-09-24: section 1 shows "City, ST → City, ST", never the
                full street address — "it's too much… not pleasant to watch". The
                full address stays on the trip itself, one click away. Search
                below still matches the full address. */}
            <p className="truncate text-sm font-bold">
              {cityState(t.origin) || 'Origin TBD'} → {cityState(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, redesigned 2026-09-25) ---------------- */

/**
 * State information over the chat column, at that column's full width. The
 * topics, the structure and the mobile layout all live in
 * `@/components/app/state-info-view`, which the driver apps and the pilot
 * assignment view show as a dialog — one design everywhere.
 */
function StateInfoPanel({
  code,
  onClose,
  className = '',
}: {
  code: string
  onClose: () => void
  className?: string
}) {
  return (
    <aside
      className={`w-full min-w-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 information</span>
        </p>
        <Button size="sm" variant="ghost" onClick={onClose} title="Close">
          ✕
        </Button>
      </div>
      <StateInfoView code={code} />
    </aside>
  )
}

/* ---------------- 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,
  myAccountRole,
  carrier,
  warnings,
  signedUrls,
  participants,
  routeCredits,
  cart,
  pageContext,
  documents,
  resumeDraftId,
  resumeSetupRef,
  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 || '—'} />
            <CarrierField
              tripId={trip.id}
              fallbackName={trip.carrier_name}
              carrier={carrier}
              canChoose={canAssignCarrier({ viewerRole: myRole, viewerIsAdmin: myAccountRole === 'admin' })}
              onAssigned={() => router.refresh()}
            />
          </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, pageContext }}
                  onAskPermit={onAskPermit}
                  onStateInfo={onStateInfo}
                />
              ))}
            </div>
          )}
          <PermitFeeSummary permits={permits} myRole={myRole} showLines />

          {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>
          )}

          {/* Route purchases paid through Synchron (2026-09-29, Task 7.2) — CD only. */}
          {pageContext === 'carrier' && (
            <RoutePurchaseHistory
              tripId={trip.id}
              permitStates={Object.fromEntries(permits.map((pm) => [pm.id, pm.state_code]))}
            />
          )}

          {/* Task 14: visible entry point at the BOTTOM of the Overview */}
          {myRole && (
            <div className="mt-4 border-t pt-4">
              <BuyPermitsDialog
                tripId={trip.id}
                tripRef={trip.ref_code}
                pageContext={pageContext}
                documents={documents}
                resumeDraftId={resumeDraftId}
                resumeSetupRef={resumeSetupRef}
                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, pageContext } = 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 automaticRoute = permitRouteRequest(p)
  const routeRequest = automaticRoute ?? 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 && !automaticRoute ? resolveRequester(routeRequest, participants) : undefined
  return (
    <PermitCard
      permit={p}
      tripId={trip.id}
      // A permit pushed by Synchron Permits includes its route (D5; Nash
      // 2026-10-01) — never a credit or a purchase for it. Cart / checkout is
      // Carrier Dispatch only this phase (D6).
      canBuyRoute={!!myRole && !routeIncludedWithPermit(trip, p) && pageContext === 'carrier'}
      routeIncluded={routeIncludedWithPermit(trip, p)}
      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 ? (
            <>
              <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>
              {includedRouteStatusLabel(p.route_processing_status) && (
                <span className="rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-bold text-blue-700">
                  {includedRouteStatusLabel(p.route_processing_status)}
                </span>
              )}
            </>
          ) : (
            <>
              {/* The processing chip sits BESIDE the purchase control, never
                  instead of it (D2): since 0041 the status is never null, so
                  the old branch order hid the button for every permit. */}
              {p.route_processing_status && (
                <span className="rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-bold text-blue-700">
                  {permitRouteLabel(p.route_processing_status)}
                </span>
              )}
              {canBuyRoute && p.state_code && (
                // Prepaid credit → use it from the button; money due → cart (Task 69).
                <RoutePurchaseControl
                  tripId={tripId}
                  permitId={p.id}
                  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: 'Processing…', failed: 'Processing failed', skipped: 'Manual' }[p.extraction_status] ??
              p.extraction_status}
          </span>
        )}
      </div>
      {/* What the state charged (Nash, 2026-09-24). Never rendered for a pilot. */}
      <PermitFeeLine permit={p} myRole={myRole} tripId={tripId} onSaved={onSaved} />
      {/* 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 info." 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={`State information for ${stateName(p.state_code)}`}
          >
            View state info
          </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>
  )
}

/**
 * The carrier on this trip — name, MC and DOT (Nash, 2026-09-19: "the three
 * things every broker needs to see when he goes to Overview").
 *
 *   assigned / auto → name + "MC … · DOT …"
 *   choose          → the dispatcher (or an admin) picks from their carrier
 *                     companies; everyone else sees "waiting for the
 *                     dispatcher" — nothing is shown until they choose
 *   none            → the plain carrier text the trip was created with
 */
function CarrierField({
  tripId,
  fallbackName,
  carrier,
  canChoose,
  onAssigned,
}: {
  tripId: string
  fallbackName: string | null
  carrier?: { resolution: CarrierResolution; adminOptions: CarrierSummary[] }
  canChoose: boolean
  onAssigned: () => void
}) {
  const [choice, setChoice] = useState('')
  const [busy, setBusy] = useState(false)
  const res = carrier?.resolution
  // The dispatcher's own carriers; an admin additionally gets every other carrier company.
  const options = res?.kind === 'choose' ? res.options : []
  const others = (carrier?.adminOptions ?? []).filter((c) => !options.some((o) => o.id === c.id))

  async function apply() {
    if (!choice) return
    setBusy(true)
    try {
      const r = await fetch(`/api/trips/${tripId}/carrier`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ company_id: choice }) })
      const body = await r.json().catch(() => ({}))
      if (!r.ok) throw new Error(body.error ?? 'Could not set the carrier.')
      toast.success(`Carrier set: ${body.carrier?.name ?? ''}`)
      onAssigned()
    } catch (e) {
      toast.error(e instanceof Error ? e.message : 'Could not set the carrier.')
    } finally {
      setBusy(false)
    }
  }

  const known = res && (res.kind === 'assigned' || res.kind === 'auto') ? res.carrier : null
  return (
    <div className="rounded-lg bg-neutral-50 p-2.5">
      <dt className="text-[10px] font-bold uppercase tracking-wide text-neutral-400">Carrier</dt>
      {known ? (
        <dd className="mt-0.5">
          <span className="block font-medium">{known.name}</span>
          <span className="block text-xs text-neutral-600">{carrierIdLine(known) || 'MC / DOT not on file'}</span>
        </dd>
      ) : res?.kind === 'choose' && canChoose && options.length > 0 ? (
        <dd className="mt-1 space-y-1.5">
          <p className="text-xs text-neutral-600">{fallbackName ? `Trip was created for "${fallbackName}". Which of your carriers is performing it?` : 'Which carrier is performing this trip?'}</p>
          <select value={choice} onChange={(e) => setChoice(e.target.value)} className="w-full rounded-md border bg-white px-2 py-1.5 text-xs">
            <option value="">Choose a carrier…</option>
            {others.length === 0 ? (
              options.map((c) => <option key={c.id} value={c.id}>{c.name}{carrierIdLine(c) ? ` — ${carrierIdLine(c)}` : ''}</option>)
            ) : (
              <>
                <optgroup label="Dispatcher's carriers">
                  {options.map((c) => <option key={c.id} value={c.id}>{c.name}{carrierIdLine(c) ? ` — ${carrierIdLine(c)}` : ''}</option>)}
                </optgroup>
                <optgroup label="All carriers (admin)">
                  {others.map((c) => <option key={c.id} value={c.id}>{c.name}{carrierIdLine(c) ? ` — ${carrierIdLine(c)}` : ''}</option>)}
                </optgroup>
              </>
            )}
          </select>
          <button type="button" onClick={apply} disabled={!choice || busy} className="rounded-md bg-[#0f1b2d] px-2.5 py-1 text-xs font-bold text-white disabled:opacity-50">
            {busy ? 'Applying…' : 'Apply'}
          </button>
        </dd>
      ) : res?.kind === 'choose' ? (
        <dd className="mt-0.5">
          {fallbackName && <span className="block font-medium">{fallbackName}</span>}
          <span className="block text-xs text-amber-800">Waiting for the dispatcher to {fallbackName ? 'confirm' : 'select'} the carrier.</span>
        </dd>
      ) : (
        <dd className="mt-0.5">
          <span className="block font-medium">{fallbackName || '—'}</span>
          {fallbackName && <span className="block text-xs text-neutral-500">MC / DOT unknown — no carrier company linked yet</span>}
        </dd>
      )}
    </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>
  )
}

/* BuyPermitsDialog moved to src/components/app/buy-permits-dialog.tsx (2026-09-29, two-step flow). */

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

function DocumentsTab({
  trip,
  documents,
  signedUrls,
  myRole,
  permits,
  requests,
  participants,
  warnings,
  routeCredits,
  cart,
  pageContext,
  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 processed 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, pageContext }}
                      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, myUserId, myAccountRole, contacts, permits, viewerContext }: TripWorkspaceProps) {
  const router = useRouter()
  const invitable = myRole ? INVITE_RULES[myRole] : []
  const isAdmin = myAccountRole === 'admin' || myRole === 'admin'
  // Decision D17 (2026-09-30): the invite matrix, the person whose personal
  // rule added someone, or a Company Admin in that company's mode may remove.
  const viewer = {
    userId: myUserId,
    role: myRole ?? '',
    isAdmin,
    activeCompanyId: viewerContext?.activeCompanyId ?? null,
    activeIsCompanyAdmin: viewerContext?.activeIsCompanyAdmin === true,
  }
  const isMe = (p: TripParticipant) => p.user_id === myUserId

  return (
    <div className="space-y-4 pt-4">
      <div className="space-y-2">
        {participants.map((p) => {
          const provenance = provenanceLine(p)
          const removable = !!myRole && !isMe(p) && canRemoveParticipant({ viewer, target: p })
          return (
            <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>
                  {/* Phone (Nash, 2026-09-18): so a driver can be called from
                      here. A pilot's number stays with the carrier side and
                      whoever invited them — canSeeParticipantPhone. */}
                  {canSeeParticipantPhone({ participant: p, viewerRole: myRole, viewerUserId: myUserId, viewerIsAdmin: myAccountRole === 'admin' }) ? (
                    p.phone ? (
                      <p className="text-xs text-neutral-700">
                        <a href={`tel:${p.phone.replace(/[^\d+]/g, '')}`} className="font-medium hover:underline">{p.phone}</a>
                        {p.phone_ext ? ` ext. ${p.phone_ext}` : ''}
                      </p>
                    ) : (
                      <p className="text-xs text-neutral-400">No phone on file</p>
                    )
                  ) : (
                    <p className="text-xs text-neutral-400">Contact details stay with the carrier</p>
                  )}
                </div>
              </div>
              <div className="flex flex-col items-end gap-1">
                <Badge variant="secondary" className="capitalize">{p.role}</Badge>
                {/* Provenance (§17, §26): an informational line, never a role
                    or a badge — Auto-Participant is how someone got here. */}
                {provenance && <p className="text-right text-xs text-neutral-500">{provenance}</p>}
                <span className={`text-[11px] font-semibold ${p.status === 'active' ? 'text-green-700' : 'text-amber-700'}`}>
                  {p.status === 'active' ? '● Joined' : '○ Invited'}
                </span>
                {removable && (
                  <RemoveParticipantButton tripId={trip.id} participant={p} onDone={() => router.refresh()} />
                )}
              </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>
      {/* Nash, 2026-09-18: "the form to invite someone should be below the
          list of people that are already a part of it." */}
      {invitable.length > 0 && (
        <InviteForm
          tripId={trip.id}
          roles={invitable}
          contacts={contacts}
          permits={permits}
          alreadyOn={participants.map((p) => p.email)}
          onDone={() => router.refresh()}
        />
      )}
    </div>
  )
}

/**
 * "Remove" on a People-tab row (Task 10.3, decision D17). Confirms, then
 * POSTs the general removal endpoint. Removing never touches the
 * Auto-Participant rule that added the person (§20) — the copy says so.
 */
function RemoveParticipantButton({ tripId, participant, onDone }: { tripId: string; participant: TripParticipant; onDone: () => void }) {
  const [open, setOpen] = useState(false)
  const [pending, setPending] = useState(false)
  const name = participant.name || participant.email

  async function remove() {
    setPending(true)
    try {
      const res = await fetch(`/api/trips/${tripId}/participants/${participant.id}/remove`, { method: 'POST' })
      const body = await res.json().catch(() => ({}))
      if (!res.ok) {
        toast.error(body.error || 'Could not remove this participant.')
        return
      }
      toast.success('Removed')
      setOpen(false)
      onDone()
    } finally {
      setPending(false)
    }
  }

  return (
    <>
      <Button type="button" variant="ghost" size="sm" className="h-7 px-2 text-xs text-red-700 hover:text-red-800" onClick={() => setOpen(true)}>
        Remove
      </Button>
      <Dialog open={open} onOpenChange={(v) => { if (!pending) setOpen(v) }}>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>Remove {name} from this Trip?</DialogTitle>
            <DialogDescription>Their Auto-Participant rule is not changed.</DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <Button type="button" variant="outline" disabled={pending} onClick={() => setOpen(false)}>Cancel</Button>
            <Button type="button" variant="destructive" disabled={pending} onClick={remove}>
              {pending ? 'Removing…' : 'Remove'}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  )
}

function InviteForm({
  tripId, roles, onDone, contacts, permits = [], alreadyOn = [],
}: {
  tripId: string
  roles: TripRole[]
  onDone: () => void
  contacts?: {
    drivers: CarrierContact[]
    brokers: CarrierContact[]
    /** Everyone this person may invite from — saved + shared a trip (2026-09-18). */
    invite?: InviteContact[]
  }
  /** 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. */}
        {/* Nash, 2026-09-18: anyone you shared a trip with is a contact —
            grouped by every role you may invite here. */}
        {contacts && (
          contacts.invite ? (
            <ContactPicker
              contacts={contacts.invite}
              roles={roles}
              alreadyAdded={alreadyOn}
              onPick={fillFrom}
              className="mt-3"
            />
          ) : (
            <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, permits, myRole, myUserId, myAccountRole, myAccountCreatedAt, trip }: TripWorkspaceProps & { pilots: Array<TripParticipant | DemoPilotContact> }) {
  const router = useRouter()
  const { invoices } = useInvoices()
  const { expenses } = useExpenses()
  const paperworkAccess = useViewerPaperworkAccess(myAccountCreatedAt)
  const viewer: 'carrier' | 'broker' = myRole === 'broker' ? 'broker' : 'carrier'
  // One rule for what a pilot holds — the invitation OR a later "Share
  // permits", latest wins (2026-09-17). Events arrive newest-first.
  const oldestFirst = [...events].reverse()
  const accessByEmail = pilotAccessFromEvents(oldestFirst)
  const accessFor = (email: string): string => accessByEmail[email.toLowerCase()] ?? 'Decide later'
  const paperworkRequestedAt = paperworkRequestsFromEvents(oldestFirst)
  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,
      participantId: demo ? undefined : p.id,
      // Who may SEE this pilot's paperwork: whoever started the invitation
      // chain (carrier dispatch → pilot dispatch → driver), or admin. A broker
      // who did not bring the pilot company in sees none (Nash, 2026-09-17).
      paperworkVisible:
        demo ||
        canViewPilotPaperwork({
          pilot: { id: p.id, invited_by: p.invited_by },
          participants,
          viewerUserId: myUserId,
          viewerIsAdmin: myAccountRole === 'admin',
        }),
      // Only the person who invited this pilot asks for their paperwork.
      canRequestPaperwork:
        !demo &&
        canManagePilotAccess({
          pilotInvitedBy: p.invited_by,
          viewerUserId: myUserId,
          viewerIsAdmin: myAccountRole === 'admin',
        }),
      paperworkRequestedAt: demo ? undefined : paperworkRequestedAt[p.email.toLowerCase()],
      // Nash, 2026-09-17: the person who invited THIS pilot shares permits
      // with them; an admin may do it for any pilot car. Enforced again in
      // /api/trips/[id]/pilot-access — this only draws the button.
      canManageAccess:
        !demo &&
        canManagePilotAccess({
          pilotInvitedBy: p.invited_by,
          viewerUserId: myUserId,
          viewerIsAdmin: myAccountRole === 'admin',
        }),
      // 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}
        permits={permits.map((pm) => ({ id: pm.id, state_code: pm.state_code, permit_number: pm.permit_number }))}
        viewerAccess={myRole === 'pilot' ? parsePilotAccessLine(accessFor(participants.find((x) => x.user_id === myUserId)?.email ?? ''), permits) : null}
        onRemovePilot={async (entry) => {
          const res = await fetch(`/api/trips/${trip.id}/pilot-remove`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ participant_id: entry.participantId }),
          })
          const body = await res.json().catch(() => ({}))
          if (!res.ok) throw new Error(body.error ?? 'Could not remove the pilot.')
          router.refresh()
        }}
        onRequestPaperwork={async (entry) => {
          const res = await fetch(`/api/trips/${trip.id}/pilot-paperwork-request`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ participant_id: entry.participantId }),
          })
          const body = await res.json().catch(() => ({}))
          if (!res.ok) throw new Error(body.error ?? 'Could not request paperwork.')
          router.refresh()
          return body.email_status === 'sent' ? 'sent' : 'recorded'
        }}
        onShareAccess={async (entry, access) => {
          const res = await fetch(`/api/trips/${trip.id}/pilot-access`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ participant_id: entry.participantId, access }),
          })
          const body = await res.json().catch(() => ({}))
          if (!res.ok) throw new Error(body.error ?? 'Could not share permits.')
          router.refresh()
        }}
        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',
    carrier_assigned: 'Carrier attached to trip',
    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(' · ')
}
