'use client';

import Link from 'next/link';
import { useParams } from 'next/navigation';
import { format, formatDistanceToNowStrict } from 'date-fns';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { Anchor, CheckCircle2, Circle, FileSignature, Send, Wallet } from 'lucide-react';
import { toast } from 'sonner';

import type { DetailTab } from '@/components/shared/detail-layout';
import { Button } from '@/components/ui/button';
import { DatePicker } from '@/components/ui/date-picker';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from '@/components/ui/accordion';
import { NotesList } from '@/components/shared/notes-list';
import { ProxyCard } from '@/components/shared/proxy-card';
import { InlineEditableField } from '@/components/shared/inline-editable-field';
import { FieldHistoryProvider, FieldHistoryIcon } from '@/components/shared/field-history';
import { ClientChannelEditor } from '@/components/clients/client-channel-editor';
import { InlineTagEditor } from '@/components/shared/inline-tag-editor';
import { RemindersInline } from '@/components/reminders/reminders-inline';
// Legacy `RecommendationList` removed 2026-05-15 - replaced by the same
// rule-based `BerthRecommenderPanel` (already imported above) used on the
// Overview tab so the scoring + UI stay consistent. The old component
// pulled stale "AI"-style rows that all scored 50% because the underlying
// generate endpoint was orphaned.
import { BerthRecommenderPanel } from '@/components/interests/berth-recommender-panel';
import { LinkedBerthsList } from '@/components/interests/linked-berths-list';
import { EoiGenerateDialog } from '@/components/documents/eoi-generate-dialog';
import { SigningProgress } from '@/components/documents/signing-progress';

// Shared parser for the interest's stringly-typed numeric columns (Drizzle
// returns Postgres numeric as string). Used by both the Overview milestone
// classifier and the Recommendations tab so the conversion stays
// consistent regardless of entry point.
function toNum(v: string | null | undefined): number | null {
  if (v === null || v === undefined) return null;
  const n = parseFloat(v);
  return Number.isFinite(n) ? n : null;
}
import { InterestTimeline } from '@/components/interests/interest-timeline';
import { WonStatusPanel } from '@/components/interests/won-status-panel';
import { SupplementalInfoRequestButton } from '@/components/interests/supplemental-info-request-button';
import { InterestDocumentsTab } from '@/components/interests/interest-documents-tab';
import {
  LEAD_CATEGORIES,
  PIPELINE_STAGES,
  SOURCES,
  STAGE_BADGE,
  canTransitionStage,
  stageLabel,
  type PipelineStage,
} from '@/lib/constants';
import { InterestEoiTab } from '@/components/interests/interest-eoi-tab';
import { InterestContactLogTab } from '@/components/interests/interest-contact-log-tab';
import { QualificationChecklist } from '@/components/interests/qualification-checklist';
import { PaymentsSection } from '@/components/interests/payments-section';
import { SkipAheadBanner } from '@/components/interests/skip-ahead-banner';
import { InterestBerthStatusBanner } from '@/components/interests/interest-berth-status-banner';
import { InterestContractTab } from '@/components/interests/interest-contract-tab';
import { InterestReservationTab } from '@/components/interests/interest-reservation-tab';
import { useConfirmation } from '@/hooks/use-confirmation';
import { apiFetch } from '@/lib/api/client';
import { cn } from '@/lib/utils';

type InterestPatchField =
  | 'leadCategory'
  | 'source'
  | 'desiredLengthFt'
  | 'desiredWidthFt'
  | 'desiredDraftFt'
  | 'desiredLengthM'
  | 'desiredWidthM'
  | 'desiredDraftM'
  | 'dateEoiSent'
  | 'dateEoiSigned'
  | 'dateReservationSigned'
  | 'dateDepositReceived'
  | 'dateContractSent'
  | 'dateContractSigned';

const LEAD_CATEGORY_OPTIONS = LEAD_CATEGORIES.map((c) => ({
  value: c,
  label: c.replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()),
}));

// Convert raw enum values like `waiting_for_signatures` → `Waiting For Signatures`.
function humanizeStatus(value: string | null): string | null {
  if (!value) return null;
  return value.replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase());
}

interface InterestTabsOptions {
  interestId: string;
  currentUserId?: string;
  /** Used by the dedicated EOI tab to deep-link to the client's record
   *  for inline edits ("wrong details? edit on the client's page"). */
  clientId?: string | null;
  interest: {
    pipelineStage: string;
    /** Drives the recommender panel mounted on the Overview tab. */
    desiredLengthFt?: string | null;
    desiredWidthFt?: string | null;
    desiredDraftFt?: string | null;
    /** Metric counterparts persisted alongside the imperial columns. The
     *  Berth-requirements row toggles between the two based on
     *  `desiredLengthUnit`. */
    desiredLengthM?: string | null;
    desiredWidthM?: string | null;
    desiredDraftM?: string | null;
    /** Unit the rep originally entered the dims in - drives the
     *  recommender header's display so a metric-entered deal doesn't
     *  render as ft. The three columns share an entry unit in practice. */
    desiredLengthUnit?: string | null;
    /** Linked yacht id - exposed so the OverviewTab "from yacht"
     *  inheritance pills can write back to the yacht record on
     *  confirmation. */
    yachtId?: string | null;
    /** Yacht dimensions surfaced by getInterestById when the interest
     *  has a linked yacht. Drives the "from yacht" inheritance pill in
     *  the Berth Requirements section when a desired_* column is empty
     *  but the yacht carries the measurement. Null when no yacht is
     *  linked or the yacht has no dimensions at all. */
    yachtDimensions?: {
      lengthFt: string | null;
      widthFt: string | null;
      draftFt: string | null;
      lengthM: string | null;
      widthM: string | null;
      draftM: string | null;
    } | null;
    leadCategory: string | null;
    source: string | null;
    eoiStatus: string | null;
    contractStatus: string | null;
    depositStatus: string | null;
    reservationStatus: string | null;
    /** Captured at reservation-agreement time. Drives the deposit-paid
     *  auto-advance once payment totals catch up. */
    depositExpectedAmount?: string | null;
    depositExpectedCurrency?: string | null;
    /** Doc-bearing stage sub-status badges - drive the milestone past/current
     *  classification independently of the pipeline stage. NULL until the
     *  matching stage is reached. */
    eoiDocStatus?: string | null;
    reservationDocStatus?: string | null;
    contractDocStatus?: string | null;
    /** Final outcome - 'won' surfaces the wrap-up checklist panel. */
    outcome?: string | null;
    /** Interest id - needed for the queryClient.invalidateQueries calls
     *  that fire after an inline contact edit. The parent passes this
     *  through `interestId` already, but the inline-edit handlers below
     *  use the structured object form. */
    id: string;
    /** Linked client id - required for the PATCH /api/v1/clients/[id]/
     *  contacts/[contactId] flow that the inline Email + Phone editors
     *  use. Null on an unlinked interest (rare but possible). */
    clientId: string | null;
    /** Primary contact channels resolved from the linked client record by
     *  getInterestById - both editable inline. The contact row's id is
     *  exposed alongside so the inline editor can PATCH the right row
     *  without an extra fetch. */
    clientPrimaryEmail?: string | null;
    clientPrimaryEmailContactId?: string | null;
    clientPrimaryPhone?: string | null;
    clientPrimaryPhoneContactId?: string | null;
    clientPrimaryPhoneE164?: string | null;
    clientPrimaryPhoneCountry?: string | null;
    dateFirstContact: string | null;
    dateLastContact: string | null;
    dateEoiSent: string | null;
    dateEoiSigned: string | null;
    dateReservationSigned?: string | null;
    dateContractSent: string | null;
    dateContractSigned: string | null;
    dateDepositReceived: string | null;
    reminderEnabled: boolean;
    reminderDays: number | null;
    reminderLastFired: string | null;
    /** Count of berths linked via the interest_berths junction -
     *  drives the "Berth Interest" milestone on the Overview tab. */
    linkedBerthCount?: number;
    notes: string | null;
    /** Surfaced by getInterestById for the Overview "most recent note"
     *  teaser - saves a click into the Notes tab to peek at the latest. */
    notesCount?: number;
    /** Aggregated note count across linked entities (interest + client +
     *  yacht + companies the client is a member of). Drives the badge
     *  on the Notes tab so reps see the full surface area at a glance. */
    notesCountAggregated?: number;
    recentNote?: {
      id: string;
      content: string;
      authorId: string;
      authorName: string | null;
      createdAt: string;
    } | null;
    tags?: Array<{ id: string; name: string; color: string }>;
  };
}

