'use client';

import { useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Loader2, ChevronsUpDown, Check, Plus } from 'lucide-react';
import { toast } from 'sonner';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from '@/components/ui/command';
import { Checkbox } from '@/components/ui/checkbox';
import { Separator } from '@/components/ui/separator';
import { TagPicker } from '@/components/shared/tag-picker';
import { ReminderDaysInput } from '@/components/shared/reminder-days-input';
import { ClientForm } from '@/components/clients/client-form';
import { YachtForm } from '@/components/yachts/yacht-form';
import { YachtPicker } from '@/components/yachts/yacht-picker';
import { apiFetch } from '@/lib/api/client';
import { useEntityOptions } from '@/hooks/use-entity-options';
import { formatBerthRange } from '@/lib/templates/berth-range';
import { useFormScrollToError } from '@/hooks/use-form-scroll-to-error';
import { FormErrorSummary } from '@/components/forms/form-error-summary';
import type { z } from 'zod';
import { createInterestSchema, type CreateInterestInput } from '@/lib/validators/interests';
import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES, SOURCES } from '@/lib/constants';
import { cn } from '@/lib/utils';

const CATEGORY_LABELS: Record<string, string> = {
  general_interest: 'General interest',
  specific_qualified: 'Specific qualified',
  hot_lead: 'Hot lead',
};

interface InterestFormProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  /**
   * Pre-fill clientId when launching the form from a client detail page.
   * Ignored when `interest` is provided (edit mode).
   */
  defaultClientId?: string;
  interest?: {
    id: string;
    clientId: string;
    clientName?: string | null;
    yachtId?: string | null;
    berthId?: string | null;
    berthMooringNumber?: string | null;
    pipelineStage: string;
    leadCategory?: string | null;
    source?: string | null;
    reminderEnabled?: boolean;
    reminderDays?: number | null;
    tags?: Array<{ id: string }>;
    desiredLengthFt?: string | number | null;
    desiredWidthFt?: string | number | null;
    desiredDraftFt?: string | number | null;
  };
}

