import * as React from 'react'
import { useEffect, useState } from 'react'
import { useSearchInput } from '../hooks/useSearchInput.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import type { KeyboardEvent } from '../core/events/keyboard-event.js'
import { clamp } from '../core/layout/geometry.js'
import { Box, Text, useTerminalFocus } from '../index.js'
import { SearchBox } from './SearchBox.js'
import { Byline } from './Byline.js'
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
import { ListItem } from './ListItem.js'
import { Pane } from './Pane.js'

type PickerAction<T> = {
  /** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */
  action: string
  handler: (item: T) => void
}

type Props<T> = {
  title: string
  placeholder?: string
  initialQuery?: string
  items: readonly T[]
  getKey: (item: T) => string
  /** Keep to one line — preview handles overflow. */
  renderItem: (item: T, isFocused: boolean) => React.ReactNode
  renderPreview?: (item: T) => React.ReactNode
  /** 'right' keeps hints stable (no bounce), but needs width. */
  previewPosition?: 'bottom' | 'right'
  visibleCount?: number
  /**
   * 'up' puts items[0] at the bottom next to the input (atuin-style). Arrows
   * always match screen direction — ↑ walks visually up regardless.
   */
  direction?: 'down' | 'up'
  /** Caller owns filtering: re-filter on each call and pass new items. */
  onQueryChange: (query: string) => void
  /** Enter key. Primary action. */
  onSelect: (item: T) => void
  /**
   * Tab key. If provided, Tab no longer aliases Enter — it gets its own
   * handler and hint. Shift+Tab falls through to this if onShiftTab is unset.
   */
  onTab?: PickerAction<T>
  /** Shift+Tab key. Gets its own hint. */
  onShiftTab?: PickerAction<T>
  /**
   * Fires when the focused item changes (via arrows or when items reset).
   * Useful for async preview loading — keeps I/O out of renderPreview.
   */
  onFocus?: (item: T | undefined) => void
  onCancel: () => void
  /** Shown when items is empty. Caller bakes loading/searching state into this. */
  emptyMessage?: string | ((query: string) => string)
  /**
   * Status line below the list, e.g. "500+ matches" or "42 matches…".
   * Caller decides when to show it — pass undefined to hide.
   */
  matchLabel?: string
  selectAction?: string
  extraHints?: React.ReactNode
}

const DEFAULT_VISIBLE = 8
// Pane (paddingTop + Divider) + title + 3 gaps + SearchBox (rounded border = 3
// rows) + hints. matchLabel adds +1 when present, accounted for separately.
const CHROME_ROWS = 10
const MIN_VISIBLE = 2

