'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { Drawer as VaulDrawer } from 'vaul';
import { Clock, History, Search, X } from 'lucide-react';

import { apiFetch } from '@/lib/api/client';
import { cn } from '@/lib/utils';
import { useSearch, type BucketType, type SearchResults } from '@/hooks/use-search';
import { useUIStore } from '@/stores/ui-store';
import { buildFlatRows, type FlatRow } from './command-search';
import { HighlightMatch } from './highlight-match';

// Match the desktop bucket order - feels consistent when reps switch contexts.
const BUCKETS: { type: BucketType; label: string }[] = [
  { type: 'clients', label: 'Clients' },
  { type: 'yachts', label: 'Yachts' },
  { type: 'companies', label: 'Companies' },
  { type: 'interests', label: 'Interests' },
  { type: 'berths', label: 'Berths' },
  { type: 'documents', label: 'Documents' },
  { type: 'invoices', label: 'Invoices' },
  { type: 'reminders', label: 'Reminders' },
];

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

const BADGE_TONE: Record<'neutral' | 'warning' | 'success' | 'danger', string> = {
  neutral: 'bg-muted text-muted-foreground',
  warning: 'bg-amber-100 text-amber-900',
  success: 'bg-emerald-100 text-emerald-900',
  danger: 'bg-red-100 text-red-900',
};

interface MobileSearchOverlayProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayProps) {
  const [query, setQuery] = useState('');
  const [activeBucket, setActiveBucket] = useState<BucketType | 'all'>('all');
  // Tracks the visible-above-keyboard height. iOS Safari ignores
  // keyboard area in `dvh`, so we use the visualViewport API directly:
  // visualViewport.height is the actual visible area in CSS pixels,
  // updates in real time as the keyboard rises/falls.
  const [visibleHeight, setVisibleHeight] = useState<number | null>(null);
  const router = useRouter();
  const queryClient = useQueryClient();
  const portSlug = useUIStore((s) => s.currentPortSlug);
  const inputRef = useRef<HTMLInputElement>(null);

  // The overlay is mounted once at the layout root, so the recently-
  // viewed query won't refetch via the usual mount path. Bump it every
  // time the drawer opens - the user is about to look at it, and the
  // staleTime cache may have missed an entity view that happened in a
  // route that doesn't render <TrackEntityView>.
  useEffect(() => {
    if (!open) return;
    queryClient.invalidateQueries({ queryKey: ['search', 'recently-viewed'] });
    queryClient.invalidateQueries({ queryKey: ['search', 'recent-terms'] });
  }, [open, queryClient]);