function useInterestPatch(interestId: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (patch: Partial<Record<InterestPatchField, string | null>>) =>
      apiFetch(`/api/v1/interests/${interestId}`, {
        method: 'PATCH',
        body: patch,
      }),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['interests', interestId] });
    },
  });
}

type Phase = 'past' | 'current' | 'future';

function useStageMutation(interestId: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({
      stage,
      reason,
      override,
      milestoneDate,
    }: {
      stage: string;
      reason?: string;
      override?: boolean;
      /** Optional ISO date for the milestone column (instead of "now"). */
      milestoneDate?: string;
    }) =>
      apiFetch(`/api/v1/interests/${interestId}/stage`, {
        method: 'PATCH',
        body: { pipelineStage: stage, reason, override, milestoneDate },
      }),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['interests', interestId] });
      qc.invalidateQueries({ queryKey: ['interests'] });
    },
  });
}

function EditableRow({
  label,
  children,
  historyPath,
  inheritedFrom,
}: {
  label: string;
  children: React.ReactNode;
  /** When set, renders a clock icon (when at least one override row
   *  exists for this path on the surrounding FieldHistoryProvider scope)
   *  that opens the field-history popover. The icon renders nothing
   *  without history, so it's safe to pass on every row. */
  historyPath?: string;
  /** Optional inheritance hint. Renders a small pill next to the label
   *  to signal that the value editable here lives on a different entity
   *  (e.g. `'client'` for primary email/phone, `'yacht'` for hull
   *  dimensions). Tells the rep that an edit propagates outside the
   *  deal scope. */
  inheritedFrom?: 'client' | 'yacht' | 'company';
}) {
  return (
    <div className="flex gap-2 py-1.5 border-b last:border-0 items-center">
      <dt className="w-44 shrink-0 text-sm text-muted-foreground flex items-center gap-1.5">
        <span>{label}</span>
        {inheritedFrom ? (
          <span className="inline-flex items-center rounded-full border bg-muted px-1.5 py-0 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
            from {inheritedFrom}
          </span>
        ) : null}
      </dt>
      <dd className="flex-1 min-w-0 flex items-center gap-1">
        <div className="flex-1 min-w-0">{children}</div>
        {historyPath ? <FieldHistoryIcon fieldPath={historyPath} /> : null}
      </dd>
    </div>
  );
}

function InfoRow({ label, value }: { label: string; value?: string | null }) {
  if (!value) return null;
  return (
    <div className="flex gap-2 py-1.5 border-b last:border-0">
      <dt className="w-44 shrink-0 text-sm text-muted-foreground">{label}</dt>
      <dd className="text-sm">{value}</dd>
    </div>
  );
}

function formatDate(date: string | null) {
  if (!date) return null;
  return format(new Date(date), 'MMM d, yyyy');
}

function relativeDate(date: string | null) {
  if (!date) return null;
  return `${formatDistanceToNowStrict(new Date(date))} ago`;
}

interface MilestoneSectionProps {
  title: string;
  icon: React.ComponentType<{ className?: string }>;
  /** Lifecycle for this milestone, in chronological order. */
  steps: Array<{
    label: string;
    date: string | null;
    /** Stage to advance to when the user clicks the action button for this step. */
    advanceStage?: string;
    /** Optional override for the action label. */
    actionLabel?: string;
    /** Suppress the inline "Mark as…" button for this step. Use when the
     *  parent supplies a richer CTA via `footer` (e.g. Deposit, where we
     *  want the invoice flow to be the primary path). */
    hideAutoButton?: boolean;
  }>;
  status: string | null;
  onAdvance: (stage: string, milestoneDate?: string) => void | Promise<void>;
  isPending: boolean;
  /** Current pipelineStage. Used to mark steps as done when the pipeline has
   *  moved past their advanceStage even if the date stamp is missing - e.g.
   *  a seed-data interest that started already at eoi_signed will show both
   *  EOI sub-steps as done. Stage truth > date truth. */
  currentStage: string;
  /** When true, this milestone is the next one the user should act on:
   *  card gets a brand-accent ring and the next-step CTA becomes a primary
   *  button. Computed by the parent based on currentStage. */
  isActive?: boolean;
  /** Extra nodes (e.g. "Create deposit invoice" link) rendered below the steps. */
  footer?: React.ReactNode;
}

/**
 * One milestone section (EOI / Deposit / Contract) - shows a vertical lifecycle
 * with completed steps checked, the next step exposing a quick "mark as…"
 * button that bumps the pipeline stage. Each stage flip auto-stamps its date
 * via the service layer (interests.service.ts). When external systems wire in
 * (Documenso webhook, paid invoice → deposit, etc.), they patch the same
 * stage endpoint and these checkmarks light up automatically.
 */
/**
 * Button that opens a date-picker popover before advancing a milestone. The
 * default is today, but the rep can back-date the event (e.g. "deposit
 * landed yesterday") so the stamped milestone column reflects the real date
 * rather than the click time.
 */
function MilestoneAdvanceButton({
  label,
  variant,
  disabled,
  onConfirm,
}: {
  label: string;
  variant: 'default' | 'outline' | 'ghostLink';
  disabled?: boolean;
  onConfirm: (milestoneDate: string) => void;
}) {
  const [open, setOpen] = useState(false);
  const [date, setDate] = useState<string>(() => new Date().toISOString().slice(0, 10));

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        {variant === 'ghostLink' ? (
          <button
            type="button"
            disabled={disabled}
            className="text-muted-foreground hover:text-foreground disabled:opacity-50"
          >
            {label}
          </button>
        ) : (
          <Button
            type="button"
            variant={variant}
            size="sm"
            disabled={disabled}
            className="mt-2 h-7 px-2.5 text-xs"
          >
            {label}
          </Button>
        )}
      </PopoverTrigger>
      <PopoverContent align="start" className="w-64 space-y-2 p-3">
        <div className="space-y-1">
          <label className="text-xs font-medium" htmlFor="milestone-date">
            Date completed
          </label>
          <DatePicker
            id="milestone-date"
            value={date}
            toDate={new Date()}
            onChange={setDate}
            placeholder="Pick a date"
          />
          <p className="text-xs text-muted-foreground">
            Defaults to today - back-date if the event happened earlier.
          </p>
        </div>
        <div className="flex justify-end gap-2">
          <Button type="button" variant="ghost" size="sm" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button
            type="button"
            size="sm"
            disabled={!date || disabled}
            onClick={() => {
              onConfirm(date);
              setOpen(false);
            }}
          >
            Confirm
          </Button>
        </div>
      </PopoverContent>
    </Popover>
  );
}

/**
 * Skip-ahead backfill control: shown next to past milestones whose
 * date column is null. Opens the same date popover as
 * MilestoneAdvanceButton but PATCHes the date column directly without
 * triggering a stage transition - the stage was already advanced
 * manually upstream.
 */
function MilestoneBackfillButton({
  label,
  disabled,
  onConfirm,
}: {
  label: string;
  disabled?: boolean;
  onConfirm: (milestoneDate: string) => void | Promise<void>;
}) {
  const [open, setOpen] = useState(false);
  const [date, setDate] = useState<string>(() => new Date().toISOString().slice(0, 10));
  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <button
          type="button"
          disabled={disabled}
          className="text-xs font-medium text-primary hover:underline disabled:opacity-50"
        >
          {label}
        </button>
      </PopoverTrigger>
      <PopoverContent align="start" className="w-64 space-y-2 p-3">
        <div className="space-y-1">
          <label className="text-xs font-medium" htmlFor="backfill-date">
            Date completed
          </label>
          <DatePicker
            id="backfill-date"
            value={date}
            toDate={new Date()}
            onChange={setDate}
            placeholder="Pick a date"
          />
          <p className="text-xs text-muted-foreground">
            Records the date the milestone happened. Does not change the deal&apos;s pipeline stage.
          </p>
        </div>
        <div className="flex justify-end gap-2">
          <Button type="button" variant="ghost" size="sm" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button
            type="button"
            size="sm"
            disabled={!date || disabled}
            onClick={async () => {
              await onConfirm(date);
              setOpen(false);
            }}
          >
            Save date
          </Button>
        </div>
      </PopoverContent>
    </Popover>
  );
}

