import * as React from 'react'
import { SearchIcon, XIcon } from 'lucide-react'

import { cn } from './utils'

/* ------------------------------------------------------------------ */
/* Types                                                               */
/* ------------------------------------------------------------------ */

export interface SearchResult {
  label: string
  value: string
  icon?: React.ReactNode
  description?: string
  group?: string
}

export interface CommandEntry {
  id: string
  label: string
  description?: string
  icon?: React.ReactNode
  group?: string
  keybinding?: string
  handler: () => void
}

/* ------------------------------------------------------------------ */
/* Search Input                                                        */
/* ------------------------------------------------------------------ */

export interface SearchProps extends Omit<React.ComponentProps<'input'>, 'onSelect'> {
  /** Callback invoked on search submit (Enter key) */
  onSearch?: (query: string) => void
  /** Async suggest function for typeahead */
  suggest?: (query: string) => Promise<SearchResult[]> | SearchResult[]
  /** Callback when a suggestion is selected */
  onSelect?: (result: SearchResult) => void
  /** Debounce delay in ms */
  debounceMs?: number
  /** Show clear button */
  clearable?: boolean
}

function Search({
  className,
  onSearch,
  suggest,
  onSelect,
  debounceMs = 200,
  clearable = true,
  value: controlledValue,
  onChange,
  ...props
}: SearchProps) {
  const [internalValue, setInternalValue] = React.useState('')
  const value = controlledValue !== undefined ? String(controlledValue) : internalValue
  const [results, setResults] = React.useState<SearchResult[]>([])
  const [activeIndex, setActiveIndex] = React.useState(-1)
  const [showResults, setShowResults] = React.useState(false)
  const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
  const rootRef = React.useRef<HTMLDivElement>(null)
  const inputRef = React.useRef<HTMLInputElement>(null)

  // Update internal value
  const handleChange = React.useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const val = e.target.value
      if (controlledValue === undefined) setInternalValue(val)
      onChange?.(e)

      if (suggest === undefined) return

      if (debounceRef.current !== null) clearTimeout(debounceRef.current)

      const suggestFn = suggest
      debounceRef.current = setTimeout(() => {
        if (val.trim().length === 0) {
          setResults([])
          setShowResults(false)
          return
        }
        const resultOrPromise = suggestFn(val)
        if (resultOrPromise instanceof Promise) {
          void resultOrPromise.then(res => {
            setResults(res)
            setActiveIndex(-1)
            setShowResults(res.length > 0)
          })
        } else {
          setResults(resultOrPromise)
          setActiveIndex(-1)
          setShowResults(resultOrPromise.length > 0)
        }
      }, debounceMs)
    },
    [controlledValue, onChange, suggest, debounceMs]
  )

  const selectResult = React.useCallback(
    (result: SearchResult) => {
      if (controlledValue === undefined) setInternalValue(result.label)
      setShowResults(false)
      setResults([])
      onSelect?.(result)
      inputRef.current?.focus()
    },
    [controlledValue, onSelect]
  )

  const handleKeyDown = React.useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
      if (showResults && results.length > 0) {
        if (e.key === 'ArrowDown') {
          e.preventDefault()
          setActiveIndex(i => Math.min(i + 1, results.length - 1))
          return
        }
        if (e.key === 'ArrowUp') {
          e.preventDefault()
          setActiveIndex(i => Math.max(i - 1, 0))
          return
        }
        if (e.key === 'Enter' && activeIndex >= 0) {
          e.preventDefault()
          const selected = results[activeIndex]
          if (selected !== undefined) {
            selectResult(selected)
          }
          return
        }
        if (e.key === 'Escape') {
          e.preventDefault()
          setShowResults(false)
          return
        }
      }

      if (e.key === 'Enter') {
        onSearch?.(value)
      }
    },
    [showResults, results, activeIndex, value, onSearch, selectResult]
  )

  // Close on outside click
  React.useEffect(() => {
    const handleClick = (e: MouseEvent) => {
      if (rootRef.current !== null && !rootRef.current.contains(e.target as Node)) {
        setShowResults(false)
      }
    }
    document.addEventListener('click', handleClick)
    return () => {
      document.removeEventListener('click', handleClick)
    }
  }, [])

  // Cleanup debounce
  React.useEffect(() => {
    return () => {
      if (debounceRef.current !== null) clearTimeout(debounceRef.current)
    }
  }, [])

  const handleClear = React.useCallback(() => {
    if (controlledValue === undefined) setInternalValue('')
    setResults([])
    setShowResults(false)
    inputRef.current?.focus()
  }, [controlledValue])

  // Group results
  const grouped = React.useMemo(() => {
    const groups = new Map<string, SearchResult[]>()
    const ungrouped: SearchResult[] = []

    for (const r of results) {
      if (r.group !== undefined) {
        let list = groups.get(r.group)
        if (list === undefined) {
          list = []
          groups.set(r.group, list)
        }
        list.push(r)
      } else {
        ungrouped.push(r)
      }
    }

    return { ungrouped, groups }
  }, [results])

  let flatIdx = 0

  return (
    <div ref={rootRef} data-slot="search" className={cn('relative', className)}>
      <div className="relative">
        <SearchIcon className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-foreground-muted pointer-events-none" />
        <input
          ref={inputRef}
          data-slot="search-input"
          type="text"
          role="combobox"
          aria-expanded={showResults}
          aria-autocomplete="list"
          aria-haspopup="listbox"
          autoComplete="off"
          autoCorrect="off"
          autoCapitalize="off"
          spellCheck={false}
          value={value}
          onChange={handleChange}
          onKeyDown={handleKeyDown}
          className={cn(
            'h-10 w-full rounded-xl border border-border-glass bg-glass pl-9 pr-9 text-sm text-foreground placeholder:text-foreground-muted',
            'backdrop-blur-[20px] outline-none transition-colors',
            'focus:border-primary focus:ring-2 focus:ring-primary/20'
          )}
          {...props}
        />
        {clearable && value.length > 0 && (
          <button
            type="button"
            className="absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1 text-foreground-muted hover:text-foreground transition-colors"
            onClick={handleClear}
            aria-label="Clear search"
          >
            <XIcon className="size-3.5" />
          </button>
        )}
      </div>

      {showResults && (
        <div
          data-slot="search-results"
          role="listbox"
          aria-label="Search suggestions"
          className="absolute top-full left-0 z-50 mt-1 w-full overflow-hidden rounded-xl border border-border-glass bg-popover p-1 shadow-xl backdrop-blur-xl"
        >
          {grouped.ungrouped.map(result => {
            const idx = flatIdx++
            return (
              <SearchResultItem
                key={result.value}
                result={result}
                active={idx === activeIndex}
                onSelect={() => {
                  selectResult(result)
                }}
                onHover={() => {
                  setActiveIndex(idx)
                }}
              />
            )
          })}
          {Array.from(grouped.groups).map(([groupName, items]) => (
            <React.Fragment key={groupName}>
              <div className="px-3 py-1.5 text-xs font-semibold text-foreground-muted">
                {groupName}
              </div>
              {items.map(result => {
                const idx = flatIdx++
                return (
                  <SearchResultItem
                    key={result.value}
                    result={result}
                    active={idx === activeIndex}
                    onSelect={() => {
                      selectResult(result)
                    }}
                    onHover={() => {
                      setActiveIndex(idx)
                    }}
                  />
                )
              })}
            </React.Fragment>
          ))}
        </div>
      )}
    </div>
  )
}