  useEffect(() => {
    if (!open) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setVisibleHeight(null);
      return;
    }
    const vv = window.visualViewport;
    if (!vv) return;
    // Subscribing to the visualViewport external store - canonical
    // useEffect+setState shape for that pattern.
    const update = () => setVisibleHeight(vv.height);
    update();
    vv.addEventListener('resize', update);
    vv.addEventListener('scroll', update);
    return () => {
      vv.removeEventListener('resize', update);
      vv.removeEventListener('scroll', update);
    };
  }, [open]);

  const { results, isFetching, recentSearches, recentlyViewed } = useSearch(query, {
    type: activeBucket === 'all' ? undefined : activeBucket,
    limit: activeBucket === 'all' ? 5 : 25,
  });

  // Persist counts from the last "all" query so chip counts stay visible
  // when the user narrows to a single bucket. Narrowed queries only
  // return counts for the active bucket, which would otherwise wipe the
  // counts off every other chip the moment the user taps one. Backed
  // by state, not a ref, so render reads are pure (Compiler-safe).
  const [lastAllTotals, setLastAllTotals] = useState<SearchResults['totals'] | null>(null);
  useEffect(() => {
    if (activeBucket === 'all' && results?.totals) {
      // 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);

  // Auto-focus is delegated to Vaul's `autoFocus` + the input's
  // `autoFocus` attribute (synchronous in-gesture, which iOS Safari
  // requires before it'll pop the keyboard on programmatic focus).
  // A useEffect setTimeout was the previous approach but broke the
  // user-gesture chain - input was focused, keyboard stayed hidden.

  // Body scroll lock is delegated to Vaul (modal=true + noBodyStyles=false
  // defaults). Manual position:fixed locking caused a visible scroll-then-
  // jump on iOS Safari because the body briefly snaps to scrollY=0 after
  // being taken out of flow, before the negative-top compensation paints.
  // Vaul handles the lock natively via overflow:hidden which doesn't
  // remove the body from flow. The trick to avoid Vaul's iOS scroll-lock
  // race is `repositionInputs={false}` on the Drawer.Root (set below).

  // Reset query when the drawer closes. Without this, reopening the
  // overlay would flash stale results before the empty state renders.
  // setState in effect is intentional - the trigger is a discrete
  // open/close transition.
  useEffect(() => {
    if (!open) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setQuery('');
      setActiveBucket('all');
    }
  }, [open]);

  const close = useCallback(() => {
    onOpenChange(false);
    inputRef.current?.blur();
  }, [onOpenChange]);

  const navigate = useCallback(
    (path: string) => {
      close();
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      router.push(path as any);
    },
    [close, router],
  );

  // Paste a UUID or invoice number → jump straight to the entity.
  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();
          navigate(res.href);
        }
      } catch {
        // Best-effort - fall through to text search.
      }
    },
    [navigate],
  );

  const rows = useMemo<FlatRow[]>(
    () =>
      buildFlatRows({
        query,
        results,
        recentlyViewed,
        recentSearches,
        activeBucket,
        portSlug,
      }),
    [query, results, recentlyViewed, recentSearches, activeBucket, portSlug],
  );

  const showingEmptyHints = query.length < 2;
  const noResults = !showingEmptyHints && rows.length === 0 && !isFetching;

  return (
    <VaulDrawer.Root
      open={open}
      onOpenChange={onOpenChange}
      // iOS Safari fluidity recipe sourced from:
      //   - github.com/shadcn-ui/ui/issues/4321 (page reflow on open)
      //   - gracefullight.dev fix-ios-safari-scroll-issue-with-vaul-drawer
      //
      // - shouldScaleBackground=false: page doesn't shrink behind us;
      //   feels in-app rather than card-over-page.
      // - repositionInputs=false: don't let Vaul jiggle the viewport
      //   when the input autofocuses and the keyboard appears - that
      //   was the source of the "scroll then jump back" we were seeing.
      //   Vaul still locks scroll via its modal=true default.
      // - autoFocus=true: Vaul focuses the input synchronously inside
      //   the user-gesture frame, which is the only way iOS Safari
      //   will pop the keyboard on programmatic focus. The input has
      //   `autoFocus` set below so Vaul picks it as the target.
      shouldScaleBackground={false}
      repositionInputs={false}
    >
      <VaulDrawer.Portal>
        <VaulDrawer.Overlay className="fixed inset-0 z-50 bg-black/30 backdrop-blur-xs" />
        <VaulDrawer.Content
          // Anchor by top + explicit height (not bottom: 0). iOS treats
          // `bottom: 0` on position:fixed inconsistently when the
          // keyboard is up (sometimes layout viewport, sometimes visual);
          // anchoring by top + height removes that ambiguity. Height
          // comes from visualViewport.height - the only iOS-reliable
          // source for "visible area above keyboard". 12px gap at the
          // top keeps a strip of backdrop visible.
          style={
            visibleHeight != null
              ? { top: '12px', bottom: 'auto', height: `${Math.max(0, visibleHeight - 12)}px` }
              : undefined
          }
          className={cn(
            'fixed inset-x-0 z-50 flex flex-col rounded-t-2xl',
            // Fallback when visibleHeight hasn't measured yet (first
            // frame, SSR): top+bottom CSS-only sizing.
            visibleHeight == null && 'top-3 bottom-0',
            'border-t bg-background shadow-[0_-12px_40px_-12px_rgba(0,0,0,0.25)]',
            // Respect the bottom safe-area so the home indicator never
            // overlaps the scroll region.
            'pb-safe-bottom',
          )}
        >
          {/* Visually-hidden title for screen readers. Radix Dialog (which
              Vaul wraps) requires a DialogTitle in the accessibility tree;
              without this, the console throws an a11y violation. */}
          <VaulDrawer.Title className="sr-only">Search</VaulDrawer.Title>

          {/* Drag handle  -  Vaul reads this as a swipe target. Centered grip
              + a small label below feels iOS-native. */}
          <div className="flex flex-col items-center pt-2.5 pb-1.5">
            <div className="h-1.5 w-12 rounded-full bg-muted" aria-hidden />
          </div>

          {/* Sticky header: input + Cancel. The Cancel slides in from the
              right when the input has focus, otherwise it sits flat. */}
          <div className="flex items-center gap-2 px-4 pb-3">
            <label className="relative flex h-11 flex-1 items-center rounded-xl bg-muted/70 px-3 transition-colors focus-within:bg-muted">
              <Search className="size-4 shrink-0 text-muted-foreground" aria-hidden />
              <input
                ref={inputRef}
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                onPaste={onPaste}
                placeholder="Search clients, yachts, interests…"
                aria-label="Search"
                inputMode="search"
                enterKeyHint="search"
                autoCapitalize="off"
                autoCorrect="off"
                spellCheck={false}
                className={cn(
                  'ml-2 h-full w-full min-w-0 bg-transparent text-base outline-hidden',
                  'placeholder:text-muted-foreground',
                )}
              />
              {query.length > 0 ? (
                <button
                  type="button"
                  onClick={() => {
                    setQuery('');
                    inputRef.current?.focus();
                  }}
                  aria-label="Clear search"
                  className="ml-1 inline-flex size-7 shrink-0 items-center justify-center rounded-full text-muted-foreground active:bg-foreground/10"
                >
                  <X className="size-4" />
                </button>
              ) : null}
            </label>
            <button
              type="button"
              onClick={close}
              className="text-sm font-medium text-primary active:opacity-60"
            >
              Cancel
            </button>
          </div>

          {/* Bucket chips: horizontally scrollable so all buckets fit no
              matter the phone width. "All" is sticky-left so it's always
              one tap away when the user is deep in a bucket. */}
          <div className="border-b pb-3">
            <div className="flex gap-1.5 overflow-x-auto px-4 [-webkit-overflow-scrolling:touch] scrollbar-none [&::-webkit-scrollbar]:hidden">
              <BucketChip
                label="All"
                active={activeBucket === 'all'}
                onClick={() => setActiveBucket('all')}
              />
              {BUCKETS.map((b) => {
                const count = chipTotals?.[b.type] ?? 0;
                // Hide chips with zero matches in the last "all" snapshot,
                // unless this is the currently active chip. Always show all
                // before a query has run (chipTotals undefined → count 0
                // and active 'all' means none get hidden).
                if (query.length >= 2 && count === 0 && activeBucket !== b.type) {
                  return null;
                }
                return (
                  <BucketChip
                    key={b.type}
                    label={b.label}
                    count={count > 0 ? count : undefined}
                    active={activeBucket === b.type}
                    onClick={() => setActiveBucket(b.type)}
                  />
                );
              })}
            </div>
          </div>

          {/* Results scroll region. overscroll-contain prevents the body
              from rubber-banding when the user scrolls past the bottom. */}
          <div className="flex-1 overflow-y-auto overscroll-contain px-2 pb-4 pt-1">
            {showingEmptyHints && rows.length === 0 ? (
              <EmptyHint />
            ) : showingEmptyHints ? (
              <RowList rows={rows} query={query} onSelect={navigate} variant="empty" />
            ) : noResults ? (
              <NoResults query={query} />
            ) : (
              <RowList rows={rows} query={query} onSelect={navigate} variant="results" />
            )}
          </div>
        </VaulDrawer.Content>
      </VaulDrawer.Portal>
    </VaulDrawer.Root>
  );
}

