import {
  computed,
  defineComponent,
  h,
  nextTick,
  onMounted,
  onUnmounted,
  PropType,
  ref,
  useId,
  watch,
} from 'vue'

import { createPopper, Instance as PopperInstance, Placement } from '@popperjs/core'

import { CConditionalTeleport } from '../conditional-teleport'
import { CFormControlWrapper } from '../form/CFormControlWrapper'
import { CAutocompleteOptions } from './CAutocompleteOptions'

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

const CAutocomplete = defineComponent({
  name: 'CAutocomplete',
  props: {
    /**
     * 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: {
      type: Boolean,
      default: false,
    },
    /**
     * Sets the accessible label (`aria-label`) for the cleaner button, describing its action for screen readers.
     *
     * @since 5.20.0
     */
    ariaCleanerLabel: {
      type: String,
      default: 'Clear selection',
    },
    /**
     * Sets the accessible label (`aria-label`) for the indicator button that toggles the options menu.
     *
     * @since 5.20.0
     */
    ariaIndicatorLabel: {
      type: String,
      default: 'Toggle visibility of options menu',
    },
    /**
     * 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: {
      type: Boolean,
      default: false,
    },
    /**
     * 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 `search-value` prop. In such cases, clearing must be handled externally.
     *
     * @default true
     */
    clearSearchOnSelect: {
      type: Boolean,
      default: true,
    },
    /**
     * Specifies the container element for positioning the dropdown.
     * - `HTMLElement`: Direct reference to a DOM element
     * - `Function`: Function that returns a DOM element
     * - `string`: CSS selector string to identify the container element
     * Used in conjunction with the teleport prop to control dropdown positioning.
     *
     * @default 'body'
     */
    container: {
      type: [Object, String] as PropType<HTMLElement | (() => HTMLElement) | string>,
      default: 'body',
    },
    /**
     * Toggle the disabled state for the component.
     * When `true`, the Vue.js autocomplete is non-interactive and appears visually disabled.
     * Users cannot type, select options, or trigger the dropdown.
     */
    disabled: {
      type: Boolean,
      default: false,
    },
    /**
     * Provide valuable, actionable feedback to your users with HTML5 form validation feedback.
     */
    feedback: String,
    /**
     * Provide valuable, actionable invalid feedback when using standard HTML form validation which applied `invalid` prop.
     */
    feedbackInvalid: String,
    /**
     * Provide valuable, actionable valid feedback when using standard HTML form validation which applied `valid` prop.
     */
    feedbackValid: String,
    /**
     * 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: {
      type: Boolean,
      default: false,
    },
    /**
     * 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,
    /**
     * Set component validation state to invalid.
     */
    invalid: Boolean,
    /**
     * Add a caption for a component.
     */
    label: String,
    /**
     * 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,
    /**
     * 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 Vue.js autocomplete needs options to function.
     */
    options: {
      type: Array as PropType<(Option | OptionsGroup | string)[]>,
      required: true,
    },
    /**
     * 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: {
      type: [Number, String] as PropType<number | string>,
      default: 'auto',
    },
    /**
     * 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,
    /**
     * 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: {
      type: Boolean,
      default: false,
    },
    /**
     * 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: {
      type: Boolean,
      default: false,
    },
    /**
     * 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: {
      type: Boolean,
      default: false,
    },
    /**
     * 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: [String, Object] as PropType<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
     *
     * @default false
     */
    searchNoResultsLabel: {
      type: [Boolean, String] as PropType<boolean | string>,
      default: false,
    },
    /**
     * 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: {
      type: Boolean,
      default: false,
    },
    /**
     * Size the component small or large.
     * - `'sm'`: Small size variant
     * - `'lg'`: Large size variant
     * - `undefined`: Default/medium size
     */
    size: String as PropType<'sm' | 'lg'>,
    /**
     * Enable teleportation of the dropdown to a different container.
     * When `true`, the dropdown is rendered in the container specified by the `container` prop
     * instead of being rendered inline. This is useful for avoiding z-index issues and positioning
     * problems when the autocomplete is inside elements with overflow constraints.
     *
     * @default false
     */
    teleport: {
      type: Boolean,
      default: false,
    },
    /**
     * Add helper text to a form control.
     */
    text: String,
    /**
     * Display validation feedback in a styled tooltip.
     */
    tooltipFeedback: Boolean,
    /**
     * Set component validation state to valid.
     */
    valid: Boolean,
    /**
     * The model value for v-model support.
     * Can be a string (matched against option labels) or number (matched against option values).
     * Used for two-way data binding with v-model.
     */
    modelValue: [Number, String] as PropType<number | string>,
    /**
     * Sets the initially selected value for the Vue.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] as PropType<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 `visible-items` prop.
     *
     * @default false
     */
    virtualScroller: {
      type: Boolean,
      default: false,
    },
    /**
     * Toggle the visibility of autocomplete dropdown.
     * Controls whether the dropdown is initially visible.
     * The dropdown visibility can still be toggled through user interaction.
     *
     * @default false
     */
    visible: {
      type: Boolean,
      default: false,
    },
    /**
     * 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: {
      type: Number,
      default: 10,
    },
  },
  emits: [
    /**
     * Execute a function when a user changes the selected option.
     * Called with the selected option object or `null` when cleared.
     *
     * @property {function(option: Option | null): void}
     */
    'change',
    /**
     * Execute a function when the filter/search value changes.
     * Called whenever the user types in the search input.
     *
     * @property {function(value: string): void}
     */
    'input',
    /**
     * 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.
     *
     * @property {function(): void}
     */
    'hide',
    /**
     * The callback is fired when the dropdown requests to be shown.
     * Called when the dropdown opens due to user interaction, focus, or programmatic changes.
     *
     * @property {function(): void}
     */
    'show',
    /**
     * Update the model value for v-model support.
     * Emitted when the selected value changes to support two-way data binding.
     *
     * @property {function(value: number | string | null): void}
     */
    'update:modelValue',
  ],
  setup(props, { emit, attrs, slots }) {
    const autoCompleteRef = ref<HTMLDivElement>()
    const dropdownRef = ref<HTMLDivElement>()
    const togglerRef = ref<HTMLDivElement>()
    const inputRef = ref<HTMLInputElement>()
    const inputHintRef = ref<HTMLInputElement>()

    const hint = ref<Option | undefined>()
    const searchValue = ref('')
    const selected = ref<Option | null>(null)
    const visible = ref(props.visible)

    const uniqueId = props.id ?? useId()
    let popperInstance: PopperInstance | null = null

    const filteredOptions = computed(() =>
      isExternalSearch(props.search)
        ? props.options
        : filterOptions(props.options, searchValue.value)
    )

    const popperConfig = computed(() => ({
      placement: 'bottom-start' as Placement,
      modifiers: [
        {
          name: 'preventOverflow',
          options: {
            boundary: 'clippingParents',
          },
        },
        {
          name: 'offset',
          options: {
            offset: [0, 2],
          },
        },
      ],
    }))

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

      searchValue.value = ''
      selected.value = null
      emit('change', null)
      emit('update:modelValue', null)
    }

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

    const handleInputBlur = (event: Event) => {
      const inputValue = (event.target as HTMLInputElement).value

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

      const inputValueLower = inputValue.toLowerCase()
      const exactMatches = flattenOptionsArray(props.options as Option[] | OptionsGroup[]).filter(
        (option) => getOptionLabel(option).toLowerCase() === inputValueLower
      )

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

      if (props.allowOnlyDefinedOptions) {
        handleClear()
        return
      }

      // Allow custom value: clear hint, emit change
      hint.value = undefined
      emit('change', inputValue)
    }

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

      if (selected.value !== null) {
        emit('change', null)
        selected.value = null
      }
    }

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

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

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

          if (firstOption) {
            firstOption.focus()
          }
        })

        return
      }

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

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

        handleDropdownHide()
        return
      }

      if (event.key === 'Backspace' || event.key === 'Delete') {
        if (selected.value !== null) {
          selected.value = null
          emit('change', null)
        }

        return
      }
    }

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

      if (autoCompleteRef.value && !autoCompleteRef.value.contains(event.target as HTMLElement)) {
        handleDropdownHide()
      }
    }

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

      handleDropdownHide()
    }

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

    const handleSearch = (search: string) => {
      emit('input', search)
      searchValue.value = search
    }

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

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

      selected.value = option ?? null
      emit('change', option ?? null)

      // Emit v-model update
      if (typeof option === 'string') {
        emit('update:modelValue', option)
      } else if (option && typeof option === 'object') {
        emit('update:modelValue', option.value)
      } else {
        emit('update:modelValue', null)
      }

      if (props.clearSearchOnSelect) {
        handleSearch('')
      } else {
        hint.value = undefined
        return
      }
    }

    const handleDropdownShow = () => {
      if (props.disabled || props.readOnly || visible.value) {
        return
      }

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

      if (props.teleport && dropdownRef.value && togglerRef.value) {
        dropdownRef.value.style.minWidth = `${(togglerRef.value as HTMLElement).offsetWidth}px`
      }

      visible.value = true
      emit('show')

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

      if (togglerRef.value && dropdownRef.value) {
        popperInstance = createPopper(togglerRef.value, dropdownRef.value, popperConfig.value)
      }

      nextTick(() => {
        inputRef.value?.focus()
      })
    }

    const handleDropdownHide = () => {
      visible.value = false
      emit('hide')

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

      if (popperInstance) {
        popperInstance.destroy()
        popperInstance = null
      }

      nextTick(() => {
        inputRef.value?.focus()
      })
    }

    watch(
      () => props.options,
      () => {
        if (props.resetSelectionOnOptionsChange) {
          handleClear()
        }
      }
    )

    const initializeValue = () => {
      if (!inputRef.value) {
        return
      }

      // Handle modelValue prop for v-model support
      const currentValue = props.modelValue ?? props.value

      if (typeof currentValue === 'string' || typeof currentValue === 'number') {
        const foundOption = getFirstOptionByValue(currentValue, props.options)

        if (foundOption) {
          handleSelect(foundOption)
          return
        }

        if (typeof currentValue === 'string') {
          const foundOptionByLabel = getFirstOptionByLabel(currentValue, props.options)

          if (foundOptionByLabel) {
            handleSelect(foundOptionByLabel)
            return
          }

          if (!props.allowOnlyDefinedOptions) {
            handleSelect(currentValue)
          }
        }

        return
      }

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

      if (_selected) {
        handleSelect(_selected)
      }
    }

    onMounted(() => {
      initializeValue()
    })

    watch(
      () => [props.options, props.value, props.modelValue],
      () => {
        initializeValue()
      }
    )

    watch(
      () => [filteredOptions.value, searchValue.value, props.showHints],
      () => {
        if (!props.showHints) {
          return
        }

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

        hint.value = findOption
      }
    )

    watch(
      () => filteredOptions.value,
      () => {
        if (
          !props.searchNoResultsLabel &&
          searchValue.value !== '' &&
          filteredOptions.value.length === 0
        ) {
          if (visible.value) {
            handleDropdownHide()
          }

          return
        }

        if (searchValue.value.length > 0) {
          handleDropdownShow()
        }
      }
    )

    watch(
      () => props.visible,
      (newVisible) => {
        if (newVisible) {
          handleDropdownShow()
        } else {
          handleDropdownHide()
        }
      }
    )

    onUnmounted(() => {
      window.removeEventListener('mouseup', handleMouseUp)
      window.removeEventListener('keyup', handleKeyUp)
      if (popperInstance) {
        popperInstance.destroy()
      }
    })

    return () =>
      h(
        CFormControlWrapper,
        {
          ...(typeof attrs['aria-describedby'] === 'string' && {
            describedby: attrs['aria-describedby'],
          }),
          feedback: props.feedback,
          feedbackInvalid: props.feedbackInvalid,
          feedbackValid: props.feedbackValid,
          id: uniqueId,
          invalid: props.invalid,
          label: props.label,
          text: props.text,
          tooltipFeedback: props.tooltipFeedback,
          valid: props.valid,
        },
        () =>
          h(
            'div',
            {
              class: [
                'autocomplete',
                {
                  [`autocomplete-${props.size}`]: props.size,
                  disabled: props.disabled,
                  'is-invalid': props.invalid,
                  'is-valid': props.valid,
                  show: visible.value,
                },
              ],
              onKeydown: handleGlobalSearch,
              ref: autoCompleteRef,
            },
            [
              h(
                'div',
                {
                  class: 'autocomplete-input-group',
                  onClick: () => handleDropdownShow(),
                  ref: togglerRef,
                },
                [
                  props.showHints &&
                    searchValue.value !== '' &&
                    h('input', {
                      class: 'autocomplete-input autocomplete-input-hint',
                      id: `hint-${uniqueId}`,
                      autocomplete: 'off',
                      readonly: true,
                      tabindex: -1,
                      'aria-hidden': 'true',
                      value: hint.value
                        ? `${searchValue.value}${getOptionLabel(hint.value).slice(searchValue.value.length)}`
                        : '',
                      ref: inputHintRef,
                    }),
                  h('input', {
                    type: 'text',
                    class: 'autocomplete-input',
                    disabled: props.disabled,
                    id: uniqueId,
                    name: props.name || uniqueId,
                    onBlur: handleInputBlur,
                    onInput: handleInputChange,
                    onKeydown: handleInputKeyDown,
                    placeholder: props.placeholder,
                    autocomplete: 'off',
                    required: props.required,
                    'aria-autocomplete': 'list',
                    'aria-expanded': visible.value,
                    'aria-haspopup': 'listbox',
                    'aria-controls': `autocomplete-listbox-${uniqueId}`,
                    ...(props.teleport && { 'aria-owns': `autocomplete-listbox-${uniqueId}` }),
                    readonly: props.readOnly,
                    role: 'combobox',
                    ref: inputRef,
                  }),
                  (props.cleaner || props.indicator) &&
                    h('div', { class: 'autocomplete-buttons' }, [
                      !props.disabled &&
                        !props.readOnly &&
                        props.cleaner &&
                        selected.value &&
                        h('button', {
                          type: 'button',
                          class: 'autocomplete-cleaner',
                          'aria-label': props.ariaCleanerLabel,
                          onClick: (event: Event) => {
                            event.preventDefault()
                            event.stopPropagation()
                            handleClear()
                          },
                        }),
                      props.indicator &&
                        h('button', {
                          type: 'button',
                          class: 'autocomplete-indicator',
                          'aria-label': props.ariaIndicatorLabel,
                          disabled:
                            !(props.searchNoResultsLabel || filteredOptions.value.length > 0) &&
                            !isExternalSearch(props.search),
                          onClick: (event: Event) => {
                            event.preventDefault()
                            event.stopPropagation()
                            if (visible.value) {
                              handleDropdownHide()
                            } else {
                              handleDropdownShow()
                            }
                          },
                        }),
                    ]),
                ]
              ),
              h(
                CConditionalTeleport,
                {
                  container: props.container,
                  teleport: props.teleport,
                },
                {
                  default: () =>
                    h(
                      'div',
                      {
                        class: [
                          'autocomplete-dropdown',
                          {
                            show: props.teleport && visible.value,
                          },
                        ],
                        id: `autocomplete-listbox-${uniqueId}`,
                        role: 'listbox',
                        'aria-labelledby': uniqueId,
                        ref: dropdownRef,
                      },
                      [
                        h(CAutocompleteOptions, {
                          highlightOptionsOnSearch: props.highlightOptionsOnSearch,
                          loading: props.loading,
                          onOptionClick: (option: Option) =>
                            !props.disabled && !props.readOnly && handleOptionClick(option),
                          options: filteredOptions.value,
                          optionsMaxHeight: props.optionsMaxHeight,
                          scopedSlots: slots,
                          searchNoResultsLabel: props.searchNoResultsLabel,
                          searchValue: searchValue.value,
                          selected: selected.value,
                          virtualScroller: props.virtualScroller,
                          visible: visible.value,
                          visibleItems: props.visibleItems,
                        }),
                      ]
                    ),
                }
              ),
            ]
          )
      )
  },
})

export { CAutocomplete }