function MilestoneSection({
  title,
  icon: Icon,
  steps,
  status,
  onAdvance,
  isPending,
  currentStage,
  isActive,
  footer,
}: MilestoneSectionProps) {
  const currentStageIdx = PIPELINE_STAGES.indexOf(currentStage as PipelineStage);
  // A step counts as done if either:
  //   (a) its `advanceStage` is at or behind the current pipeline stage, OR
  //   (b) it has an explicit date stamp (from a manual mark or webhook).
  // (a) handles seeded/imported interests that arrived at a later stage
  // without per-step dates.
  const doneFlags = steps.map((step) => {
    if (step.date) return true;
    if (!step.advanceStage) return false;
    const stepIdx = PIPELINE_STAGES.indexOf(step.advanceStage as PipelineStage);
    return stepIdx !== -1 && currentStageIdx !== -1 && currentStageIdx >= stepIdx;
  });
  const firstUnsetIdx = doneFlags.findIndex((d) => !d);

  return (
    <section
      className={cn(
        'rounded-xl border bg-card p-4 shadow-sm transition-colors',
        isActive ? 'border-brand-300 bg-brand-50/40 ring-1 ring-brand-200' : 'border-border',
      )}
    >
      <header className="mb-3 flex items-center justify-between gap-2">
        <div className="flex items-center gap-2">
          <Icon className={cn('size-4', isActive ? 'text-brand-600' : 'text-muted-foreground')} />
          <h3 className="text-sm font-semibold tracking-tight text-foreground">{title}</h3>
          {isActive ? (
            <span className="inline-flex items-center gap-1 rounded-full bg-brand-600 px-2 py-0.5 text-xs font-semibold uppercase tracking-[0.08em] text-white shadow-sm">
              <span className="size-1.5 rounded-full bg-white/90" aria-hidden />
              Next step
            </span>
          ) : null}
        </div>
        {status ? (
          <span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
            {humanizeStatus(status)}
          </span>
        ) : null}
      </header>

      <ol className="space-y-2">
        {steps.map((step, i) => {
          const done = doneFlags[i] ?? false;
          const isNext = !done && i === firstUnsetIdx;
          return (
            <li key={step.label} className="flex items-start gap-2 text-sm">
              {done ? (
                <CheckCircle2 className="mt-0.5 size-4 shrink-0 text-emerald-600" aria-hidden />
              ) : (
                <Circle
                  className={cn(
                    'mt-0.5 size-4 shrink-0',
                    isNext ? 'text-foreground/60' : 'text-muted-foreground/40',
                  )}
                />
              )}
              <div className="min-w-0 flex-1">
                <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
                  <span
                    className={cn(
                      'truncate',
                      done
                        ? 'text-foreground'
                        : isNext
                          ? 'text-foreground'
                          : 'text-muted-foreground',
                    )}
                  >
                    {step.label}
                  </span>
                  {step.date ? (
                    <span className="text-xs text-muted-foreground">
                      {formatDate(step.date)} · {relativeDate(step.date)}
                    </span>
                  ) : null}
                </div>
                {isNext && step.advanceStage && !step.hideAutoButton ? (
                  <MilestoneAdvanceButton
                    label={step.actionLabel ?? `Mark as ${step.label.toLowerCase()}`}
                    variant={isActive ? 'default' : 'outline'}
                    disabled={isPending}
                    onConfirm={(date) => onAdvance(step.advanceStage!, date)}
                  />
                ) : null}
              </div>
            </li>
          );
        })}
      </ol>
      {footer ? <div className="mt-3 border-t pt-3 text-xs">{footer}</div> : null}
    </section>
  );
}

/**
 * Collapsible wrapper for future-phase milestones. Hidden by default so
 * the overview stays focused on the current stage; expanding lets reps
 * record skipped milestones (the action click then routes through the
 * advance() override-confirm).
 */
function FutureMilestones({
  milestones,
  stageMutation,
  advance,
  activeMilestone,
  currentStage,
}: {
  milestones: Array<{
    key: 'berth_interest' | 'eoi' | 'reservation' | 'deposit' | 'contract';
    title: string;
    icon: React.ComponentType<{ className?: string }>;
    status: string | null;
    steps: MilestoneSectionProps['steps'];
    footer?: React.ReactNode;
  }>;
  stageMutation: ReturnType<typeof useStageMutation>;
  advance: (stage: string) => void | Promise<void>;
  activeMilestone: 'berth_interest' | 'eoi' | 'reservation' | 'deposit' | 'contract' | null;
  currentStage: string;
}) {
  const [expanded, setExpanded] = useState(false);
  return (
    <div className="rounded-lg border border-dashed">
      <button
        type="button"
        onClick={() => setExpanded((v) => !v)}
        aria-expanded={expanded}
        aria-controls="future-milestones-body"
        className="flex w-full items-center justify-between gap-2 px-4 py-2.5 text-sm text-muted-foreground hover:text-foreground hover:bg-muted/30 transition-colors"
      >
        <span>
          {expanded ? 'Hide' : 'Show'} upcoming milestones
          <span className="ml-2 text-xs">({milestones.map((m) => m.title).join(' · ')})</span>
        </span>
        <span className="text-xs">{expanded ? '▴' : '▾'}</span>
      </button>
      {expanded && (
        <div
          id="future-milestones-body"
          className={cn(
            'grid grid-cols-1 gap-4 p-4 pt-0',
            milestones.length === 1 ? '' : 'lg:grid-cols-2',
          )}
        >
          {milestones.map((m) => (
            <MilestoneSection
              key={m.key}
              title={m.title}
              icon={m.icon}
              status={m.status}
              isPending={stageMutation.isPending}
              onAdvance={advance}
              currentStage={currentStage}
              isActive={activeMilestone === m.key}
              steps={m.steps}
              footer={m.footer}
            />
          ))}
        </div>
      )}
    </div>
  );
}

/**
 * Compact per-signer progress widget for the Overview tab's EOI milestone
 * card. Mounts inside the milestone footer when the EOI is sent but not
 * yet fully signed, so reps see who's signed at a glance without leaving
 * Overview. Heavy lifting (resend, void, etc.) stays on the EOI tab — a
 * "View EOI" link below the widget routes the rep there.
 */
function EoiMilestoneActiveProgress({
  interestId,
  portSlug,
}: {
  interestId: string;
  portSlug: string;
}) {
  const { data: docsRes } = useQuery<{
    data: Array<{ id: string; status: string }>;
  }>({
    queryKey: ['documents', { interestId, documentType: 'eoi' }],
    queryFn: () =>
      apiFetch<{ data: Array<{ id: string; status: string }> }>(
        `/api/v1/documents?interestId=${interestId}&documentType=eoi`,
      ),
    staleTime: 30_000,
  });
  const activeDoc = (docsRes?.data ?? []).find(
    (d) => d.status === 'sent' || d.status === 'partially_signed' || d.status === 'signed',
  );
  const { data: signersRes } = useQuery<{
    data: Array<{
      id: string;
      signerName: string;
      signerEmail: string;
      signerRole: string;
      signingOrder: number;
      status: string;
      signedAt?: string | null;
      invitedAt?: string | null;
      openedAt?: string | null;
      lastReminderSentAt?: string | null;
      signingUrl?: string | null;
    }>;
  }>({
    queryKey: ['documents', activeDoc?.id, 'signers'],
    queryFn: () =>
      apiFetch<{ data: Array<never> }>(`/api/v1/documents/${activeDoc!.id}/signers`) as never,
    enabled: !!activeDoc,
    staleTime: 30_000,
  });

  if (!activeDoc) return null;
  const signers = signersRes?.data ?? [];
  if (signers.length === 0) return null;

  return (
    <div className="space-y-2">
      <SigningProgress documentId={activeDoc.id} signers={signers} />
      <div className="flex justify-end">
        <Button asChild type="button" size="sm" variant="ghost" className="h-7 text-xs">
          <Link
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            href={`/${portSlug}/interests/${interestId}?tab=eoi` as any}
          >
            View EOI →
          </Link>
        </Button>
      </div>
    </div>
  );
}

