import React, {
  FormEvent,
  forwardRef,
  HTMLAttributes,
  ReactNode,
  useEffect,
  useState,
  useRef,
  useMemo,
  useCallback,
  useId,
} from 'react'

import classNames from 'classnames'
import PropTypes from 'prop-types'

import type { Placement } from '@popperjs/core'

import { CConditionalPortal } from '../conditional-portal'
import { CFormControlWrapper, CFormControlWrapperProps } from '../form/CFormControlWrapper'
import { CAutocompleteOptions } from './CAutocompleteOptions'

import { useForkedRef, usePopper } from '../../hooks'
import { isRTL } from '../../utils'
import type { Option, OptionsGroup, Search } from './types'
import {
  filterOptions,
  flattenOptionsArray,
  getOptionLabel,
  getFirstOptionByLabel,
  isGlobalSearch,
  getFirstOptionByValue,
  isExternalSearch,
} from './utils'

export interface CAutocompleteProps
  extends
    Omit<CFormControlWrapperProps, 'floatingClassName' | 'floatingLabel'>,
    Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onInput'> {
  /**
   * Only allow selection of predefined options.
   * When `true`, users cannot enter custom values that are not in the options list.
   * When `false`, users can enter and select custom values.
   *
   * @default false
   */
  allowOnlyDefinedOptions?: boolean

  /**
   * A string of all className you want applied to the base component.
   * These classes will be merged with the default React autocomplete classes.
   */
  className?: string

  /**
   * Enables selection cleaner element.
   * When `true`, displays a clear button that allows users to reset the selection.
   * The cleaner button is only shown when there is a selection and the component is not disabled or read-only.
   *
   * @default false
   */
  cleaner?: boolean

  /**
   * Whether to clear the internal search state after selecting an option.
   *
   * When set to `true`, the internal search value used for filtering options is cleared
   * after a selection is made. This affects only the component's internal logic.
   *
   * Note: This does **not** clear the visible input field if the component is using external search
   * or is controlled via the `searchValue` prop. In such cases, clearing must be handled externally.
   *
   * @default true
   */
  clearSearchOnSelect?: boolean

  /**
   * Specifies the DOM element where the dropdown should be rendered when using portal mode.
   * Can be a DOM element reference, DocumentFragment, function that returns an element or null,
   * or null (renders to document body). Works in conjunction with the `portal` prop to control
   * where the portal content is mounted in the DOM tree.
   */
  container?: DocumentFragment | Element | (() => DocumentFragment | Element | null) | null

  /**
   * Toggle the disabled state for the component.
   * When `true`, the React.js autocomplete is non-interactive and appears visually disabled.
   * Users cannot type, select options, or trigger the dropdown.
   */
  disabled?: boolean

  /**
   * Highlight options that match the search criteria.
   * When `true`, matching portions of option labels are visually highlighted
   * based on the current search input value.
   *
   * @default false
   */
  highlightOptionsOnSearch?: boolean

  /**
   * Set the id attribute for the native input element.
   * This id is used for accessibility purposes and form associations.
   * If not provided, a unique id may be generated automatically.
   */
  id?: string

  /**
   * Show dropdown indicator/arrow button.
   * When `true`, displays a dropdown arrow button that can be clicked
   * to manually show or hide the options dropdown.
   */
  indicator?: boolean

  /**
   * When set, the options list will have a loading style: loading spinner and reduced opacity.
   * Use this to indicate that options are being fetched asynchronously.
   * The dropdown remains functional but shows visual loading indicators.
   */
  loading?: boolean

  /**
   * The name attribute for the input element.
   * Used for form submission and identification in form data.
   * Important for proper form handling and accessibility.
   */
  name?: string

  /**
   * Execute a function when a user changes the selected option.
   * Called with the selected option object or `undefined` when cleared.
   * This is the primary callback for handling selection changes.
   *
   * @param option - The selected option object, or `undefined` if cleared
   */
  onChange?: (option: Option | null) => void

  /**
   * Execute a function when the filter/search value changes.
   * Called whenever the user types in the search input.
   * Useful for implementing external search functionality or analytics.
   *
   * @param value - The current search input value
   */
  onInput?: (value: string) => void

  /**
   * The callback is fired when the dropdown requests to be hidden.
   * Called when the dropdown closes due to user interaction, clicks outside,
   * escape key, or programmatic changes.
   */
  onHide?: () => void

  /**
   * The callback is fired when the dropdown requests to be shown.
   * Called when the dropdown opens due to user interaction, focus,
   * or programmatic changes.
   */
  onShow?: () => void

  /**
   * List of option elements.
   * Can contain Option objects, OptionsGroup objects, or plain strings.
   * Plain strings are converted to simple Option objects internally.
   * This is a required prop - the React autocomplete needs options to function.
   */
  options: (Option | OptionsGroup | string)[]

  /**
   * Sets maxHeight of options list.
   * Controls the maximum height of the dropdown options container.
   * Can be a number (pixels) or a CSS length string (e.g., '200px', '10rem').
   * When content exceeds this height, a scrollbar will appear.
   *
   * @default 'auto'
   */
  optionsMaxHeight?: number | string

  /**
   * Custom template for rendering individual options.
   * Allows complete customization of how each option appears in the dropdown.
   * The function receives an Option object and should return a ReactNode.
   *
   * @param option - The option object to render
   * @returns ReactNode to display for this option
   */
  optionsTemplate?: (option: Option) => ReactNode

  /**
   * Custom template for rendering option groups.
   * Allows customization of how option group headers appear in the dropdown.
   * The function receives an OptionsGroup object and should return a ReactNode.
   *
   * @param option - The options group object to render
   * @returns ReactNode to display for this group header
   */
  optionsGroupsTemplate?: (option: OptionsGroup) => ReactNode

  /**
   * Specifies a short hint that is visible in the search input.
   * Displayed when the input is empty to guide user interaction.
   * Standard HTML input placeholder behavior.
   */
  placeholder?: string

  /**
   * Renders the autocomplete dropdown in a React portal, allowing it to break out of its
   * parent container's styling constraints (like overflow, z-index, or positioning).
   * When enabled, the dropdown is rendered at the document root level, ensuring it appears
   * above other page elements and isn't clipped by parent containers.
   *
   * @default false
   */
  portal?: boolean

  /**
   * Toggle the readonly state for the component.
   * When `true`, users can view and interact with the dropdown but cannot
   * type in the search input or modify the selection through typing.
   * Selection via clicking options may still be possible.
   */
  readOnly?: boolean

  /**
   * When it is present, it indicates that the user must choose a value before submitting the form.
   * Adds HTML5 form validation requirement. The form will not submit
   * until a valid selection is made.
   */
  required?: boolean

  /**
   * Determines whether the selected options should be cleared when the options list is updated.
   * When `true`, any previously selected options will be reset whenever the options
   * list undergoes a change. This ensures that outdated selections are not retained
   * when new options are provided.
   *
   * @default false
   */
  resetSelectionOnOptionsChange?: boolean

  /**
   * Enables and configures search functionality.
   * - `'external'`: Search is handled externally, filtering is not applied internally
   * - `'global'`: Enables global keyboard search when dropdown is closed
   * - Object with `external` and `global` boolean properties for fine-grained control
   */
  search?: Search

  /**
   * Sets the label for no results when filtering.
   * - `false`: Don't show any message when no results found
   * - `true`: Show default "No results found" message
   * - `string`: Show custom text message
   * - `ReactNode`: Show custom component/element
   *
   * @default false
   */
  searchNoResultsLabel?: boolean | string | ReactNode

  /**
   * Show hint options based on the current input value.
   * When `true`, displays a preview/hint of the first matching option
   * as semi-transparent text in the input field, similar to browser autocomplete.
   *
   * @default false
   */
  showHints?: boolean

  /**
   * Size the component small or large.
   * - `'sm'`: Small size variant
   * - `'lg'`: Large size variant
   * - `undefined`: Default/medium size
   */
  size?: 'sm' | 'lg'

  /**
   * Sets the initially selected value for the React.js autocomplete component.
   * Can be a string (matched against option labels) or number (matched against option values).
   * The component will attempt to find and select the matching option on mount.
   */
  value?: number | string

  /**
   * Enable virtual scroller for the options list.
   * When `true`, only visible options are rendered in the DOM for better performance
   * with large option lists. Works in conjunction with `visibleItems` prop.
   *
   * @default false
   */
  virtualScroller?: boolean

  /**
   * Toggle the visibility of autocomplete dropdown.
   * Controls whether the dropdown is initially visible.
   * The dropdown visibility can still be toggled through user interaction.
   */
  visible?: boolean

  /**
   * Amount of visible items when virtualScroller is enabled.
   * Determines how many option items are rendered at once when virtual scrolling is active.
   * Higher values show more items but use more memory. Lower values improve performance.
   *
   * @default 10
   */
  visibleItems?: number
}

