'use client';

import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { AlertTriangle, ExternalLink, FileSignature, Pencil } from 'lucide-react';

import { Checkbox } from '@/components/ui/checkbox';

import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
} from '@/components/ui/sheet';
import { Button } from '@/components/ui/button';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Skeleton } from '@/components/ui/skeleton';
import { apiFetch } from '@/lib/api/client';
import { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/ui-store';
import { Input } from '@/components/ui/input';
import { CountryCombobox } from '@/components/shared/country-combobox';
import { YachtForm } from '@/components/yachts/yacht-form';
import { toast } from 'sonner';
import { toastError } from '@/lib/api/toast-error';

const DOCUMENSO_TEMPLATE_VALUE = 'documenso-template';

interface InAppTemplate {
  id: string;
  name: string;
  description?: string | null;
  templateType: string;
}

interface EoiContextResponse {
  data: {
    client: {
      id: string;
      fullName: string;
      nationality: string | null;
      primaryEmail: string | null;
      primaryPhone: string | null;
      address: {
        street: string;
        city: string;
        subdivision: string;
        postalCode: string;
        country: string;
        countryIso: string;
      } | null;
    };
    yacht: {
      id: string;
      name: string;
      lengthFt: string | null;
      widthFt: string | null;
      draftFt: string | null;
      lengthM: string | null;
      widthM: string | null;
      draftM: string | null;
      /** Which unit the rep originally entered the dimensions in - drives
       *  the toggle's default position. The trio of *Unit columns usually
       *  share a value in practice; we read `lengthUnit` as the
       *  representative. */
      lengthUnit: 'ft' | 'm';
      widthUnit: 'ft' | 'm';
      draftUnit: 'ft' | 'm';
      hullNumber: string | null;
      flag: string | null;
    } | null;
    berth: {
      mooringNumber: string;
      area: string | null;
      lengthFt: string | null;
    } | null;
    eoiBerthRange: string;
    port: { name: string };
    /** Phase 3b - every contact row the dialog renders in its
     *  override comboboxes. Populated by the eoi-context route. */
    available: {
      emails: Array<{ id: string; value: string; isPrimary: boolean; source: string }>;
      phones: Array<{
        id: string;
        value: string;
        isPrimary: boolean;
        channel: 'phone' | 'whatsapp';
        source: string;
      }>;
      addresses: Array<{
        id: string;
        streetAddress: string | null;
        city: string | null;
        subdivisionIso: string | null;
        postalCode: string | null;
        countryIso: string | null;
        isPrimary: boolean;
        source: string;
      }>;
    };
  };
}

/**
 * Phase 3b - per-field override state captured by the dialog. Sent
 * verbatim on the generate-and-sign POST and translated server-side
 * into the documents.override_* columns + (optionally) client_contacts
 * mutations.
 */
interface FieldOverrideState {
  /** When null, no override is active for this field. */
  value: string | null;
  /** The client_contacts.id the value came from (when picked from the
   *  combobox). Null = fresh typed value. */
  contactId: string | null;
  useOnlyForThisEoi: boolean;
  setAsDefault: boolean;
}

/**
 * Phase 3 follow-up - address override state. Treated as one logical
 * field with one pair of checkboxes (intent flags apply to the whole
 * address rather than per-component).
 */
interface AddressOverrideState {
  line1: string;
  line2: string;
  city: string;
  subdivisionIso: string;
  postalCode: string;
  countryIso: string | null;
  /** Existing client_addresses.id when the rep picked one; null = fresh. */
  addressId: string | null;
  useOnlyForThisEoi: boolean;
  setAsDefault: boolean;
}

interface EoiGenerateDialogProps {
  interestId: string;
  /** Used to wire the "Edit on client" deep-link inside the dialog. */
  clientId?: string | null;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

/**
 * Pre-flight preview + generate dialog. Replaces the earlier ✓/✗
 * checklist with the actual values that will be auto-filled into the
 * EOI, so a sales rep can spot a wrong email or missing yacht
 * dimensions before sending the document for signing.
 *
 * Editing is deliberately routed back to the canonical client / yacht
 * detail pages (via "Edit on client" links) rather than allowing
 * inline edits inside the dialog. Why: the EOI's source-of-truth is
 * the underlying records, and forking the edit surface here would
 * silently bypass tag/audit-log/permission flows that the entity
 * detail pages enforce.
 */
export function EoiGenerateDialog({
  interestId,
  clientId,
  open,
  onOpenChange,
}: EoiGenerateDialogProps) {
  const queryClient = useQueryClient();
  const portSlug = useUIStore((s) => s.currentPortSlug);
  const router = useRouter();
  const [isGenerating, setIsGenerating] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [selectedTemplate, setSelectedTemplate] = useState<string>(DOCUMENSO_TEMPLATE_VALUE);
  // Unit picker for the Length/Width/Draft preview row + the values that
  // ship to Documenso. Defaults to whichever side the rep originally typed
  // (drives off the yacht's `lengthUnit` column). Stored as state so the
  // rep can flip ft↔m before generating without losing the underlying data.
  const [dimensionUnit, setDimensionUnit] = useState<'ft' | 'm' | null>(null);
  // Phase 3b - per-field override state. null entries = no override.
  const [emailOverride, setEmailOverride] = useState<FieldOverrideState | null>(null);
  const [phoneOverride, setPhoneOverride] = useState<FieldOverrideState | null>(null);
  const [yachtNameOverride, setYachtNameOverride] = useState<FieldOverrideState | null>(null);
  const [addressOverride, setAddressOverride] = useState<AddressOverrideState | null>(null);
  // Phase 3c - yacht spawn flow.
  const [yachtSpawnOpen, setYachtSpawnOpen] = useState(false);
  // Section 3 (yacht details) inclusion toggle. Defaults to ON; the toggle
  // only renders when a yacht is actually linked (no toggle = always ON,
  // which matches today's behaviour). Set OFF to blank yacht.* + owner.*
  // merge fields on the generated PDF even though the yacht record exists
  // (early-stage clients, multi-berth deals where yacht-dims don't apply,
  // explicit client request to keep Section 3 off the document).
  const [includeYachtDetails, setIncludeYachtDetails] = useState(true);

  // Resolved EOI context - the actual values the document will be
  // auto-filled with. Loaded only while the dialog is open so we don't
  // pay for the join tree on every interest detail page render.
  const {
    data: ctxRes,
    isLoading: ctxLoading,
    error: ctxError,
  } = useQuery<EoiContextResponse>({
    queryKey: ['interests', interestId, 'eoi-context'],
    queryFn: () => apiFetch<EoiContextResponse>(`/api/v1/interests/${interestId}/eoi-context`),
    enabled: open,
    staleTime: 30_000,
    retry: false,
  });

  const ctx = ctxRes?.data;
  // Server-side EOI validators throw `Cannot generate EOI - missing
  // required client details: client name, client email, client address`.
  // Parse that list so the dialog can render an inline fix-it form
  // (no need to bounce out to the client detail page).
  const ctxErrorMessage = ctxError instanceof Error && ctxError.message ? ctxError.message : null;
  const missingFields = useMemo(() => {
    if (!ctxErrorMessage) return new Set<'name' | 'email' | 'address'>();
    const m = ctxErrorMessage.match(/missing required client details:\s*([^.]+)/i);
    if (!m) return new Set<'name' | 'email' | 'address'>();
    const tokens = m[1]!.split(',').map((s) => s.trim().toLowerCase());
    const out = new Set<'name' | 'email' | 'address'>();
    for (const t of tokens) {
      if (t.includes('name')) out.add('name');
      if (t.includes('email')) out.add('email');
      if (t.includes('address')) out.add('address');
    }
    return out;
  }, [ctxErrorMessage]);

  const { data: templatesRes } = useQuery<{ data: InAppTemplate[] }>({
    queryKey: ['document-templates', { templateType: 'eoi', isActive: true }],
    queryFn: () =>
      apiFetch<{ data: InAppTemplate[] }>(
        '/api/v1/document-templates?templateType=eoi&isActive=true',
      ),
    enabled: open,
  });

  // U66 (c) - EOI berth-scope picker. Pulls every linked berth so the
  // rep can confirm signature scope (`isInEoiBundle`) and public-map
  // visibility (`isSpecificInterest`) at the moment of EOI generation
  // - the moment the "which berths does this EOI cover?" question is
  // actually live in their head - instead of relying on them having
  // visited the LinkedBerthsList toggles upstream. Post-(a) defaults
  // (in_bundle=true; specific=primary) mean the picker is mostly
  // already correct; this surface lets them carve exceptions.
  const { data: linkedBerthsRes } = useQuery<{
    data: Array<{
      berthId: string;
      mooringNumber: string | null;
      isPrimary: boolean;
      isInEoiBundle: boolean;
      isSpecificInterest: boolean;
    }>;
  }>({
    queryKey: ['interests', interestId, 'berths'],
    queryFn: () =>
      apiFetch<{
        data: Array<{
          berthId: string;
          mooringNumber: string | null;
          isPrimary: boolean;
          isInEoiBundle: boolean;
          isSpecificInterest: boolean;
        }>;
      }>(`/api/v1/interests/${interestId}/berths`),
    enabled: open,
    staleTime: 30_000,
  });

  // Local draft of the two flags per berth. Initialized from the server
  // values once the query resolves; subsequent server updates after that
  // point are ignored (the rep is actively editing this surface, and we
  // don't want a background refetch to wipe their pending checks).
  const [berthScope, setBerthScope] = useState<
    Map<string, { isInEoiBundle: boolean; isSpecificInterest: boolean }>
  >(new Map());
  const [berthScopeInitialized, setBerthScopeInitialized] = useState(false);

  useEffect(() => {
    if (berthScopeInitialized || !linkedBerthsRes) return;
    const next = new Map<string, { isInEoiBundle: boolean; isSpecificInterest: boolean }>();
    for (const link of linkedBerthsRes.data) {
      next.set(link.berthId, {
        isInEoiBundle: link.isInEoiBundle,
        isSpecificInterest: link.isSpecificInterest,
      });
    }
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setBerthScope(next);

    setBerthScopeInitialized(true);
  }, [linkedBerthsRes, berthScopeInitialized]);

  // Reset the picker when the dialog closes so a re-open against a
  // different interest doesn't show stale rows.
  useEffect(() => {
    if (open) return;
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setBerthScopeInitialized(false);

    setBerthScope(new Map());
  }, [open]);

  function setBerthFlag(
    berthId: string,
    flag: 'isInEoiBundle' | 'isSpecificInterest',
    value: boolean,
  ) {
    setBerthScope((prev) => {
      const next = new Map(prev);
      const current = next.get(berthId) ?? { isInEoiBundle: false, isSpecificInterest: false };
      next.set(berthId, { ...current, [flag]: value });
      return next;
    });
  }

  const inAppTemplates = useMemo(() => templatesRes?.data ?? [], [templatesRes]);
  // Only show the template picker when there's a real choice - the
  // Documenso path is always present, so we show the dropdown once at
  // least one in-app pdf-lib template is configured. Otherwise it's a
  // 1-item select which adds noise.
  const showTemplatePicker = inAppTemplates.length > 0;

  // ─── Inline fix-it form for missing client fields ──────────────────────────
  // Drafted as one piece of local state so a partial save (e.g. address
  // succeeds but email fails) leaves the rest of the inputs untouched.
  const [fixDraft, setFixDraft] = useState<{
    name: string;
    email: string;
    street: string;
    city: string;
    postalCode: string;
    subdivisionIso: string;
    countryIso: string | null;
  }>({
    name: '',
    email: '',
    street: '',
    city: '',
    postalCode: '',
    subdivisionIso: '',
    countryIso: null,
  });
  const [fixSaving, setFixSaving] = useState(false);

  const persistMissingFields = async (): Promise<void> => {
    if (!clientId) {
      toastError(new Error('Client ID missing  -  refresh the page.'));
      return;
    }
    setFixSaving(true);
    try {
      // Issue one PATCH/POST per missing field. Sequential rather than
      // parallel so a downstream failure surfaces a coherent error rather
      // than partial-and-confused state.
      if (missingFields.has('name')) {
        if (!fixDraft.name.trim()) throw new Error('Client name is required.');
        await apiFetch(`/api/v1/clients/${clientId}`, {
          method: 'PATCH',
          body: { fullName: fixDraft.name.trim() },
        });
      }
      if (missingFields.has('email')) {
        if (!fixDraft.email.trim()) throw new Error('Client email is required.');
        await apiFetch(`/api/v1/clients/${clientId}/contacts`, {
          method: 'POST',
          body: { channel: 'email', value: fixDraft.email.trim(), isPrimary: true },
        });
      }
      if (missingFields.has('address')) {
        if (!fixDraft.street.trim()) throw new Error('Street address is required.');
        await apiFetch(`/api/v1/clients/${clientId}/addresses`, {
          method: 'POST',
          body: {
            streetAddress: fixDraft.street.trim(),
            city: fixDraft.city.trim() || null,
            postalCode: fixDraft.postalCode.trim() || null,
            subdivisionIso: fixDraft.subdivisionIso.trim() || null,
            countryIso: fixDraft.countryIso,
            isPrimary: true,
          },
        });
      }
      // Refetch the EOI context so the dialog flips into preview-ready mode.
      // Also bounce caches that downstream surfaces watch (client detail,
      // interest detail) so the rep sees the edits everywhere immediately.
      await queryClient.invalidateQueries({
        queryKey: ['interests', interestId, 'eoi-context'],
      });
      await queryClient.invalidateQueries({ queryKey: ['clients', clientId] });
      await queryClient.invalidateQueries({ queryKey: ['interests', interestId] });
    } catch (err) {
      toastError(err);
    } finally {
      setFixSaving(false);
    }
  };

  async function patchClient(body: Record<string, unknown>) {
    if (!ctx) return;
    await apiFetch(`/api/v1/clients/${ctx.client.id}`, { method: 'PATCH', body });
    queryClient.invalidateQueries({ queryKey: ['interests', interestId, 'eoi-context'] });
  }

  // Required for the EOI's top paragraph (Section 2). Without these
  // the document is unsignable, so generation is blocked.
  //
  // Email is rendered separately below with the Phase 3b override
  // controls (combobox + 2 checkboxes), so it's omitted from the row
  // array here - but its required-met status still gates `requiredMet`
  // via `emailPresent` below.
  const required = ctx
    ? [
        {
          key: 'name',
          label: 'Full name',
          value: ctx.client.fullName,
          present: !!ctx.client.fullName,
          edit: {
            onSave: async (next: string | null) => await patchClient({ fullName: next ?? '' }),
            placeholder: 'Full legal name',
          },
        },
        // Address moved out to <OverridableAddressField> below so it can
        // surface the per-component combobox + 2 checkboxes alongside
        // the canonical preview.
      ]
    : [];

  // Default the dimension toggle to the unit the rep originally typed in
  // (yacht.lengthUnit). We fall back to 'ft' for legacy rows where the
  // unit column was never set.
  const defaultDimensionUnit: 'ft' | 'm' = ctx?.yacht?.lengthUnit ?? 'ft';
  const effectiveDimensionUnit: 'ft' | 'm' = dimensionUnit ?? defaultDimensionUnit;
  const dimensionsForRender = ctx?.yacht
    ? effectiveDimensionUnit === 'ft'
      ? [ctx.yacht.lengthFt, ctx.yacht.widthFt, ctx.yacht.draftFt]
      : [ctx.yacht.lengthM, ctx.yacht.widthM, ctx.yacht.draftM]
    : [];

  // Optional - Section 3 of the EOI. Generation proceeds without them.
  // Yacht-name + phone are rendered separately below with Phase 3b
  // override controls; the remainder show as straight previews.
  const optional = ctx
    ? [
        {
          key: 'dimensions',
          label: `Dimensions (L × W × D, ${effectiveDimensionUnit})`,
          value: ctx.yacht ? dimensionsForRender.map((v) => v ?? ' - ').join(' × ') : null,
        },
        {
          key: 'berth',
          label: 'Berth (primary mooring)',
          value: ctx.berth?.mooringNumber ?? null,
        },
        {
          key: 'berthRange',
          label: 'Berth bundle range',
          value: ctx.eoiBerthRange || null,
        },
      ]
    : [];

  const emailPresent = ctx ? !!(emailOverride?.value ?? ctx.client.primaryEmail) : false;
  // Address is now required-via-override-field; either the canonical
  // address exists, OR the rep has typed line1+country in the override.
  const addressPresent = ctx
    ? !!(addressOverride && addressOverride.line1 && addressOverride.countryIso) ||
      !!ctx.client.address
    : false;
  const requiredMet =
    !!ctx &&
    required.length > 0 &&
    required.every((r) => r.present) &&
    emailPresent &&
    addressPresent;

  async function handleGenerate() {
    if (!requiredMet) return;
    setIsGenerating(true);
    setError(null);
    try {
      // U66 (c) - persist any berth-scope edits BEFORE kicking off the
      // envelope so the EOI/public-map state is consistent with what the
      // rep just confirmed. Diff against the server snapshot so an
      // unchanged scope is a no-op (avoids spurious audit-log rows).
      const initial = new Map<string, { isInEoiBundle: boolean; isSpecificInterest: boolean }>();
      for (const link of linkedBerthsRes?.data ?? []) {
        initial.set(link.berthId, {
          isInEoiBundle: link.isInEoiBundle,
          isSpecificInterest: link.isSpecificInterest,
        });
      }
      const patches: Array<Promise<unknown>> = [];
      for (const [berthId, draft] of berthScope.entries()) {
        const orig = initial.get(berthId);
        if (!orig) continue;
        const body: { isInEoiBundle?: boolean; isSpecificInterest?: boolean } = {};
        if (orig.isInEoiBundle !== draft.isInEoiBundle) body.isInEoiBundle = draft.isInEoiBundle;
        if (orig.isSpecificInterest !== draft.isSpecificInterest)
          body.isSpecificInterest = draft.isSpecificInterest;
        if (Object.keys(body).length > 0) {
          patches.push(
            apiFetch(`/api/v1/interests/${interestId}/berths/${berthId}`, {
              method: 'PATCH',
              body,
            }),
          );
        }
      }
      if (patches.length > 0) await Promise.all(patches);

      const isDocumenso = selectedTemplate === DOCUMENSO_TEMPLATE_VALUE;
      const url = `/api/v1/document-templates/${encodeURIComponent(selectedTemplate)}/generate-and-sign`;
      // Phase 3b - pack the per-field overrides the rep selected. Each
      // is null when untouched; the server validator accepts an absent
      // entry and falls back to the canonical record.
      const overridePayload = (s: FieldOverrideState | null) =>
        s && s.value !== null
          ? {
              value: s.value,
              useOnlyForThisEoi: s.useOnlyForThisEoi,
              setAsDefault: s.setAsDefault,
              ...(s.contactId ? { contactId: s.contactId } : {}),
            }
          : undefined;
      const addressPayload =
        addressOverride && addressOverride.line1 && addressOverride.countryIso
          ? {
              line1: addressOverride.line1,
              line2: addressOverride.line2 || undefined,
              city: addressOverride.city || undefined,
              subdivisionIso: addressOverride.subdivisionIso || undefined,
              postalCode: addressOverride.postalCode || undefined,
              countryIso: addressOverride.countryIso,
              useOnlyForThisEoi: addressOverride.useOnlyForThisEoi,
              setAsDefault: addressOverride.setAsDefault,
              ...(addressOverride.addressId ? { addressId: addressOverride.addressId } : {}),
            }
          : undefined;
      const overrides = {
        clientEmail: overridePayload(emailOverride),
        clientPhone: overridePayload(phoneOverride),
        yachtName: overridePayload(yachtNameOverride),
        clientAddress: addressPayload,
      };
      const hasAnyOverride =
        overrides.clientEmail ||
        overrides.clientPhone ||
        overrides.yachtName ||
        overrides.clientAddress;

      await apiFetch(url, {
        method: 'POST',
        body: {
          interestId,
          pathway: isDocumenso ? 'documenso-template' : 'inapp',
          // Signers derived server-side from EOI context for both pathways.
          signers: [],
          // Dimension unit chosen in the drawer's toggle - drives which
          // side (ft|m) of the yacht's stored dimensions flows into the
          // EOI's Length/Width/Draft formValues. Defaults server-side to
          // the yacht's own `lengthUnit` column when unspecified.
          dimensionUnit: effectiveDimensionUnit,
          ...(hasAnyOverride ? { overrides } : {}),
          // Only send the flag when the rep explicitly opted out; default
          // (toggle ON) keeps existing behaviour without bloating the body.
          ...(ctx?.yacht && !includeYachtDetails ? { includeYachtDetails: false } : {}),
        },
      });
      // Bounce every cache that surfaces the interest's EOI state so the
      // Overview tab flips immediately from "Generate EOI" prompt to
      // "EOI sent / awaiting signatures", the EOI tab picks up the new
      // signers row, and the timeline reflects the just-stamped milestone.
      await Promise.all([
        queryClient.invalidateQueries({
          predicate: (q) => q.queryKey[0] === 'documents',
        }),
        queryClient.invalidateQueries({ queryKey: ['interests', interestId] }),
        queryClient.invalidateQueries({ queryKey: ['interests', interestId, 'eoi-context'] }),
        queryClient.invalidateQueries({ queryKey: ['interests', interestId, 'timeline'] }),
        queryClient.invalidateQueries({ queryKey: ['interests', interestId, 'berths'] }),
      ]);
      toast.success(
        isDocumenso ? 'EOI generated and sent for signature.' : 'EOI generated. Ready to send.',
        portSlug && interestId
          ? {
              action: {
                label: 'View EOI',
                onClick: () => router.push(`/${portSlug}/interests/${interestId}?tab=eoi` as never),
              },
            }
          : undefined,
      );
      onOpenChange(false);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to generate EOI');
    } finally {
      setIsGenerating(false);
    }
  }

  return (
    <Sheet open={open} onOpenChange={onOpenChange}>
      <SheetContent className="w-full sm:max-w-2xl lg:max-w-4xl overflow-y-auto">
        <SheetHeader>
          <SheetTitle className="flex items-center gap-2">
            <FileSignature className="size-4" aria-hidden />
            Generate Expression of Interest
          </SheetTitle>
          <SheetDescription>
            Review the values that will be auto-filled. Edit anything inline - changes save back to
            the client / interest record automatically. The EOI is generated once everything looks
            right.
          </SheetDescription>
        </SheetHeader>

        <div className="space-y-4 py-4">
          {showTemplatePicker && (
            <div className="space-y-2">
              <Label htmlFor="eoi-template">Template</Label>
              <Select value={selectedTemplate} onValueChange={setSelectedTemplate}>
                <SelectTrigger id="eoi-template">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value={DOCUMENSO_TEMPLATE_VALUE}>
                    Standard EOI - sent for e-signature (recommended)
                  </SelectItem>
                  {inAppTemplates.map((t) => (
                    <SelectItem key={t.id} value={t.id}>
                      {t.name}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
          )}

          {ctxLoading ? (
            <div className="space-y-2">
              <Skeleton className="h-4 w-full" aria-hidden />
              <Skeleton className="h-4 w-3/4" aria-hidden />
              <Skeleton className="h-4 w-2/3" aria-hidden />
            </div>
          ) : ctx ? (
            <div className="space-y-3 rounded-md border bg-muted/20 p-3">
              <div className="space-y-1">
                <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                  Required (Section 2 of the EOI)
                </p>
                <dl className="space-y-1.5">
                  {required.map((row) => (
                    <PreviewRow
                      key={row.key}
                      label={row.label}
                      value={row.value}
                      missing={!row.present}
                      edit={row.edit}
                    />
                  ))}
                  <OverridableContactField
                    label="Email address"
                    canonicalValue={ctx.client.primaryEmail ?? null}
                    canonicalContactId={ctx.available.emails.find((e) => e.isPrimary)?.id ?? null}
                    options={ctx.available.emails}
                    override={emailOverride}
                    onChange={setEmailOverride}
                    missing={!emailPresent}
                  />
                  <OverridableAddressField
                    canonical={ctx.client.address}
                    canonicalAddressId={
                      ctx.available.addresses.find((a) => a.isPrimary)?.id ?? null
                    }
                    options={ctx.available.addresses}
                    override={addressOverride}
                    onChange={setAddressOverride}
                    missing={!addressPresent}
                  />
                </dl>
              </div>
              <div className="space-y-1 border-t pt-2">
                <div className="flex items-center justify-between gap-2">
                  <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                    Section 3 - yacht details
                  </p>
                  <div className="flex items-center gap-2">
                    {ctx.yacht ? (
                      <label className="inline-flex cursor-pointer items-center gap-1.5 text-xs text-muted-foreground">
                        <input
                          type="checkbox"
                          checked={includeYachtDetails}
                          onChange={(e) => setIncludeYachtDetails(e.target.checked)}
                          className="h-3.5 w-3.5 cursor-pointer accent-primary"
                          aria-label="Include yacht details on the EOI"
                        />
                        <span>Include on EOI</span>
                      </label>
                    ) : null}
                    {ctx.yacht && includeYachtDetails ? (
                      <div className="inline-flex rounded-md border bg-muted/30 p-0.5 text-xs">
                        <button
                          type="button"
                          onClick={() => setDimensionUnit('ft')}
                          className={
                            'rounded px-2 py-0.5 transition-colors ' +
                            (effectiveDimensionUnit === 'ft'
                              ? 'bg-background font-medium shadow-sm'
                              : 'text-muted-foreground hover:text-foreground')
                          }
                          aria-pressed={effectiveDimensionUnit === 'ft'}
                        >
                          ft
                        </button>
                        <button
                          type="button"
                          onClick={() => setDimensionUnit('m')}
                          className={
                            'rounded px-2 py-0.5 transition-colors ' +
                            (effectiveDimensionUnit === 'm'
                              ? 'bg-background font-medium shadow-sm'
                              : 'text-muted-foreground hover:text-foreground')
                          }
                          aria-pressed={effectiveDimensionUnit === 'm'}
                        >
                          m
                        </button>
                      </div>
                    ) : null}
                  </div>
                </div>
                {ctx.yacht && !includeYachtDetails ? (
                  <p className="text-xs text-muted-foreground">
                    Yacht details will be omitted from the EOI even though a yacht is linked.
                  </p>
                ) : null}
                <dl className="space-y-1.5">
                  <div className="flex items-start gap-2">
                    <div className="flex-1">
                      <OverridableContactField
                        label="Yacht name"
                        canonicalValue={ctx.yacht?.name ?? null}
                        canonicalContactId={null}
                        options={[]}
                        override={yachtNameOverride}
                        onChange={setYachtNameOverride}
                      />
                    </div>
                    {clientId ? (
                      <button
                        type="button"
                        onClick={() => setYachtSpawnOpen(true)}
                        className="mt-0.5 shrink-0 text-xs text-primary hover:underline"
                        title="Create a new yacht linked to this client"
                      >
                        + New yacht
                      </button>
                    ) : null}
                  </div>
                  {optional.map((row) => (
                    <PreviewRow key={row.key} label={row.label} value={row.value} />
                  ))}
                  <OverridableContactField
                    label="Phone"
                    canonicalValue={ctx.client.primaryPhone ?? null}
                    canonicalContactId={ctx.available.phones.find((p) => p.isPrimary)?.id ?? null}
                    options={ctx.available.phones}
                    override={phoneOverride}
                    onChange={setPhoneOverride}
                  />
                </dl>
              </div>
              {linkedBerthsRes && linkedBerthsRes.data.length > 0 ? (
                <div className="border-t pt-2 space-y-2">
                  <div>
                    <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                      EOI scope ({linkedBerthsRes.data.length} linked berth
                      {linkedBerthsRes.data.length === 1 ? '' : 's'})
                    </p>
                    <p className="text-xs text-muted-foreground">
                      Confirm signature scope and public-map visibility for each berth before
                      generating. Defaults reflect what&apos;s saved on the interest.
                    </p>
                  </div>
                  <div className="rounded-md border bg-muted/20 divide-y">
                    {linkedBerthsRes.data.map((link) => {
                      const draft = berthScope.get(link.berthId) ?? {
                        isInEoiBundle: link.isInEoiBundle,
                        isSpecificInterest: link.isSpecificInterest,
                      };
                      return (
                        <div
                          key={link.berthId}
                          className="grid grid-cols-[1fr_auto_auto] items-center gap-3 px-3 py-2"
                        >
                          <div className="flex items-center gap-2 min-w-0">
                            <span className="font-mono text-sm">{link.mooringNumber ?? '-'}</span>
                            {link.isPrimary ? (
                              <span className="text-xs uppercase tracking-wide text-primary">
                                Primary
                              </span>
                            ) : null}
                          </div>
                          <label
                            className={
                              link.isPrimary
                                ? 'flex items-center gap-1.5 text-xs cursor-not-allowed opacity-70'
                                : 'flex items-center gap-1.5 text-xs cursor-pointer'
                            }
                            title={
                              link.isPrimary
                                ? 'Primary berth is always included in the EOI bundle.'
                                : undefined
                            }
                          >
                            <Checkbox
                              checked={link.isPrimary ? true : draft.isInEoiBundle}
                              disabled={link.isPrimary}
                              onCheckedChange={(v) => {
                                if (link.isPrimary) return;
                                setBerthFlag(link.berthId, 'isInEoiBundle', v === true);
                              }}
                            />
                            <span>In EOI</span>
                          </label>
                          <label className="flex items-center gap-1.5 text-xs cursor-pointer">
                            <Checkbox
                              checked={draft.isSpecificInterest}
                              onCheckedChange={(v) =>
                                setBerthFlag(link.berthId, 'isSpecificInterest', v === true)
                              }
                            />
                            <span>Public map</span>
                          </label>
                        </div>
                      );
                    })}
                  </div>
                  <p className="text-xs text-muted-foreground">
                    <strong>In EOI</strong>: covered by this signed envelope.{' '}
                    <strong>Public map</strong>: shown as &quot;Under Offer&quot; on the marketing
                    site.
                  </p>
                </div>
              ) : null}
              {portSlug && clientId && (
                <div className="border-t pt-2 space-y-1">
                  <p className="text-xs text-muted-foreground">
                    Editing name / yacht name above patches the underlying records directly. For
                    phone, address, or to manage linked berths, jump to the canonical page:
                  </p>
                  <div className="flex flex-wrap gap-3">
                    <Link
                      // eslint-disable-next-line @typescript-eslint/no-explicit-any
                      href={`/${portSlug}/clients/${clientId}` as any}
                      className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
                      onClick={() => onOpenChange(false)}
                    >
                      <Pencil className="size-3" aria-hidden />
                      Edit client details
                      <ExternalLink className="size-3" aria-hidden />
                    </Link>
                    <Link
                      // eslint-disable-next-line @typescript-eslint/no-explicit-any
                      href={`/${portSlug}/interests/${interestId}` as any}
                      className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
                      onClick={() => onOpenChange(false)}
                    >
                      <Pencil className="size-3" aria-hidden />
                      Manage linked berths
                      <ExternalLink className="size-3" aria-hidden />
                    </Link>
                  </div>
                </div>
              )}
            </div>
          ) : missingFields.size > 0 && clientId ? (
            <div className="rounded-md border border-amber-200 bg-amber-50/60 p-3 space-y-3">
              <div className="space-y-0.5">
                <p className="text-xs font-medium text-amber-900">
                  Missing required client details
                </p>
                <p className="text-xs text-amber-800/80">
                  Fill the fields below - they&apos;ll be saved to the client&apos;s record before
                  the EOI renders.
                </p>
              </div>
              <div className="space-y-3">
                {missingFields.has('name') && (
                  <div className="space-y-1">
                    <Label htmlFor="fix-name" className="text-xs">
                      Client full name
                    </Label>
                    <Input
                      id="fix-name"
                      value={fixDraft.name}
                      onChange={(e) => setFixDraft((d) => ({ ...d, name: e.target.value }))}
                      placeholder="Jane Smith"
                    />
                  </div>
                )}
                {missingFields.has('email') && (
                  <div className="space-y-1">
                    <Label htmlFor="fix-email" className="text-xs">
                      Client email
                    </Label>
                    <Input
                      id="fix-email"
                      type="email"
                      value={fixDraft.email}
                      onChange={(e) => setFixDraft((d) => ({ ...d, email: e.target.value }))}
                      placeholder="jane@example.com"
                    />
                  </div>
                )}
                {missingFields.has('address') && (
                  <div className="space-y-2">
                    <div className="space-y-1">
                      <Label htmlFor="fix-street" className="text-xs">
                        Street address
                      </Label>
                      <Input
                        id="fix-street"
                        value={fixDraft.street}
                        onChange={(e) => setFixDraft((d) => ({ ...d, street: e.target.value }))}
                        placeholder="123 Marina Way"
                      />
                    </div>
                    <div className="grid grid-cols-2 gap-2">
                      <div className="space-y-1">
                        <Label htmlFor="fix-city" className="text-xs">
                          City
                        </Label>
                        <Input
                          id="fix-city"
                          value={fixDraft.city}
                          onChange={(e) => setFixDraft((d) => ({ ...d, city: e.target.value }))}
                          placeholder="Athens"
                        />
                      </div>
                      <div className="space-y-1">
                        <Label htmlFor="fix-postal" className="text-xs">
                          Postal code
                        </Label>
                        <Input
                          id="fix-postal"
                          value={fixDraft.postalCode}
                          onChange={(e) =>
                            setFixDraft((d) => ({ ...d, postalCode: e.target.value }))
                          }
                          placeholder="98000"
                        />
                      </div>
                    </div>
                    <div className="grid grid-cols-2 gap-2">
                      <div className="space-y-1">
                        <Label htmlFor="fix-region" className="text-xs">
                          Region / State <span className="text-muted-foreground">(optional)</span>
                        </Label>
                        <Input
                          id="fix-region"
                          value={fixDraft.subdivisionIso}
                          onChange={(e) =>
                            setFixDraft((d) => ({ ...d, subdivisionIso: e.target.value }))
                          }
                          placeholder="ISO-3166-2 e.g. US-CA"
                        />
                      </div>
                      <div className="space-y-1">
                        <Label className="text-xs">Country</Label>
                        <CountryCombobox
                          value={fixDraft.countryIso}
                          onChange={(iso) =>
                            setFixDraft((d) => ({ ...d, countryIso: iso ?? null }))
                          }
                        />
                      </div>
                    </div>
                  </div>
                )}
              </div>
              <div className="flex flex-wrap items-center justify-end gap-2">
                <Button
                  type="button"
                  size="sm"
                  onClick={() => void persistMissingFields()}
                  disabled={fixSaving}
                >
                  {fixSaving ? 'Saving…' : 'Save & preview EOI'}
                </Button>
              </div>
            </div>
          ) : (
            <div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 space-y-1">
              {ctxErrorMessage ? (
                <p className="font-medium">{ctxErrorMessage}</p>
              ) : (
                <p>
                  Couldn&apos;t load the EOI preview data. Try closing and reopening the dialog.
                </p>
              )}
            </div>
          )}

          {!ctxLoading && ctx && !requiredMet && (
            <p className="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
              <AlertTriangle className="size-3.5 shrink-0 mt-0.5" aria-hidden />
              Add the missing required details on the client&apos;s record before generating the
              EOI.
            </p>
          )}
        </div>

        {error && <p className="text-sm text-destructive">{error}</p>}

        <SheetFooter>
          <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isGenerating}>
            Cancel
          </Button>
          <Button onClick={handleGenerate} disabled={!requiredMet || isGenerating || ctxLoading}>
            {isGenerating ? 'Generating…' : 'Generate EOI'}
          </Button>
        </SheetFooter>
      </SheetContent>

      {/* Phase 3c  -  nested yacht-spawn Sheet. Pre-selects the linked
          client as owner so the rep only types the yacht-specific
          fields. After save, PATCH the interest with the new yachtId so
          the EOI's yacht block populates without a manual re-link. */}
      {clientId ? (
        <YachtForm
          open={yachtSpawnOpen}
          onOpenChange={setYachtSpawnOpen}
          initialOwner={{ type: 'client', id: clientId }}
          createExtras={{ source: 'eoi-generated' }}
          onCreated={async (created) => {
            try {
              await apiFetch(`/api/v1/interests/${interestId}`, {
                method: 'PATCH',
                body: { yachtId: created.id },
              });
              await queryClient.invalidateQueries({
                queryKey: ['interests', interestId, 'eoi-context'],
              });
              await queryClient.invalidateQueries({ queryKey: ['interests', interestId] });
            } catch (err) {
              toastError(err);
            }
          }}
        />
      ) : null}
    </Sheet>
  );
}

function PreviewRow({
  label,
  value,
  missing = false,
  edit,
}: {
  label: string;
  value: string | null;
  missing?: boolean;
  /** When provided, renders a pencil affordance that opens an inline editor.
   *  The save handler is owned by the row config so each field can hit the
   *  right API (clients PATCH, yachts PATCH, …). */
  edit?: {
    onSave: (next: string | null) => Promise<void>;
    variant?: 'text' | 'country';
    placeholder?: string;
  };
}) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(value ?? '');
  const [saving, setSaving] = useState(false);

  async function commit(next: string) {
    const trimmed = next.trim();
    if (!edit) return;
    if (trimmed === (value ?? '')) {
      setEditing(false);
      return;
    }
    setSaving(true);
    try {
      await edit.onSave(trimmed === '' ? null : trimmed);
      setEditing(false);
    } catch (err) {
      toastError(err);
    } finally {
      setSaving(false);
    }
  }

  return (
    <div className="flex items-baseline gap-2 text-sm">
      <dt className="w-32 shrink-0 text-xs text-muted-foreground">{label}</dt>
      <dd
        className={cn(
          'flex-1 wrap-break-word inline-flex items-center gap-2',
          missing
            ? 'text-rose-700 font-medium'
            : value
              ? 'text-foreground'
              : 'text-muted-foreground italic',
        )}
      >
        {edit && editing ? (
          edit.variant === 'country' ? (
            <CountryCombobox
              value={value}
              onChange={(iso) => void commit(iso ?? '')}
              defaultOpen
              onOpenChange={(o) => !o && setEditing(false)}
              compact={false}
              className="h-7 w-full"
            />
          ) : (
            <Input
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === 'Enter') void commit(draft);
                if (e.key === 'Escape') {
                  setDraft(value ?? '');
                  setEditing(false);
                }
              }}
              onBlur={() => !saving && void commit(draft)}
              placeholder={edit.placeholder}
              autoFocus
              disabled={saving}
              className="h-7 text-sm"
            />
          )
        ) : (
          <>
            <span className="flex-1">
              {value ?? (missing ? 'Missing  -  required' : 'Not set')}
            </span>
            {edit ? (
              <button
                type="button"
                onClick={() => {
                  setDraft(value ?? '');
                  setEditing(true);
                }}
                className="ml-1 inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-muted/60 hover:text-foreground"
                aria-label={`Edit ${label}`}
              >
                <Pencil className="h-3 w-3" aria-hidden />
              </button>
            ) : null}
          </>
        )}
      </dd>
    </div>
  );
}