export function InterestForm({ open, onOpenChange, defaultClientId, interest }: InterestFormProps) {
  const queryClient = useQueryClient();
  const router = useRouter();
  const params = useParams<{ portSlug: string }>();
  const portSlug = params?.portSlug ?? '';
  const isEdit = !!interest;

  const [clientOpen, setClientOpen] = useState(false);
  const [berthOpen, setBerthOpen] = useState(false);
  const [desiredUnit, setDesiredUnit] = useState<'ft' | 'm'>('ft');

  const {
    handleSubmit,
    watch,
    setValue,
    reset,
    formState: { errors, isSubmitting, isDirty },
  } = useForm<z.input<typeof createInterestSchema>, unknown, CreateInterestInput>({
    resolver: zodResolver(createInterestSchema),
    defaultValues: {
      clientId: '',
      yachtId: undefined,
      pipelineStage: 'enquiry',
      // Default a manually-created interest's source to 'manual' so the
      // rep doesn't have to remember to pick it (mirrors the same
      // default on client-form.tsx). Inquiry-inbox / website conversion
      // flows can override via prefill once that path lands here.
      source: 'manual',
      reminderEnabled: false,
      tagIds: [],
    },
  });

  const tagIds = watch('tagIds') ?? [];
  const reminderEnabled = watch('reminderEnabled');
  const selectedClientId = watch('clientId');
  const selectedBerthId = watch('berthId');
  const selectedYachtId = watch('yachtId');
  const [createYachtOpen, setCreateYachtOpen] = useState(false);
  const [createClientOpen, setCreateClientOpen] = useState(false);
  const [discardConfirmOpen, setDiscardConfirmOpen] = useState(false);

  // Auto-fill pipelineStage + leadCategory based on whether a berth was
  // picked. Once the rep manually edits either field we stop touching it,
  // so we don't fight the user. Edit mode skips the auto-fill entirely -
  // changing the berth on an in-flight interest shouldn't silently demote
  // it back to "enquiry".
  const userTouchedStage = useRef(false);
  const userTouchedCategory = useRef(false);
  useEffect(() => {
    if (isEdit) return;
    const hasBerth = !!selectedBerthId;
    if (!userTouchedStage.current) {
      setValue('pipelineStage', hasBerth ? 'qualified' : 'enquiry');
    }
    if (!userTouchedCategory.current) {
      setValue('leadCategory', hasBerth ? 'specific_qualified' : 'general_interest');
    }
    // setValue is stable from RHF; isEdit doesn't change after mount.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedBerthId]);

  const scrollOnError = useFormScrollToError(handleSubmit, errors);

  function requestClose() {
    if (isDirty && !isSubmitting && !mutation.isPending) {
      setDiscardConfirmOpen(true);
      return;
    }
    onOpenChange(false);
  }

  // Fetch the selected client's company memberships so the YachtPicker can
  // include yachts owned by companies the client belongs to (e.g. a
  // managing-director client whose yachts are titled to the company).
  const { data: clientDetail } = useQuery<{
    data: { companies?: Array<{ company: { id: string } }> };
  }>({
    queryKey: ['client-detail-for-interest-form', selectedClientId],
    queryFn: () => apiFetch(`/api/v1/clients/${selectedClientId}`),
    enabled: !!selectedClientId,
  });
  const memberCompanyIds: string[] = clientDetail?.data.companies?.map((m) => m.company.id) ?? [];
  const yachtOwnerFilter = selectedClientId
    ? [
        { type: 'client' as const, id: selectedClientId },
        ...memberCompanyIds.map((id) => ({ type: 'company' as const, id })),
      ]
    : undefined;

  // Probe whether the selected client (or their member companies) owns any
  // yachts. When zero, the form swaps the picker for an "Add yacht" CTA so
  // reps don't get stuck on an empty dropdown wondering what to do. We hit
  // the same autocomplete endpoint the picker uses but with an empty query
  // to get the full unfiltered list scoped to the owner filter.
  // Tags-availability probe - drives whether the whole Tags section
  // (label + picker) renders. The picker itself returns null when empty,
  // but the wrapping label/separator needed the same gate.
  const { data: tagsList } = useQuery<{ data: Array<{ id: string }> }>({
    queryKey: ['tag-availability-for-interest-form'],
    queryFn: () => apiFetch('/api/v1/tags/options'),
    staleTime: 60_000,
  });
  const tagsAvailable = (tagsList?.data?.length ?? 0) > 0;

  const { data: yachtCount } = useQuery<{ data: Array<{ id: string }> }>({
    queryKey: [
      'yacht-count-for-interest-form',
      selectedClientId,
      memberCompanyIds.sort().join(','),
    ],
    queryFn: () => {
      const params = new URLSearchParams({ q: '' });
      if (selectedClientId) params.set('ownerClientId', selectedClientId);
      if (memberCompanyIds.length > 0) {
        params.set('ownerCompanyIds', memberCompanyIds.join(','));
      }
      return apiFetch(`/api/v1/yachts/autocomplete?${params.toString()}`);
    },
    enabled: !!selectedClientId,
  });
  const hasAnyYachts = (yachtCount?.data?.length ?? 0) > 0;

  const {
    options: clientOptions,
    isLoading: clientsLoading,
    setSearch: setClientSearch,
  } = useEntityOptions({
    endpoint: '/api/v1/clients/options',
    labelKey: 'fullName',
  });

  const {
    options: berthOptions,
    isLoading: berthsLoading,
    setSearch: setBerthSearch,
  } = useEntityOptions({
    endpoint: '/api/v1/berths/options',
    labelKey: 'mooringNumber',
  });

  useEffect(() => {
    if (interest && open) {
      reset({
        clientId: interest.clientId,
        yachtId: interest.yachtId ?? undefined,
        berthId: interest.berthId ?? undefined,
        pipelineStage: interest.pipelineStage as (typeof PIPELINE_STAGES)[number],
        leadCategory: interest.leadCategory as (typeof LEAD_CATEGORIES)[number] | undefined,
        source: interest.source ?? undefined,
        reminderEnabled: interest.reminderEnabled ?? false,
        reminderDays: interest.reminderDays ?? undefined,
        tagIds: interest.tags?.map((t) => t.id) ?? [],
        desiredLengthFt:
          interest.desiredLengthFt === null || interest.desiredLengthFt === undefined
            ? undefined
            : String(interest.desiredLengthFt),
        desiredWidthFt:
          interest.desiredWidthFt === null || interest.desiredWidthFt === undefined
            ? undefined
            : String(interest.desiredWidthFt),
        desiredDraftFt:
          interest.desiredDraftFt === null || interest.desiredDraftFt === undefined
            ? undefined
            : String(interest.desiredDraftFt),
      });
    } else if (!interest && open) {
      reset({
        clientId: defaultClientId ?? '',
        yachtId: undefined,
        pipelineStage: 'enquiry',
        // Mirror the defaultValues block — manual-create flow always
        // defaults source to 'manual'. The reset path was dropping it,
        // leaving a freshly-opened drawer with a blank source selector.
        source: 'manual',
        reminderEnabled: false,
        tagIds: [],
      });
    }
  }, [interest, defaultClientId, open, reset]);

  const mutation = useMutation({
    mutationFn: async (data: CreateInterestInput) => {
      // Enrich with the dual-store ft+m values + the entry-unit. The form
      // tracks the canonical ft via DimensionInput; we compute the matching
      // m value for the API and stamp the unit so a future edit can render
      // the rep's literal entry without conversion drift.
      const enriched: CreateInterestInput = {
        ...data,
        desiredLengthM: ftToMStr(data.desiredLengthFt),
        desiredWidthM: ftToMStr(data.desiredWidthFt),
        desiredDraftM: ftToMStr(data.desiredDraftFt),
        desiredLengthUnit: desiredUnit,
        desiredWidthUnit: desiredUnit,
        desiredDraftUnit: desiredUnit,
      };
      if (isEdit) {
        const { tagIds: tIds, ...rest } = enriched;
        await apiFetch(`/api/v1/interests/${interest!.id}`, { method: 'PATCH', body: rest });
        if (tIds) {
          await apiFetch(`/api/v1/interests/${interest!.id}/tags`, {
            method: 'PUT',
            body: { tagIds: tIds },
          });
        }
        return { id: interest!.id, created: false };
      }
      const res = await apiFetch<{ data: { id: string } }>('/api/v1/interests', {
        method: 'POST',
        body: enriched,
      });
      // Materialise any additional berths the rep picked in the multi-
      // select. The first (primary) berth is already linked via the create
      // payload's berthId; everything else gets a follow-up POST to the
      // junction endpoint. We fire them in parallel - failure on one is
      // surfaced as a toast but doesn't roll back the interest creation.
      if (additionalBerthIds.length > 0) {
        await Promise.allSettled(
          additionalBerthIds.map((berthId) =>
            apiFetch(`/api/v1/interests/${res.data.id}/berths`, {
              method: 'POST',
              body: { berthId, isSpecificInterest: false },
            }),
          ),
        );
      }
      return { id: res.data.id, created: true };
    },
    onSuccess: (result) => {
      queryClient.invalidateQueries({ queryKey: ['interests'] });
      // M-U10: confirm the write landed.
      toast.success(result.created ? 'Interest created' : 'Interest updated');
      onOpenChange(false);
      // F20: navigate to the new interest's detail page so the rep can
      // start the workflow immediately. Edits stay in place - no point
      // re-loading the same row's detail page they just came from.
      if (result.created && portSlug) {
        router.push(`/${portSlug}/interests/${result.id}` as never);
      }
    },
  });

  function ftToMStr(ft: string | number | undefined | null): string | undefined {
    if (ft === undefined || ft === null || ft === '') return undefined;
    const n = typeof ft === 'number' ? ft : Number(ft);
    if (!Number.isFinite(n) || n <= 0) return undefined;
    return String(Math.round(n * 0.3048 * 100) / 100);
  }

  const selectedClient = clientOptions.find((c) => c.value === selectedClientId);
  const selectedBerth = berthOptions.find((b) => b.value === selectedBerthId);

  // Additional berths (beyond the primary `berthId`) accumulated by the
  // multi-select. On create, after the interest row exists, each id here
  // gets a follow-up POST /interests/{id}/berths so they show up in the
  // linked-berths list with isPrimary=false. The primary berth (the form's
  // `berthId`) is materialised by the standard create path. Edit mode
  // doesn't surface this - managing extra berths post-create happens on
  // the interest detail page's linked-berths section.
  const [additionalBerthIds, setAdditionalBerthIds] = useState<string[]>([]);

  return (
    <Sheet
      open={open}
      onOpenChange={(next) => {
        if (next) {
          onOpenChange(true);
          return;
        }
        requestClose();
      }}
    >
      <SheetContent className="w-full sm:max-w-2xl lg:max-w-4xl overflow-y-auto">
        <SheetHeader>
          <SheetTitle>{isEdit ? 'Edit Interest' : 'New Interest'}</SheetTitle>
        </SheetHeader>

        <form onSubmit={scrollOnError((data) => mutation.mutate(data))} className="space-y-6 py-6">
          <FormErrorSummary errors={errors} />
          {/* Client */}
          <div className="space-y-4">
            <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
              Client & Berth
            </h3>

            <div className="space-y-1">
              <div className="flex items-center justify-between gap-2">
                <Label>Client *</Label>
                {!isEdit && (
                  <Button
                    type="button"
                    variant="ghost"
                    size="sm"
                    className="h-7 text-xs"
                    onClick={() => setCreateClientOpen(true)}
                  >
                    <Plus className="mr-1 h-3 w-3" aria-hidden />
                    Add new
                  </Button>
                )}
              </div>
              <Popover open={clientOpen} onOpenChange={setClientOpen} modal>
                <PopoverTrigger asChild>
                  <Button
                    variant="outline"
                    role="combobox"
                    aria-expanded={clientOpen}
                    className={cn(
                      'w-full justify-between',
                      !selectedClientId && 'text-muted-foreground',
                    )}
                    disabled={isEdit}
                  >
                    {selectedClient?.label ?? interest?.clientName ?? 'Select client...'}
                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
                  </Button>
                </PopoverTrigger>
                <PopoverContent className="w-(--radix-popper-anchor-width) min-w-[280px] p-0">
                  {/* shouldFilter={false}: server-side search via setClientSearch
                      drives the result set. Without this, cmdk's default filter
                      matches the user's typed text against CommandItem.value
                      (the client UUID) and silently drops every result that
                      doesn't contain the typed substring in its id. */}
                  <Command shouldFilter={false}>
                    <CommandInput placeholder="Search clients..." onValueChange={setClientSearch} />
                    <CommandList>
                      <CommandEmpty>
                        {clientsLoading ? 'Loading...' : 'No clients found.'}
                      </CommandEmpty>
                      <CommandGroup>
                        {clientOptions.map((option) => (
                          <CommandItem
                            key={option.value}
                            value={option.value}
                            onSelect={(val) => {
                              setValue('clientId', val);
                              setClientOpen(false);
                            }}
                          >
                            <Check
                              className={cn(
                                'mr-2 h-4 w-4',
                                selectedClientId === option.value ? 'opacity-100' : 'opacity-0',
                              )}
                            />
                            {option.label}
                          </CommandItem>
                        ))}
                      </CommandGroup>
                    </CommandList>
                  </Command>
                </PopoverContent>
              </Popover>
              {errors.clientId && (
                <p className="text-xs text-destructive">{errors.clientId.message}</p>
              )}
            </div>

            <div className="space-y-1">
              <Label>Berths (optional)</Label>
              <Popover open={berthOpen} onOpenChange={setBerthOpen} modal>
                <PopoverTrigger asChild>
                  <Button
                    variant="outline"
                    role="combobox"
                    aria-expanded={berthOpen}
                    className={cn(
                      'w-full justify-between',
                      !selectedBerthId &&
                        additionalBerthIds.length === 0 &&
                        'text-muted-foreground',
                    )}
                  >
                    <span className="truncate">
                      {selectedBerthId
                        ? (() => {
                            const primaryLabel =
                              selectedBerth?.label ??
                              interest?.berthMooringNumber ??
                              selectedBerthId;
                            const additionalLabels = additionalBerthIds
                              .map((id) => berthOptions.find((b) => b.value === id)?.label)
                              .filter((label): label is string => Boolean(label));
                            const allLabels = [primaryLabel, ...additionalLabels];
                            const range = formatBerthRange(allLabels);
                            // Cap at 5 segments after range collapse so "A1-A3, B5, C2, D7, E4 +N more"
                            // doesn't overflow the trigger.
                            const segments = range ? range.split(', ') : [];
                            if (segments.length <= 5) return range || primaryLabel;
                            const head = segments.slice(0, 5).join(', ');
                            const overflow = segments.length - 5;
                            return `${head} +${overflow} more`;
                          })()
                        : 'Select berths…'}
                    </span>
                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
                  </Button>
                </PopoverTrigger>
                <PopoverContent className="w-(--radix-popper-anchor-width) min-w-[280px] p-0">
                  <Command shouldFilter={false}>
                    <CommandInput placeholder="Search berths..." onValueChange={setBerthSearch} />
                    <CommandList>
                      <CommandEmpty>
                        {berthsLoading ? 'Loading...' : 'No berths found.'}
                      </CommandEmpty>
                      <CommandGroup>
                        <CommandItem
                          value="__clear__"
                          onSelect={() => {
                            setValue('berthId', undefined);
                            setAdditionalBerthIds([]);
                          }}
                        >
                          <Check
                            className={cn(
                              'mr-2 h-4 w-4',
                              !selectedBerthId && additionalBerthIds.length === 0
                                ? 'opacity-100'
                                : 'opacity-0',
                            )}
                          />
                          None
                        </CommandItem>
                        {berthOptions.map((option) => {
                          const isPrimary = selectedBerthId === option.value;
                          const isAdditional = additionalBerthIds.includes(option.value);
                          const isSelected = isPrimary || isAdditional;
                          return (
                            <CommandItem
                              key={option.value}
                              value={option.value}
                              onSelect={(val) => {
                                // Multi-select toggle. First pick becomes
                                // the primary berthId (the one the API uses
                                // for templates / list views). Subsequent
                                // picks go into additionalBerthIds and are
                                // materialised via POST /berths after the
                                // interest is created.
                                if (isPrimary) {
                                  // Demote primary; promote first additional
                                  // (if any) to primary so the deal still
                                  // has one primary berth.
                                  const promote = additionalBerthIds[0];
                                  setValue('berthId', promote ?? undefined);
                                  setAdditionalBerthIds(additionalBerthIds.slice(1));
                                } else if (isAdditional) {
                                  setAdditionalBerthIds(
                                    additionalBerthIds.filter((id) => id !== val),
                                  );
                                } else if (!selectedBerthId) {
                                  setValue('berthId', val);
                                } else {
                                  setAdditionalBerthIds([...additionalBerthIds, val]);
                                }
                              }}
                            >
                              <Check
                                className={cn(
                                  'mr-2 h-4 w-4',
                                  isSelected ? 'opacity-100' : 'opacity-0',
                                )}
                              />
                              <span className="flex-1">{option.label}</span>
                              {isPrimary && (
                                <span className="ml-2 rounded bg-primary/15 px-1.5 py-0.5 text-xs font-medium uppercase tracking-wide text-primary">
                                  primary
                                </span>
                              )}
                            </CommandItem>
                          );
                        })}
                      </CommandGroup>
                    </CommandList>
                  </Command>
                </PopoverContent>
              </Popover>
              <p className="text-xs text-muted-foreground">
                Pick one or more berths. The first becomes the primary berth (used in templates and
                list views); the rest get linked as alternates and can be promoted later from the
                interest detail page.
              </p>
            </div>

            <div className="space-y-2">
              <div className="flex items-center justify-between">
                <Label>
                  Yacht <span className="text-muted-foreground font-normal">(optional)</span>
                </Label>
                {selectedClientId && hasAnyYachts && (
                  <Button
                    type="button"
                    variant="ghost"
                    size="sm"
                    className="h-7 text-xs"
                    onClick={() => setCreateYachtOpen(true)}
                  >
                    <Plus className="mr-1 h-3 w-3" aria-hidden />
                    Add new
                  </Button>
                )}
              </div>
              {/* Hide the picker entirely when the selected client has no
                  yachts on file (and isn't linked to a company with yachts).
                  An empty dropdown is a dead-end UX - the only useful action
                  in that state is "create a yacht for this client". */}
              {selectedClientId && !hasAnyYachts ? (
                <div className="rounded-md border border-dashed bg-muted/40 p-3 text-sm">
                  <p className="text-muted-foreground">This client has no yachts on file yet.</p>
                  <Button
                    type="button"
                    size="sm"
                    className="mt-2"
                    onClick={() => setCreateYachtOpen(true)}
                  >
                    <Plus className="mr-1 h-3.5 w-3.5" aria-hidden />
                    Add a yacht for this client
                  </Button>
                </div>
              ) : (
                <YachtPicker
                  value={selectedYachtId ?? null}
                  onChange={(id) => setValue('yachtId', id ?? undefined)}
                  ownerFilter={yachtOwnerFilter}
                  disabled={!selectedClientId}
                  placeholder={selectedClientId ? 'Select yacht...' : 'Select a client first'}
                />
              )}
              <p className="text-xs text-muted-foreground">
                Required before the interest can leave the New Enquiry stage.
                {memberCompanyIds.length > 0 && (
                  <>
                    {' '}
                    Includes yachts from {memberCompanyIds.length}{' '}
                    {memberCompanyIds.length === 1 ? 'member company' : 'member companies'}.
                  </>
                )}
              </p>
            </div>
          </div>

          <Separator />

          {/* Pipeline */}
          <div className="space-y-4">
            <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
              Pipeline
            </h3>
            <div className="grid grid-cols-2 gap-4">
              <div className="space-y-1">
                <Label>Stage</Label>
                <Select
                  value={watch('pipelineStage') ?? 'enquiry'}
                  onValueChange={(v) => {
                    userTouchedStage.current = true;
                    setValue('pipelineStage', v as (typeof PIPELINE_STAGES)[number]);
                  }}
                >
                  <SelectTrigger>
                    <SelectValue placeholder="Select stage" />
                  </SelectTrigger>
                  <SelectContent>
                    {PIPELINE_STAGES.map((s) => (
                      <SelectItem key={s} value={s}>
                        {STAGE_LABELS[s]}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>

              <div className="space-y-1">
                <Label>Lead Category</Label>
                <Select
                  value={watch('leadCategory') ?? ''}
                  onValueChange={(v) => {
                    userTouchedCategory.current = true;
                    setValue(
                      'leadCategory',
                      v ? (v as (typeof LEAD_CATEGORIES)[number]) : undefined,
                    );
                  }}
                >
                  <SelectTrigger>
                    <SelectValue placeholder="Select category" />
                  </SelectTrigger>
                  <SelectContent>
                    {LEAD_CATEGORIES.map((c) => (
                      <SelectItem key={c} value={c}>
                        {CATEGORY_LABELS[c] ?? c}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>

              <div className="space-y-1">
                <Label>Source</Label>
                <Select
                  value={watch('source') ?? ''}
                  onValueChange={(v) => setValue('source', v || undefined)}
                >
                  <SelectTrigger>
                    <SelectValue placeholder="Select source" />
                  </SelectTrigger>
                  <SelectContent>
                    {SOURCES.map((s) => (
                      <SelectItem key={s.value} value={s.value}>
                        {s.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
            </div>
          </div>

          <Separator />

          {/* Desired berth dimensions (recommender inputs) */}
          <div className="space-y-3">
            <div className="flex items-start justify-between gap-3">
              <div>
                <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
                  Berth size desired
                </h3>
                <p className="mt-1 text-xs text-muted-foreground">
                  Optional - the recommender treats blank fields as no constraint on that axis.
                </p>
              </div>
              <UnitToggle value={desiredUnit} onChange={setDesiredUnit} />
            </div>
            <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
              <DimensionInput
                htmlId="desiredLengthFt"
                label="Length"
                placeholder={desiredUnit === 'ft' ? 'e.g. 60' : 'e.g. 18.29'}
                unit={desiredUnit}
                ftValue={watch('desiredLengthFt') as string | undefined}
                onChangeFt={(v) => setValue('desiredLengthFt', v, { shouldDirty: true })}
              />
              <DimensionInput
                htmlId="desiredWidthFt"
                label="Width"
                placeholder={desiredUnit === 'ft' ? 'e.g. 18' : 'e.g. 5.49'}
                unit={desiredUnit}
                ftValue={watch('desiredWidthFt') as string | undefined}
                onChangeFt={(v) => setValue('desiredWidthFt', v, { shouldDirty: true })}
              />
              <DimensionInput
                htmlId="desiredDraftFt"
                label="Draft"
                placeholder={desiredUnit === 'ft' ? 'e.g. 6' : 'e.g. 1.83'}
                unit={desiredUnit}
                ftValue={watch('desiredDraftFt') as string | undefined}
                onChangeFt={(v) => setValue('desiredDraftFt', v, { shouldDirty: true })}
              />
            </div>
          </div>

          <Separator />

          {/* Reminder */}
          <div className="space-y-4">
            <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
              Reminder
            </h3>
            <div className="flex items-center gap-2">
              <Checkbox
                id="reminderEnabled"
                checked={reminderEnabled ?? false}
                onCheckedChange={(v) => setValue('reminderEnabled', !!v)}
              />
              <Label htmlFor="reminderEnabled">Enable reminder</Label>
            </div>
            {reminderEnabled && (
              <div className="space-y-1">
                <Label htmlFor="reminderDays">Reminder cadence</Label>
                <ReminderDaysInput
                  id="reminderDays"
                  value={watch('reminderDays') ?? null}
                  onChange={(v) => setValue('reminderDays', v ?? undefined)}
                />
              </div>
            )}
          </div>

          {/* Tags - TagPicker itself returns null when the port has no tags
              configured AND the form has nothing selected. We hide the
              wrapping label + separator in that same case so an empty
              "Tags" header doesn't sit in the form. */}
          {(tagIds.length > 0 || tagsAvailable) && (
            <>
              <Separator />
              <div className="space-y-2">
                <Label>Tags</Label>
                <TagPicker selectedIds={tagIds} onChange={(ids) => setValue('tagIds', ids)} />
              </div>
            </>
          )}

          <SheetFooter>
            <Button type="button" variant="outline" onClick={requestClose}>
              Cancel
            </Button>
            <Button type="submit" disabled={isSubmitting || mutation.isPending}>
              {(isSubmitting || mutation.isPending) && (
                <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
              )}
              {isEdit ? 'Save changes' : 'Create Interest'}
            </Button>
          </SheetFooter>
        </form>

        <AlertDialog open={discardConfirmOpen} onOpenChange={setDiscardConfirmOpen}>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Discard unsaved changes?</AlertDialogTitle>
              <AlertDialogDescription>
                You&apos;ve filled in some fields. Closing now will lose them.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Keep editing</AlertDialogCancel>
              <AlertDialogAction
                onClick={() => {
                  setDiscardConfirmOpen(false);
                  onOpenChange(false);
                }}
                className="bg-destructive text-destructive-foreground hover:bg-destructive/90 focus:ring-destructive"
              >
                Discard
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      </SheetContent>
      {createYachtOpen && selectedClientId && (
        <YachtForm
          open={createYachtOpen}
          onOpenChange={setCreateYachtOpen}
          initialOwner={{ type: 'client', id: selectedClientId }}
          onCreated={(y) => setValue('yachtId', y.id, { shouldDirty: true })}
        />
      )}
      {createClientOpen && (
        <ClientForm
          open={createClientOpen}
          onOpenChange={setCreateClientOpen}
          onCreated={(id) => setValue('clientId', id, { shouldDirty: true })}
        />
      )}
    </Sheet>
  );
}

// ── Helpers for the "Berth size desired" section ──────────────────────────────

const FT_PER_M = 1 / 0.3048;

function round2(n: number): number {
  return Math.round(n * 100) / 100;
}

function UnitToggle({ value, onChange }: { value: 'ft' | 'm'; onChange: (v: 'ft' | 'm') => void }) {
  return (
    <div
      className="inline-flex rounded-md border bg-muted/30 p-0.5 text-xs"
      role="radiogroup"
      aria-label="Display unit"
    >
      {(['ft', 'm'] as const).map((u) => (
        <button
          key={u}
          type="button"
          role="radio"
          aria-checked={value === u}
          onClick={() => onChange(u)}
          className={cn(
            'h-7 rounded px-3 font-medium transition-colors',
            value === u
              ? 'bg-background text-foreground shadow-sm'
              : 'text-muted-foreground hover:text-foreground',
          )}
        >
          {u}
        </button>
      ))}
    </div>
  );
}

interface DimensionInputProps {
  htmlId: string;
  label: string;
  placeholder?: string;
  unit: 'ft' | 'm';
  ftValue: string | number | undefined;
  onChangeFt: (next: string | undefined) => void;
  /** B3 #1: greyed out when the interest is set to read dimensions
   *  off the linked yacht instead of these fields. */
  disabled?: boolean;
}

/**
 * Single dimension input bound to a form value stored in feet. Renders the
 * value in the rep's chosen display unit and converts back on edit. The form
 * state stays canonical ft so the recommender (which queries `b.length_ft`
 * etc.) sees the same number regardless of which unit the rep typed in.
 *
 * Local `display` state preserves mid-typing strings like "18." that would
 * otherwise be lost to round-tripping through Number().
 */
function DimensionInput({
  htmlId,
  label,
  placeholder,
  unit,
  ftValue,
  onChangeFt,
  disabled,
}: DimensionInputProps) {
  const focusedRef = useRef(false);
  const [display, setDisplay] = useState<string>(() => computeDisplay(ftValue, unit));

  // Re-sync from the canonical ft value when it changes externally (form
  // reset, unit toggle). Skip while focused so we don't fight keystrokes.
  useEffect(() => {
    if (focusedRef.current) return;
    setDisplay(computeDisplay(ftValue, unit));
  }, [ftValue, unit]);

  const altValue = computeAltDisplay(ftValue, unit);

  return (
    <div className="space-y-1">
      <Label htmlFor={htmlId}>
        {label} <span className="text-muted-foreground">({unit})</span>
      </Label>
      <Input
        id={htmlId}
        type="number"
        inputMode="decimal"
        step="0.01"
        min={0}
        placeholder={placeholder}
        value={display}
        disabled={disabled}
        onFocus={() => {
          focusedRef.current = true;
        }}
        onBlur={() => {
          focusedRef.current = false;
          // Canonicalize the display from the ft source-of-truth on blur so
          // any mid-typed garbage clears.
          setDisplay(computeDisplay(ftValue, unit));
        }}
        onChange={(e) => {
          const raw = e.target.value;
          setDisplay(raw);
          if (raw === '') {
            onChangeFt(undefined);
            return;
          }
          const n = parseFloat(raw);
          if (!Number.isFinite(n) || n <= 0) {
            onChangeFt(undefined);
            return;
          }
          const ft = unit === 'ft' ? n : n * FT_PER_M;
          onChangeFt(String(round2(ft)));
        }}
      />
      {altValue ? (
        <p className="text-xs leading-tight text-muted-foreground">≈ {altValue}</p>
      ) : null}
    </div>
  );
}

function computeDisplay(ftValue: string | number | undefined, unit: 'ft' | 'm'): string {
  if (ftValue === undefined || ftValue === null || ftValue === '') return '';
  const ft = typeof ftValue === 'number' ? ftValue : Number(ftValue);
  if (!Number.isFinite(ft)) return '';
  const v = unit === 'ft' ? ft : ft * 0.3048;
  return String(round2(v));
}

function computeAltDisplay(ftValue: string | number | undefined, unit: 'ft' | 'm'): string | null {
  if (ftValue === undefined || ftValue === null || ftValue === '') return null;
  const ft = typeof ftValue === 'number' ? ftValue : Number(ftValue);
  if (!Number.isFinite(ft) || ft <= 0) return null;
  return unit === 'ft' ? `${round2(ft * 0.3048)} m` : `${round2(ft)} ft`;
}