function BucketChip({
  label,
  count,
  active,
  onClick,
}: {
  label: string;
  count?: number;
  active: boolean;
  onClick: () => void;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-pressed={active}
      className={cn(
        'shrink-0 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
        active
          ? 'border-primary bg-primary text-primary-foreground'
          : 'border-border bg-background text-muted-foreground active:bg-accent active:text-accent-foreground',
      )}
    >
      {label}
      {typeof count === 'number' && <span className="ml-1 opacity-70">({count})</span>}
    </button>
  );
}

function EmptyHint() {
  return (
    <div className="flex h-full flex-col items-center gap-3 px-6 pb-12 pt-28 text-center">
      <div className="flex size-14 items-center justify-center rounded-full bg-muted text-muted-foreground">
        <Search className="size-7" aria-hidden />
      </div>
      <p className="text-sm text-muted-foreground">
        Search clients, yachts, interests, berths, invoices, documents - paste a UUID or invoice
        number to jump directly.
      </p>
    </div>
  );
}

function NoResults({ query }: { query: string }) {
  return (
    <div className="flex h-full flex-col items-center justify-center gap-2 px-6 pb-12 text-center">
      <p className="text-sm font-medium text-foreground">No matches for &ldquo;{query}&rdquo;</p>
      <p className="text-xs text-muted-foreground">
        Try a different spelling, or switch buckets above.
      </p>
    </div>
  );
}