/* ------------------------------------------------------------------ */
/* Search Result Item (internal)                                       */
/* ------------------------------------------------------------------ */

function SearchResultItem({
  result,
  active,
  onSelect,
  onHover,
}: {
  result: SearchResult
  active: boolean
  onSelect: () => void
  onHover: () => void
}) {
  return (
    <div
      role="option"
      aria-selected={active}
      className={cn(
        'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors outline-none select-none',
        'text-foreground',
        active ? 'bg-accent' : 'hover:bg-accent'
      )}
      onClick={onSelect}
      onMouseEnter={onHover}
    >
      {result.icon !== undefined && <span className="shrink-0">{result.icon}</span>}
      <div className="flex flex-col gap-0.5 overflow-hidden">
        <span className="truncate">{result.label}</span>
        {result.description !== undefined && (
          <span className="truncate text-xs text-foreground-muted">{result.description}</span>
        )}
      </div>
    </div>
  )
}

/* ------------------------------------------------------------------ */
/* Command Palette                                                     */
/* ------------------------------------------------------------------ */

export interface CommandPaletteProps {
  /** Whether the palette is open */
  open?: boolean
  /** Callback when open state changes */
  onOpenChange?: (open: boolean) => void
  /** Available commands */
  commands: CommandEntry[]
  /** Input placeholder */
  placeholder?: string
  /** Global keyboard shortcut (e.g. 'ctrl+k') */
  keybinding?: string
}