export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
  (
    {
      allowOnlyDefinedOptions = false,
      className,
      cleaner = false,
      clearSearchOnSelect = true,
      container,
      disabled,
      feedback,
      feedbackInvalid,
      feedbackValid,
      highlightOptionsOnSearch = false,
      id,
      indicator,
      invalid,
      label,
      loading,
      name,
      onChange,
      onHide,
      onInput,
      onShow,
      options,
      optionsMaxHeight = 'auto',
      optionsTemplate,
      optionsGroupsTemplate,
      placeholder,
      portal = false,
      readOnly,
      required,
      resetSelectionOnOptionsChange = false,
      search,
      searchNoResultsLabel = false,
      showHints = false,
      size,
      text,
      tooltipFeedback,
      valid,
      value,
      virtualScroller,
      visible,
      visibleItems = 10,
      ...rest
    },
    ref
  ) => {
    const autoCompleteRef = useRef<HTMLDivElement>(null)
    const autoCompleteForkedRef = useForkedRef(ref, autoCompleteRef)

    const dropdownRef = useRef<HTMLDivElement>(null)
    const togglerRef = useRef<HTMLDivElement>(null)
    const inputRef = useRef<HTMLInputElement>(null)
    const inputHintRef = useRef<HTMLInputElement>(null)
    const uniqueId = useId()

    const { initPopper, destroyPopper } = usePopper()

    const [_visible, setVisible] = useState(visible)
    const [hint, setHint] = useState<Option | undefined>()
    const [searchValue, setSearchValue] = useState('')
    const [selected, setSelected] = useState<Option | null>(null)

    const filteredOptions = useMemo(
      () => (isExternalSearch(search) ? options : filterOptions(options, searchValue)),
      [options, searchValue, search]
    )

    const popperConfig = useMemo(
      () => ({
        placement: (isRTL(autoCompleteRef.current) ? 'bottom-end' : 'bottom-start') as Placement,
        modifiers: [
          {
            name: 'preventOverflow',
            options: {
              boundary: 'clippingParents',
            },
          },
          {
            name: 'offset',
            options: {
              offset: [0, 2],
            },
          },
        ],
      }),
      [autoCompleteRef.current]
    )

    useEffect(() => {
      if (resetSelectionOnOptionsChange) {
        handleClear()
      }
    }, [options])

    useEffect(() => {
      if (value && typeof value === 'string') {
        handleSelect(value)
        return
      }

      if (value && typeof value === 'number') {
        const foundOption = getFirstOptionByValue(value, options)
        if (foundOption) {
          handleSelect(foundOption)
        }

        return
      }

      const _selected = flattenOptionsArray(filteredOptions).find(
        (option: Option) => typeof option !== 'string' && option.selected === true
      )

      if (_selected) {
        handleSelect(_selected)
      }
    }, [options, value])

    useEffect(() => {
      if (!showHints) {
        return
      }

      const findOption =
        searchValue.length > 0
          ? filteredOptions.find((option) =>
              getOptionLabel(option).toLowerCase().startsWith(searchValue.toLowerCase())
            )
          : undefined

      setHint(findOption)
    }, [filteredOptions, searchValue, showHints])

    useEffect(() => {
      if (
        !searchNoResultsLabel &&
        searchValue.length > 0 &&
        filteredOptions.length === 0 &&
        _visible
      ) {
        handleDropdownHide()
        return
      }

      if (searchValue.length > 0 && filteredOptions.length > 0 && !_visible) {
        handleDropdownShow()
      }
    }, [filteredOptions])

    useEffect(() => {
      if (visible === true) {
        handleDropdownShow()
      } else if (visible === false) {
        handleDropdownHide()
      }
    }, [visible])

    const handleClear = () => {
      if (inputRef.current) {
        inputRef.current.value = ''
      }

      setSearchValue('')
      setSelected(null)
      onChange?.(null)
    }

    const handleGlobalSearch = (event: React.KeyboardEvent<HTMLDivElement>) => {
      if (
        isGlobalSearch(search) &&
        inputRef.current &&
        (event.key.length === 1 || event.key === 'Backspace' || event.key === 'Delete')
      ) {
        inputRef.current.focus()
      }
    }

    const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
      const inputValue = event.target.value

      if (inputValue.length === 0) {
        return
      }

      const inputValueLower = inputValue.toLowerCase()
      const exactMatches = flattenOptionsArray(options).filter(
        (option) => getOptionLabel(option).toLowerCase() === inputValueLower
      )

      if (exactMatches.length === 1) {
        handleSelect(exactMatches[0] as Option)
        return
      }

      if (allowOnlyDefinedOptions) {
        handleClear()
        return
      }

      setHint(undefined)
      onChange?.(inputValue)
    }

    const handleInputChange = (event: FormEvent<HTMLInputElement>) => {
      const value = (event.target as HTMLInputElement).value
      handleSearch(value)

      if (selected !== null) {
        onChange?.(null)
        setSelected(null)
      }
    }

    const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
      if (event.key === 'Escape') {
        handleDropdownHide()
        return
      }

      if (
        (event.key === 'Down' || event.key === 'ArrowDown') &&
        inputRef.current?.value.length === inputRef.current?.selectionStart
      ) {
        event.preventDefault()
        handleDropdownShow()

        const target = event.target as HTMLElement
        const firstOption = target.parentElement?.parentElement?.querySelectorAll(
          '.autocomplete-option'
        )[0] as HTMLElement | null

        if (firstOption) {
          firstOption.focus()
        }

        return
      }

      if (showHints && hint && event.key === 'Tab') {
        event.preventDefault()
        handleSelect(hint)
        handleDropdownHide()
        return
      }

      if (event.key === 'Enter') {
        const input = event.target as HTMLInputElement
        const foundOptions = getFirstOptionByLabel(input.value, filteredOptions)
        if (foundOptions) {
          handleSelect(foundOptions)
        } else {
          if (!allowOnlyDefinedOptions) {
            handleSelect(input.value)
          }
        }

        handleDropdownHide()
        return
      }

      if (event.key === 'Backspace' || event.key === 'Delete') {
        if (selected !== null) {
          setSelected(null)
          onChange?.(null)
        }

        return
      }
    }

    const handleKeyUp = useCallback((event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        handleDropdownHide()
      }

      if (
        autoCompleteRef.current &&
        !autoCompleteRef.current.contains(event.target as HTMLElement)
      ) {
        handleDropdownHide()
      }
    }, [])

    const handleMouseUp = useCallback((event: Event) => {
      if (
        autoCompleteRef.current &&
        autoCompleteRef.current.contains(event.target as HTMLElement)
      ) {
        return
      }

      handleDropdownHide()
    }, [])

    const handleOptionClick = (option: Option) => {
      handleSelect(option)
      handleDropdownHide()
    }

    const handleSearch = (search: string) => {
      onInput?.(search)
      setSearchValue(search)
    }

    const handleSelect = (option?: Option | undefined) => {
      if (option && typeof option === 'object' && option.disabled) {
        return
      }

      if (inputRef.current) {
        inputRef.current.value = option ? getOptionLabel(option) : ''
      }

      if (clearSearchOnSelect) {
        handleSearch('')
      } else {
        setHint('')
      }

      setSelected(option ?? null)
      onChange?.(option ?? null)
    }

    const handleDropdownShow = useCallback(() => {
      if (disabled || readOnly || _visible) {
        return
      }

      if (
        !isExternalSearch(search) &&
        filteredOptions.length === 0 &&
        searchNoResultsLabel === false
      ) {
        return
      }

      if (portal && dropdownRef.current && togglerRef.current) {
        dropdownRef.current.style.minWidth = `${(togglerRef.current as HTMLElement).offsetWidth}px`
      }

      setVisible(true)
      onShow?.()

      window.addEventListener('mouseup', handleMouseUp)
      window.addEventListener('keyup', handleKeyUp)

      if (togglerRef.current && dropdownRef.current) {
        setTimeout(() => {
          initPopper(
            togglerRef.current as HTMLDivElement,
            dropdownRef.current as HTMLDivElement,
            popperConfig
          )
        }, 1) // Allow DOM updates to complete before initializing Popper
      }

      inputRef.current?.focus()
    }, [
      disabled,
      readOnly,
      _visible,
      filteredOptions.length,
      searchNoResultsLabel,
      allowOnlyDefinedOptions,
      onShow,
      handleMouseUp,
      handleKeyUp,
      initPopper,
      popperConfig,
    ])

    const handleDropdownHide = useCallback(() => {
      setVisible(false)
      onHide?.()

      window.removeEventListener('mouseup', handleMouseUp)
      window.removeEventListener('keyup', handleKeyUp)

      destroyPopper()
      inputRef.current?.focus()
    }, [onHide, handleMouseUp, handleKeyUp, destroyPopper])

    return (
      <CFormControlWrapper
        describedby={rest['aria-describedby']}
        feedback={feedback}
        feedbackInvalid={feedbackInvalid}
        feedbackValid={feedbackValid}
        id={id || `autocomplete-${uniqueId}`}
        invalid={invalid}
        label={label}
        text={text}
        tooltipFeedback={tooltipFeedback}
        valid={valid}
      >
        <div
          className={classNames(
            'autocomplete',
            {
              [`autocomplete-${size}`]: size,
              disabled,
              'is-invalid': invalid,
              'is-valid': valid,
              show: _visible,
            },
            className
          )}
          onKeyDown={handleGlobalSearch}
          ref={autoCompleteForkedRef}
        >
          <div
            className="autocomplete-input-group"
            onClick={() => handleDropdownShow()}
            ref={togglerRef}
          >
            {showHints && searchValue !== '' && (
              <input
                className="autocomplete-input autocomplete-input-hint"
                id={id || `autocomplete-hint-${uniqueId}`}
                autoComplete="off"
                readOnly
                tabIndex={-1}
                aria-hidden="true"
                value={
                  hint ? `${searchValue}${getOptionLabel(hint).slice(searchValue.length)}` : ''
                }
                ref={inputHintRef}
              />
            )}
            <input
              type="text"
              className="autocomplete-input"
              disabled={disabled}
              id={id || `autocomplete-${uniqueId}`}
              name={name || `autocomplete-${uniqueId}`}
              onBlur={handleInputBlur}
              onChange={handleInputChange}
              onKeyDown={handleInputKeyDown}
              placeholder={placeholder}
              autoComplete="off"
              required={required}
              aria-autocomplete="list"
              aria-expanded={_visible}
              aria-haspopup="listbox"
              {...(portal && { 'aria-owns': `autocomplete-listbox-${uniqueId}` })}
              readOnly={readOnly}
              role="combobox"
              ref={inputRef}
            />
            {(cleaner || indicator) && (
              <div className="autocomplete-buttons">
                {!disabled && !readOnly && cleaner && selected && (
                  <button
                    type="button"
                    className="autocomplete-cleaner"
                    onClick={(event) => {
                      event.preventDefault()
                      event.stopPropagation()
                      handleClear()
                    }}
                  ></button>
                )}
                {indicator && (
                  <button
                    type="button"
                    className="autocomplete-indicator"
                    disabled={
                      !(searchNoResultsLabel || filteredOptions.length > 0) &&
                      isExternalSearch(search)
                    }
                    onClick={(event) => {
                      event.preventDefault()
                      event.stopPropagation()
                      if (_visible) {
                        handleDropdownHide()
                      } else {
                        handleDropdownShow()
                      }
                    }}
                  ></button>
                )}
              </div>
            )}
          </div>
          <CConditionalPortal container={container} portal={portal}>
            <div
              className={classNames('autocomplete-dropdown', {
                show: portal && _visible,
              })}
              id={`autocomplete-listbox-${uniqueId}`}
              role="listbox"
              aria-labelledby={id || `autocomplete-${uniqueId}`}
              ref={dropdownRef}
            >
              <CAutocompleteOptions
                highlightOptionsOnSearch={highlightOptionsOnSearch}
                loading={loading}
                onOptionClick={(option: Option) =>
                  !disabled && !readOnly && handleOptionClick(option)
                }
                options={filteredOptions}
                optionsMaxHeight={optionsMaxHeight}
                optionsTemplate={optionsTemplate}
                optionsGroupsTemplate={optionsGroupsTemplate}
                searchNoResultsLabel={searchNoResultsLabel}
                searchValue={searchValue}
                selected={selected}
                virtualScroller={virtualScroller}
                visibleItems={visibleItems}
              />
            </div>
          </CConditionalPortal>
        </div>
      </CFormControlWrapper>
    )
  }
)