function OverviewTab({
  interestId,
  interest,
  clientId,
}: {
  interestId: string;
  interest: InterestTabsOptions['interest'];
  clientId: string | null;
}) {
  const params = useParams<{ portSlug: string }>();
  const portSlug = params?.portSlug ?? '';
  // Same gate the parent uses to skip the dedicated Berth Recommendations
  // tab — without desired length the recommender has no ranking signal,
  // so the empty-state guidance card was reading as Overview noise.
  const hasDesiredDims = toNum(interest.desiredLengthFt) !== null;
  // QueryClient lifted to the top of the tab so the inline-edit email +
  // Lift the EOI generate dialog into the Overview so the milestone card
  // can launch it inline - same dialog the dedicated EOI tab uses, so the
  // editing/confirmation flow is identical regardless of entry point.
  const [eoiGenerateOpen, setEoiGenerateOpen] = useState(false);
  const mutation = useInterestPatch(interestId);
  const stageMutation = useStageMutation(interestId);
  const { confirm, dialog: confirmDialog } = useConfirmation();
  const save = (field: InterestPatchField) => async (next: string | null) => {
    await mutation.mutateAsync({ [field]: next });
  };
  /**
   * Advance the pipeline. When the requested target isn't a legal next
   * step (e.g. user clicked "Mark deposit received" while still on
   * EOI Sent), prompt for confirmation and pass `override:true` so the
   * backend transition guard lets the change through. Mirrors the
   * skip-ahead pattern from the inline stage picker so audit trails
   * stay consistent regardless of which surface the rep used.
   */
  const advance = async (stage: string, milestoneDate?: string) => {
    const fromStage = interest.pipelineStage as PipelineStage;
    const toStage = stage as PipelineStage;
    const isOverride = fromStage !== toStage && !canTransitionStage(fromStage, toStage);
    if (isOverride) {
      const ok = await confirm({
        title: 'Skip-ahead stage change',
        description: `This advances the stage from "${fromStage.replace(/_/g, ' ')}" to "${toStage.replace(
          /_/g,
          ' ',
        )}", which isn't a standard next step. The change will be flagged in the audit log.`,
        confirmLabel: 'Continue',
      });
      if (!ok) return;
    }
    stageMutation.mutate({
      stage,
      reason: isOverride ? 'Skip-ahead from overview milestones' : 'Marked from overview',
      override: isOverride || undefined,
      milestoneDate,
    });
  };

  // Determine each milestone's phase relative to the current pipeline
  // stage. The overview hides future-phase milestones by default - it
  // was visually noisy to see Deposit + Contract cards on a deal still
  // at the EOI stage, and the empty cards invited mis-clicks.
  //
  // Past milestones still render (collapsed history) so reps can see
  // what's been completed. Future milestones are gated behind a "Show
  // upcoming milestones" toggle so the rep CAN reach them when a deal
  // genuinely skips stages - the click then routes through the same
  // override-confirm flow as the inline stage picker.
  const stageIdx = PIPELINE_STAGES.indexOf(interest.pipelineStage as PipelineStage);
  const reservationIdx = PIPELINE_STAGES.indexOf('reservation');
  const depositIdx = PIPELINE_STAGES.indexOf('deposit_paid');

  // Sub-status carries the "is this milestone's doc actually signed?" bit
  // for the doc-bearing stages (eoi / reservation / contract). A milestone
  // is 'past' when stage is BEYOND its index OR when stage equals its index
  // AND the doc sub-status is 'signed'.
  const eoiSigned = interest.eoiDocStatus === 'signed';
  const reservationSigned = interest.reservationDocStatus === 'signed';
  const contractSigned = interest.contractDocStatus === 'signed';

  // 2026-05-15: rewrote phase classification so the Overview always
  // surfaces a CURRENT milestone for the rep, regardless of where the
  // pipeline-stage column happens to sit. The previous "phase === current
  // only when stageIdx exactly matches" rule produced an empty Overview
  // for the qualified + nurturing stages (no milestone marked current, EOI
  // hidden under "show upcoming") - exactly the gap the rep complained
  // about. New model: the FIRST not-yet-complete milestone in the fixed
  // berth_interest → eoi → reservation → deposit → contract order is
  // 'current'. Everything before is 'past'; everything after is 'future'.
  const hasLinkedBerth = (interest.linkedBerthCount ?? 0) > 0;
  const reservationStageReached = stageIdx >= reservationIdx;
  const depositComplete = stageIdx > depositIdx;
  const milestoneCompletion = {
    berth_interest: hasLinkedBerth,
    eoi: eoiSigned,
    reservation: reservationSigned,
    deposit: depositComplete,
    contract: contractSigned,
  } as const;
  const order = ['berth_interest', 'eoi', 'reservation', 'deposit', 'contract'] as const;
  const firstIncompleteKey = order.find((k) => !milestoneCompletion[k]) ?? null;
  // 2026-05-21: a "stage-owning" milestone is the one that maps 1:1 with
  // the deal's current pipelineStage column. When the rep manually
  // jumps the stage forward (Reservation+) but earlier sub-statuses
  // are still un-signed, we need the current-stage milestone to stay
  // marked `'current'` regardless of completion - otherwise EOI gets
  // flagged as NEXT STEP and the actual current stage hides under
  // "Upcoming milestones". Earlier-than-stage milestones go to
  // `'past'` so the rep can render backfill controls against them.
  const STAGE_TO_MILESTONE: Partial<Record<PipelineStage, (typeof order)[number]>> = {
    eoi: 'eoi',
    reservation: 'reservation',
    deposit_paid: 'deposit',
    contract: 'contract',
  };
  const stageOwnedMilestoneRaw =
    STAGE_TO_MILESTONE[interest.pipelineStage as PipelineStage] ?? null;
  // B2 (2026-06-18): if the stage-owned milestone is already COMPLETE — e.g. a
  // migrated deal left at stage=eoi with a signed EOI that never auto-advanced —
  // don't pin it as the current "NEXT STEP". Falling back to null makes phaseFor
  // use completion ordering, so the signed milestone shows as done/past and the
  // next incomplete one (Reservation) becomes current. Display-only; the
  // pipeline_stage column is unchanged.
  const stageOwnedMilestoneComplete = stageOwnedMilestoneRaw
    ? milestoneCompletion[stageOwnedMilestoneRaw]
    : false;
  const stageOwnedMilestone = stageOwnedMilestoneComplete ? null : stageOwnedMilestoneRaw;
  const stageOwnedIdx = stageOwnedMilestone ? order.indexOf(stageOwnedMilestone) : -1;
  const phaseFor = (k: (typeof order)[number]): Phase => {
    // Stage owns this milestone → always current, never collapsed.
    if (stageOwnedMilestone === k) return 'current';
    // Otherwise: completion drives past/future ordering when no stage
    // owns a milestone (enquiry/qualified/nurturing stages).
    if (!stageOwnedMilestone) {
      if (milestoneCompletion[k]) return 'past';
      if (k === firstIncompleteKey) return 'current';
      return 'future';
    }
    // A stage DOES own a different milestone - bucket by position
    // relative to it. Earlier slots go to `past` even if incomplete
    // (the backfill controls live there); later slots go to `future`.
    const idx = order.indexOf(k);
    return idx < stageOwnedIdx ? 'past' : 'future';
  };
  const berthInterestPhase: Phase = phaseFor('berth_interest');
  const eoiPhase: Phase = phaseFor('eoi');
  const reservationPhase: Phase = phaseFor('reservation');
  const depositPhase: Phase = phaseFor('deposit');
  const contractPhase: Phase = phaseFor('contract');
  // Payments-section visibility: useless real estate until a deposit is
  // actually expected (reservation stage onwards). Reps on enquiry /
  // qualified / nurturing should see stage-guidance instead.
  const showPaymentsSection = reservationStageReached;

  const activeMilestone: 'berth_interest' | 'eoi' | 'reservation' | 'deposit' | 'contract' | null =
    berthInterestPhase === 'current'
      ? 'berth_interest'
      : eoiPhase === 'current'
        ? 'eoi'
        : reservationPhase === 'current'
          ? 'reservation'
          : depositPhase === 'current'
            ? 'deposit'
            : contractPhase === 'current'
              ? 'contract'
              : null;

  // toNum extracted to module scope so the Recommendations tab can use it
  // alongside the Overview tab. See top of file.

  const milestones: Array<{
    key: 'berth_interest' | 'eoi' | 'reservation' | 'deposit' | 'contract';
    phase: Phase;
    title: string;
    icon: React.ComponentType<{ className?: string }>;
    status: string | null;
    steps: MilestoneSectionProps['steps'];
    footer?: React.ReactNode;
    /** Brief one-liner shown when the milestone is in the past-strip. */
    pastSummary: React.ReactNode;
  }> = [
    {
      key: 'berth_interest',
      phase: berthInterestPhase,
      title: 'Berth Interest',
      icon: Anchor,
      // No status badge - the count IS the status. Showing "0 berths"
      // would just duplicate the empty-state copy below.
      status: hasLinkedBerth
        ? `${interest.linkedBerthCount} berth${(interest.linkedBerthCount ?? 0) === 1 ? '' : 's'}`
        : null,
      // No advanceStage step - the milestone tracks a state (berths
      // linked) rather than a stage transition. Hide the row chrome by
      // passing an empty steps array; the footer renders the action.
      steps: [],
      footer:
        berthInterestPhase === 'current' ? (
          <div className="text-xs text-muted-foreground">
            Add a berth from the Recommendations tab or the client&apos;s active interest panel to
            mark this milestone complete.
          </div>
        ) : null,
      pastSummary: hasLinkedBerth
        ? `${interest.linkedBerthCount} berth${(interest.linkedBerthCount ?? 0) === 1 ? '' : 's'} linked`
        : 'Skipped',
    },
    {
      key: 'eoi',
      phase: eoiPhase,
      title: 'EOI',
      icon: Send,
      status: interest.eoiDocStatus ?? interest.eoiStatus,
      steps: [
        {
          label: 'EOI sent',
          date: interest.dateEoiSent,
          advanceStage: 'eoi',
          // 99% of the time the EOI is sent through Documenso and this
          // stamps automatically via the webhook. Label as "manually" so
          // reps reach for it only when Documenso fails to deliver or the
          // EOI was sent outside the integrated flow.
          actionLabel: 'Mark EOI as sent manually',
        },
        {
          label: 'EOI signed',
          date: interest.dateEoiSigned,
          // Stage stays at 'eoi'; the sub-status badge flips via a separate
          // PATCH (see MilestoneAdvanceButton.onConfirm fallback below).
          advanceStage: 'eoi',
          actionLabel: 'Mark EOI as signed manually',
        },
      ],
      // When the EOI milestone is the active next step but nothing's been
      // sent yet, surface the actual generation entry points instead of
      // making the rep navigate to the EOI tab first. Mirrors the EOI
      // tab's Generate flow exactly - same dialog component, same
      // confirmation step - so behaviour stays consistent.
      footer:
        eoiPhase === 'current' && !interest.dateEoiSent ? (
          <div className="flex flex-wrap items-center gap-2 pt-1">
            <Button type="button" size="sm" onClick={() => setEoiGenerateOpen(true)}>
              Generate EOI
            </Button>
            <Button asChild type="button" size="sm" variant="outline">
              <Link
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                href={`/${portSlug}/interests/${interestId}?tab=eoi` as any}
              >
                Open EOI tab
              </Link>
            </Button>
          </div>
        ) : eoiPhase === 'current' && interest.dateEoiSent && !interest.dateEoiSigned ? (
          <EoiMilestoneActiveProgress interestId={interestId} portSlug={portSlug} />
        ) : null,
      pastSummary: interest.dateEoiSigned ? (
        `Signed ${formatDate(interest.dateEoiSigned)}`
      ) : (
        <span className="inline-flex items-center gap-1.5">
          <span>Completed</span>
          <MilestoneBackfillButton
            label="Set date"
            disabled={mutation.isPending}
            onConfirm={async (d) => {
              await mutation.mutateAsync({ dateEoiSigned: d });
            }}
          />
        </span>
      ),
    },
    {
      key: 'reservation',
      phase: reservationPhase,
      title: 'Reservation',
      icon: FileSignature,
      status: interest.reservationDocStatus ?? null,
      steps: [
        {
          label: 'Reservation agreement signed',
          date: interest.dateReservationSigned ?? null,
          advanceStage: 'reservation',
          actionLabel: 'Mark reservation as signed',
        },
      ],
      pastSummary: interest.dateReservationSigned ? (
        `Signed ${formatDate(interest.dateReservationSigned)}`
      ) : (
        <span className="inline-flex items-center gap-1.5">
          <span>Completed</span>
          <MilestoneBackfillButton
            label="Set date"
            disabled={mutation.isPending}
            onConfirm={async (d) => {
              await mutation.mutateAsync({ dateReservationSigned: d });
            }}
          />
        </span>
      ),
    },
    {
      key: 'deposit',
      phase: depositPhase,
      title: 'Deposit',
      icon: Wallet,
      status: interest.depositStatus,
      steps: [
        {
          label: 'Deposit received',
          date: interest.dateDepositReceived,
          advanceStage: 'deposit_paid',
          hideAutoButton: true,
        },
      ],
      footer:
        depositPhase === 'current' && !interest.dateDepositReceived ? (
          <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
            <MilestoneAdvanceButton
              label="Mark received manually"
              variant="ghostLink"
              disabled={stageMutation.isPending}
              onConfirm={(date) => advance('deposit_paid', date)}
            />
            <span className="text-xs text-muted-foreground">
              Or record a payment in the Payments section.
            </span>
          </div>
        ) : null,
      pastSummary: interest.dateDepositReceived ? (
        `Received ${formatDate(interest.dateDepositReceived)}`
      ) : (
        <span className="inline-flex items-center gap-1.5">
          <span>Recorded</span>
          <MilestoneBackfillButton
            label="Set date"
            disabled={mutation.isPending}
            onConfirm={async (d) => {
              await mutation.mutateAsync({ dateDepositReceived: d });
            }}
          />
        </span>
      ),
    },
    {
      key: 'contract',
      phase: contractPhase,
      title: 'Contract',
      icon: FileSignature,
      status: interest.contractDocStatus ?? interest.contractStatus,
      steps: [
        {
          label: 'Contract sent',
          date: interest.dateContractSent,
          advanceStage: 'contract',
          actionLabel: 'Mark contract as sent',
        },
        {
          label: 'Contract signed',
          date: interest.dateContractSigned,
          advanceStage: 'contract',
          actionLabel: 'Mark contract as signed',
        },
      ],
      pastSummary: interest.dateContractSigned ? (
        `Signed ${formatDate(interest.dateContractSigned)}`
      ) : (
        <span className="inline-flex items-center gap-1.5">
          <span>Completed</span>
          <MilestoneBackfillButton
            label="Set date"
            disabled={mutation.isPending}
            onConfirm={async (d) => {
              await mutation.mutateAsync({ dateContractSigned: d });
            }}
          />
        </span>
      ),
    },
  ];

  const pastMilestones = milestones.filter((m) => m.phase === 'past');
  const currentMilestones = milestones.filter((m) => m.phase === 'current');
  const futureMilestones = milestones.filter((m) => m.phase === 'future');

  return (
    <FieldHistoryProvider scope={{ type: 'interest', id: interestId }}>
      <div className="space-y-6">
        {/* Skip-ahead nudge  -  informational only; fires when the deal jumped
          past a milestone without stamping the matching date. */}
        <SkipAheadBanner interest={interest} />

        {/* Conflict callout  -  fires when a linked berth is sold or already
          under offer to another active deal. Doesn't block the rep; just
          surfaces the situation so they treat the deal as a backup. */}
        <InterestBerthStatusBanner
          interestId={interestId}
          interestPipelineStage={interest.pipelineStage}
          interestOutcome={interest.outcome}
          archivedAt={null}
        />

        {/* CM-9: per-deal point-of-contact (overrides the client's default). */}
        <ProxyCard entityType="interest" entityId={interestId} />

        {/* Qualification checklist  -  surfaces the port's per-port criteria so
          the rep can mark each one confirmed before the deal advances out
          of 'enquiry'. Hidden when the port has no enabled criteria. */}
        <QualificationChecklist interestId={interestId} currentStage={interest.pipelineStage} />

        {/* Payments  -  bank-issued invoices live elsewhere; this is the
          internal audit record of money received against the deal. The
          running deposit total here drives the auto-advance into the
          deposit_paid stage server-side. Hidden before the reservation
          stage: no deposit is expected yet, so the empty card is just
          noise  -  the next-milestone card carries the actionable copy
          instead. Render order: deprioritized below the milestone strip
          so the rep's eye lands on the active step first. */}
        {/* Pre-reservation: the dedicated "Next step" guidance card was
          removed in favour of a brighter NEXT STEP pill on the active
          MilestoneSection below (it already owns the workflow actions  - 
          two surfaces was redundant). Nurturing keeps a slim helper
          since no milestone is naturally "current" while a deal is
          paused. */}
        {interest.pipelineStage === 'nurturing' ? (
          <div className="rounded-xl border bg-card p-4 text-sm">
            <p className="font-medium text-foreground">Deal is on nurture</p>
            <p className="mt-1 text-xs text-muted-foreground">
              Schedule a follow-up reminder or log a contact when the prospect re-engages, then move
              them back to Qualified.
            </p>
          </div>
        ) : null}

        {/* Sales-process milestones  -  phase-aware so the user only sees
          what's actionable now. Past milestones collapse into a tight
          history strip; the current milestone gets the full card; future
          milestones are hidden behind a toggle so reps can still
          skip-ahead when reality calls for it (an override-confirm
          gates the actual stage move). */}
        {pastMilestones.length > 0 && (
          <div className="rounded-lg border bg-muted/20">
            <div className="flex items-center gap-2 border-b px-4 py-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
              <span>Past</span>
            </div>
            <Accordion type="multiple" className="px-4">
              {pastMilestones.map((m) => (
                <AccordionItem key={m.key} value={m.key} className="border-0">
                  <AccordionTrigger className="py-2 text-xs font-medium hover:no-underline">
                    <div className="flex flex-1 items-center gap-2 text-left text-muted-foreground">
                      <CheckCircle2 className="size-3 shrink-0 text-emerald-600" aria-hidden />
                      <span className="font-medium text-foreground">{m.title}</span>
                      <span className="text-xs">·</span>
                      <span className="truncate text-xs">{m.pastSummary}</span>
                    </div>
                  </AccordionTrigger>
                  <AccordionContent>
                    {/* Reuse the same MilestoneSection layout used for the
                      current milestone - the steps list, sub-status badge,
                      and any inline doc actions all render the same way.
                      `isActive={false}` keeps the NEXT-STEP pill off. */}
                    <MilestoneSection
                      title={m.title}
                      icon={m.icon}
                      status={m.status}
                      isPending={stageMutation.isPending}
                      onAdvance={advance}
                      currentStage={interest.pipelineStage}
                      isActive={false}
                      steps={m.steps}
                      footer={m.footer}
                    />
                  </AccordionContent>
                </AccordionItem>
              ))}
            </Accordion>
          </div>
        )}

        {currentMilestones.length > 0 && (
          <div
            className={cn(
              'grid grid-cols-1 gap-4',
              currentMilestones.length === 1 ? '' : 'lg:grid-cols-2',
            )}
          >
            {currentMilestones.map((m) => (
              <MilestoneSection
                key={m.key}
                title={m.title}
                icon={m.icon}
                status={m.status}
                isPending={stageMutation.isPending}
                onAdvance={advance}
                currentStage={interest.pipelineStage}
                isActive={activeMilestone === m.key}
                steps={m.steps}
                footer={m.footer}
              />
            ))}
          </div>
        )}

        {futureMilestones.length > 0 && (
          <FutureMilestones
            milestones={futureMilestones}
            stageMutation={stageMutation}
            advance={advance}
            activeMilestone={activeMilestone}
            currentStage={interest.pipelineStage}
          />
        )}

        {/* Payments section relocated below milestones (was above): the
          deposit-tracking surface is reference/history, not the rep's
          primary focus once they're at Reservation+. The active
          milestone above carries the actionable copy. */}
        {showPaymentsSection ? (
          <PaymentsSection
            interestId={interestId}
            depositExpectedAmount={interest.depositExpectedAmount ?? null}
            depositExpectedCurrency={interest.depositExpectedCurrency ?? null}
          />
        ) : null}

        <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
          {/* Lead & Source (editable) */}
          <div className="space-y-1">
            <h3 className="text-sm font-medium mb-2">Lead</h3>
            <dl>
              <EditableRow label="Lead Category">
                <InlineEditableField
                  variant="select"
                  options={LEAD_CATEGORY_OPTIONS}
                  value={interest.leadCategory}
                  onSave={save('leadCategory')}
                />
              </EditableRow>
              <EditableRow label="Source">
                <InlineEditableField
                  variant="select"
                  options={SOURCES.map((s) => ({ value: s.value, label: s.label }))}
                  value={interest.source}
                  onSave={save('source')}
                />
              </EditableRow>
            </dl>
          </div>

          {/* Contact  -  client's primary email + phone (from the linked client
            record) AND the first/last-contact activity dates from the
            contact log. Phone is rendered via libphonenumber-js's
            international formatter so `+33633219796` reads as
            `+33 6 33 21 97 96` (matches the canonical client-page display).
            Both email + phone are click-to-edit: the PATCH flows to the
            underlying client_contacts row (resolved via the
            `*ContactId` fields surfaced by the interest read). */}
          <div className="space-y-1">
            <h3 className="text-sm font-medium mb-2">Contact</h3>
            <dl>
              <EditableRow label="Email" historyPath="client.primaryEmail" inheritedFrom="client">
                {interest.clientId ? (
                  <ClientChannelEditor
                    clientId={interest.clientId}
                    channel="email"
                    primaryContactId={interest.clientPrimaryEmailContactId ?? null}
                    primaryValue={interest.clientPrimaryEmail ?? null}
                    invalidateKeys={[['interest', interest.id]]}
                  />
                ) : (
                  <span className="text-muted-foreground">-</span>
                )}
              </EditableRow>
              <EditableRow label="Phone" historyPath="client.primaryPhone" inheritedFrom="client">
                {interest.clientId ? (
                  <ClientChannelEditor
                    clientId={interest.clientId}
                    channel="phone"
                    primaryContactId={interest.clientPrimaryPhoneContactId ?? null}
                    primaryValue={interest.clientPrimaryPhone ?? null}
                    primaryValueE164={interest.clientPrimaryPhoneE164 ?? null}
                    primaryValueCountry={interest.clientPrimaryPhoneCountry ?? null}
                    invalidateKeys={[['interest', interest.id]]}
                  />
                ) : (
                  <span className="text-muted-foreground">-</span>
                )}
              </EditableRow>
              {interest.dateFirstContact || interest.dateLastContact ? (
                <>
                  <InfoRow label="First Contact" value={formatDate(interest.dateFirstContact)} />
                  <InfoRow label="Last Contact" value={formatDate(interest.dateLastContact)} />
                </>
              ) : (
                <p className="mt-1 text-xs text-muted-foreground italic">
                  No contact activity logged yet - log a call, email, or meeting from the Contact
                  log tab to start tracking.
                </p>
              )}
              {interest.reservationStatus ? (
                <InfoRow label="Reservation" value={interest.reservationStatus} />
              ) : null}
            </dl>
          </div>

          {/* Berth requirements  -  desired length / width / draft. Editable
            inline so reps can capture or correct a buyer's needs without
            leaving the Overview tab. These values drive the auto-tick on
            the "Dimensions confirmed" qualification row + the
            BerthRecommenderPanel rankings below. */}
          <div className="space-y-1">
            <h3 className="text-sm font-medium mb-2">Berth requirements</h3>
            {(() => {
              // Honour the interest's `desiredLengthUnit` so a deal whose rep
              // entered metric values doesn't render labelled "(ft)" with
              // empty inputs. On save we patch BOTH the chosen-unit column
              // and the canonical counterpart so downstream surfaces
              // (recommender, EOI merge fields) stay in lockstep.
              const unitIsM = interest.desiredLengthUnit === 'm';
              const FT_PER_M = 3.28084;
              const toCounterpart = (v: string | null): string | null => {
                if (!v) return null;
                const n = Number(v);
                if (!Number.isFinite(n)) return null;
                return unitIsM ? (n * FT_PER_M).toFixed(4) : (n / FT_PER_M).toFixed(4);
              };
              // Inheritance: when a desired_* field is blank but the linked
              // yacht carries that measurement, render a small "from yacht"
              // pill alongside the empty inline field. We don't auto-copy
              // the yacht's value into the interest (the rep may want a
              // different deal-specific target) - the pill makes it
              // discoverable + a single click on the pill copies it across.
              const yachtDims = interest.yachtDimensions ?? null;
              const yachtVal = (axis: 'length' | 'width' | 'draft'): string | null => {
                if (!yachtDims) return null;
                if (unitIsM) {
                  return axis === 'length'
                    ? yachtDims.lengthM
                    : axis === 'width'
                      ? yachtDims.widthM
                      : yachtDims.draftM;
                }
                return axis === 'length'
                  ? yachtDims.lengthFt
                  : axis === 'width'
                    ? yachtDims.widthFt
                    : yachtDims.draftFt;
              };
              const onSavePair =
                (
                  primary: InterestPatchField,
                  counterpart: InterestPatchField,
                ): ((next: string | null) => Promise<void>) =>
                async (next: string | null) => {
                  await mutation.mutateAsync({
                    [primary]: next,
                    [counterpart]: toCounterpart(next),
                  });
                  // Surface a write-back CTA: if the saved value differs
                  // from the yacht's current value AND the yacht has a
                  // value for this axis, prompt the rep to update the
                  // yacht record too. The toast keeps the action
                  // non-modal so it never interrupts a flow.
                  const axis: 'length' | 'width' | 'draft' = primary.includes('Length')
                    ? 'length'
                    : primary.includes('Width')
                      ? 'width'
                      : 'draft';
                  const yachtCurrent = yachtVal(axis);
                  if (next && yachtCurrent !== null && next !== yachtCurrent && interest.yachtId) {
                    const yachtId = interest.yachtId;
                    const yachtField = unitIsM
                      ? axis === 'length'
                        ? 'lengthM'
                        : axis === 'width'
                          ? 'widthM'
                          : 'draftM'
                      : axis === 'length'
                        ? 'lengthFt'
                        : axis === 'width'
                          ? 'widthFt'
                          : 'draftFt';
                    const counterpartField = unitIsM
                      ? axis === 'length'
                        ? 'lengthFt'
                        : axis === 'width'
                          ? 'widthFt'
                          : 'draftFt'
                      : axis === 'length'
                        ? 'lengthM'
                        : axis === 'width'
                          ? 'widthM'
                          : 'draftM';
                    toast(`Update yacht ${axis} too?`, {
                      description: `Yacht is ${yachtCurrent}${unitLabel}; this deal is now ${next}${unitLabel}.`,
                      action: {
                        label: 'Update yacht',
                        onClick: async () => {
                          await apiFetch(`/api/v1/yachts/${yachtId}`, {
                            method: 'PATCH',
                            body: {
                              [yachtField]: next,
                              [counterpartField]: toCounterpart(next),
                            },
                          });
                          toast.success('Yacht record updated.');
                        },
                      },
                    });
                  }
                };
              const unitLabel = unitIsM ? 'm' : 'ft';
              // The yacht-source pill: shown next to a desired_* input
              // whenever the interest's value is blank but the yacht has
              // a value to inherit. Click copies the yacht's value into
              // the interest via the same patch path. Rendered via a
              // render helper (returns JSX, not a Component) so React
              // doesn't reset the inner state on each parent render.
              const renderInheritedPill = (
                axis: 'length' | 'width' | 'draft',
                primary: InterestPatchField,
                counterpart: InterestPatchField,
              ) => {
                const v = yachtVal(axis);
                if (!v) return null;
                return (
                  <button
                    type="button"
                    onClick={() =>
                      mutation.mutateAsync({
                        [primary]: v,
                        [counterpart]: toCounterpart(v),
                      })
                    }
                    title={`Inherit ${v}${unitLabel} from the linked yacht`}
                    className="ms-2 inline-flex items-center gap-1 rounded-md border border-sky-200 bg-sky-50 px-1.5 py-0.5 text-xs text-sky-800 hover:bg-sky-100"
                  >
                    <span aria-hidden>↩</span>
                    <span>
                      {v}
                      {unitLabel} from yacht
                    </span>
                  </button>
                );
              };
              return (
                <dl>
                  <EditableRow label={`Desired length (${unitLabel})`}>
                    <div className="flex items-center">
                      <InlineEditableField
                        value={
                          unitIsM
                            ? (interest.desiredLengthM ?? null)
                            : (interest.desiredLengthFt ?? null)
                        }
                        onSave={onSavePair(
                          unitIsM ? 'desiredLengthM' : 'desiredLengthFt',
                          unitIsM ? 'desiredLengthFt' : 'desiredLengthM',
                        )}
                        placeholder={unitIsM ? 'e.g. 18' : 'e.g. 60'}
                        emptyText=" - "
                      />
                      {!(unitIsM ? interest.desiredLengthM : interest.desiredLengthFt)
                        ? renderInheritedPill(
                            'length',
                            unitIsM ? 'desiredLengthM' : 'desiredLengthFt',
                            unitIsM ? 'desiredLengthFt' : 'desiredLengthM',
                          )
                        : null}
                    </div>
                  </EditableRow>
                  <EditableRow label={`Desired width (${unitLabel})`}>
                    <div className="flex items-center">
                      <InlineEditableField
                        value={
                          unitIsM
                            ? (interest.desiredWidthM ?? null)
                            : (interest.desiredWidthFt ?? null)
                        }
                        onSave={onSavePair(
                          unitIsM ? 'desiredWidthM' : 'desiredWidthFt',
                          unitIsM ? 'desiredWidthFt' : 'desiredWidthM',
                        )}
                        placeholder={unitIsM ? 'e.g. 7.5' : 'e.g. 25'}
                        emptyText=" - "
                      />
                      {!(unitIsM ? interest.desiredWidthM : interest.desiredWidthFt)
                        ? renderInheritedPill(
                            'width',
                            unitIsM ? 'desiredWidthM' : 'desiredWidthFt',
                            unitIsM ? 'desiredWidthFt' : 'desiredWidthM',
                          )
                        : null}
                    </div>
                  </EditableRow>
                  <EditableRow label={`Desired draft (${unitLabel})`}>
                    <div className="flex items-center">
                      <InlineEditableField
                        value={
                          unitIsM
                            ? (interest.desiredDraftM ?? null)
                            : (interest.desiredDraftFt ?? null)
                        }
                        onSave={onSavePair(
                          unitIsM ? 'desiredDraftM' : 'desiredDraftFt',
                          unitIsM ? 'desiredDraftFt' : 'desiredDraftM',
                        )}
                        placeholder={unitIsM ? 'e.g. 2' : 'e.g. 6'}
                        emptyText=" - "
                      />
                      {!(unitIsM ? interest.desiredDraftM : interest.desiredDraftFt)
                        ? renderInheritedPill(
                            'draft',
                            unitIsM ? 'desiredDraftM' : 'desiredDraftFt',
                            unitIsM ? 'desiredDraftFt' : 'desiredDraftM',
                          )
                        : null}
                    </div>
                  </EditableRow>
                </dl>
              );
            })()}
          </div>

          {/* Legacy `interest.reminderEnabled` / `reminderDays` / `reminderLastFired`
            still drive the auto-follow-up worker (`processFollowUpReminders`),
            but the Overview surface for them is hidden: the REMINDERS
            section below shows the full reminders table and the bell-in-
            header surfaces active counts. Removing the duplicate read-only
            panel cleans Overview without affecting the backend job. */}

          {/* Most-recent threaded note teaser. Saves a click into the Notes
            tab when the rep just wants to peek at "what was discussed last."
            Always rendered now that the redundant `interests.notes` blob is
            gone  -  falls back to an empty-state prompt so reps still have an
            obvious entry point to the Notes tab from Overview. */}
          <div className="space-y-1 md:col-span-2">
            <div className="mb-2 flex items-center justify-between">
              <h3 className="text-sm font-medium">Latest note</h3>
              <Link
                href={`/${portSlug}/interests/${interestId}?tab=notes`}
                className="text-xs font-medium text-primary hover:underline"
              >
                {interest.recentNote
                  ? `View all${interest.notesCount && interest.notesCount > 1 ? ` ${interest.notesCount}` : ''}`
                  : 'Add note'}
              </Link>
            </div>
            {interest.recentNote ? (
              <div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm">
                <p className="line-clamp-3 whitespace-pre-wrap text-foreground/90">
                  {interest.recentNote.content}
                </p>
                <p className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
                  {/* Stage pill = the deal's current stage. Source-of-truth
                    interpretation: the note is about the deal as it
                    stands today; reading it on Overview, "current stage"
                    answers the implicit "where in the deal is this?". A
                    historical "stage-at-note-time" lookup would need an
                    audit_logs read per teaser render - over-engineered for
                    a context hint. */}
                  <span
                    className={cn(
                      'inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium',
                      STAGE_BADGE[interest.pipelineStage as PipelineStage] ??
                        'bg-muted text-muted-foreground',
                    )}
                  >
                    {stageLabel(interest.pipelineStage)}
                  </span>
                  <span>
                    {formatDistanceToNowStrict(new Date(interest.recentNote.createdAt), {
                      addSuffix: true,
                    })}
                    {interest.recentNote.authorId
                      ? ` · ${
                          interest.recentNote.authorId === 'system'
                            ? 'system'
                            : (interest.recentNote.authorName ?? 'Unknown')
                        }`
                      : ''}
                  </span>
                </p>
              </div>
            ) : (
              <div className="rounded-md border border-dashed border-border bg-muted/10 px-3 py-2 text-xs text-muted-foreground">
                No notes yet.
              </div>
            )}
          </div>

          <InlineTagEditor
            heading="Tags"
            wrapperClassName="md:col-span-2"
            endpoint={`/api/v1/interests/${interestId}/tags`}
            currentTags={interest.tags ?? []}
            invalidateKey={['interests', interestId]}
          />

          <div className="md:col-span-2">
            <RemindersInline interestId={interestId} />
          </div>
        </div>

        {/* Linked berths (plan §5.5) - shown ABOVE the recommender so reps see
          what's already linked before browsing more options. Each row exposes
          per-berth role-flag toggles and the EOI bypass control (only visible
          once the parent interest's primary EOI is signed). */}
        {/* Won-status wrap-up checklist  -  only renders when this interest's
          outcome is `won`. Surfaces upload slots for the manual paperwork
          that didn't flow through the EOI->Contract chain automatically. */}
        <WonStatusPanel interestId={interestId} outcome={interest.outcome ?? null} />

        {/* Pre-EOI supplemental info request. Sends the client a one-time
          public form pre-filled with what's on file so they can confirm /
          correct details before the EOI is drafted. Hides itself once
          the EOI is signed. */}
        <SupplementalInfoRequestButton interestId={interestId} eoiStatus={interest.eoiStatus} />

        <LinkedBerthsList interestId={interestId} />

        {/* Berth recommender (plan §5.3) — surfaces only when the rep has
          captured at least desired length. Without dimensions the panel's
          guidance card is noise on Overview; the dedicated tab is also
          hidden in the same condition below. */}
        {hasDesiredDims ? (
          <BerthRecommenderPanel
            interestId={interestId}
            desiredLengthFt={toNum(interest.desiredLengthFt)}
            desiredWidthFt={toNum(interest.desiredWidthFt)}
            desiredDraftFt={toNum(interest.desiredDraftFt)}
            desiredUnit={interest.desiredLengthUnit === 'm' ? 'm' : 'ft'}
            linkedBerthCount={interest.linkedBerthCount ?? 0}
          />
        ) : null}
        {confirmDialog}
        {/* Mounted at the Overview level so the EOI milestone's "Generate EOI"
          footer button can launch the dialog without leaving the tab. Same
          dialog component the dedicated EOI tab uses  -  single source of
          truth for the editing/confirmation flow. */}
        <EoiGenerateDialog
          interestId={interestId}
          clientId={clientId}
          open={eoiGenerateOpen}
          onOpenChange={setEoiGenerateOpen}
        />
      </div>
    </FieldHistoryProvider>
  );
}

