import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { useNavigate } from "react-router-dom";
import { Search } from "lucide-react";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import {
  destinationFor,
  flattenResults,
  useSearch,
  type SearchResult,
} from "@/hooks/useSearch";
import { SearchResults, type SearchResultsHandle } from "./SearchResults";

/**
 * Global Cmd-K / Ctrl-K search bar.
 *
 * Two pieces:
 *
 *  1. **Top-bar button** — always-visible chip in the Layout's top
 *     bar. Clicking opens the palette; pressing ⌘K / Ctrl-K also
 *     opens it from anywhere on the page.
 *
 *  2. **Command palette dialog** — centered modal built on the
 *     project's `Dialog` primitive (Radix underneath, free focus
 *     management + ESC-to-close). Hosts an autofocused `<input>` and
 *     a `<SearchResults>` listbox with ↑/↓/Enter keyboard nav.
 *
 * Keyboard contract:
 *
 *   - ⌘K / Ctrl+K  → toggle open (from anywhere)
 *   - ↑ / k        → move selection up
 *   - ↓ / j        → move selection down
 *   - Enter        → open the selected result
 *   - Esc          → close the palette (handled by Radix's Dialog)
 *   - Backspace / typing in the input  → updates the query
 *
 * When the dialog is closed, keyboard input flows normally. When
 * the dialog is open, the ↑/↓/Enter listeners run while the input
 * is focused — they're owned by the palette, not the page.
 */

/**
 * True when running on a Mac-class platform. Computed per call rather
 * than at module load — tests mutate `navigator.platform` and we want
 * the chord-detection to honor the current value.
 */
function isMac(): boolean {
  return (
    typeof navigator !== "undefined" &&
    /Mac|iPod|iPhone|iPad/.test(navigator.platform)
  );
}

/** Pretty label for the modifier key in the top-bar trigger. */
const MOD_LABEL = isMac() ? "⌘" : "Ctrl";

/**
 * Render the trigger button. Lives in the top bar; opens the
 * palette on click. Pure presentation — the parent passes `onOpen`.
 */
function SearchTrigger({ onOpen }: { onOpen: () => void }) {
  return (
    <button
      type="button"
      onClick={onOpen}
      data-testid="search-trigger"
      aria-label="Open global search (Cmd or Ctrl + K)"
      className={cn(
        "group inline-flex items-center gap-2.5 h-8 pl-2.5 pr-2 rounded-md",
        "border border-border/60 bg-background/40 hover:bg-background/70 hover:border-border/80",
        "text-[12px] text-muted-foreground hover:text-foreground",
        "transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
      )}
    >
      <Search className="h-3.5 w-3.5 text-muted-foreground group-hover:text-foreground transition-colors" strokeWidth={1.75} aria-hidden />
      <span className="font-medium tracking-tight">Search</span>
      <span className="ml-2 inline-flex items-center gap-0.5">
        <kbd className="kbd">{MOD_LABEL}</kbd>
        <kbd className="kbd">K</kbd>
      </span>
    </button>
  );
}