/**
 * Phase 3b - overridable row for a contact channel (email/phone) or a
 * single-value field (yacht name). Renders as a plain text row showing
 * the canonical value, with a small "Override" affordance that expands
 * into a Select (over `options`) + Input (for fresh values) + the two
 * intent checkboxes.
 *
 * State is owned by the parent dialog so cancellation collapses cleanly
 * back to canonical without round-tripping through the row.
 */
function OverridableContactField({
  label,
  canonicalValue,
  canonicalContactId,
  options,
  override,
  onChange,
  missing,
}: {
  label: string;
  canonicalValue: string | null;
  /** id of the row that holds `canonicalValue` in `options`. Used to
   *  pre-select the matching Select item when the user opens override
   *  mode without changing anything. */
  canonicalContactId: string | null;
  /** Picker options. For yacht-name pass [] - only the manual text path
   *  is available. */
  options: Array<{ id: string; value: string; isPrimary: boolean }>;
  override: FieldOverrideState | null;
  onChange: (next: FieldOverrideState | null) => void;
  missing?: boolean;
}) {
  const [expanded, setExpanded] = useState(false);
  const [manualValue, setManualValue] = useState('');

  // Synthetic combobox value: either an existing contact id, "__manual__"
  // for free-text entry, or "__canonical__" for "use the canonical primary".
  const selectValue =
    override?.contactId ??
    (override?.value != null && override?.contactId == null ? '__manual__' : '__canonical__');

  // Resolve the displayed effective value for the read-only header (when
  // collapsed): show the override if active, otherwise canonical.
  const effective = override?.value ?? canonicalValue ?? null;

  const collapseAndClear = () => {
    setExpanded(false);
    setManualValue('');
    onChange(null);
  };

  return (
    <div className="space-y-1.5">
      <div className="flex items-baseline gap-2 text-sm">
        <dt className="w-32 shrink-0 text-xs text-muted-foreground">{label}</dt>
        <dd
          className={cn(
            'flex-1 wrap-break-word inline-flex items-center gap-2',
            missing
              ? 'text-rose-700 font-medium'
              : effective
                ? 'text-foreground'
                : 'text-muted-foreground italic',
          )}
        >
          <span className="flex-1">
            {effective ?? (missing ? 'Missing  -  required' : 'Not set')}
            {override?.value != null ? (
              <span className="ml-1 inline-flex items-center rounded bg-amber-100 px-1 text-xs font-medium text-amber-800">
                [EOI]
              </span>
            ) : null}
          </span>
          {!expanded ? (
            <button
              type="button"
              onClick={() => setExpanded(true)}
              className="text-xs text-primary hover:underline"
            >
              {override?.value != null ? 'Edit override' : 'Override'}
            </button>
          ) : (
            <button
              type="button"
              onClick={collapseAndClear}
              className="text-xs text-muted-foreground hover:underline"
            >
              Clear & close
            </button>
          )}
        </dd>
      </div>

      {expanded ? (
        <div className="ml-32 space-y-2 rounded-md border bg-background/60 p-2">
          {options.length > 0 ? (
            <Select
              value={selectValue}
              onValueChange={(v) => {
                if (v === '__canonical__') {
                  onChange(null);
                  setManualValue('');
                  return;
                }
                if (v === '__manual__') {
                  onChange({
                    value: manualValue.trim() || null,
                    contactId: null,
                    useOnlyForThisEoi: override?.useOnlyForThisEoi ?? false,
                    setAsDefault: override?.setAsDefault ?? false,
                  });
                  return;
                }
                const picked = options.find((o) => o.id === v);
                if (!picked) return;
                onChange({
                  value: picked.value,
                  contactId: picked.id,
                  useOnlyForThisEoi: override?.useOnlyForThisEoi ?? false,
                  setAsDefault: override?.setAsDefault ?? false,
                });
              }}
            >
              <SelectTrigger className="h-8 text-xs">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="__canonical__">
                  Use canonical {label.toLowerCase()}
                  {canonicalContactId && options.find((o) => o.id === canonicalContactId)
                    ? ` (${canonicalValue ?? ''})`
                    : ''}
                </SelectItem>
                {options
                  .filter((o) => !o.isPrimary)
                  .map((o) => (
                    <SelectItem key={o.id} value={o.id}>
                      {o.value}
                    </SelectItem>
                  ))}
                <SelectItem value="__manual__">+ Type a new value…</SelectItem>
              </SelectContent>
            </Select>
          ) : null}
          {selectValue === '__manual__' || options.length === 0 ? (
            <Input
              value={manualValue || override?.value || ''}
              placeholder={`New ${label.toLowerCase()}`}
              onChange={(e) => {
                setManualValue(e.target.value);
                onChange({
                  value: e.target.value.trim() || null,
                  contactId: null,
                  useOnlyForThisEoi: override?.useOnlyForThisEoi ?? false,
                  setAsDefault: override?.setAsDefault ?? false,
                });
              }}
              className="h-8 text-xs"
            />
          ) : null}
          <div className="space-y-1">
            <label className="flex items-start gap-2 text-xs text-muted-foreground cursor-pointer">
              <input
                type="checkbox"
                className="mt-0.5"
                checked={override?.useOnlyForThisEoi ?? false}
                disabled={!override || !override.value}
                onChange={(e) =>
                  override &&
                  onChange({
                    ...override,
                    useOnlyForThisEoi: e.target.checked,
                    // Mutually exclusive intent - both true at once doesn't
                    // make sense (per-doc vs. promote-to-canonical).
                    setAsDefault: e.target.checked ? false : override.setAsDefault,
                  })
                }
              />
              <span>
                Use only for this EOI
                <span className="block text-xs">
                  Records the deviation on this document; canonical record untouched.
                </span>
              </span>
            </label>
            <label className="flex items-start gap-2 text-xs text-muted-foreground cursor-pointer">
              <input
                type="checkbox"
                className="mt-0.5"
                checked={override?.setAsDefault ?? false}
                disabled={!override || !override.value}
                onChange={(e) =>
                  override &&
                  onChange({
                    ...override,
                    setAsDefault: e.target.checked,
                    useOnlyForThisEoi: e.target.checked ? false : override.useOnlyForThisEoi,
                  })
                }
              />
              <span>
                Set as default for future docs
                <span className="block text-xs">
                  Promotes this value to the canonical primary on save.
                </span>
              </span>
            </label>
          </div>
        </div>
      ) : null}
    </div>
  );
}

