import {
  type ChangeEvent,
  type FocusEvent,
  type KeyboardEvent,
  Children,
  isValidElement,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from 'react'
import type { IPktComboboxOption, TPktComboboxDisplayValue } from 'shared-types/combobox'
import {
  findOptionByValue,
  filterOptionsBySearch,
  buildFulltext,
  isMaxSelectionReached,
  parseValueToArray,
  findTypeaheadMatches,
  focusFirstOption,
  focusFirstOrSelectedOption,
  focusNextOption,
  focusPreviousOption,
  getSearchInfoMessage,
  getInputKeyAction,
  getInputValueAction,
  getSingleValueForInput,
} from 'shared-utils/combobox'

import type { IPktCombobox, IComboboxState } from './types'

// Options sync helper (React-specific: immutable, returns new array)
function syncOptionsWithValues(options: IPktComboboxOption[], values: string[]): IPktComboboxOption[] {
  return options.map((opt) => ({
    ...opt,
    label: opt.label || opt.value,
    fulltext: buildFulltext(opt),
    selected: values.includes(opt.value),
  }))
}

export function useComboboxState(props: IPktCombobox, ref: React.ForwardedRef<HTMLDivElement>): IComboboxState {
  const {
    id = '',
    label,
    value,
    defaultValue,
    options: optionsProp,
    defaultOptions,
    multiple = false,
    maxlength,
    typeahead = false,
    includeSearch = false,
    allowUserInput = false,
    displayValueAs = 'label' as TPktComboboxDisplayValue,
    tagPlacement,
    searchPlaceholder,
    placeholder,
    disabled = false,
    required = false,
    fullwidth = false,
    hasError = false,
    errorMessage,
    helptext,
    helptextDropdown,
    helptextDropdownButton,
    optionalTag = false,
    optionalText,
    requiredTag = false,
    requiredText,
    tagText,
    useWrapper = true,
    inputSize = 'medium',
    name,
    className,
    onChange,
    onValueChange,
    children,
  } = props

  // Identity
  const inputId = `${id}-input`
  const listboxId = `${id}-listbox`
  const hasTextInput = typeahead || allowUserInput

  // Refs
  const inputRef = useRef<HTMLInputElement>(null)
  const changeInputRef = useRef<HTMLInputElement>(null)
  const triggerRef = useRef<HTMLDivElement>(null)
  const listboxRef = useRef<HTMLDivElement>(null)
  const wrapperRef = useRef<HTMLDivElement>(null)

  // Focus the appropriate trigger: text input for editable, combobox div for select-only
  const focusTrigger = useCallback(() => {
    if (hasTextInput) {
      inputRef.current?.focus()
    } else {
      triggerRef.current?.focus()
    }
  }, [hasTextInput])

  // Parse <option> children into options
  const optionsFromChildren = useMemo(() => {
    const result: IPktComboboxOption[] = []
    Children.forEach(children, (child) => {
      if (isValidElement(child) && child.type === 'option') {
        const childProps = child.props as {
          value?: string
          children?: string
          selected?: boolean
          disabled?: boolean
        }
        result.push({
          value: childProps.value ?? (childProps.children as string) ?? '',
          label: (childProps.children as string) ?? childProps.value ?? '',
          selected: childProps.selected,
          disabled: childProps.disabled,
        })
      }
    })
    return result
  }, [children])

  // Merge options from all sources
  const baseOptions = useMemo(() => {
    // defaultOptions take precedence, then optionsProp, then children
    if (defaultOptions && defaultOptions.length > 0) {
      return defaultOptions.map((opt) => ({ ...opt, fulltext: buildFulltext(opt) }))
    }
    if (optionsProp && optionsProp.length > 0) {
      return optionsProp.map((opt) => ({ ...opt, fulltext: buildFulltext(opt) }))
    }
    if (optionsFromChildren.length > 0) {
      return optionsFromChildren.map((opt) => ({ ...opt, fulltext: buildFulltext(opt) }))
    }
    return []
  }, [defaultOptions, optionsProp, optionsFromChildren])

  // Controlled/uncontrolled value
  const isControlled = value !== undefined

  const [internalValues, setInternalValues] = useState<string[]>(() => {
    const initial = value ?? defaultValue
    return parseValueToArray(initial as string | string[] | null | undefined, multiple)
  })

  // Store initial default values for form reset
  const initialValuesRef = useRef(internalValues)

  // Sync external controlled value
  useEffect(() => {
    if (isControlled) {
      setInternalValues(parseValueToArray(value as string | string[] | null | undefined, multiple))
    }
  }, [value, isControlled, multiple])

  const values = isControlled
    ? parseValueToArray(value as string | string[] | null | undefined, multiple)
    : internalValues

  // Options state (with selection tracking and user-added options)
  const [userAddedOptions, setUserAddedOptions] = useState<IPktComboboxOption[]>([])

  const options = useMemo(() => {
    const merged = [
      ...userAddedOptions.filter((ua) => !baseOptions.some((bo) => bo.value === ua.value)),
      ...baseOptions,
    ]
    return syncOptionsWithValues(merged, values)
  }, [baseOptions, userAddedOptions, values])

  // UI state
  const [isOpen, setIsOpen] = useState(false)
  const [searchValue, setSearchValue] = useState('')
  const [inputValue, setInputValue] = useState('')
  const [userInfoMessage, setUserInfoMessage] = useState('')
  const [addValueText, setAddValueText] = useState<string | null>(null)
  const [inputFocus, setInputFocus] = useState(false)
  const [editingSingleValue, setEditingSingleValue] = useState(false)
  const [focusedTagIndex, setFocusedTagIndex] = useState<number>(-1)
  const suppressNextOpenRef = useRef(false)

  // Derived state
  const maxIsReached = isMaxSelectionReached(values.length, maxlength ?? null)
  const hasCounter = multiple && maxlength != null

  // Filter options by search (typeahead uses fulltext filtering, includeSearch uses same)
  const filteredOptions = useMemo(() => {
    if (!searchValue) return options
    if (typeahead) return findTypeaheadMatches(options, searchValue).filtered
    return filterOptionsBySearch(options, searchValue)
  }, [options, searchValue, typeahead])

  // Hidden input — dispatch native events for React synthetic event compat
  const nativeInputValueSetter = useMemo(
    () => Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set,
    [],
  )

  const dispatchChange = useCallback(
    (newValues: string[]) => {
      const input = changeInputRef.current
      if (!input || !nativeInputValueSetter) return
      const stringVal = multiple ? newValues.join(',') : newValues[0] || ''
      nativeInputValueSetter.call(input, stringVal)
      input.dispatchEvent(new Event('input', { bubbles: true }))
    },
    [nativeInputValueSetter, multiple],
  )

  // Sync hidden input value
  const formValue = multiple ? values.join(',') : values[0] || ''
  useEffect(() => {
    if (changeInputRef.current) {
      changeInputRef.current.value = formValue
    }
  }, [formValue])

  // Sync text input display value for single+typeahead when dropdown is closed
  useEffect(() => {
    if (isOpen || multiple || !inputRef.current) return
    const displayValue = values[0] ? getSingleValueForInput(values[0], options, displayValueAs) : ''
    if (inputRef.current.value !== displayValue) {
      inputRef.current.value = displayValue
    }
  }, [values, options, displayValueAs, multiple, isOpen])

  // Value update helper
  const updateValues = useCallback(
    (newValues: string[], notify = true) => {
      if (!isControlled) {
        setInternalValues(newValues)
      }
      if (notify) {
        onValueChange?.(newValues)
        dispatchChange(newValues)
      }
    },
    [isControlled, onValueChange, dispatchChange],
  )

  // Selection operations
  const setSelected = useCallback(
    (optValue: string | null) => {
      if (!optValue) return
      if (values.includes(optValue)) return
      if (multiple && isMaxSelectionReached(values.length, maxlength ?? null)) {
        setUserInfoMessage('Maks antall valg nådd')
        return
      }
      const newValues = multiple ? [...values, optValue] : [optValue]
      updateValues(newValues)
    },
    [values, multiple, maxlength, updateValues],
  )

  const removeSelected = useCallback(
    (optValue: string | null) => {
      if (!optValue) return
      const newValues = values.filter((v) => v !== optValue)
      // Remove user-added option if it was deselected
      const opt = findOptionByValue(options, optValue)
      if (opt?.userAdded) {
        setUserAddedOptions((prev) => prev.filter((o) => o.value !== optValue))
      }
      updateValues(newValues)
    },
    [values, options, updateValues],
  )

  const removeAllSelected = useCallback(() => {
    setUserAddedOptions([])
    updateValues([])
  }, [updateValues])

  const addNewUserValue = useCallback(
    (val: string | null) => {
      if (!val || val.trim() === '') return

      let newValues: string[]
      if (!multiple) {
        newValues = [val]
      } else {
        if (findOptionByValue(options, val)) return
        if (isMaxSelectionReached(values.length, maxlength ?? null)) return
        newValues = [...values, val]
      }

      const newOption: IPktComboboxOption = {
        value: val,
        label: val,
        userAdded: true,
        selected: true,
      }
      setUserAddedOptions((prev) => [newOption, ...prev.filter((o) => o.value !== val)])
      updateValues(newValues)
    },
    [multiple, options, values, maxlength, updateValues],
  )

  // Reset input field
  // currentValue overrides stale closure reads of values[0] (React batches state updates,
  // so values[0] may not reflect changes from setSelected/removeSelected in the same callback)
  const resetInput = useCallback(
    (shouldReset: boolean = true, currentValue?: string) => {
      setAddValueText(null)
      if (shouldReset && inputRef.current && inputRef.current.type !== 'hidden') {
        setSearchValue('')
        setInputValue('')
        const val = currentValue !== undefined ? currentValue : values[0]
        if (!multiple && val) {
          inputRef.current.value = getSingleValueForInput(val, options, displayValueAs)
        } else {
          inputRef.current.value = ''
        }
        if (!multiple) {
          setEditingSingleValue(false)
          setUserInfoMessage('')
        }
      }
    },
    [multiple, values, options, displayValueAs],
  )

  // Core toggle logic (matches Elements toggleValue)
  const toggleValue = useCallback(
    (val: string | null) => {
      if (disabled) return

      setUserInfoMessage('')
      setAddValueText(null)

      const valueFromOptions = findOptionByValue(options, val)?.value || null
      const isSelected = values.includes(val || valueFromOptions || '')
      const isInOption = !!valueFromOptions
      const isDisabled = options.find((o) => o.value === val)?.disabled || false
      const isEmpty = !val?.trim()
      const isSingle = !multiple
      const isMultiple = multiple
      const isMaxItemsReached = isMaxSelectionReached(values.length, maxlength ?? null)

      let shouldOptionsBeOpen = false
      let shouldResetInput = true
      let newUserInfoMessage = ''
      let newSearchValue = ''
      // Track the resulting single value for resetInput (avoids stale closure reads)
      let resultingSingleValue: string | undefined

      if (isDisabled) return

      // Not in list + allowUserInput
      if (!isInOption && allowUserInput && !isEmpty) {
        addNewUserValue(val)
        newUserInfoMessage = 'Ny verdi lagt til'
        shouldOptionsBeOpen = isMultiple
        if (isSingle) resultingSingleValue = val || ''
      }
      // Not in list + no user input
      else if (!isInOption && !allowUserInput) {
        if (isSingle && values[0]) {
          removeSelected(values[0])
        }
        shouldResetInput = false
        shouldOptionsBeOpen = true
        newUserInfoMessage = 'Ingen treff i søket'
      }
      // Already selected — deselect
      else if (isSelected) {
        removeSelected(valueFromOptions)
        shouldOptionsBeOpen = true
        resultingSingleValue = ''
        // For single+typeahead: clear the input immediately so tab-out doesn't re-select
        if (isSingle && hasTextInput && inputRef.current && inputRef.current.type !== 'hidden') {
          inputRef.current.value = ''
        }
      }
      // Empty + single — clear
      else if (isEmpty && isSingle) {
        removeAllSelected()
        shouldOptionsBeOpen = true
        resultingSingleValue = ''
      }
      // Single — replace
      else if (isSingle) {
        if (values[0]) removeSelected(values[0])
        setSelected(valueFromOptions)
        shouldOptionsBeOpen = false
        resultingSingleValue = valueFromOptions || ''
      }
      // Multi with room
      else if (isMultiple && !isMaxItemsReached) {
        setSelected(valueFromOptions)
        shouldOptionsBeOpen = true
      }
      // Multi at max
      else if (isMultiple && isMaxItemsReached) {
        newUserInfoMessage = 'Maks antall valg nådd'
        shouldResetInput = false
        newSearchValue = val || ''
      }
      // Fallback
      else {
        if (isSingle) removeAllSelected()
        newUserInfoMessage = 'Ingen gyldig verdi valgt'
        shouldResetInput = false
        shouldOptionsBeOpen = true
        newSearchValue = val || ''
        resultingSingleValue = ''
      }

      setIsOpen(shouldOptionsBeOpen)
      setUserInfoMessage(newUserInfoMessage)
      setSearchValue(newSearchValue)
      resetInput(shouldResetInput, resultingSingleValue)

      if (!shouldOptionsBeOpen) {
        if (isSingle && hasTextInput) {
          // Suppress the next handleInputFocus from reopening the dropdown,
          // then move focus back to the text input so screen readers
          // announce the selected value instead of the browser window.
          suppressNextOpenRef.current = true
        }
        window.setTimeout(() => {
          focusTrigger()
        }, 0)
      }
    },
    [
      disabled,
      options,
      values,
      multiple,
      maxlength,
      allowUserInput,
      addNewUserValue,
      removeSelected,
      removeAllSelected,
      setSelected,
      resetInput,
      focusTrigger,
    ],
  )

  // Close and process input (used by focusout and outside click)
  const closeAndProcessInput = useCallback(() => {
    setInputFocus(false)
    setAddValueText(null)
    setUserInfoMessage('')
    setSearchValue('')

    if (inputRef.current && inputRef.current.type !== 'hidden') {
      const inputText = inputRef.current.value
      // Track the resulting value after processing (to avoid stale closure reads)
      let resultingValue: string | undefined = values[0]

      if (!multiple) {
        if (!inputText) {
          // Empty input — clear the selection
          if (values[0]) removeSelected(values[0])
          resultingValue = undefined
        } else {
          // Try to match input text to an option (by value or label)
          const match = findOptionByValue(options, inputText)
          if (match && match.value !== values[0]) {
            // Input matches a different option — select it
            if (values[0]) removeSelected(values[0])
            setSelected(match.value)
            resultingValue = match.value
          } else if (!match && allowUserInput) {
            // No match + allowUserInput — set as user value
            if (values[0]) removeSelected(values[0])
            addNewUserValue(inputText)
            resultingValue = inputText
          }
          // No match, no allowUserInput — discard, keep previous selection
        }
      } else if (inputText !== '') {
        // Multi: process typed text (add/select/remove)
        const { action, value: actionValue } = getInputValueAction(inputText, values, options, allowUserInput, multiple)
        switch (action) {
          case 'addUserValue':
            addNewUserValue(actionValue)
            break
          case 'selectOption':
            setSelected(actionValue)
            break
          case 'removeValue':
            removeSelected(actionValue)
            break
        }
      }

      // Restore input to display text of current selection (or clear)
      if (!multiple && resultingValue) {
        inputRef.current.value = getSingleValueForInput(resultingValue, options, displayValueAs)
      } else {
        inputRef.current.value = ''
      }
    }
    setIsOpen(false)
    setEditingSingleValue(false)
  }, [values, options, allowUserInput, multiple, displayValueAs, addNewUserValue, setSelected, removeSelected])

  // Check for matches while typing (info messages)
  const checkForMatches = useCallback(
    (currentInputValue: string) => {
      const search = currentInputValue.trim().toLowerCase()

      if (!search) {
        setAddValueText(null)
        setUserInfoMessage('')
        if (!multiple && values[0]) {
          removeSelected(values[0])
        }
        return
      }

      const result = getSearchInfoMessage(currentInputValue, values, options, allowUserInput)
      setAddValueText(result.addValueText)
      setUserInfoMessage(result.userInfoMessage)
    },
    [values, options, allowUserInput, multiple, removeSelected],
  )

  // Event handlers

  const handleInputChange = useCallback(
    (e: ChangeEvent<HTMLInputElement>) => {
      if (disabled) return

      const newInputValue = e.target.value
      setInputValue(newInputValue)
      setSearchValue(newInputValue)
      checkForMatches(newInputValue)

      if (typeahead) {
        if (newInputValue) {
          const { suggestion } = findTypeaheadMatches(options, newInputValue)
          if (
            suggestion?.label &&
            inputRef.current &&
            !(e.nativeEvent as InputEvent).inputType?.includes('deleteContent')
          ) {
            inputRef.current.value = suggestion.label
            window.setTimeout(
              () => inputRef.current?.setSelectionRange(newInputValue.length, suggestion.label!.length),
              0,
            )
          }
        }
      }
    },
    [disabled, typeahead, options, checkForMatches],
  )

  const handleInputKeydown = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Backspace') {
        const inputEmpty = !inputRef.current?.value
        if (!searchValue && inputEmpty && multiple && values.length > 0) {
          e.preventDefault()
          const lastVal = values[values.length - 1]
          removeSelected(lastVal)
        }
        return
      }

      if (e.key === 'ArrowLeft' && multiple && values.length > 0) {
        const cursorPos = inputRef.current?.selectionStart ?? 0
        if (cursorPos === 0 && !inputRef.current?.value) {
          e.preventDefault()
          setFocusedTagIndex(values.length - 1)
          return
        }
      }

      const action = getInputKeyAction(e.key, e.shiftKey, multiple)
      if (!action) return

      // When the dropdown is closed, let Tab move focus naturally.
      if (e.key === 'Tab' && !isOpen) return

      // For Tab/'focusListbox': only focus the listbox if it has focusable options.
      // If the listbox is empty (no matches), close and let Tab move focus naturally.
      if (action === 'focusListbox' && e.key === 'Tab') {
        const hasFocusable = listboxRef.current?.querySelector(
          '[role="option"]:not([data-disabled]), [data-type="new-option"]',
        )
        if (!hasFocusable) {
          closeAndProcessInput()
          return
        }
      }

      e.preventDefault()
      switch (action) {
        case 'addValue': {
          const input = inputRef.current?.value.trim() || ''
          setSearchValue(input)
          // If the typed value is already selected, don't toggle (which would deselect).
          // Just reset the input and keep the "already selected" message.
          if (input && values.includes(input)) {
            resetInput(true)
            setUserInfoMessage('Verdien er allerede valgt')
            break
          }
          toggleValue(input)
          break
        }
        case 'focusListbox':
          if (listboxRef.current) {
            if (!isOpen) setIsOpen(true)
            focusFirstOrSelectedOption(listboxRef.current, {
              disabled,
              allowUserInput,
              customUserInput: addValueText,
              includeSearch,
            })
          }
          break
        case 'closeOptions':
          setIsOpen(false)
          // Don't refocus — the text input already has focus, and focusing
          // it again would trigger handleInputFocus which reopens the dropdown
          break
      }
    },
    [
      searchValue,
      values,
      multiple,
      disabled,
      allowUserInput,
      addValueText,
      includeSearch,
      isOpen,
      removeSelected,
      toggleValue,
      focusTrigger,
    ],
  )

  const handleInputFocus = useCallback(() => {
    if (disabled) return

    // After selecting a value in single+typeahead, focus returns to the input.
    // Skip reopening the dropdown so screen readers announce the selected value.
    if (suppressNextOpenRef.current) {
      suppressNextOpenRef.current = false
      setInputFocus(true)
      return
    }

    if (!multiple && values[0] && inputRef.current && inputRef.current.type !== 'hidden') {
      setEditingSingleValue(true)
      inputRef.current.value = getSingleValueForInput(values[0], options, displayValueAs)
    }
    setInputFocus(true)
    setSearchValue('')
    setIsOpen(true)
  }, [disabled, multiple, values, options, displayValueAs])

  const handleInputBlur = useCallback(() => {
    setInputFocus(false)
    setEditingSingleValue(false)
  }, [])

  const handleFocusOut = useCallback(
    (e: FocusEvent) => {
      if (disabled || !isOpen) return

      const related = e.relatedTarget as Element | null
      const wrapper = wrapperRef.current

      // Check if focus moved to another element within the combobox
      if (wrapper && related && wrapper.contains(related)) return

      closeAndProcessInput()
    },
    [disabled, isOpen, closeAndProcessInput],
  )

  const handleInputClick = useCallback(
    (e: React.MouseEvent) => {
      if (disabled) {
        e.preventDefault()
        e.stopPropagation()
        return
      }
      if (hasTextInput) {
        inputRef.current?.focus()
      } else {
        // Select-only: toggle the listbox
        e.stopPropagation()
        e.preventDefault()
        const newOpen = !isOpen
        setIsOpen(newOpen)
        if (newOpen && listboxRef.current) {
          focusFirstOrSelectedOption(listboxRef.current, {
            disabled,
            allowUserInput,
            customUserInput: addValueText,
            includeSearch,
          })
        }
      }
    },
    [disabled, hasTextInput, isOpen, allowUserInput, addValueText, includeSearch],
  )

  const handlePlaceholderClick = useCallback(
    (e: React.MouseEvent) => {
      if (disabled) return
      e.stopPropagation()
      if (hasTextInput && inputRef.current) {
        inputRef.current.focus()
        setInputFocus(true)
      } else {
        // Select-only mode: toggle the listbox
        setIsOpen((prev) => !prev)
      }
    },
    [disabled, hasTextInput],
  )

  const handleSelectOnlyKeydown = useCallback(
    (e: React.KeyboardEvent) => {
      if (disabled) return

      switch (e.key) {
        case 'Enter':
        case ' ':
        case 'ArrowDown':
        case 'ArrowUp':
          e.preventDefault()
          if (!isOpen) {
            setIsOpen(true)
            if (listboxRef.current) {
              window.setTimeout(() => {
                if (!listboxRef.current) return
                focusFirstOrSelectedOption(listboxRef.current, {
                  disabled,
                  allowUserInput: allowUserInput && !maxIsReached,
                  customUserInput: addValueText,
                  includeSearch,
                })
              }, 0)
            }
          } else {
            setIsOpen(false)
          }
          break
        case 'Escape':
          if (isOpen) {
            e.preventDefault()
            setIsOpen(false)
          }
          break
        case 'Home':
        case 'End':
          e.preventDefault()
          if (!isOpen) setIsOpen(true)
          if (listboxRef.current) {
            window.setTimeout(() => {
              if (!listboxRef.current) return
              focusFirstOrSelectedOption(listboxRef.current, {
                disabled,
                allowUserInput: allowUserInput && !maxIsReached,
                customUserInput: addValueText,
                includeSearch,
              })
            }, 0)
          }
          break
        case 'ArrowLeft':
          if (multiple && values.length > 0) {
            e.preventDefault()
            setFocusedTagIndex(values.length - 1)
          }
          break
        case 'Backspace':
        case 'Delete':
          if (multiple && values.length > 0) {
            e.preventDefault()
            const lastVal = values[values.length - 1]
            removeSelected(lastVal)
          }
          break
      }
    },
    [disabled, isOpen, allowUserInput, maxIsReached, addValueText, includeSearch, multiple, values, removeSelected],
  )

  const handleOptionClick = useCallback(
    (optValue: string) => {
      toggleValue(optValue)
    },
    [toggleValue],
  )

  const handleOptionKeydown = useCallback(
    (e: KeyboardEvent<HTMLLIElement>, optValue: string) => {
      switch (e.key) {
        case 'Enter':
        case ' ':
          e.preventDefault()
          toggleValue(optValue)
          break
        case 'ArrowDown':
          e.preventDefault()
          focusNextOption(e.currentTarget)
          break
        case 'ArrowUp':
          e.preventDefault()
          if (listboxRef.current) {
            focusPreviousOption(e.currentTarget, listboxRef.current, includeSearch)
          }
          break
        case 'Escape':
          e.preventDefault()
          setIsOpen(false)
          focusTrigger()
          break
        case 'Tab':
          // Don't preventDefault — let Tab move focus naturally
          closeAndProcessInput()
          break
      }
    },
    [toggleValue, includeSearch, focusTrigger, closeAndProcessInput],
  )

  const handleTagRemove = useCallback(
    (optValue: string) => {
      removeSelected(optValue)
      setFocusedTagIndex(-1)
      if (hasTextInput && inputRef.current) {
        setInputFocus(true)
        inputRef.current.focus()
      }
    },
    [removeSelected, hasTextInput],
  )

  const handleTagKeydown = useCallback(
    (e: React.KeyboardEvent, index: number) => {
      e.stopPropagation()
      switch (e.key) {
        case 'ArrowLeft':
          e.preventDefault()
          if (index > 0) {
            setFocusedTagIndex(index - 1)
          }
          break
        case 'ArrowRight':
          e.preventDefault()
          if (index < values.length - 1) {
            setFocusedTagIndex(index + 1)
          } else {
            setFocusedTagIndex(-1)
            if (hasTextInput && inputRef.current) {
              inputRef.current.focus()
            } else {
              triggerRef.current?.focus()
            }
          }
          break
        case 'Backspace':
        case 'Delete':
          e.preventDefault()
          {
            const val = values[index]
            const nextIndex = index >= values.length - 1 ? index - 1 : index
            removeSelected(val)
            if (nextIndex >= 0) {
              setFocusedTagIndex(nextIndex)
            } else {
              setFocusedTagIndex(-1)
              if (hasTextInput && inputRef.current) {
                inputRef.current.focus()
              } else {
                triggerRef.current?.focus()
              }
            }
          }
          break
        case 'Tab':
          // Let Tab move focus naturally, but reset roving tabindex
          // so the next Tab into the combobox lands on the trigger, not a tag
          setFocusedTagIndex(-1)
          break
        case 'Escape':
          e.preventDefault()
          setFocusedTagIndex(-1)
          if (hasTextInput && inputRef.current) {
            inputRef.current.focus()
          } else {
            triggerRef.current?.focus()
          }
          break
      }
    },
    [values, hasTextInput, removeSelected],
  )

  const handleSearchInput = useCallback(
    (e: ChangeEvent<HTMLInputElement>) => {
      setSearchValue(e.target.value)
      checkForMatches(e.target.value)
    },
    [checkForMatches],
  )

  const handleSearchKeydown = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      switch (e.key) {
        case 'ArrowDown':
          e.preventDefault()
          if (listboxRef.current) {
            const firstOption = listboxRef.current.querySelector<HTMLElement>(
              '[role="option"]:not([data-disabled]), [data-type="new-option"]',
            )
            firstOption?.focus()
          }
          break
        case 'Tab':
          // Don't preventDefault — let Tab move focus naturally
          closeAndProcessInput()
          break
        case 'Escape':
          e.preventDefault()
          setIsOpen(false)
          focusTrigger()
          break
      }
    },
    [focusTrigger, closeAndProcessInput],
  )

  const handleNewOptionClick = useCallback(
    (optValue: string) => {
      toggleValue(optValue)
    },
    [toggleValue],
  )

  const handleNewOptionKeydown = useCallback(
    (e: KeyboardEvent<HTMLDivElement>) => {
      switch (e.key) {
        case 'Enter':
        case ' ':
          e.preventDefault()
          toggleValue((e.currentTarget as HTMLElement).dataset.value || '')
          break
        case 'ArrowDown':
          e.preventDefault()
          if (listboxRef.current) {
            focusFirstOption(listboxRef.current)
          }
          break
        case 'ArrowUp':
          e.preventDefault()
          if (listboxRef.current) {
            focusPreviousOption(e.currentTarget, listboxRef.current, includeSearch)
          }
          break
        case 'Escape':
          e.preventDefault()
          setIsOpen(false)
          focusTrigger()
          break
      }
    },
    [toggleValue, includeSearch, focusTrigger],
  )

  // Outside click handler
  useEffect(() => {
    if (!isOpen) return

    const handleClick = (e: MouseEvent) => {
      const target = e.target as Node
      if (wrapperRef.current && !wrapperRef.current.contains(target)) {
        closeAndProcessInput()
      }
    }

    document.addEventListener('click', handleClick, true)
    return () => {
      document.removeEventListener('click', handleClick, true)
    }
  }, [isOpen, closeAndProcessInput])

  // Form reset: listen for the parent <form>'s reset event and restore initial values
  useEffect(() => {
    const form = wrapperRef.current?.closest('form')
    if (!form) return

    const handleReset = () => {
      // Use setTimeout so the reset event finishes before we update state
      window.setTimeout(() => {
        setInternalValues(initialValuesRef.current)
        setUserAddedOptions([])
        setInputValue('')
        setSearchValue('')
        setAddValueText(null)
        setUserInfoMessage('')
        if (changeInputRef.current) {
          const resetVal = multiple ? initialValuesRef.current.join(',') : initialValuesRef.current[0] || ''
          changeInputRef.current.value = resetVal
        }
      }, 0)
    }

    form.addEventListener('reset', handleReset)
    return () => form.removeEventListener('reset', handleReset)
  }, [multiple])

  // Expose value getter/setter on ref for RHF register() compatibility.
  // RHF reads ref.value via getFieldValue(field._f) — we read directly from the
  // hidden input DOM element so the value is always fresh (not stale from a
  // batched state update). RHF calls ref.value = x on mount and on reset.
  useImperativeHandle(
    ref,
    () =>
      ({
        get value() {
          return changeInputRef.current?.value ?? ''
        },
        set value(newVal: string | string[]) {
          const newValues = Array.isArray(newVal) ? newVal : newVal ? String(newVal).split(',').filter(Boolean) : []
          if (!isControlled) {
            setInternalValues(newValues)
          }
          if (changeInputRef.current) {
            changeInputRef.current.value = newValues.join(',')
          }
        },
        focus() {
          if (hasTextInput) {
            inputRef.current?.focus()
          } else {
            triggerRef.current?.focus()
          }
        },
        blur() {
          if (hasTextInput) {
            inputRef.current?.blur()
          } else {
            triggerRef.current?.blur()
          }
        },
      }) as unknown as HTMLDivElement,
    [isControlled],
  )

  return {
    // Identity
    id,
    inputId,
    listboxId,

    // Values
    values,
    inputValue,
    searchValue,

    // Options
    options,
    filteredOptions,

    // UI state
    isOpen,
    userInfoMessage,
    addValueText,
    maxIsReached,
    editingSingleValue,
    inputFocus,
    focusedTagIndex,

    // Props passthrough
    label,
    multiple,
    maxlength,
    typeahead,
    includeSearch,
    allowUserInput,
    displayValueAs,
    tagPlacement,
    searchPlaceholder,
    placeholder,
    disabled,
    required,
    fullwidth,
    hasError,
    errorMessage,
    helptext,
    helptextDropdown,
    helptextDropdownButton,
    optionalTag,
    optionalText,
    requiredTag,
    requiredText,
    tagText,
    useWrapper,
    inputSize,
    name,
    className,

    // Derived
    hasCounter,

    // Refs
    inputRef,
    changeInputRef,
    triggerRef,
    listboxRef,
    wrapperRef,

    // Handlers
    handleInputChange,
    handleInputKeydown,
    handleInputFocus,
    handleInputBlur,
    handleFocusOut,
    handleInputClick,
    handlePlaceholderClick,
    handleSelectOnlyKeydown,
    handleOptionClick,
    handleOptionKeydown,
    handleTagRemove,
    handleTagKeydown,
    handleSearchInput,
    handleSearchKeydown,
    handleNewOptionClick,
    handleNewOptionKeydown,

    // Form integration
    onChange,
  }
}
