// SearchModal — cmd-k modal with live filter + keyboard navigation.
//
// Generic-enough that any app can wire it: pass a `search(query)`
// function (returns a list of hits in whatever shape you have) and
// render each hit with the `renderItem` prop. Selecting a hit calls
// `onSelect(hit)` — typical use: navigate to `hit.href`.
//
// Keyboard:
//   ⌘K / Ctrl+K   toggle open
//   Esc           close
//   ↑ / ↓         move highlight
//   Enter         select highlighted hit
//
// Visual: Fumadocs/Linear-style centered modal, dark backdrop, blur,
// rounded sharp-cornered card, monospaced ⌘K + Esc hints in the
// header. Uses the kit's tokens so light + dark modes both work.
//
// Implementation notes:
//   - The modal is conditionally mounted (only in the DOM when open)
//     so we don't pay portal/listener cost on every page.
//   - The cmd-k global listener is always attached when this component
//     is in the tree, so the user can hit ⌘K from anywhere.

import {
  useCallback, useEffect, useId, useMemo, useRef, useState,
  type KeyboardEvent, type ReactNode,
} from 'react'
import { cn } from '../cn'
import { SearchIcon, ArrowReturnIcon } from './docIcons'

export interface SearchModalProps<TItem> {
  /** Search function — called on every keystroke. Should return
   *  results synchronously (the kit doesn't manage async state here;
   *  if you need async, debounce + useState in the caller). */
  readonly search: (query: string) => ReadonlyArray<TItem>
  /** Renders a single hit. Receives the item + whether it's currently
   *  highlighted (for hover/keyboard focus styles). */
  readonly renderItem: (item: TItem, isActive: boolean) => ReactNode
  /** Stable key for an item — used for React keys + the active index
   *  match across renders. */
  readonly itemKey: (item: TItem) => string
  /** Selecting a hit (click OR Enter). Caller typically navigates. */
  readonly onSelect: (item: TItem) => void

  /** Placeholder for the input. */
  readonly placeholder?: string
  /** Label shown when there are no hits + the query has 1+ char. */
  readonly emptyLabel?: string
  /** Initial empty state — when the input is empty. Show recent
   *  searches, sections, whatever fits. */
  readonly empty?: ReactNode

  /** Open state — controlled. Default uses internal state. */
  readonly open?: boolean
  readonly onOpenChange?: (open: boolean) => void

  /** User-facing strings for the modal's own chrome (dialog/backdrop
   *  aria-labels, footer hint captions, initial-state prompt). Defaults
   *  are English so the modal works unconfigured. */
  readonly labels?: SearchModalLabels
}

export interface SearchModalLabels {
  /** `aria-label` of the dialog. Default `'Search'`. */
  readonly dialog?: string
  /** `aria-label` of the click-to-close backdrop. Default `'Close search'`. */
  readonly close?: string
  /** Prompt shown when the input is empty (and no `empty` node given).
   *  Default `'Start typing to search.'`. */
  readonly startTyping?: string
  /** Footer caption next to the ↑↓ keys. Default `'navigate'`. */
  readonly navigate?: string
  /** Footer caption next to the return key. Default `'select'`. */
  readonly select?: string
  /** Footer caption next to the ⌘K key. Default `'toggle'`. */
  readonly toggle?: string
}

const DEFAULT_SEARCH_LABELS: Required<SearchModalLabels> = {
  dialog: 'Search',
  close: 'Close search',
  startTyping: 'Start typing to search.',
  navigate: 'navigate',
  select: 'select',
  toggle: 'toggle',
}

