/* eslint-disable react-refresh/only-export-components, react/prop-types */
import * as React from 'react'
import { Check, X } from 'lucide-react'

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

type ComboboxContextValue = {
  items: readonly string[]
  open: boolean
  multiple: boolean
  values: string[]
  inputValue: string
  setInputValue: (value: string) => void
  setOpen: (open: boolean) => void
  selectValue: (value: string) => void
  removeValue: (value: string) => void
}

const ComboboxContext = React.createContext<ComboboxContextValue | null>(null)

function useComboboxContext(): ComboboxContextValue {
  const context = React.useContext(ComboboxContext)

  if (!context) {
    throw new Error('Combobox components must be used inside Combobox')
  }

  return context
}

export function useComboboxAnchor(): React.RefObject<HTMLDivElement | null> {
  return React.useRef<HTMLDivElement>(null)
}

type ComboboxProps = {
  children: React.ReactNode
  items: readonly string[]
  multiple?: boolean
  autoHighlight?: boolean
  value?: string[]
  defaultValue?: string[]
  onValueChange?: (values: string[]) => void
}

function Combobox({
  children,
  items,
  multiple = false,
  value,
  defaultValue = [],
  onValueChange
}: ComboboxProps): React.JSX.Element {
  const [open, setOpen] = React.useState(false)
  const [inputValue, setInputValue] = React.useState('')
  const [internalValues, setInternalValues] = React.useState(defaultValue)
  const values = value ?? internalValues

  function updateValues(nextValues: string[]): void {
    if (!value) {
      setInternalValues(nextValues)
    }

    onValueChange?.(nextValues)
  }

  function selectValue(rawValue: string): void {
    const nextValue = rawValue.trim()

    if (!nextValue) {
      return
    }

    if (multiple) {
      if (!values.includes(nextValue)) {
        updateValues([...values, nextValue])
      }
    } else {
      updateValues([nextValue])
      setOpen(false)
    }

    setInputValue('')
  }

  function removeValue(valueToRemove: string): void {
    updateValues(values.filter((item) => item !== valueToRemove))
  }

  return (
    <ComboboxContext.Provider
      value={{
        items,
        open,
        multiple,
        values,
        inputValue,
        setInputValue,
        setOpen,
        selectValue,
        removeValue
      }}
    >
      <div className="relative">{children}</div>
    </ComboboxContext.Provider>
  )
}

const ComboboxChips = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, onClick, ...props }, ref) => {
  const { setOpen } = useComboboxContext()

  return (
    <div
      ref={ref}
      className={cn(
        'flex min-h-9 w-full flex-wrap items-center gap-1 rounded-md border border-input bg-transparent px-2 py-1 text-sm shadow-sm transition-colors focus-within:ring-1 focus-within:ring-ring',
        className
      )}
      onClick={(event) => {
        setOpen(true)
        onClick?.(event)
      }}
      {...props}
    />
  )
})
ComboboxChips.displayName = 'ComboboxChips'

function ComboboxValue({
  children
}: {
  children: (values: string[]) => React.ReactNode
}): React.JSX.Element {
  const { values } = useComboboxContext()

  return <>{children(values)}</>
}

function ComboboxChip({
  children,
  className
}: {
  children: string
  className?: string
}): React.JSX.Element {
  const { removeValue } = useComboboxContext()

  return (
    <span
      className={cn(
        'inline-flex h-7 max-w-full items-center gap-1 rounded-md bg-secondary px-2 text-xs font-medium text-secondary-foreground',
        className
      )}
    >
      <span className="truncate">{children}</span>
      <button
        type="button"
        className="grid size-4 shrink-0 place-items-center rounded-sm hover:bg-background/80"
        aria-label={`Remove ${children}`}
        onClick={(event) => {
          event.stopPropagation()
          removeValue(children)
        }}
      >
        <X className="size-3" />
      </button>
    </span>
  )
}

function ComboboxChipsInput(
  props: React.InputHTMLAttributes<HTMLInputElement>
): React.JSX.Element {
  const { inputValue, setInputValue, setOpen, selectValue } = useComboboxContext()

  return (
    <input
      className="h-7 min-w-24 flex-1 bg-transparent px-1 text-sm outline-none placeholder:text-muted-foreground"
      value={inputValue}
      onChange={(event) => {
        setInputValue(event.target.value)
        setOpen(true)
      }}
      onFocus={() => setOpen(true)}
      onKeyDown={(event) => {
        if (event.key === 'Enter' || event.key === ',' || event.key === '\uff0c') {
          event.preventDefault()
          selectValue(inputValue)
        }

        if (event.key === 'Escape') {
          setOpen(false)
        }
      }}
      {...props}
    />
  )
}

function ComboboxContent({
  children,
  className
}: React.HTMLAttributes<HTMLDivElement> & {
  anchor?: React.RefObject<HTMLDivElement | null>
}): React.JSX.Element | null {
  const { open } = useComboboxContext()

  if (!open) {
    return null
  }

  return (
    <div
      className={cn(
        'absolute z-50 mt-1 w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md',
        className
      )}
      onMouseDown={(event) => event.preventDefault()}
    >
      {children}
    </div>
  )
}

function ComboboxEmpty({ children }: { children: React.ReactNode }): React.JSX.Element {
  const { items, values, inputValue } = useComboboxContext()
  const keyword = inputValue.trim().toLowerCase()
  const visibleItems = items.filter(
    (item) => !values.includes(item) && (!keyword || item.toLowerCase().includes(keyword))
  )

  if (visibleItems.length > 0 || inputValue.trim()) {
    return <></>
  }

  return <div className="px-3 py-2 text-sm text-muted-foreground">{children}</div>
}

function ComboboxList({
  children
}: {
  children: (item: string) => React.ReactNode
}): React.JSX.Element {
  const { items, values, inputValue } = useComboboxContext()
  const keyword = inputValue.trim().toLowerCase()
  const visibleItems = items.filter(
    (item) => !values.includes(item) && (!keyword || item.toLowerCase().includes(keyword))
  )
  const shouldShowCustomValue =
    inputValue.trim() && !items.includes(inputValue.trim()) && !values.includes(inputValue.trim())

  return (
    <div className="max-h-56 overflow-y-auto p-1">
      {visibleItems.map((item) => children(item))}
      {shouldShowCustomValue ? children(inputValue.trim()) : null}
    </div>
  )
}

function ComboboxItem({
  value,
  children,
  className
}: {
  value: string
  children: React.ReactNode
  className?: string
}): React.JSX.Element {
  const { values, selectValue } = useComboboxContext()
  const selected = values.includes(value)

  return (
    <button
      type="button"
      className={cn(
        'flex w-full items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-accent hover:text-accent-foreground',
        className
      )}
      onClick={() => selectValue(value)}
    >
      <span className="truncate">{children}</span>
      {selected && <Check className="size-4" />}
    </button>
  )
}

export {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxValue
}