export function SearchBar() {
  const [open, setOpen] = useState(false);
  const navigate = useNavigate();
  const { query, setQuery, results, isLoading } = useSearch();
  const [selectedIndex, setSelectedIndex] = useState(0);
  const resultsRef = useRef<SearchResultsHandle>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  /**
   * Global ⌘K / Ctrl+K listener. Mounted once at the SearchBar level
   * (which lives in the Layout — always present). Ignored when the
   * palette is already open so a second chord doesn't reopen it
   * while the user is typing.
   */
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const mod = isMac() ? e.metaKey : e.ctrlKey;
      if (!mod) return;
      if (e.key.toLowerCase() !== "k") return;
      e.preventDefault();
      setOpen((prev) => !prev);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  /**
   * Reset state when the palette closes — clear the query, drop the
   * selection. Without this, reopening the palette shows the stale
   * query from the last session.
   */
  useEffect(() => {
    if (open) return;
    setQuery("");
    setSelectedIndex(0);
  }, [open, setQuery]);

  /**
   * Whenever the result list changes, snap the selected index back
   * to 0 so the user always sees a selection at the top of the new
   * list (rather than carrying a stale index from the previous
   * query that may now point past the end).
   */
  useEffect(() => {
    setSelectedIndex(0);
  }, [results.query]);

  /** Flat ordered list — what ↑/↓ step through. */
  const flat = useMemo(() => flattenResults(results), [results]);
  const flatTotal = flat.length;

  const moveNext = useCallback(() => {
    if (flatTotal === 0) return;
    setSelectedIndex((i) => (i + 1) % flatTotal);
  }, [flatTotal]);

  const movePrev = useCallback(() => {
    if (flatTotal === 0) return;
    // `+ flatTotal` keeps the modulo positive when `i === 0`.
    setSelectedIndex((i) => (i - 1 + flatTotal) % flatTotal);
  }, [flatTotal]);

  const openResult = useCallback(
    (result: SearchResult) => {
      const dest = destinationFor(result);
      setOpen(false);
      // Push the destination via react-router. The page-level
      // effects (e.g. Claims reading `?claim=` to open its drawer)
      // react to the new URL on the next render.
      navigate(dest);
    },
    [navigate]
  );

  /**
   * Input keydown — own ↑/↓/Enter here so the keys are intercepted
   * BEFORE the browser handles them (arrows would otherwise move
   * the text caret, which is wrong inside a listbox picker).
   * Esc is owned by Radix and bubbles via `onOpenChange`.
   */
  const onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        moveNext();
        break;
      case "ArrowUp":
        e.preventDefault();
        movePrev();
        break;
      case "Enter": {
        e.preventDefault();
        const r = flat[selectedIndex];
        if (r) openResult(r);
        break;
      }
      // j/k — vim-style. We don't preempt ⌘K here because that
      // would conflict with the chord-detection in the global listener.
      case "j":
        if (!e.metaKey && !e.ctrlKey) {
          e.preventDefault();
          moveNext();
        }
        break;
      case "k":
        if (!e.metaKey && !e.ctrlKey) {
          e.preventDefault();
          movePrev();
        }
        break;
    }
  };

  return (
    <>
      <SearchTrigger onOpen={() => setOpen(true)} />
      <Dialog
        open={open}
        onOpenChange={(o) => {
          if (!o) setOpen(false);
        }}
      >
        <DialogContent
          // Slightly wider than the default `max-w-lg` to give the
          // listbox room for two-line rows without truncation.
          className={cn(
            "max-w-2xl p-0 gap-0 overflow-hidden",
            "bg-[color:var(--m-surface)] text-[color:var(--m-ink-primary)]",
            "border border-[color:var(--m-border-heavy)]/30 shadow-2xl"
          )}
          aria-describedby={undefined}
          data-testid="search-dialog"
        >
          {/*
           * Search input row. Autofocuses on mount via Radix's
           * focus management (the Dialog primitive focuses the first
           * tabbable element on open). `pl-9` makes room for the
           * magnifier icon on the left.
           */}
          <div className="relative border-b border-[color:var(--m-border-heavy)]/20">
            <Search
              className="absolute left-3.5 top-1/2 -translate-y-1/2 h-4 w-4 text-[color:var(--m-ink-tertiary)] pointer-events-none"
              strokeWidth={1.75}
              aria-hidden
            />
            <Input
              ref={inputRef}
              type="text"
              autoComplete="off"
              spellCheck={false}
              placeholder="Search claims, remittances, activity…"
              role="combobox"
              aria-label="Search claims, remittances, and activity"
              aria-expanded={open}
              aria-controls="search-results"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              onKeyDown={onInputKeyDown}
              data-testid="search-input"
              className={cn(
                "h-12 pl-10 pr-3 border-0 bg-transparent rounded-none",
                "text-[14px] mono tracking-tight",
                "shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:bg-transparent",
                "text-[color:var(--m-ink-primary)] placeholder:text-[color:var(--m-ink-tertiary)]"
              )}
            />
          </div>

          {/* Results listbox. `id="search-results"` is the value of the
              input's `aria-controls` attribute — links the two for
              screen readers per the combobox pattern. */}
          <div id="search-results">
            <SearchResults
              ref={resultsRef}
              grouped={results}
              selectedIndex={selectedIndex}
              onSelect={openResult}
              onHoverIndex={setSelectedIndex}
            />
          </div>

          {/* Footer hint — shows the keyboard contract. */}
          <div className="flex items-center justify-between gap-3 px-4 py-2.5 border-t border-[color:var(--m-border-heavy)]/20 bg-[color:var(--m-surface)]/80 text-[10px] mono uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
            <div className="flex items-center gap-3">
              <span className="inline-flex items-center gap-1">
                <kbd className="kbd">↑</kbd>
                <kbd className="kbd">↓</kbd>
                navigate
              </span>
              <span className="inline-flex items-center gap-1">
                <kbd className="kbd">↵</kbd>
                open
              </span>
              <span className="inline-flex items-center gap-1">
                <kbd className="kbd">Esc</kbd>
                close
              </span>
            </div>
            {isLoading ? (
              <span className="inline-flex items-center gap-1.5">
                <span
                  aria-hidden
                  className="inline-block h-1.5 w-1.5 rounded-full animate-pulse-dot"
                  style={{ background: "var(--tt-amber, hsl(var(--accent)))" }}
                />
                loading…
              </span>
            ) : (
              <span className="tabular-nums">
                {flatTotal} {flatTotal === 1 ? "result" : "results"}
              </span>
            )}
          </div>
        </DialogContent>
      </Dialog>
    </>
  );
}