export function getInterestTabs({
  interestId,
  currentUserId,
  clientId = null,
  interest,
}: InterestTabsOptions): DetailTab[] {
  // The EOI / Contract / Reservation tabs are stage-conditional -
  // each appears only at the stages where the rep is likely to act
  // on it. Hides clutter from later-stage deals where earlier docs
  // are ancient history. Each tab still queries for its own past
  // documents; if a deal regresses the past docs remain accessible
  // via the generic Documents tab.
  const stageIdx = PIPELINE_STAGES.indexOf(interest.pipelineStage as PipelineStage);
  const qualifiedIdx = PIPELINE_STAGES.indexOf('qualified');
  const reservationIdx = PIPELINE_STAGES.indexOf('reservation');
  const depositIdx = PIPELINE_STAGES.indexOf('deposit_paid');
  const contractIdx = PIPELINE_STAGES.indexOf('contract');
  // EOI: from qualified through contract (the deal's whole life past lead-only).
  const showEoiTab = stageIdx >= qualifiedIdx;
  // Reservation: once the EOI is signed onward - the reservation agreement
  // is the v1 step between EOI and deposit. Stays visible through contract
  // so the rep can re-open the signed reservation later.
  const showReservationTab = stageIdx >= reservationIdx;
  // Contract: from deposit_paid onward (deal is committed and the contract
  // becomes the next active document).
  const showContractTab = stageIdx >= depositIdx && stageIdx <= contractIdx;
  // Berth recommendations: surface ONLY when the rep has captured at
  // least one desired dimension on the interest. Without dimensions the
  // recommender has no signal to rank against — it would render the
  // empty "set desired dimensions" guidance card, which the user flagged
  // as noise on the Overview tab AND as a wasted tab in the strip.
  // Hide both surfaces when length is missing (length is the primary
  // ranking input; width / draft fall back to length when null).
  const hasDesiredDims = toNum(interest.desiredLengthFt) !== null;

  const tabs: DetailTab[] = [
    {
      id: 'overview',
      label: 'Overview',
      content: <OverviewTab interestId={interestId} interest={interest} clientId={clientId} />,
    },
    {
      id: 'contact-log',
      label: 'Contact log',
      content: <InterestContactLogTab interestId={interestId} />,
    },
    {
      id: 'notes',
      label: 'Notes',
      badge:
        interest.notesCountAggregated && interest.notesCountAggregated > 0
          ? interest.notesCountAggregated
          : undefined,
      content: (
        <NotesList
          entityType="interests"
          entityId={interestId}
          currentUserId={currentUserId}
          parentInvalidateKey={['interests', interestId]}
        />
      ),
    },
  ];

  if (showEoiTab) {
    tabs.push({
      id: 'eoi',
      label: 'EOI',
      content: <InterestEoiTab interestId={interestId} clientId={clientId} />,
    });
  }

  if (showContractTab) {
    tabs.push({
      id: 'contract',
      label: 'Contract',
      content: <InterestContractTab interestId={interestId} clientId={clientId} />,
    });
  }

  if (showReservationTab) {
    tabs.push({
      id: 'reservation',
      label: 'Reservation',
      content: <InterestReservationTab interestId={interestId} clientId={clientId} />,
    });
  }

  tabs.push(
    {
      id: 'documents',
      label: 'Documents',
      content: <InterestDocumentsTab interestId={interestId} />,
    },
    ...(hasDesiredDims
      ? [
          {
            id: 'recommendations',
            label: 'Berth Recommendations',
            content: (
              <BerthRecommenderPanel
                interestId={interestId}
                desiredLengthFt={toNum(interest.desiredLengthFt)}
                desiredWidthFt={toNum(interest.desiredWidthFt)}
                desiredDraftFt={toNum(interest.desiredDraftFt)}
                desiredUnit={interest.desiredLengthUnit === 'm' ? 'm' : 'ft'}
              />
            ),
          } satisfies DetailTab,
        ]
      : []),
    {
      id: 'activity',
      label: 'Activity',
      content: <InterestTimeline interestId={interestId} />,
    },
  );

  return tabs;
}