export function FuzzyPicker<T>({
  title,
  placeholder = 'Type to search…',
  initialQuery,
  items,
  getKey,
  renderItem,
  renderPreview,
  previewPosition = 'bottom',
  visibleCount: requestedVisible = DEFAULT_VISIBLE,
  direction = 'down',
  onQueryChange,
  onSelect,
  onTab,
  onShiftTab,
  onFocus,
  onCancel,
  emptyMessage = 'No results',
  matchLabel,
  selectAction = 'select',
  extraHints,
}: Props<T>): React.ReactNode {
  const isTerminalFocused = useTerminalFocus()
  const { rows, columns } = useTerminalSize()
  const [focusedIndex, setFocusedIndex] = useState(0)

  // Cap visibleCount so the picker never exceeds the terminal height. When it
  // overflows, each re-render (arrow key, ctrl+p) mis-positions the cursor-up
  // by the overflow amount and a previously-drawn line flashes blank.
  const visibleCount = Math.max(
    MIN_VISIBLE,
    Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)),
  )

  // Full hint row with onTab+onShiftTab is ~100 chars and wraps inconsistently
  // below that. Compact mode drops shift+tab and shortens labels.
  const compact = columns < 120

  const step = (delta: 1 | -1) => {
    setFocusedIndex(i => clamp(i + delta, 0, items.length - 1))
  }

  // onKeyDown fires after useSearchInput's useInput, so onExit must be a
  // no-op — return/downArrow are handled by handleKeyDown below. onCancel
  // still covers escape/ctrl+c/ctrl+d. Backspace-on-empty is disabled so
  // a held backspace doesn't eject the user from the dialog.
  const { query, cursorOffset } = useSearchInput({
    isActive: true,
    onExit: () => {},
    onCancel,
    initialQuery,
    backspaceExitsOnEmpty: false,
  })

  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
      e.preventDefault()
      e.stopImmediatePropagation()
      step(direction === 'up' ? 1 : -1)
      return
    }
    if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
      e.preventDefault()
      e.stopImmediatePropagation()
      step(direction === 'up' ? -1 : 1)
      return
    }
    if (e.key === 'return') {
      e.preventDefault()
      e.stopImmediatePropagation()
      const selected = items[focusedIndex]
      if (selected) onSelect(selected)
      return
    }
    if (e.key === 'tab') {
      e.preventDefault()
      e.stopImmediatePropagation()
      const selected = items[focusedIndex]
      if (!selected) return
      const tabAction = e.shift ? (onShiftTab ?? onTab) : onTab
      if (tabAction) {
        tabAction.handler(selected)
      } else {
        onSelect(selected)
      }
    }
  }

  useEffect(() => {
    onQueryChange(query)
    setFocusedIndex(0)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [query])

  useEffect(() => {
    setFocusedIndex(i => clamp(i, 0, items.length - 1))
  }, [items.length])

  const focused = items[focusedIndex]
  useEffect(() => {
    onFocus?.(focused)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [focused])

  const windowStart = clamp(
    focusedIndex - visibleCount + 1,
    0,
    items.length - visibleCount,
  )
  const visible = items.slice(windowStart, windowStart + visibleCount)

  const emptyText =
    typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage

  const searchBox = (
    <SearchBox
      query={query}
      cursorOffset={cursorOffset}
      placeholder={placeholder}
      isFocused
      isTerminalFocused={isTerminalFocused}
    />
  )

  const listBlock = (
    <List
      visible={visible}
      windowStart={windowStart}
      visibleCount={visibleCount}
      total={items.length}
      focusedIndex={focusedIndex}
      direction={direction}
      getKey={getKey}
      renderItem={renderItem}
      emptyText={emptyText}
    />
  )

  const preview =
    renderPreview && focused ? (
      <Box flexDirection="column" flexGrow={1}>
        {renderPreview(focused)}
      </Box>
    ) : null

  // Structure must not depend on preview truthiness — when focused goes
  // undefined (e.g. delete clears matches), switching row→fragment would
  // change both layout AND gap count, bouncing the searchBox below.
  const listGroup =
    renderPreview && previewPosition === 'right' ? (
      <Box
        flexDirection="row"
        gap={2}
        height={visibleCount + (matchLabel ? 1 : 0)}
      >
        <Box flexDirection="column" flexShrink={0}>
          {listBlock}
          {matchLabel && <Text dimColor>{matchLabel}</Text>}
        </Box>
        {preview ?? <Box flexGrow={1} />}
      </Box>
    ) : (
      // Box (not fragment) so the outer gap={1} doesn't insert a blank line
      // between list/matchLabel/preview — that read as extra space above the
      // prompt in direction='up'.
      <Box flexDirection="column">
        {listBlock}
        {matchLabel && <Text dimColor>{matchLabel}</Text>}
        {preview}
      </Box>
    )

  const inputAbove = direction !== 'up'
  return (
    <Pane color="permission">
      <Box
        flexDirection="column"
        gap={1}
        tabIndex={0}
        autoFocus
        onKeyDown={handleKeyDown}
      >
        <Text bold color="permission">
          {title}
        </Text>
        {inputAbove && searchBox}
        {listGroup}
        {!inputAbove && searchBox}
        <Text dimColor>
          <Byline>
            <KeyboardShortcutHint
              shortcut="↑/↓"
              action={compact ? 'nav' : 'navigate'}
            />
            <KeyboardShortcutHint
              shortcut="Enter"
              action={compact ? firstWord(selectAction) : selectAction}
            />
            {onTab && (
              <KeyboardShortcutHint shortcut="Tab" action={onTab.action} />
            )}
            {onShiftTab && !compact && (
              <KeyboardShortcutHint
                shortcut="shift+tab"
                action={onShiftTab.action}
              />
            )}
            <KeyboardShortcutHint shortcut="Esc" action="cancel" />
            {extraHints}
          </Byline>
        </Text>
      </Box>
    </Pane>
  )
}

type ListProps<T> = Pick<
  Props<T>,
  'visibleCount' | 'direction' | 'getKey' | 'renderItem'
> & {
  visible: readonly T[]
  windowStart: number
  total: number
  focusedIndex: number
  emptyText: string
}

function List<T>({
  visible,
  windowStart,
  visibleCount,
  total,
  focusedIndex,
  direction,
  getKey,
  renderItem,
  emptyText,
}: ListProps<T>): React.ReactNode {
  if (visible.length === 0) {
    return (
      <Box height={visibleCount} flexShrink={0}>
        <Text dimColor>{emptyText}</Text>
      </Box>
    )
  }

  const rows = visible.map((item, i) => {
    const actualIndex = windowStart + i
    const isFocused = actualIndex === focusedIndex
    const atLowEdge = i === 0 && windowStart > 0
    const atHighEdge =
      i === visible.length - 1 && windowStart + visibleCount! < total
    return (
      <ListItem
        key={getKey(item)}
        isFocused={isFocused}
        showScrollUp={direction === 'up' ? atHighEdge : atLowEdge}
        showScrollDown={direction === 'up' ? atLowEdge : atHighEdge}
        styled={false}
      >
        {renderItem(item, isFocused)}
      </ListItem>
    )
  })

  return (
    <Box
      height={visibleCount}
      flexShrink={0}
      flexDirection={direction === 'up' ? 'column-reverse' : 'column'}
    >
      {rows}
    </Box>
  )
}

function firstWord(s: string): string {
  const i = s.indexOf(' ')
  return i === -1 ? s : s.slice(0, i)
}
