import { useState, useEffect, useCallback, useMemo } from 'react'
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
import { useDebounce } from '@/hooks/useDebounce'

import { cn } from '@/lib/utils'
import Button from '@/components/ui/Button'
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList
} from '@/components/ui/Command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'

export interface Option {
  value: string
  label: string
  disabled?: boolean
  description?: string
  icon?: React.ReactNode
}

export interface AsyncSelectProps<T> {
  /** Async function to fetch options */
  fetcher: (query?: string) => Promise<T[]>
  /** Preload all data ahead of time */
  preload?: boolean
  /** Function to filter options */
  filterFn?: (option: T, query: string) => boolean
  /** Function to render each option */
  renderOption: (option: T) => React.ReactNode
  /** Function to get the value from an option */
  getOptionValue: (option: T) => string
  /** Function to get the display value for the selected option */
  getDisplayValue: (option: T) => React.ReactNode
  /** Custom not found message */
  notFound?: React.ReactNode
  /** Custom loading skeleton */
  loadingSkeleton?: React.ReactNode
  /** Currently selected value */
  value: string
  /** Callback when selection changes */
  onChange: (value: string) => void
  /** Callback before opening the dropdown (async supported) */
  onBeforeOpen?: () => void | Promise<void>
  /** Accessibility label for the select field */
  ariaLabel?: string
  /** Placeholder text when no selection */
  placeholder?: string
  /** Display text for search placeholder */
  searchPlaceholder?: string
  /** Disable the entire select */
  disabled?: boolean
  /** Custom width for the popover *
  width?: string | number
  /** Custom class names */
  className?: string
  /** Custom trigger button class names */
  triggerClassName?: string
  /** Custom search input class names */
  searchInputClassName?: string
  /** Custom no results message */
  noResultsMessage?: string
  /** Custom trigger tooltip */
  triggerTooltip?: string
  /** Allow clearing the selection */
  clearable?: boolean
  /** Debounce time in milliseconds */
  debounceTime?: number
}

export function AsyncSelect<T>({
  fetcher,
  preload,
  filterFn,
  renderOption,
  getOptionValue,
  getDisplayValue,
  notFound,
  loadingSkeleton,
  ariaLabel,
  placeholder = 'Select...',
  searchPlaceholder,
  value,
  onChange,
  onBeforeOpen,
  disabled = false,
  className,
  triggerClassName,
  searchInputClassName,
  noResultsMessage,
  triggerTooltip,
  clearable = true,
  debounceTime = 150
}: AsyncSelectProps<T>) {
  const [open, setOpen] = useState(false)
  const [originalOptions, setOriginalOptions] = useState<T[]>([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : debounceTime)

  // Derive the displayed options from the fetched list and search term, instead
  // of mirroring them via setState in an effect.
  const options = useMemo(() => {
    if (preload && debouncedSearchTerm) {
      return originalOptions.filter((option) =>
        filterFn ? filterFn(option, debouncedSearchTerm) : true
      )
    }
    return originalOptions
  }, [preload, debouncedSearchTerm, filterFn, originalOptions])

  // Derive selected option from value + currently-loaded options.
  const selectedOption = useMemo(
    () => (value ? options.find((opt) => getOptionValue(opt) === value) ?? null : null),
    [value, options, getOptionValue]
  )

  // Show the raw value as a placeholder until the matching option is loaded.
  const initialValueDisplay = useMemo(
    () => (value && !selectedOption ? <div>{value}</div> : null),
    [value, selectedOption]
  )

  // Fetch options whenever search term changes (skip filtering-only re-runs in preload mode)
  useEffect(() => {
    const fetchOptions = async () => {
      try {
        setLoading(true)
        setError(null)
        const data = await fetcher(debouncedSearchTerm)
        setOriginalOptions(data)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to fetch options')
      } finally {
        setLoading(false)
      }
    }

    if (preload && originalOptions.length > 0) {
      // Already fetched; rely on the memoised filter above.
      return
    }
    fetchOptions()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [fetcher, debouncedSearchTerm, preload])

  const handleSelect = useCallback(
    (currentValue: string) => {
      const newValue = clearable && currentValue === value ? '' : currentValue
      onChange(newValue)
      setOpen(false)
    },
    [value, onChange, clearable]
  )

  const handleOpenChange = useCallback(
    async (newOpen: boolean) => {
      if (newOpen && onBeforeOpen) {
        await onBeforeOpen()
      }
      setOpen(newOpen)
    },
    [onBeforeOpen]
  )

  return (
    <Popover open={open} onOpenChange={handleOpenChange}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          aria-label={ariaLabel}
          className={cn(
            'justify-between',
            disabled && 'cursor-not-allowed opacity-50',
            triggerClassName
          )}
          disabled={disabled}
          tooltip={triggerTooltip}
          side="bottom"
        >
          {value === '*' ? <div>*</div> : (selectedOption ? getDisplayValue(selectedOption) : (initialValueDisplay || placeholder))}
          <ChevronsUpDown className="opacity-50" size={10} />
        </Button>
      </PopoverTrigger>
      <PopoverContent
        className={cn('p-0', className)}
        onCloseAutoFocus={(e) => e.preventDefault()}
        align="start"
        sideOffset={8}
        collisionPadding={5}
      >
        <Command shouldFilter={false}>
          <div className="relative w-full border-b">
            <CommandInput
              placeholder={searchPlaceholder || 'Search...'}
              value={searchTerm}
              onValueChange={(value) => {
                setSearchTerm(value)
              }}
              className={searchInputClassName}
            />
            {loading && options.length > 0 && (
              <div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
                <Loader2 className="h-4 w-4 animate-spin" />
              </div>
            )}
          </div>
          <CommandList>
            {error && <div className="text-destructive p-4 text-center">{error}</div>}
            {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
            {!loading &&
              !error &&
              options.length === 0 &&
              (notFound || (
                <CommandEmpty>
                  {noResultsMessage || 'No results found.'}
                </CommandEmpty>
              ))}
            <CommandGroup>
              {options.map((option) => {
                const optionValue = getOptionValue(option);
                // Fix cmdk filtering issue: use empty string when search is empty
                // This ensures all items are shown when searchTerm is empty
                const itemValue = searchTerm.trim() === '' ? '' : optionValue;

                return (
                  <CommandItem
                    key={optionValue}
                    value={itemValue}
                    onSelect={() => {
                      handleSelect(optionValue);
                    }}
                    className="truncate"
                  >
                    {renderOption(option)}
                    <Check
                      className={cn(
                        'ml-auto h-3 w-3',
                        value === optionValue ? 'opacity-100' : 'opacity-0'
                      )}
                    />
                  </CommandItem>
                );
              })}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}

function DefaultLoadingSkeleton() {
  return (
    <CommandGroup>
      <CommandItem disabled>
        <div className="flex w-full items-center gap-2">
          <div className="bg-muted h-6 w-6 animate-pulse rounded-full" />
          <div className="flex flex-1 flex-col gap-1">
            <div className="bg-muted h-4 w-24 animate-pulse rounded" />
            <div className="bg-muted h-3 w-16 animate-pulse rounded" />
          </div>
        </div>
      </CommandItem>
    </CommandGroup>
  )
}