CAutocomplete.propTypes = {
  allowOnlyDefinedOptions: PropTypes.bool,
  className: PropTypes.string,
  clearSearchOnSelect: PropTypes.bool,
  cleaner: PropTypes.bool,
  container: PropTypes.any,
  disabled: PropTypes.bool,
  highlightOptionsOnSearch: PropTypes.bool,
  indicator: PropTypes.bool,
  loading: PropTypes.bool,
  name: PropTypes.string,
  onChange: PropTypes.func,
  onHide: PropTypes.func,
  onInput: PropTypes.func,
  onShow: PropTypes.func,
  options: PropTypes.array.isRequired,
  optionsMaxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  optionsTemplate: PropTypes.func,
  optionsGroupsTemplate: PropTypes.func,
  placeholder: PropTypes.string,
  portal: PropTypes.bool,
  required: PropTypes.bool,
  resetSelectionOnOptionsChange: PropTypes.bool,
  search: PropTypes.oneOfType([
    PropTypes.oneOf<'external' | 'global'>(['external', 'global']),
    PropTypes.shape({
      external: PropTypes.bool.isRequired,
      global: PropTypes.bool.isRequired,
    }),
  ]),
  searchNoResultsLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  showHints: PropTypes.bool,
  size: PropTypes.oneOf(['sm', 'lg']),
  value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  virtualScroller: PropTypes.bool,
  visible: PropTypes.bool,
  visibleItems: PropTypes.number,
  ...CFormControlWrapper.propTypes,
}

CAutocomplete.displayName = 'CAutocomplete'
