'use client';

import {
  type KeyboardEvent,
  type ReactNode,
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import {
  Anchor,
  Bell,
  Briefcase,
  Building2,
  Camera,
  Clock,
  FileText,
  Folder,
  History,
  Home,
  LayoutDashboard,
  MessageSquare,
  Plus,
  Receipt,
  Search,
  Settings as SettingsIcon,
  Ship,
  Tag as TagIcon,
  TrendingUp,
  User,
} from 'lucide-react';

import { apiFetch } from '@/lib/api/client';
import { cn } from '@/lib/utils';
import { formatCurrency } from '@/lib/utils/currency';
import { STAGE_LABELS, formatOutcome, type PipelineStage } from '@/lib/constants';
import {
  useSearch,
  type BucketType,
  type RecentlyViewedItem,
  type SearchResults,
} from '@/hooks/use-search';
import { useUIStore } from '@/stores/ui-store';
import { HighlightMatch } from '@/components/search/highlight-match';

// ─── Bucket configuration ────────────────────────────────────────────────────

interface BucketConfig {
  type: BucketType;
  label: string;
  icon: typeof User;
}

const BUCKETS: BucketConfig[] = [
  { type: 'clients', label: 'Clients', icon: User },
  { type: 'residentialClients', label: 'Residential', icon: Home },
  { type: 'yachts', label: 'Yachts', icon: Ship },
  { type: 'companies', label: 'Companies', icon: Building2 },
  { type: 'interests', label: 'Interests', icon: TrendingUp },
  { type: 'residentialInterests', label: 'Res. interests', icon: TrendingUp },
  { type: 'berths', label: 'Berths', icon: Anchor },
  { type: 'invoices', label: 'Invoices', icon: FileText },
  { type: 'expenses', label: 'Expenses', icon: Receipt },
  { type: 'documents', label: 'Documents', icon: Briefcase },
  { type: 'files', label: 'Files', icon: Folder },
  { type: 'reminders', label: 'Reminders', icon: Bell },
  { type: 'brochures', label: 'Brochures', icon: Camera },
  { type: 'tags', label: 'Tags', icon: TagIcon },
  // Pipeline-stage shortcuts ("eoi sent" → /interests?pipelineStage=eoi).
  // Sits below Tags so direct entity matches always win the dropdown;
  // stage filters are macro affordances that surface when the rep types
  // a stage name verbatim.
  { type: 'stages', label: 'Stages', icon: TrendingUp },
  // Notes are noisy content search.
  { type: 'notes', label: 'Notes', icon: MessageSquare },
  // Navigation (settings pages + admin sub-cards) lives at the very bottom -
  // users open the search to find entity records first; pages/settings are
  // the long-tail jump targets.
  { type: 'navigation', label: 'Settings', icon: SettingsIcon },
];

const NAV_ICON: Record<string, typeof User> = {
  dashboard: LayoutDashboard,
  settings: SettingsIcon,
  admin: SettingsIcon,
};

// ─── Paste detection ─────────────────────────────────────────────────────────

const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const INVOICE_RE = /^INV-\d{6}-\d+$/i;

function looksLikePastedId(input: string): boolean {
  const trimmed = input.trim();
  return UUID_RE.test(trimmed) || INVOICE_RE.test(trimmed);
}

// ─── Component ───────────────────────────────────────────────────────────────

export function CommandSearch() {
  const [query, setQuery] = useState('');
  const [focused, setFocused] = useState(false);
  const [activeBucket, setActiveBucket] = useState<BucketType | 'all'>('all');
  const [focusIndex, setFocusIndex] = useState<number>(-1);

  const router = useRouter();
  const queryClient = useQueryClient();
  const portSlug = useUIStore((s) => s.currentPortSlug);

  const wrapperRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const listboxId = useId();

  const { results, isFetching, recentSearches, recentlyViewed } = useSearch(query, {
    type: activeBucket === 'all' ? undefined : activeBucket,
    // Slightly higher cap when narrowed to one bucket - gives the user
    // room to scan more matches without paging out to /search.
    limit: activeBucket === 'all' ? 5 : 15,
  });

  // Persist the totals from the last "all" query so the filter chips stay
  // populated when the user narrows to a single bucket. Without this, the
  // narrowed query only returns counts for the active bucket and every
  // other chip would vanish - making it impossible to swap between
  // filters without clearing back to "All" first. Backed by state (not
  // a ref) so render reads are pure - React Compiler-safe.
  const [lastAllTotals, setLastAllTotals] = useState<SearchResults['totals'] | null>(null);
  useEffect(() => {
    if (activeBucket === 'all' && results?.totals) {
      // Snapshot the totals at the moment the user is on "All" so the
      // chips stay stable when they switch to a filtered bucket.
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setLastAllTotals(results.totals);
    }
  }, [activeBucket, results]);
  const chipTotals: SearchResults['totals'] | undefined =
    activeBucket === 'all' ? results?.totals : (lastAllTotals ?? results?.totals);

  const showDropdown = focused;

  // CommandSearch lives in the header and persists across navigations,
  // so its React Query cache never sees a remount. Invalidate the
  // recently-viewed + recent-terms queries whenever the dropdown opens
  // so the user sees fresh data after navigating around the app.
  useEffect(() => {
    if (!showDropdown) return;
    queryClient.invalidateQueries({ queryKey: ['search', 'recently-viewed'] });
    queryClient.invalidateQueries({ queryKey: ['search', 'recent-terms'] });
  }, [showDropdown, queryClient]);

  // Cmd/Ctrl+K focuses the input from anywhere on the page.
  useEffect(() => {
    function onKeyDown(e: globalThis.KeyboardEvent) {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        inputRef.current?.focus();
        inputRef.current?.select();
      }
    }
    document.addEventListener('keydown', onKeyDown);
    return () => document.removeEventListener('keydown', onKeyDown);
  }, []);

  // Click outside closes the dropdown.
  useEffect(() => {
    if (!focused) return;
    function onClick(e: MouseEvent) {
      if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
        setFocused(false);
        setFocusIndex(-1);
      }
    }
    document.addEventListener('mousedown', onClick);
    return () => document.removeEventListener('mousedown', onClick);
  }, [focused]);

  const closeAndNavigate = useCallback(
    (path: string) => {
      setFocused(false);
      setQuery('');
      setFocusIndex(-1);
      inputRef.current?.blur();
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      router.push(path as any);
    },
    [router],
  );

  // ── Paste detection: if the user pastes a UUID/INV-… into the input,
  //    fire the resolve-id endpoint and jump straight to the entity if
  //    it exists. Falls through to normal search otherwise.
  const onPaste = useCallback(
    async (e: React.ClipboardEvent<HTMLInputElement>) => {
      const pasted = e.clipboardData.getData('text').trim();
      if (!looksLikePastedId(pasted)) return;
      try {
        const res = await apiFetch<{ found: boolean; href: string | null }>(
          `/api/v1/search/resolve-id?id=${encodeURIComponent(pasted)}`,
        );
        if (res.found && res.href) {
          e.preventDefault();
          closeAndNavigate(res.href);
        }
      } catch {
        // Best-effort - fall through to normal search.
      }
    },
    [closeAndNavigate],
  );

  // Build the flat list of focusable rows in render order, so arrow-key
  // navigation walks them in the same order they appear visually.
  const flatRows = useMemo<FlatRow[]>(() => {
    if (!showDropdown) return [];
    return buildFlatRows({
      query,
      results,
      recentlyViewed,
      recentSearches,
      activeBucket,
      portSlug: portSlug ?? null,
    });
  }, [showDropdown, query, results, recentlyViewed, recentSearches, activeBucket, portSlug]);

  // Reset focus index when the visible row set changes. The set-state
  // is intentional - external state (bucket / query) drives focus.
  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setFocusIndex(-1);
  }, [activeBucket, query]);

  function onInputKeyDown(e: KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Escape') {
      e.preventDefault();
      setFocused(false);
      setFocusIndex(-1);
      inputRef.current?.blur();
      return;
    }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      setFocusIndex((i) => Math.min(i + 1, flatRows.length - 1));
      return;
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      setFocusIndex((i) => Math.max(i - 1, -1));
      return;
    }
    if (e.key === 'Enter') {
      const row = focusIndex >= 0 ? flatRows[focusIndex] : null;
      if (row) {
        e.preventDefault();
        if (row.kind === 'recent-term') {
          setQuery(row.term);
          return;
        }
        closeAndNavigate(row.href);
      }
      // Enter without a focused row is a no-op - the dropdown is already
      // showing every relevant match (filter chips raise the cap when the
      // user wants to see more of one bucket). No standalone /search
      // page; refining the query is faster than scrolling further.
    }
  }

  const activeOptionId =
    focusIndex >= 0 && flatRows[focusIndex]
      ? `${listboxId}-${flatRows[focusIndex].key}`
      : undefined;

  return (
    <div ref={wrapperRef} className="relative w-full">
      <div
        className={cn(
          'flex items-center gap-2 rounded-lg border bg-background px-3 shadow-xs transition-colors w-full',
          focused ? 'border-brand/70' : 'border-input',
        )}
      >
        <Search className="h-4 w-4 shrink-0 text-muted-foreground" />
        <input
          ref={inputRef}
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onFocus={() => setFocused(true)}
          onPaste={onPaste}
          onKeyDown={onInputKeyDown}
          placeholder="Search clients, yachts, berths, invoices…  (⌘K)"
          aria-label="Search"
          role="combobox"
          aria-expanded={showDropdown}
          aria-controls={listboxId}
          aria-autocomplete="list"
          aria-activedescendant={activeOptionId}
          // Wrapper border swap is the focus indicator; suppress the
          // global *:focus-visible ring that would otherwise paint a
          // rectangular box clashing with the rounded wrapper.
          className="h-9 flex-1 min-w-0 bg-transparent text-sm outline-hidden ring-0 focus:outline-hidden focus:ring-0 focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground"
        />
        {isFetching && query.length >= 2 && (
          <span
            className="h-3 w-3 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent"
            aria-hidden
          />
        )}
      </div>

      {showDropdown && (
        <div
          id={listboxId}
          role="listbox"
          aria-label="Search results"
          className={cn(
            // Explicit white/popover background pair guards against subtle
            // transparency artifacts UAT 2026-05-24 captured (rows on the
            // Berths page bled through to the table behind even though
            // bg-popover resolves opaque on paper). bg-white wins the
            // tailwind-merge race so light mode is unambiguously solid;
            // dark mode falls through to bg-popover via the dark: prefix.
            'absolute top-[calc(100%+4px)] left-0 z-50 rounded-md border bg-white dark:bg-popover shadow-lg overflow-hidden',
            // Desktop: anchored to the input width, capped on viewport
            'w-full max-w-[min(720px,calc(100vw-2rem))]',
            // Mobile (<lg): full-screen sheet so cramped phone widths
            // still render comfortable rows
            'max-lg:fixed max-lg:inset-x-2 max-lg:top-16 max-lg:bottom-2 max-lg:max-w-none',
          )}
        >
          {/* Filter chip row - always visible while the dropdown is open. */}
          <FilterChipRow
            totals={chipTotals}
            active={activeBucket}
            onChange={setActiveBucket}
            disabled={query.length < 2}
          />

          <div className="max-h-[60vh] max-lg:max-h-[calc(100%-3rem)] overflow-y-auto py-1">
            {/* No query yet - recently viewed + recent terms. */}
            {query.length < 2 && (
              <EmptyStateBeforeSearch
                listboxId={listboxId}
                recentlyViewed={recentlyViewed}
                recentSearches={recentSearches}
                flatRows={flatRows}
                focusIndex={focusIndex}
                onSelect={closeAndNavigate}
                onSelectTerm={setQuery}
              />
            )}

            {/* Active query - results or zero-state. */}
            {query.length >= 2 && (
              <ResultsRegion
                listboxId={listboxId}
                query={query}
                results={results}
                portSlug={portSlug ?? null}
                activeBucket={activeBucket}
                flatRows={flatRows}
                focusIndex={focusIndex}
                onSelect={closeAndNavigate}
              />
            )}
          </div>

          {/* Footer - keyboard hint only. */}
          {query.length >= 2 && (
            <div className="border-t bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
              ↑↓ navigate · ↵ open · esc close
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ─── Filter chips ────────────────────────────────────────────────────────────

function FilterChipRow({
  totals,
  active,
  onChange,
  disabled,
}: {
  /** Counts from the last "all" query, persisted so chips stay visible
   *  when the user narrows to a single bucket. Falls back to the current
   *  results.totals when no "all" snapshot exists yet. */
  totals: SearchResults['totals'] | undefined;
  active: BucketType | 'all';
  onChange: (b: BucketType | 'all') => void;
  disabled: boolean;
}) {
  return (
    <div
      role="tablist"
      aria-label="Filter results by type"
      className="flex gap-1 overflow-x-auto border-b bg-muted/20 px-2 py-1.5"
    >
      <ChipButton
        active={active === 'all'}
        disabled={disabled}
        onClick={() => onChange('all')}
        count={undefined}
      >
        All
      </ChipButton>
      {BUCKETS.map((b) => {
        const count = totals?.[b.type] ?? 0;
        // Hide chips for buckets with zero matches in the last "all"
        // snapshot - keeps the row tight and avoids dead-end clicks.
        // Always show the active chip + every chip before a query has run.
        if (!disabled && count === 0 && active !== b.type) return null;
        return (
          <ChipButton
            key={b.type}
            active={active === b.type}
            disabled={disabled}
            onClick={() => onChange(b.type)}
            count={count > 0 ? count : undefined}
          >
            {b.label}
          </ChipButton>
        );
      })}
    </div>
  );
}

function ChipButton({
  active,
  disabled,
  count,
  onClick,
  children,
}: {
  active: boolean;
  disabled: boolean;
  count?: number;
  onClick: () => void;
  children: ReactNode;
}) {
  return (
    <button
      type="button"
      role="tab"
      aria-selected={active}
      disabled={disabled}
      onClick={onClick}
      className={cn(
        'shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors',
        active
          ? 'bg-brand text-white'
          : 'bg-background text-muted-foreground hover:text-foreground hover:bg-accent',
        disabled && 'opacity-50 cursor-not-allowed',
      )}
    >
      {children}
      {typeof count === 'number' && <span className="ml-1 opacity-70">({count})</span>}
    </button>
  );
}

// ─── Empty state (no query) ──────────────────────────────────────────────────

function EmptyStateBeforeSearch({
  listboxId,
  recentlyViewed,
  recentSearches,
  flatRows,
  focusIndex,
  onSelect,
  onSelectTerm,
}: {
  listboxId: string;
  recentlyViewed: RecentlyViewedItem[];
  recentSearches: string[];
  flatRows: FlatRow[];
  focusIndex: number;
  onSelect: (href: string) => void;
  onSelectTerm: (term: string) => void;
}) {
  if (recentlyViewed.length === 0 && recentSearches.length === 0) {
    return (
      <div className="px-3 py-6 text-center text-sm text-muted-foreground">
        Type at least 2 characters to search clients, yachts, berths, invoices, and more.
      </div>
    );
  }

  return (
    <>
      {recentlyViewed.length > 0 && (
        <div>
          <SectionHeading icon={History}>Recently viewed</SectionHeading>
          {recentlyViewed.map((item) => {
            const row = flatRows.find(
              (r) => r.kind === 'recent-view' && r.item.id === item.id && r.item.type === item.type,
            );
            const isFocused = !!row && focusIndex >= 0 && flatRows[focusIndex] === row;
            return (
              <button
                key={`${item.type}:${item.id}`}
                id={row ? `${listboxId}-${row.key}` : undefined}
                role="option"
                aria-selected={isFocused}
                onClick={() => onSelect(item.href)}
                className={cn(
                  'flex w-full items-center gap-2 px-3 py-2 text-sm text-left hover:bg-accent cursor-pointer',
                  isFocused && 'bg-accent',
                )}
              >
                <Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
                <span className="truncate font-medium">{item.label}</span>
                {item.sub && (
                  <span className="ml-auto truncate text-xs text-muted-foreground">{item.sub}</span>
                )}
              </button>
            );
          })}
        </div>
      )}
      {recentSearches.length > 0 && (
        <div>
          <SectionHeading icon={Search}>Recent searches</SectionHeading>
          {recentSearches.map((term) => {
            const row = flatRows.find((r) => r.kind === 'recent-term' && r.term === term);
            const isFocused = !!row && focusIndex >= 0 && flatRows[focusIndex] === row;
            return (
              <button
                key={term}
                id={row ? `${listboxId}-${row.key}` : undefined}
                role="option"
                aria-selected={isFocused}
                onClick={() => onSelectTerm(term)}
                className={cn(
                  'flex w-full items-center gap-2 px-3 py-2 text-sm text-left hover:bg-accent cursor-pointer',
                  isFocused && 'bg-accent',
                )}
              >
                <Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
                {term}
              </button>
            );
          })}
        </div>
      )}
    </>
  );
}

// ─── Results region (active query) ───────────────────────────────────────────

function ResultsRegion({
  listboxId,
  query,
  results,
  portSlug,
  activeBucket,
  flatRows,
  focusIndex,
  onSelect,
}: {
  listboxId: string;
  query: string;
  results: SearchResults | undefined;
  portSlug: string | null;
  activeBucket: BucketType | 'all';
  flatRows: FlatRow[];
  focusIndex: number;
  onSelect: (href: string) => void;
}) {
  if (!results) {
    return <div className="px-3 py-6 text-center text-sm text-muted-foreground">Searching…</div>;
  }

  const totalHits = Object.values(results.totals).reduce((acc, n) => acc + n, 0);
  if (totalHits === 0) {
    return <ZeroState query={query} portSlug={portSlug} />;
  }

  // Render every bucket the active filter allows.
  return (
    <>
      {BUCKETS.map((b) => {
        if (activeBucket !== 'all' && activeBucket !== b.type) return null;
        const rowsForBucket = flatRows.filter((r) => r.kind === 'result' && r.bucket === b.type);
        if (rowsForBucket.length === 0) return null;

        return (
          <BucketSection key={b.type} icon={b.icon} label={b.label}>
            {rowsForBucket.map((row) => {
              if (row.kind !== 'result') return null;
              const isFocused = focusIndex >= 0 && flatRows[focusIndex] === row;
              return (
                <ResultRow
                  key={row.key}
                  id={`${listboxId}-${row.key}`}
                  row={row}
                  query={query}
                  isFocused={isFocused}
                  onSelect={onSelect}
                />
              );
            })}
          </BucketSection>
        );
      })}

      {results.otherPorts && results.otherPorts.length > 0 && activeBucket === 'all' && (
        <BucketSection icon={Building2} label="Other ports (super-admin)">
          {results.otherPorts.map((row) => {
            const flatRow = flatRows.find((r) => r.kind === 'other-port' && r.item === row);
            const isFocused = !!flatRow && focusIndex >= 0 && flatRows[focusIndex] === flatRow;
            return (
              <button
                key={`${row.portId}:${row.type}:${row.id}`}
                id={flatRow ? `${listboxId}-${flatRow.key}` : undefined}
                role="option"
                aria-selected={isFocused}
                onClick={() => onSelect(`/${row.portSlug}/${pluralize(row.type)}/${row.id}`)}
                className={cn(
                  'flex w-full items-center gap-2.5 px-3 py-2 text-sm text-left hover:bg-accent cursor-pointer text-muted-foreground',
                  isFocused && 'bg-accent text-foreground',
                )}
              >
                <span className="text-xs uppercase tracking-wide text-muted-foreground">
                  {row.portName}
                </span>
                <span className="truncate text-foreground">
                  <HighlightMatch text={row.label} query={query} />
                </span>
                {row.sub && <span className="ml-auto truncate text-xs">{row.sub}</span>}
              </button>
            );
          })}
        </BucketSection>
      )}
    </>
  );
}

function ZeroState({ query, portSlug }: { query: string; portSlug: string | null }) {
  if (!portSlug) {
    return (
      <div className="px-3 py-6 text-center text-sm text-muted-foreground">
        No results for &ldquo;{query}&rdquo;
      </div>
    );
  }
  return (
    <div className="px-3 py-5">
      <p className="text-sm text-muted-foreground mb-3">
        No results for <span className="font-medium text-foreground">&ldquo;{query}&rdquo;</span>
      </p>
      <p className="text-xs uppercase tracking-wide text-muted-foreground mb-2">Quick create</p>
      <div className="flex flex-wrap gap-2">
        {/* Land on the list page with `?create=1&prefill_name=…`; the list
            page's `useCreateFromUrl` hook pops the create sheet with the
            search query pre-filled. The bogus `/<entity>/new` URLs used
            previously hit the `[id]` route and rendered "not found". */}
        <QuickCreateButton
          icon={User}
          label={`New client "${query}"`}
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          href={`/${portSlug}/clients?create=1&prefill_name=${encodeURIComponent(query)}` as any}
        />
        <QuickCreateButton
          icon={Ship}
          label={`New yacht "${query}"`}
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          href={`/${portSlug}/yachts?create=1&prefill_name=${encodeURIComponent(query)}` as any}
        />
        <QuickCreateButton
          icon={Building2}
          label={`New company "${query}"`}
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          href={`/${portSlug}/companies?create=1&prefill_name=${encodeURIComponent(query)}` as any}
        />
      </div>
    </div>
  );
}

function QuickCreateButton({
  icon: Icon,
  label,
  href,
}: {
  icon: typeof User;
  label: string;
  href: string;
}) {
  return (
    <Link
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      href={href as any}
      className="flex items-center gap-1.5 rounded-md border bg-background px-2.5 py-1 text-xs font-medium text-foreground hover:bg-accent transition-colors"
    >
      <Plus className="h-3 w-3" />
      <Icon className="h-3 w-3" />
      <span>{label}</span>
    </Link>
  );
}

// ─── Result row ──────────────────────────────────────────────────────────────

function ResultRow({
  id,
  row,
  query,
  isFocused,
  onSelect,
}: {
  id: string;
  row: Extract<FlatRow, { kind: 'result' }>;
  query: string;
  isFocused: boolean;
  onSelect: (href: string) => void;
}) {
  const Icon = row.icon;
  return (
    <button
      type="button"
      id={id}
      role="option"
      aria-selected={isFocused}
      onClick={() => onSelect(row.href)}
      className={cn(
        'flex w-full items-start gap-2.5 px-3 py-2 text-sm text-left hover:bg-accent cursor-pointer',
        isFocused && 'bg-accent',
      )}
    >
      <Icon className="h-4 w-4 shrink-0 text-muted-foreground mt-0.5" />
      <div className="flex-1 min-w-0">
        <div className="flex items-center gap-2 flex-wrap">
          <span className="truncate font-medium text-foreground">
            <HighlightMatch text={row.label} query={query} />
          </span>
          {row.badges?.map((badge) => (
            <Badge key={badge.label} tone={badge.tone} label={badge.label} />
          ))}
        </div>
        {row.sub && (
          <div className="text-xs text-muted-foreground truncate mt-0.5">
            <HighlightMatch text={row.sub} query={query} />
          </div>
        )}
        {row.relatedVia && (
          <div className="text-xs italic text-muted-foreground truncate mt-0.5">
            via {row.relatedVia.label}
          </div>
        )}
      </div>
    </button>
  );
}

function Badge({
  label,
  tone,
}: {
  label: string;
  tone: 'neutral' | 'warning' | 'success' | 'danger';
}) {
  const cls = {
    neutral: 'bg-muted text-muted-foreground',
    warning: 'bg-amber-100 text-amber-800',
    success: 'bg-emerald-100 text-emerald-800',
    danger: 'bg-rose-100 text-rose-800',
  }[tone];
  return (
    <span
      className={cn(
        'inline-flex items-center rounded px-1.5 py-0 text-xs font-semibold uppercase tracking-wide',
        cls,
      )}
    >
      {label}
    </span>
  );
}

function SectionHeading({ icon: Icon, children }: { icon: typeof User; children: ReactNode }) {
  return (
    <div className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
      <Icon className="h-3 w-3" />
      <span>{children}</span>
    </div>
  );
}

function BucketSection({
  icon,
  label,
  children,
}: {
  icon: typeof User;
  label: string;
  children: ReactNode;
}) {
  return (
    <div>
      <SectionHeading icon={icon}>{label}</SectionHeading>
      {children}
    </div>
  );
}

// ─── Flat-row construction (drives keyboard nav + ARIA) ──────────────────────

export type ResultBadge = { label: string; tone: 'neutral' | 'warning' | 'success' | 'danger' };

export type FlatRow =
  | {
      kind: 'recent-view';
      key: string;
      item: RecentlyViewedItem;
      href: string;
    }
  | {
      kind: 'recent-term';
      key: string;
      term: string;
      href: string;
    }
  | {
      kind: 'result';
      key: string;
      bucket: BucketType;
      icon: typeof User;
      label: string;
      sub: string | null;
      href: string;
      badges?: ResultBadge[];
      /** Provenance hint when the row was surfaced via graph expansion.
       *  Rendered as a subtle "via Berth A10" line below the sub. */
      relatedVia?: { type: string; label: string } | null;
    }
  | {
      kind: 'other-port';
      key: string;
      item: SearchResults['otherPorts'] extends (infer U)[] | undefined ? U : never;
      href: string;
    };

export interface BuildFlatRowsArgs {
  query: string;
  results: SearchResults | undefined;
  recentlyViewed: RecentlyViewedItem[];
  recentSearches: string[];
  activeBucket: BucketType | 'all';
  portSlug: string | null;
}

export function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
  const { query, results, recentlyViewed, recentSearches, activeBucket, portSlug } = args;
  const rows: FlatRow[] = [];

  if (query.length < 2) {
    for (const item of recentlyViewed) {
      rows.push({
        kind: 'recent-view',
        key: `recent-view:${item.type}:${item.id}`,
        item,
        href: item.href,
      });
    }
    for (const term of recentSearches) {
      rows.push({
        kind: 'recent-term',
        key: `recent-term:${term}`,
        term,
        href: '',
      });
    }
    return rows;
  }

  if (!results || !portSlug) return rows;

  const include = (b: BucketType) => activeBucket === 'all' || activeBucket === b;

  if (include('clients')) {
    for (const c of results.clients) {
      // Prefer the actual matched-contact value (real email/phone the
      // query hit) over a generic "matched on X" label. Falls back to
      // the typed matchedOn when no value is available (e.g. trigram
      // similarity match on the name itself).
      let sub: string | null = c.matchedContact ?? null;
      if (!sub && c.matchedOn) sub = `matched on ${c.matchedOn}`;
      rows.push({
        kind: 'result',
        key: `clients:${c.id}`,
        bucket: 'clients',
        icon: User,
        label: c.fullName,
        sub,
        href: `/${portSlug}/clients/${c.id}`,
        badges: c.archivedAt ? [{ label: 'Archived', tone: 'neutral' }] : undefined,
        relatedVia: c.relatedVia ?? null,
      });
    }
  }
  if (include('residentialClients')) {
    for (const c of results.residentialClients) {
      rows.push({
        kind: 'result',
        key: `residentialClients:${c.id}`,
        bucket: 'residentialClients',
        icon: Home,
        label: c.fullName,
        sub: c.email ?? c.phone ?? null,
        href: `/${portSlug}/residential/clients/${c.id}`,
      });
    }
  }
  if (include('yachts')) {
    for (const y of results.yachts) {
      const sub = [y.hullNumber, y.registration].filter(Boolean).join(' · ') || null;
      rows.push({
        kind: 'result',
        key: `yachts:${y.id}`,
        bucket: 'yachts',
        icon: Ship,
        label: y.name,
        sub,
        href: `/${portSlug}/yachts/${y.id}`,
        relatedVia: y.relatedVia ?? null,
      });
    }
  }
  if (include('companies')) {
    for (const co of results.companies) {
      const sub = [co.legalName, co.taxId].filter(Boolean).join(' · ') || null;
      rows.push({
        kind: 'result',
        key: `companies:${co.id}`,
        bucket: 'companies',
        icon: Building2,
        label: co.name,
        sub,
        href: `/${portSlug}/companies/${co.id}`,
        relatedVia: co.relatedVia ?? null,
      });
    }
  }
  if (include('interests')) {
    for (const i of results.interests) {
      const badges: ResultBadge[] = [];
      if (i.outcome) {
        badges.push({
          label: formatOutcome(i.outcome) ?? i.outcome,
          tone: i.outcome === 'won' ? 'success' : 'neutral',
        });
      } else {
        badges.push({
          label:
            STAGE_LABELS[i.pipelineStage as PipelineStage] ?? i.pipelineStage.replace(/_/g, ' '),
          tone: 'warning',
        });
      }
      rows.push({
        kind: 'result',
        key: `interests:${i.id}`,
        bucket: 'interests',
        icon: TrendingUp,
        label: i.clientName,
        sub: i.berthMooringNumber,
        href: `/${portSlug}/interests/${i.id}`,
        badges: badges.length > 0 ? badges : undefined,
        relatedVia: i.relatedVia ?? null,
      });
    }
  }
  if (include('residentialInterests')) {
    for (const i of results.residentialInterests) {
      rows.push({
        kind: 'result',
        key: `residentialInterests:${i.id}`,
        bucket: 'residentialInterests',
        icon: TrendingUp,
        label: i.clientName,
        sub: STAGE_LABELS[i.pipelineStage as PipelineStage] ?? i.pipelineStage.replace(/_/g, ' '),
        href: `/${portSlug}/residential/interests/${i.id}`,
      });
    }
  }
  if (include('berths')) {
    for (const b of results.berths) {
      const badges: ResultBadge[] = [];
      if (b.status === 'sold') badges.push({ label: 'Sold', tone: 'success' });
      else if (b.status === 'under_offer') badges.push({ label: 'Under offer', tone: 'warning' });
      const sub =
        [
          b.area,
          b.linkedInterestCount > 0
            ? `${b.linkedInterestCount} interest${b.linkedInterestCount === 1 ? '' : 's'}`
            : null,
        ]
          .filter(Boolean)
          .join(' · ') || null;
      rows.push({
        kind: 'result',
        key: `berths:${b.id}`,
        bucket: 'berths',
        icon: Anchor,
        label: b.mooringNumber,
        sub,
        href: `/${portSlug}/berths/${b.id}`,
        badges: badges.length > 0 ? badges : undefined,
        relatedVia: b.relatedVia ?? null,
      });
    }
  }
  if (include('invoices')) {
    for (const inv of results.invoices) {
      const badges: ResultBadge[] = [];
      if (inv.status === 'overdue') badges.push({ label: 'Overdue', tone: 'danger' });
      else if (inv.paymentStatus === 'paid') badges.push({ label: 'Paid', tone: 'success' });
      else if (inv.status === 'sent') badges.push({ label: 'Sent', tone: 'neutral' });
      const sub = inv.totalAmount
        ? `${inv.clientName} · ${formatCurrency(inv.totalAmount, inv.currency)}`
        : inv.clientName;
      rows.push({
        kind: 'result',
        key: `invoices:${inv.id}`,
        bucket: 'invoices',
        icon: FileText,
        label: inv.invoiceNumber,
        sub,
        href: `/${portSlug}/invoices/${inv.id}`,
        badges: badges.length > 0 ? badges : undefined,
      });
    }
  }
  if (include('expenses')) {
    for (const e of results.expenses) {
      const badges: ResultBadge[] = [];
      if (e.paymentStatus === 'paid') badges.push({ label: 'Paid', tone: 'success' });
      const sub = [e.vendor, e.tripLabel].filter(Boolean).join(' · ') || null;
      rows.push({
        kind: 'result',
        key: `expenses:${e.id}`,
        bucket: 'expenses',
        icon: Receipt,
        label: e.description ?? e.vendor ?? formatCurrency(e.amount, e.currency),
        sub,
        href: `/${portSlug}/expenses/${e.id}`,
        badges: badges.length > 0 ? badges : undefined,
      });
    }
  }
  if (include('documents')) {
    for (const d of results.documents) {
      const badges: ResultBadge[] = [];
      if (d.status === 'completed') badges.push({ label: 'Signed', tone: 'success' });
      else if (d.status === 'expired') badges.push({ label: 'Expired', tone: 'danger' });
      else if (d.status === 'sent') badges.push({ label: 'Awaiting signature', tone: 'warning' });
      rows.push({
        kind: 'result',
        key: `documents:${d.id}`,
        bucket: 'documents',
        icon: Briefcase,
        label: d.title,
        sub: d.matchedSignerName,
        href: `/${portSlug}/documents/${d.id}`,
        badges: badges.length > 0 ? badges : undefined,
      });
    }
  }
  if (include('files')) {
    for (const f of results.files) {
      rows.push({
        kind: 'result',
        key: `files:${f.id}`,
        bucket: 'files',
        icon: Folder,
        label: f.filename,
        sub: f.ownerLabel,
        href: `/${portSlug}/documents`,
      });
    }
  }
  if (include('reminders')) {
    for (const r of results.reminders) {
      const badges: ResultBadge[] = [];
      const due = new Date(r.dueAt);
      if (due.getTime() < Date.now()) badges.push({ label: 'Overdue', tone: 'danger' });
      rows.push({
        kind: 'result',
        key: `reminders:${r.id}`,
        bucket: 'reminders',
        icon: Bell,
        label: r.title,
        sub: due.toLocaleDateString(),
        href: `/${portSlug}/reminders`,
        badges: badges.length > 0 ? badges : undefined,
      });
    }
  }
  if (include('brochures')) {
    for (const b of results.brochures) {
      rows.push({
        kind: 'result',
        key: `brochures:${b.id}`,
        bucket: 'brochures',
        icon: Camera,
        label: b.label,
        sub: b.isDefault ? 'Default brochure' : null,
        href: `/${portSlug}/settings`,
      });
    }
  }
  if (include('tags')) {
    for (const t of results.tags) {
      rows.push({
        kind: 'result',
        key: `tags:${t.id}`,
        bucket: 'tags',
        icon: TagIcon,
        label: `Tag: ${t.name}`,
        sub: `${t.totalCount} tagged`,
        // Tag-filtered list view; until that list page exists, fall back
        // to the tags settings page.
        href: `/${portSlug}/clients?tag=${encodeURIComponent(t.name)}`,
      });
    }
  }
  // Stage-filter shortcuts. Typing a stage label ("eoi sent", "deposit",
  // "qualified") surfaces a row that lands the rep on the filtered
  // interests list. count is the live row total so the rep sees how
  // many records they're about to scope to.
  if (include('stages')) {
    for (const st of results.stages) {
      rows.push({
        kind: 'result',
        key: `stages:${st.stage}`,
        bucket: 'stages',
        icon: TrendingUp,
        label: `Stage: ${st.label}`,
        sub: `${st.count} ${st.count === 1 ? 'interest' : 'interests'} in this stage`,
        href: `/${portSlug}${st.href}`,
      });
    }
  }
  // Notes - content matches inside free-text notes are noisy by nature, so
  // the user sees them after the entity-specific buckets above have
  // surfaced their tighter matches.
  if (include('notes')) {
    for (const n of results.notes) {
      const sourceCollection =
        n.source === 'client'
          ? 'clients'
          : n.source === 'interest'
            ? 'interests'
            : n.source === 'yacht'
              ? 'yachts'
              : 'companies';
      rows.push({
        kind: 'result',
        key: `notes:${n.id}`,
        bucket: 'notes',
        icon: MessageSquare,
        label: `${n.source.charAt(0).toUpperCase() + n.source.slice(1)} note · ${n.sourceLabel}`,
        sub: n.snippet,
        href: `/${portSlug}/${sourceCollection}/${n.sourceId}?tab=notes`,
      });
    }
  }
  // Navigation (settings pages + admin section cards) goes LAST - these
  // are jump targets, not the primary thing a user opens the search to
  // find. Surfacing them after entity matches keeps the top of the
  // dropdown focused on records.
  if (include('navigation')) {
    for (const n of results.navigation) {
      const Icon = NAV_ICON[n.category] ?? SettingsIcon;
      rows.push({
        kind: 'result',
        key: `navigation:${n.id}`,
        bucket: 'navigation',
        icon: Icon,
        label: n.label,
        sub: n.category,
        // Catalog hrefs already have :portSlug substituted server-side.
        href: n.href,
      });
    }
  }

  if (results.otherPorts && activeBucket === 'all') {
    for (const op of results.otherPorts) {
      rows.push({
        kind: 'other-port',
        key: `other:${op.portId}:${op.type}:${op.id}`,
        item: op,
        href: `/${op.portSlug}/${pluralize(op.type)}/${op.id}`,
      });
    }
  }

  return rows;
}

function pluralize(type: string): string {
  if (type === 'company') return 'companies';
  return `${type}s`;
}

// Keep a no-op export for any legacy import sites.
export function SearchTrigger() {
  return null;
}