function RowList({
  rows,
  query,
  onSelect,
  variant,
}: {
  rows: FlatRow[];
  query: string;
  onSelect: (href: string) => void;
  variant: 'empty' | 'results';
}) {
  // Split rows by section header - "Recently viewed", "Recent searches",
  // "Results". Headers live inside the row list so they scroll with their
  // content (instead of sticky-positioning, which adds visual noise).
  const recentViews = rows.filter((r) => r.kind === 'recent-view');
  const recentTerms = rows.filter((r) => r.kind === 'recent-term');
  const results = rows.filter((r) => r.kind === 'result' || r.kind === 'other-port');

  return (
    <div className="space-y-3">
      {variant === 'empty' && recentViews.length > 0 ? (
        <Section icon={<Clock className="size-3.5" />} label="Recently viewed">
          {recentViews.map((row) =>
            row.kind === 'recent-view' ? (
              <Row
                key={row.key}
                onSelect={() => onSelect(row.href)}
                label={row.item.label}
                sub={row.item.sub}
              />
            ) : null,
          )}
        </Section>
      ) : null}

      {variant === 'empty' && recentTerms.length > 0 ? (
        <Section icon={<History className="size-3.5" />} label="Recent searches">
          <div className="flex flex-wrap gap-1.5 px-2 py-1">
            {recentTerms.map((row) =>
              row.kind === 'recent-term' ? (
                <button
                  key={row.key}
                  type="button"
                  onClick={() => {
                    // Recent-term taps populate the input rather than
                    // navigating - the rep usually wants to refine, not
                    // jump straight back to the previous result.
                    const input = document.querySelector<HTMLInputElement>(
                      'input[aria-label="Search"]',
                    );
                    if (input) {
                      input.value = row.term;
                      input.dispatchEvent(new Event('input', { bubbles: true }));
                      input.focus();
                    }
                  }}
                  className="rounded-full border border-border bg-muted/40 px-3 py-1 text-xs text-muted-foreground active:bg-accent active:text-accent-foreground"
                >
                  {row.term}
                </button>
              ) : null,
            )}
          </div>
        </Section>
      ) : null}

      {variant === 'results' && results.length > 0
        ? renderResultRows(results, query, onSelect)
        : null}
    </div>
  );
}

/**
 * Walk the flat result rows, inserting a small section header above the
 * first row of each bucket so reps know exactly what kind of entity
 * each result points to ("CLIENTS", "INTERESTS", "BERTHS", …). Bucket
 * order follows `buildFlatRows`'s ordering - most-likely matches first.
 */