function CommandPalette({
  open: controlledOpen,
  onOpenChange,
  commands,
  placeholder = 'Type a command...',
  keybinding = 'ctrl+k',
}: CommandPaletteProps) {
  const [internalOpen, setInternalOpen] = React.useState(false)
  const open = controlledOpen ?? internalOpen

  const setOpen = React.useCallback(
    (nextValue: boolean) => {
      if (controlledOpen === undefined) setInternalOpen(nextValue)
      onOpenChange?.(nextValue)
    },
    [controlledOpen, onOpenChange]
  )

  const [query, setQuery] = React.useState('')
  const [activeIndex, setActiveIndex] = React.useState(0)
  const inputRef = React.useRef<HTMLInputElement>(null)

  // Global keybinding
  React.useEffect(() => {
    const parts = keybinding.toLowerCase().split('+')
    const needsCtrl = parts.some(p => ['ctrl', 'meta', 'cmd'].includes(p.trim()))
    const key = parts.find(p => !['ctrl', 'meta', 'cmd'].includes(p.trim()))?.trim() ?? ''

    const handler = (e: KeyboardEvent) => {
      const ctrlPressed = e.ctrlKey || e.metaKey
      if (needsCtrl && ctrlPressed && e.key.toLowerCase() === key) {
        e.preventDefault()
        setOpen(!open)
      }
    }

    document.addEventListener('keydown', handler)
    return () => {
      document.removeEventListener('keydown', handler)
    }
  }, [keybinding, open, setOpen])

  // Reset on open
  React.useEffect(() => {
    if (open) {
      setQuery('')
      setActiveIndex(0)
      requestAnimationFrame(() => {
        inputRef.current?.focus()
      })
    }
  }, [open])

  const filtered = React.useMemo(() => {
    if (query.trim().length === 0) return commands
    const q = query.toLowerCase()
    return commands.filter(cmd => cmd.label.toLowerCase().includes(q))
  }, [commands, query])

  const execute = React.useCallback(
    (cmd: CommandEntry) => {
      setOpen(false)
      cmd.handler()
    },
    [setOpen]
  )

  const handleKeyDown = React.useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'ArrowDown') {
        e.preventDefault()
        setActiveIndex(i => Math.min(i + 1, filtered.length - 1))
      } else if (e.key === 'ArrowUp') {
        e.preventDefault()
        setActiveIndex(i => Math.max(i - 1, 0))
      } else if (e.key === 'Enter') {
        e.preventDefault()
        const cmd = filtered[activeIndex]
        if (cmd !== undefined) {
          execute(cmd)
        }
      } else if (e.key === 'Escape') {
        setOpen(false)
      }
    },
    [filtered, activeIndex, execute, setOpen]
  )

  if (!open) return null

  // Group filtered commands
  const groups = new Map<string, CommandEntry[]>()
  const ungrouped: CommandEntry[] = []

  for (const cmd of filtered) {
    if (cmd.group !== undefined) {
      let list = groups.get(cmd.group)
      if (list === undefined) {
        list = []
        groups.set(cmd.group, list)
      }
      list.push(cmd)
    } else {
      ungrouped.push(cmd)
    }
  }

  let flatIdx = 0

  return (
    <div
      data-slot="command-palette"
      className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]"
    >
      {/* Backdrop */}
      <div
        className="fixed inset-0 bg-black/60 backdrop-blur-sm animate-in fade-in-0"
        onClick={() => {
          setOpen(false)
        }}
      />

      {/* Palette */}
      <div className="relative z-10 w-full max-w-lg overflow-hidden rounded-xl border border-border-glass bg-glass shadow-xl backdrop-blur-xl animate-in fade-in-0 zoom-in-95">
        <div className="flex items-center border-b border-border-glass px-4">
          <SearchIcon className="size-4 shrink-0 text-foreground-muted" />
          <input
            ref={inputRef}
            type="text"
            placeholder={placeholder}
            value={query}
            onChange={e => {
              setQuery(e.target.value)
              setActiveIndex(0)
            }}
            onKeyDown={handleKeyDown}
            className="flex-1 bg-transparent px-3 py-3 text-sm text-foreground placeholder:text-foreground-muted outline-none"
          />
        </div>

        {filtered.length > 0 && (
          <div className="max-h-72 overflow-y-auto p-1">
            {ungrouped.map(cmd => {
              const idx = flatIdx++
              return (
                <CommandItem
                  key={cmd.id}
                  entry={cmd}
                  active={idx === activeIndex}
                  onSelect={() => {
                    execute(cmd)
                  }}
                  onHover={() => {
                    setActiveIndex(idx)
                  }}
                />
              )
            })}
            {Array.from(groups).map(([groupName, items]) => (
              <React.Fragment key={groupName}>
                <div className="px-3 py-1.5 text-xs font-semibold text-foreground-muted">
                  {groupName}
                </div>
                {items.map(cmd => {
                  const idx = flatIdx++
                  return (
                    <CommandItem
                      key={cmd.id}
                      entry={cmd}
                      active={idx === activeIndex}
                      onSelect={() => {
                        execute(cmd)
                      }}
                      onHover={() => {
                        setActiveIndex(idx)
                      }}
                    />
                  )
                })}
              </React.Fragment>
            ))}
          </div>
        )}

        {filtered.length === 0 && query.length > 0 && (
          <div className="px-4 py-8 text-center text-sm text-foreground-muted">
            No commands found.
          </div>
        )}
      </div>
    </div>
  )
}

/* ------------------------------------------------------------------ */
/* Command Item (internal)                                             */
/* ------------------------------------------------------------------ */

function CommandItem({
  entry,
  active,
  onSelect,
  onHover,
}: {
  entry: CommandEntry
  active: boolean
  onSelect: () => void
  onHover: () => void
}) {
  return (
    <div
      className={cn(
        'flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm transition-colors select-none',
        'text-foreground',
        active ? 'bg-accent' : 'hover:bg-accent'
      )}
      onClick={onSelect}
      onMouseEnter={onHover}
    >
      <div className="flex items-center gap-2 overflow-hidden">
        {entry.icon !== undefined && <span className="shrink-0">{entry.icon}</span>}
        <span className="truncate">{entry.label}</span>
      </div>
      {entry.keybinding !== undefined && (
        <span className="ml-4 shrink-0 text-xs text-foreground-muted tracking-wider">
          {entry.keybinding}
        </span>
      )}
    </div>
  )
}

export { Search, CommandPalette }