export const SearchModal = <TItem,>({
  search, renderItem, itemKey, onSelect,
  placeholder = 'Search…',
  emptyLabel = 'No results.',
  empty,
  open: controlledOpen,
  onOpenChange,
  labels,
}: SearchModalProps<TItem>): ReactNode => {
  const t = { ...DEFAULT_SEARCH_LABELS, ...labels }
  const isControlled = controlledOpen !== undefined
  const [internalOpen, setInternalOpen] = useState(false)
  const open = isControlled ? controlledOpen : internalOpen
  const setOpen = useCallback((next: boolean): void => {
    if (!isControlled) setInternalOpen(next)
    onOpenChange?.(next)
  }, [isControlled, onOpenChange])

  const [query, setQuery] = useState('')
  const [activeIdx, setActiveIdx] = useState(0)
  const inputRef = useRef<HTMLInputElement>(null)
  const listboxId = useId()

  // Re-run search on every query change. Cheap for our doc-sized
  // catalogues; if the result set grows past 1000 entries, debounce
  // here.
  const hits = useMemo(() => (query.trim() ? search(query) : []), [query, search])

  // Reset highlight + scroll position when results change.
  useEffect(() => { setActiveIdx(0) }, [query])

  // Global ⌘K / Ctrl+K toggle.
  useEffect(() => {
    const handler = (e: globalThis.KeyboardEvent): void => {
      const k = e.key.toLowerCase()
      if (k === 'k' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        setOpen(!open)
      }
    }
    window.addEventListener('keydown', handler)
    return () => window.removeEventListener('keydown', handler)
  }, [open, setOpen])

  // Focus the input + reset state on open.
  useEffect(() => {
    if (!open) return
    setQuery('')
    setActiveIdx(0)
    // RAF lets the modal mount before we grab focus, so the autofocus
    // ring doesn't paint twice.
    const id = requestAnimationFrame(() => inputRef.current?.focus())
    return () => cancelAnimationFrame(id)
  }, [open])

  const close = useCallback((): void => setOpen(false), [setOpen])

  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
    if (e.key === 'Escape') { e.preventDefault(); close(); return }
    if (e.key === 'ArrowDown') {
      e.preventDefault()
      setActiveIdx((i) => Math.min(i + 1, Math.max(hits.length - 1, 0)))
      return
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault()
      setActiveIdx((i) => Math.max(i - 1, 0))
      return
    }
    if (e.key === 'Enter') {
      const hit = hits[activeIdx]
      if (hit) { e.preventDefault(); onSelect(hit); close() }
    }
  }

  if (!open) return null

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-label={t.dialog}
      className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh] px-4"
    >
      {/* Backdrop — click closes */}
      <button
        type="button"
        aria-label={t.close}
        onClick={close}
        className="absolute inset-0 bg-background/70 backdrop-blur-sm cursor-default"
      />
      {/* Modal card */}
      <div
        className={cn(
          'relative w-full max-w-xl rounded-xl border border-border bg-card shadow-2xl overflow-hidden',
          'animate-in fade-in slide-in-from-top-4 duration-150',
        )}
      >
        <div className="flex items-center gap-3 px-4 py-3 border-b border-border">
          <SearchIcon className="w-4 h-4 text-muted-foreground shrink-0" />
          <input
            ref={inputRef}
            type="text"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            onKeyDown={onKeyDown}
            placeholder={placeholder}
            aria-autocomplete="list"
            aria-controls={listboxId}
            aria-activedescendant={hits[activeIdx] ? `${listboxId}-${itemKey(hits[activeIdx]!)}` : undefined}
            className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/70"
          />
          <kbd className="text-[0.65rem] text-muted-foreground border border-border rounded px-1.5 py-0.5 font-mono">
            Esc
          </kbd>
        </div>

        <div
          id={listboxId}
          role="listbox"
          className="max-h-[60vh] overflow-y-auto py-1.5"
        >
          {query.trim() === '' ? (
            empty ?? (
              <div className="px-4 py-8 text-center text-sm text-muted-foreground">
                {t.startTyping}
              </div>
            )
          ) : hits.length === 0 ? (
            <div className="px-4 py-8 text-center text-sm text-muted-foreground">
              {emptyLabel}
            </div>
          ) : (
            <ul className="px-1.5">
              {hits.map((hit, i) => {
                const isActive = i === activeIdx
                const key = itemKey(hit)
                return (
                  <li
                    key={key}
                    id={`${listboxId}-${key}`}
                    role="option"
                    aria-selected={isActive}
                  >
                    <button
                      type="button"
                      onClick={() => { onSelect(hit); close() }}
                      onMouseMove={() => setActiveIdx(i)}
                      className={cn(
                        'w-full text-left rounded-md px-3 py-2 transition-colors',
                        isActive
                          ? 'bg-accent text-accent-foreground'
                          : 'text-foreground hover:bg-accent/50',
                      )}
                    >
                      {renderItem(hit, isActive)}
                    </button>
                  </li>
                )
              })}
            </ul>
          )}
        </div>

        <div className="flex items-center justify-between gap-4 px-4 py-2 border-t border-border bg-card/40 text-[0.7rem] text-muted-foreground">
          <div className="inline-flex items-center gap-3">
            <span className="inline-flex items-center gap-1">
              <kbd className="border border-border rounded px-1 py-0.5 font-mono">↑</kbd>
              <kbd className="border border-border rounded px-1 py-0.5 font-mono">↓</kbd>
              {t.navigate}
            </span>
            <span className="inline-flex items-center gap-1">
              <ArrowReturnIcon className="w-3 h-3" /> {t.select}
            </span>
          </div>
          <span className="inline-flex items-center gap-1">
            <kbd className="border border-border rounded px-1 py-0.5 font-mono">⌘K</kbd>
            {t.toggle}
          </span>
        </div>
      </div>
    </div>
  )
}

// ---- SearchTrigger — drop-in button for top bars ----
//
// Renders a fake "input" that looks like the search field but is a
// real button. Click → caller toggles the modal. Keeps the topbar
// visually consistent across breakpoints while making the modal the
// single search surface.

interface SearchTriggerProps {
  readonly onClick: () => void
  readonly placeholder?: string
  /** `aria-label` for the trigger button. Default `'Open search'`. */
  readonly openLabel?: string
  readonly className?: string
}

export const SearchTrigger = ({
  onClick, placeholder = 'Search docs…', openLabel = 'Open search', className,
}: SearchTriggerProps): ReactNode => (
  <button
    type="button"
    onClick={onClick}
    aria-label={openLabel}
    className={cn(
      'inline-flex items-center gap-2 rounded-md border border-border bg-card/40 px-2.5 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:border-input transition-colors w-56 lg:w-64',
      className,
    )}
  >
    <SearchIcon className="w-4 h-4 shrink-0" />
    <span className="flex-1 text-left truncate">{placeholder}</span>
    <kbd className="text-[0.65rem] border border-border rounded px-1 py-0.5 font-mono hidden lg:inline-block">⌘K</kbd>
  </button>
)