function renderResultRows(
  rows: FlatRow[],
  query: string,
  onSelect: (path: string) => void,
): React.ReactNode[] {
  const nodes: React.ReactNode[] = [];
  let lastBucket: BucketType | null = null;
  rows.forEach((row, i) => {
    if (row.kind === 'result' && row.bucket !== lastBucket) {
      nodes.push(
        <div
          key={`__bucket_${row.bucket}_${i}`}
          className="px-3 pt-3 pb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground"
        >
          {BUCKET_LABELS[row.bucket] ?? row.bucket}
        </div>,
      );
      lastBucket = row.bucket;
    } else if (row.kind === 'other-port' && lastBucket !== null) {
      // Reset bucket tracker so re-grouping works on subsequent results.
      lastBucket = null;
    }

    if (row.kind === 'result') {
      const Icon = row.icon;
      const subContent = (
        <>
          {row.sub ? <HighlightMatch text={row.sub} query={query} /> : null}
          {row.relatedVia ? (
            <span className="block text-xs italic text-muted-foreground">
              via {row.relatedVia.label}
            </span>
          ) : null}
        </>
      );
      nodes.push(
        <Row
          key={row.key}
          onSelect={() => onSelect(row.href)}
          label={<HighlightMatch text={row.label} query={query} />}
          sub={row.sub || row.relatedVia ? subContent : null}
          icon={<Icon className="size-4 text-muted-foreground" aria-hidden />}
          badges={row.badges}
        />,
      );
    } else if (row.kind === 'other-port') {
      nodes.push(
        <Row
          key={row.key}
          onSelect={() => onSelect(row.href)}
          label={row.item.label}
          sub={`${row.item.portName} · other port`}
        />,
      );
    }
  });
  return nodes;
}

/** Human-readable bucket labels for the section-header rows. */
const BUCKET_LABELS: Record<BucketType, string> = {
  clients: 'Clients',
  residentialClients: 'Residential clients',
  yachts: 'Yachts',
  companies: 'Companies',
  interests: 'Interests',
  residentialInterests: 'Residential interests',
  berths: 'Berths',
  invoices: 'Invoices',
  expenses: 'Expenses',
  documents: 'Documents',
  files: 'Files',
  reminders: 'Reminders',
  brochures: 'Brochures',
  tags: 'Tags',
  navigation: 'Settings & navigation',
  notes: 'Notes',
  stages: 'Stages',
};

function Section({
  icon,
  label,
  children,
}: {
  icon: React.ReactNode;
  label: string;
  children: React.ReactNode;
}) {
  return (
    <section>
      <div className="flex items-center gap-1.5 px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
        {icon}
        {label}
      </div>
      <div>{children}</div>
    </section>
  );
}

function Row({
  onSelect,
  label,
  sub,
  icon,
  badges,
}: {
  onSelect: () => void;
  label: React.ReactNode;
  sub?: React.ReactNode;
  icon?: React.ReactNode;
  badges?: { label: string; tone: 'neutral' | 'warning' | 'success' | 'danger' }[];
}) {
  return (
    <button
      type="button"
      onClick={onSelect}
      className={cn(
        'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left',
        'min-h-[52px] active:bg-accent',
      )}
    >
      {icon ? <span className="shrink-0">{icon}</span> : null}
      <span className="flex min-w-0 flex-1 flex-col">
        <span className="truncate text-sm font-medium text-foreground">{label}</span>
        {sub ? <span className="truncate text-xs text-muted-foreground">{sub}</span> : null}
      </span>
      {badges?.length ? (
        <span className="flex shrink-0 gap-1">
          {badges.map((b) => (
            <span
              key={b.label}
              className={cn(
                'rounded-full px-1.5 py-0.5 text-xs font-medium uppercase tracking-wide',
                BADGE_TONE[b.tone],
              )}
            >
              {b.label}
            </span>
          ))}
        </span>
      ) : null}
    </button>
  );
}