/**
 * Phase 3 follow-up - address override row. Treats the address as one
 * logical field with one pair of checkboxes (master-plan decision:
 * reps think about addresses all-or-nothing). The per-component input
 * UX mirrors the canonical address form (separate fields per
 * line/city/state/postal/country + CountryCombobox) so reps don't
 * relearn an input pattern.
 */
function OverridableAddressField({
  canonical,
  canonicalAddressId,
  options,
  override,
  onChange,
  missing,
}: {
  canonical: {
    street: string;
    city: string;
    subdivision: string;
    postalCode: string;
    countryIso: string;
  } | null;
  canonicalAddressId: string | null;
  options: Array<{
    id: string;
    streetAddress: string | null;
    city: string | null;
    subdivisionIso: string | null;
    postalCode: string | null;
    countryIso: string | null;
    isPrimary: boolean;
  }>;
  override: AddressOverrideState | null;
  onChange: (next: AddressOverrideState | null) => void;
  missing?: boolean;
}) {
  const [expanded, setExpanded] = useState(false);

  const canonicalSummary = canonical
    ? [
        canonical.street,
        canonical.city,
        canonical.subdivision,
        canonical.postalCode,
        canonical.countryIso,
      ]
        .filter(Boolean)
        .join(', ')
    : null;

  const effectiveSummary = override
    ? [
        override.line1,
        override.line2,
        override.city,
        override.subdivisionIso,
        override.postalCode,
        override.countryIso,
      ]
        .filter(Boolean)
        .join(', ')
    : canonicalSummary;

  const selectValue = override?.addressId ?? (override ? '__manual__' : '__canonical__');

  const fillFromOption = (id: string) => {
    const picked = options.find((o) => o.id === id);
    if (!picked) return;
    onChange({
      line1: picked.streetAddress ?? '',
      line2: '',
      city: picked.city ?? '',
      subdivisionIso: picked.subdivisionIso ?? '',
      postalCode: picked.postalCode ?? '',
      countryIso: picked.countryIso,
      addressId: picked.id,
      useOnlyForThisEoi: override?.useOnlyForThisEoi ?? false,
      setAsDefault: override?.setAsDefault ?? false,
    });
  };

  const updateOverride = (patch: Partial<AddressOverrideState>) => {
    onChange({
      line1: '',
      line2: '',
      city: '',
      subdivisionIso: '',
      postalCode: '',
      countryIso: null,
      addressId: null,
      useOnlyForThisEoi: false,
      setAsDefault: false,
      ...(override ?? {}),
      ...patch,
    });
  };

  return (
    <div className="space-y-1.5">
      <div className="flex items-baseline gap-2 text-sm">
        <dt className="w-32 shrink-0 text-xs text-muted-foreground">Address</dt>
        <dd
          className={cn(
            'flex-1 wrap-break-word inline-flex items-center gap-2',
            missing
              ? 'text-rose-700 font-medium'
              : effectiveSummary
                ? 'text-foreground'
                : 'text-muted-foreground italic',
          )}
        >
          <span className="flex-1">
            {effectiveSummary ?? (missing ? 'Missing  -  required' : 'Not set')}
            {override ? (
              <span className="ml-1 inline-flex items-center rounded bg-amber-100 px-1 text-xs font-medium text-amber-800">
                [EOI]
              </span>
            ) : null}
          </span>
          {!expanded ? (
            <button
              type="button"
              onClick={() => setExpanded(true)}
              className="text-xs text-primary hover:underline"
            >
              {override ? 'Edit override' : 'Override'}
            </button>
          ) : (
            <button
              type="button"
              onClick={() => {
                setExpanded(false);
                onChange(null);
              }}
              className="text-xs text-muted-foreground hover:underline"
            >
              Clear & close
            </button>
          )}
        </dd>
      </div>

      {expanded ? (
        <div className="ml-32 space-y-2 rounded-md border bg-background/60 p-2">
          {options.length > 0 ? (
            <Select
              value={selectValue}
              onValueChange={(v) => {
                if (v === '__canonical__') {
                  onChange(null);
                  return;
                }
                if (v === '__manual__') {
                  updateOverride({ addressId: null });
                  return;
                }
                fillFromOption(v);
              }}
            >
              <SelectTrigger className="h-8 text-xs">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="__canonical__">
                  Use canonical address
                  {canonicalAddressId ? '' : ''}
                </SelectItem>
                {options
                  .filter((o) => !o.isPrimary)
                  .map((o) => (
                    <SelectItem key={o.id} value={o.id}>
                      {[o.streetAddress, o.city, o.countryIso].filter(Boolean).join(', ')}
                    </SelectItem>
                  ))}
                <SelectItem value="__manual__">+ Type a new address…</SelectItem>
              </SelectContent>
            </Select>
          ) : null}

          <div className="space-y-2">
            <Input
              value={override?.line1 ?? ''}
              placeholder="Street address"
              onChange={(e) => updateOverride({ line1: e.target.value, addressId: null })}
              className="h-8 text-xs"
            />
            <Input
              value={override?.line2 ?? ''}
              placeholder="Address line 2 (optional)"
              onChange={(e) => updateOverride({ line2: e.target.value, addressId: null })}
              className="h-8 text-xs"
            />
            <div className="grid grid-cols-2 gap-2">
              <Input
                value={override?.city ?? ''}
                placeholder="City"
                onChange={(e) => updateOverride({ city: e.target.value, addressId: null })}
                className="h-8 text-xs"
              />
              <Input
                value={override?.postalCode ?? ''}
                placeholder="Postal code"
                onChange={(e) => updateOverride({ postalCode: e.target.value, addressId: null })}
                className="h-8 text-xs"
              />
            </div>
            <div className="grid grid-cols-2 gap-2">
              <Input
                value={override?.subdivisionIso ?? ''}
                placeholder="ISO subdivision (e.g. US-CA)"
                onChange={(e) =>
                  updateOverride({ subdivisionIso: e.target.value, addressId: null })
                }
                className="h-8 text-xs"
              />
              <CountryCombobox
                value={override?.countryIso ?? null}
                onChange={(iso) => updateOverride({ countryIso: iso ?? null, addressId: null })}
              />
            </div>
          </div>

          <div className="space-y-1">
            <label className="flex items-start gap-2 text-xs text-muted-foreground cursor-pointer">
              <input
                type="checkbox"
                className="mt-0.5"
                checked={override?.useOnlyForThisEoi ?? false}
                disabled={!override?.line1 || !override?.countryIso}
                onChange={(e) =>
                  updateOverride({
                    useOnlyForThisEoi: e.target.checked,
                    setAsDefault: e.target.checked ? false : (override?.setAsDefault ?? false),
                  })
                }
              />
              <span>
                Use only for this EOI
                <span className="block text-xs">
                  Records the deviation on this document; canonical address untouched.
                </span>
              </span>
            </label>
            <label className="flex items-start gap-2 text-xs text-muted-foreground cursor-pointer">
              <input
                type="checkbox"
                className="mt-0.5"
                checked={override?.setAsDefault ?? false}
                disabled={!override?.line1 || !override?.countryIso}
                onChange={(e) =>
                  updateOverride({
                    setAsDefault: e.target.checked,
                    useOnlyForThisEoi: e.target.checked
                      ? false
                      : (override?.useOnlyForThisEoi ?? false),
                  })
                }
              />
              <span>
                Set as default for future docs
                <span className="block text-xs">
                  Promotes this address to the canonical primary on save.
                </span>
              </span>
            </label>
          </div>
        </div>
      ) : null}
    </div>
  );
}
