import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Check, ChevronDown } from 'lucide-react'

import { cn } from '@/lib/utils'

type NativeSelectProps = React.ComponentProps<'select'>

type SelectOption = {
  value: string
  label: React.ReactNode
  disabled: boolean
}

type MenuPlacement = 'top' | 'bottom'

function normalizeValue(value: NativeSelectProps['value'] | NativeSelectProps['defaultValue']) {
  if (Array.isArray(value)) {
    return value[0] == null ? '' : String(value[0])
  }

  return value == null ? '' : String(value)
}

function extractOptions(children: React.ReactNode) {
  const options: SelectOption[] = []

  React.Children.forEach(children, (child) => {
    if (!React.isValidElement(child) || child.type !== 'option') {
      return
    }

    const props = child.props as React.OptionHTMLAttributes<HTMLOptionElement> & {
      children?: React.ReactNode
    }

    options.push({
      value: normalizeValue(props.value),
      label: props.children,
      disabled: Boolean(props.disabled),
    })
  })

  return options
}

function getFirstEnabledIndex(options: SelectOption[]) {
  return options.findIndex((option) => !option.disabled)
}

function getLastEnabledIndex(options: SelectOption[]) {
  for (let index = options.length - 1; index >= 0; index -= 1) {
    if (!options[index]?.disabled) {
      return index
    }
  }

  return -1
}

function getNextEnabledIndex(options: SelectOption[], currentIndex: number, direction: 1 | -1) {
  if (options.length === 0) {
    return -1
  }

  let index = currentIndex

  for (let step = 0; step < options.length; step += 1) {
    index = (index + direction + options.length) % options.length
    if (!options[index]?.disabled) {
      return index
    }
  }

  return -1
}

const Select = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
  (
    {
      children,
      className,
      defaultValue,
      disabled = false,
      id,
      onBlur,
      onClick,
      onFocus,
      onKeyDown,
      value,
      ...props
    },
    forwardedRef,
  ) => {
    const options = React.useMemo(() => extractOptions(children), [children])
    const isControlled = value !== undefined
    const initialValue = React.useMemo(() => {
      if (defaultValue !== undefined) {
        return normalizeValue(defaultValue)
      }

      return options[0]?.value ?? ''
    }, [defaultValue, options])

    const [internalValue, setInternalValue] = React.useState(initialValue)
    const [open, setOpen] = React.useState(false)
    const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
    const [menuPlacement, setMenuPlacement] = React.useState<MenuPlacement>('bottom')
    const [menuStyle, setMenuStyle] = React.useState<React.CSSProperties | null>(null)

    const wrapperRef = React.useRef<HTMLDivElement>(null)
    const triggerRef = React.useRef<HTMLButtonElement>(null)
    const nativeSelectRef = React.useRef<HTMLSelectElement>(null)
    const menuRef = React.useRef<HTMLDivElement>(null)
    const optionRefs = React.useRef<Array<HTMLButtonElement | null>>([])
    const menuId = React.useId()

    const currentValue = isControlled ? normalizeValue(value) : internalValue
    const selectedIndex = options.findIndex((option) => option.value === currentValue)
    const selectedOption = selectedIndex >= 0 ? options[selectedIndex] : options[0] ?? null

    React.useEffect(() => {
      if (!isControlled && options.length > 0 && !options.some((option) => option.value === internalValue)) {
        setInternalValue(options[0]?.value ?? '')
      }
    }, [internalValue, isControlled, options])

    const updateMenuPosition = React.useCallback(() => {
      const trigger = triggerRef.current
      if (!trigger) {
        return
      }

      const rect = trigger.getBoundingClientRect()
      const viewportPadding = 12
      const gutter = 6
      const estimatedHeight = Math.min(Math.max(options.length, 1) * 44 + 18, 320)
      const spaceBelow = window.innerHeight - rect.bottom - viewportPadding
      const spaceAbove = rect.top - viewportPadding
      const openToTop = spaceBelow < estimatedHeight && spaceAbove > spaceBelow
      const maxHeight = Math.max(120, Math.min(openToTop ? spaceAbove : spaceBelow, 320))
      const width = Math.min(rect.width, window.innerWidth - viewportPadding * 2)
      const left = Math.min(Math.max(rect.left, viewportPadding), window.innerWidth - width - viewportPadding)

      setMenuPlacement(openToTop ? 'top' : 'bottom')
      setMenuStyle(
        openToTop
          ? {
              left,
              width,
              maxHeight,
              bottom: window.innerHeight - rect.top + gutter,
            }
          : {
              left,
              width,
              maxHeight,
              top: rect.bottom + gutter,
            },
      )
    }, [options.length])

    const setOpenWithHighlight = React.useCallback(
      (nextOpen: boolean, preferredIndex?: number) => {
        if (disabled) {
          return
        }

        if (nextOpen) {
          const fallbackIndex =
            preferredIndex ??
            (selectedIndex >= 0 && !options[selectedIndex]?.disabled
              ? selectedIndex
              : getFirstEnabledIndex(options))

          setHighlightedIndex(fallbackIndex)
          updateMenuPosition()
          setOpen(true)
          return
        }

        setOpen(false)
      },
      [disabled, options, selectedIndex, updateMenuPosition],
    )

    const commitValue = React.useCallback(
      (nextIndex: number) => {
        const option = options[nextIndex]
        const nativeSelect = nativeSelectRef.current

        if (!option || option.disabled) {
          return
        }

        if (!isControlled) {
          setInternalValue(option.value)
        }

        if (nativeSelect && currentValue !== option.value) {
          nativeSelect.value = option.value
          nativeSelect.dispatchEvent(new Event('change', { bubbles: true }))
        }

        setOpen(false)
        window.requestAnimationFrame(() => {
          triggerRef.current?.focus()
        })
      },
      [currentValue, isControlled, options],
    )

    const handleKeyDown = React.useCallback(
      (event: React.KeyboardEvent<HTMLButtonElement | HTMLDivElement>) => {
        onKeyDown?.(event as unknown as React.KeyboardEvent<HTMLSelectElement>)
        if (event.defaultPrevented || disabled) {
          return
        }

        switch (event.key) {
          case 'ArrowDown': {
            event.preventDefault()
            if (!open) {
              const nextIndex =
                selectedIndex >= 0
                  ? getNextEnabledIndex(options, selectedIndex, 1)
                  : getFirstEnabledIndex(options)
              setOpenWithHighlight(true, nextIndex >= 0 ? nextIndex : getFirstEnabledIndex(options))
              return
            }

            setHighlightedIndex((current) => getNextEnabledIndex(options, current, 1))
            return
          }
          case 'ArrowUp': {
            event.preventDefault()
            if (!open) {
              const nextIndex =
                selectedIndex >= 0
                  ? getNextEnabledIndex(options, selectedIndex, -1)
                  : getLastEnabledIndex(options)
              setOpenWithHighlight(true, nextIndex >= 0 ? nextIndex : getLastEnabledIndex(options))
              return
            }

            setHighlightedIndex((current) => getNextEnabledIndex(options, current, -1))
            return
          }
          case 'Home': {
            event.preventDefault()
            const firstIndex = getFirstEnabledIndex(options)
            if (!open) {
              setOpenWithHighlight(true, firstIndex)
              return
            }
            setHighlightedIndex(firstIndex)
            return
          }
          case 'End': {
            event.preventDefault()
            const lastIndex = getLastEnabledIndex(options)
            if (!open) {
              setOpenWithHighlight(true, lastIndex)
              return
            }
            setHighlightedIndex(lastIndex)
            return
          }
          case 'Enter':
          case ' ': {
            event.preventDefault()
            if (!open) {
              setOpenWithHighlight(true)
              return
            }

            if (highlightedIndex >= 0) {
              commitValue(highlightedIndex)
            }
            return
          }
          case 'Escape': {
            if (!open) {
              return
            }
            event.preventDefault()
            setOpen(false)
            return
          }
          case 'Tab': {
            setOpen(false)
            return
          }
          default:
            return
        }
      },
      [commitValue, disabled, highlightedIndex, onKeyDown, open, options, selectedIndex, setOpenWithHighlight],
    )

    React.useLayoutEffect(() => {
      if (!open) {
        return
      }

      updateMenuPosition()
    }, [open, updateMenuPosition])

    React.useEffect(() => {
      if (!open) {
        return
      }

      const handlePointerDown = (event: MouseEvent) => {
        const target = event.target as Node
        if (wrapperRef.current?.contains(target) || menuRef.current?.contains(target)) {
          return
        }

        setOpen(false)
      }

      const handleWindowChange = () => updateMenuPosition()

      document.addEventListener('mousedown', handlePointerDown)
      window.addEventListener('resize', handleWindowChange)
      window.addEventListener('scroll', handleWindowChange, true)

      return () => {
        document.removeEventListener('mousedown', handlePointerDown)
        window.removeEventListener('resize', handleWindowChange)
        window.removeEventListener('scroll', handleWindowChange, true)
      }
    }, [open, updateMenuPosition])

    React.useEffect(() => {
      if (!open || highlightedIndex < 0) {
        return
      }

      optionRefs.current[highlightedIndex]?.scrollIntoView({ block: 'nearest' })
    }, [highlightedIndex, open])

    const triggerClasses = cn(
      'flex h-11 w-full items-center justify-between gap-3 rounded-xl border border-input bg-background/80 px-3 py-2 text-left text-sm text-foreground shadow-sm outline-none transition-[border-color,box-shadow,background-color,transform] focus-visible:ring-2 focus-visible:ring-ring/70 disabled:cursor-not-allowed disabled:opacity-50 data-[state=open]:border-primary/40 data-[state=open]:bg-card data-[state=open]:shadow-[0_18px_40px_rgb(15_23_42_/_0.14)]',
      className,
    )

    const menu = open && menuStyle
      ? ReactDOM.createPortal(
          <div
            ref={menuRef}
            aria-orientation="vertical"
            className={cn(
              'custom-select-popover fixed z-[80] overflow-hidden rounded-2xl border border-border/70 bg-popover p-1.5 text-popover-foreground shadow-[0_18px_48px_rgb(15_23_42_/_0.18)] will-change-transform',
              menuPlacement === 'top' ? 'origin-bottom' : 'origin-top',
            )}
            id={menuId}
            onKeyDown={handleKeyDown}
            role="listbox"
            style={menuStyle}
            tabIndex={-1}
          >
            <div className="max-h-full overflow-y-auto pr-0.5">
              {options.map((option, index) => {
                const selected = option.value === currentValue
                const highlighted = index === highlightedIndex

                return (
                  <button
                    key={`${option.value}-${index}`}
                    ref={(node) => {
                      optionRefs.current[index] = node
                    }}
                    aria-selected={selected}
                    className={cn(
                      'flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors',
                      option.disabled ? 'cursor-not-allowed opacity-45' : 'cursor-pointer',
                      selected
                        ? 'bg-primary text-primary-foreground shadow-[0_12px_30px_rgb(37_99_235_/_0.22)]'
                        : highlighted
                          ? 'bg-accent text-accent-foreground'
                          : 'text-foreground hover:bg-accent hover:text-accent-foreground',
                    )}
                    disabled={option.disabled}
                    onClick={() => commitValue(index)}
                    onMouseEnter={() => {
                      if (!option.disabled) {
                        setHighlightedIndex(index)
                      }
                    }}
                    role="option"
                    type="button"
                  >
                    <span className="truncate">{option.label}</span>
                    <Check className={cn('h-4 w-4 shrink-0', selected ? 'opacity-100' : 'opacity-0')} />
                  </button>
                )
              })}
            </div>
          </div>,
          document.body,
        )
      : null

    return (
      <div className="relative w-full" ref={wrapperRef}>
        <select
          {...props}
          aria-hidden="true"
          className="pointer-events-none absolute h-0 w-0 opacity-0"
          defaultValue={defaultValue}
          disabled={disabled}
          id={id}
          onBlur={onBlur}
          onFocus={onFocus}
          ref={(node) => {
            nativeSelectRef.current = node
            if (typeof forwardedRef === 'function') {
              forwardedRef(node)
            } else if (forwardedRef) {
              forwardedRef.current = node
            }
          }}
          tabIndex={-1}
          value={isControlled ? currentValue : internalValue}
        >
          {children}
        </select>

        <button
          aria-controls={open ? menuId : undefined}
          aria-expanded={open}
          aria-haspopup="listbox"
          className={triggerClasses}
          data-state={open ? 'open' : 'closed'}
          disabled={disabled}
          onBlur={(event) => {
            onBlur?.(event as unknown as React.FocusEvent<HTMLSelectElement>)
          }}
          onClick={(event) => {
            onClick?.(event as unknown as React.MouseEvent<HTMLSelectElement>)
          }}
          onPointerDown={(event) => {
            if (event.button !== 0 || disabled) {
              return
            }

            event.preventDefault()
            triggerRef.current?.focus()
            setOpenWithHighlight(!open)
          }}
          onFocus={(event) => {
            onFocus?.(event as unknown as React.FocusEvent<HTMLSelectElement>)
          }}
          onKeyDown={handleKeyDown}
          ref={triggerRef}
          role="combobox"
          type="button"
        >
          <span className="min-w-0 flex-1 truncate">{selectedOption?.label ?? '请选择'}</span>
          <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted/70 text-muted-foreground transition-colors">
            <ChevronDown className={cn('h-4 w-4 transition-transform duration-200', open && 'rotate-180')} />
          </span>
        </button>

        {menu}
      </div>
    )
  },
)
Select.displayName = 'Select'

export { Select }
