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

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

import { CFormControlWrapper, CFormControlWrapperProps } from '../form/CFormControlWrapper'

import { CConditionalPortal } from '../conditional-portal'
import { CMultiSelectNativeSelect } from './CMultiSelectNativeSelect'
import { CMultiSelectOptions } from './CMultiSelectOptions'
import { CMultiSelectSelection } from './CMultiSelectSelection'

import { useDropdownWithPopper } from '../../hooks'
import { getNextActiveElement } from '../../utils'
import {
  createOption,
  filterOptionsList,
  flattenOptionsArray,
  getOptionsList,
  isExternalSearch,
  isGlobalSearch,
  selectOptions,
} from './utils'
import type { Option, OptionsGroup, Search, SelectedOption } from './types'

export interface CMultiSelectProps
  extends Omit<CFormControlWrapperProps, 'floatingClassName' | 'floatingLabel'>,
    Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
  /**
   * Allow users to create options if they are not in the list of options.
   *
   * @since 4.11.0
   */
  allowCreateOptions?: boolean
  /**
   * A string that provides an accessible label for the cleaner button. This label is read by screen readers to describe the action associated with the button.
   *
   * @since 5.8.0
   */
  ariaCleanerLabel?: string
  /**
   * A string of all className you want applied to the base component.
   */
  className?: string
  /**
   * Enables selection cleaner element.
   */
  cleaner?: boolean
  /**
   * Clear current search on selecting an item.
   *
   * @since 4.11.0
   */
  clearSearchOnSelect?: boolean
  /**
   * Appends the dropdown to a specific element. You can pass an HTML element or function that returns a single element.
   *
   * @since 5.8.0
   */
  container?: DocumentFragment | Element | (() => DocumentFragment | Element | null) | null
  /**
   * Toggle the disabled state for the component.
   */
  disabled?: boolean
  /**
   * Set the id attribute for the native select element.
   *
   * **[Deprecated since v5.3.0]** The name attribute for the native select element is generated based on the `id` property:
   * - `<select name="\{id\}-multi-select" />`
   */
  id?: string
  /**
   * When set, the options list will have a loading style: loading spinner and reduced opacity.
   *
   * @since 4.11.0
   */
  loading?: boolean
  /**
   * It specifies that multiple options can be selected at once.
   */
  multiple?: boolean
  /**
   * The name attribute for the select element.
   *
   * @since 5.3.0
   */
  name?: string
  /**
   * Execute a function when a user changes the selected option.
   */
  onChange?: (selected: Option[]) => void
  /**
   * Execute a function when the filter value changed.
   *
   * @since 4.8.0
   */
  onFilterChange?: (value: string) => void
  /**
   * The callback is fired when the Multi Select component requests to be hidden.
   */
  onHide?: () => void
  /**
   * The callback is fired when the Multi Select component requests to be shown.
   */
  onShow?: () => void
  /**
   * List of option elements.
   */
  options: (Option | OptionsGroup)[]
  /**
   * Sets maxHeight of options list.
   */
  optionsMaxHeight?: number | string
  /**
   * Sets option style.
   */
  optionsStyle?: 'checkbox' | 'text'
  /**
   * Custom template for options.
   *
   * @since 4.12.0
   */
  optionsTemplate?: (option: Option) => ReactNode
  /**
   * Custom template for options groups.
   *
   * @since 4.12.0
   */
  optionsGroupsTemplate?: (option: OptionsGroup) => ReactNode
  /**
   * Specifies a short hint that is visible in the search input.
   */
  placeholder?: string
  /**
   * Generates dropdown menu using createPortal.
   *
   * @since 5.8.0
   */
  portal?: boolean
  /**
   * When it is present, it indicates that the user must choose a value before submitting the form.
   */
  required?: boolean
  /**
   * Determines whether the selected options should be cleared when the options list is updated. When set to 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.
   *
   * @since 5.3.0
   */
  resetSelectionOnOptionsChange?: boolean
  /**
   * The `search` prop determines how the search input element is enabled and behaves. It accepts multiple types to provide flexibility in configuring search behavior:
   *
   * - `true` : Enables the default search input element with standard behavior.
   * - `'external'`: Enables an external search mechanism, possibly integrating with external APIs or services.
   * - `'global'`: When set, the user can perform searches across the entire component, regardless of where their focus is within the component.
   * - `{ external?: boolean; global?: boolean }`: Allows for granular control over the search behavior by specifying individual properties.  It is useful when you also want to use external and global search.
   */
  search?: Search
  /**
   * Sets the label for no results when filtering.
   */
  searchNoResultsLabel?: string | ReactNode
  /**
   * Enables select all button.
   */
  selectAll?: boolean
  /**
   * Sets the select all button label.
   */
  selectAllLabel?: string | ReactNode
  /**
   * Sets the selection style.
   */
  selectionType?: 'counter' | 'tags' | 'text'
  /**
   * Sets the counter selection label.
   */
  selectionTypeCounterText?: string
  /**
   * Size the component small or large.
   */
  size?: 'sm' | 'lg'
  /**
   * Enable virtual scroller for the options list.
   *
   * @since 4.8.0
   */
  virtualScroller?: boolean
  /**
   * Toggle the visibility of multi select dropdown.
   */
  visible?: boolean
  /**
   * Amount of visible items when virtualScroller is set to `true`.
   *
   * @since 4.8.0
   */
  visibleItems?: number
}

export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
  (
    {
      allowCreateOptions,
      ariaCleanerLabel = 'Clear all selections',
      className,
      cleaner = true,
      clearSearchOnSelect,
      container,
      disabled,
      feedback,
      feedbackInvalid,
      feedbackValid,
      id,
      invalid,
      label,
      loading,
      multiple = true,
      name,
      onChange,
      onFilterChange,
      onHide,
      onShow,
      options,
      optionsMaxHeight = 'auto',
      optionsStyle = 'checkbox',
      optionsTemplate,
      optionsGroupsTemplate,
      placeholder = 'Select...',
      portal = false,
      required,
      resetSelectionOnOptionsChange = false,
      search = true,
      searchNoResultsLabel = 'No results found',
      selectAll = true,
      selectAllLabel = 'Select all options',
      selectionType = 'tags',
      selectionTypeCounterText = 'item(s) selected',
      size,
      text,
      tooltipFeedback,
      valid,
      virtualScroller,
      visible = false,
      visibleItems = 10,
      ...rest
    },
    ref,
  ) => {
    const {
      dropdownMenuElement,
      dropdownRefElement,
      isOpen,
      closeDropdown,
      openDropdown,
      toggleDropdown,
      updatePopper,
    } = useDropdownWithPopper()
    const nativeSelectRef = useRef<HTMLSelectElement>(null)
    const searchRef = useRef<HTMLInputElement>(null)
    const isInitialMount = useRef(true)

    const [searchValue, setSearchValue] = useState('')
    const [selected, setSelected] = useState<SelectedOption[]>([])
    const [userOptions, setUserOptions] = useState<Option[]>([])

    const filteredOptions = useMemo(
      () =>
        flattenOptionsArray(
          isExternalSearch(search)
            ? [...options, ...filterOptionsList(searchValue, userOptions)]
            : filterOptionsList(searchValue, [...options, ...userOptions]),
          true,
        ),
      [options, searchValue, userOptions],
    )

    const flattenedOptions = useMemo(() => flattenOptionsArray(options), [options])

    const userOption = useMemo(() => {
      if (
        allowCreateOptions &&
        filteredOptions.some(
          (option) => option.label && option.label.toLowerCase() === searchValue.toLowerCase(),
        )
      ) {
        return false
      }

      return searchRef.current && createOption(String(searchValue), flattenedOptions)
    }, [filteredOptions, searchValue])

    useEffect(() => {
      if (resetSelectionOnOptionsChange) {
        return setSelected([])
      }

      const _selected = flattenedOptions.filter((option: Option) => option.selected === true)
      const deselected = flattenedOptions.filter(
        (option: Option) => option.selected === false,
      ) as Option[]

      if (_selected.length > 0) {
        setSelected(selectOptions(multiple, _selected, selected, deselected))
      }
    }, [flattenedOptions])

    useEffect(() => {
      !isInitialMount.current && onFilterChange && onFilterChange(searchValue)
    }, [searchValue])

    useEffect(() => {
      if (!isInitialMount.current && nativeSelectRef.current) {
        nativeSelectRef.current.dispatchEvent(new Event('change', { bubbles: true }))
      }

      updatePopper()
    }, [JSON.stringify(selected)])

    useEffect(() => {
      visible ? openDropdown() : closeDropdown()
    }, [visible])

    useEffect(() => {
      if (isOpen) {
        if (onShow) onShow()

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

        searchRef.current?.focus()
      }

      return () => {
        if (onHide) onHide()

        setSearchValue('')
        if (searchRef.current) {
          searchRef.current.value = ''
        }
      }
    }, [isOpen])

    useEffect(() => {
      isInitialMount.current = false
    }, [])

    const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
      setSearchValue(event.target.value)
    }

    const handleSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
      if (!isOpen) {
        openDropdown()
      }

      if (
        event.key === 'ArrowDown' &&
        dropdownMenuElement.current &&
        searchRef.current &&
        searchRef.current.value.length === searchRef.current.selectionStart
      ) {
        event.preventDefault()

        const items = getOptionsList(dropdownMenuElement.current)
        const target = event.target as HTMLDivElement

        getNextActiveElement(
          items,
          target,
          event.key === 'ArrowDown',
          !items.includes(target),
        ).focus()
        return
      }

      if (event.key === 'Enter' && searchValue && allowCreateOptions) {
        event.preventDefault()

        if (userOption) {
          setSelected([...selected, ...userOption])
          setUserOptions([...userOptions, ...userOption])
        }

        if (!userOption) {
          setSelected([
            ...selected,
            filteredOptions.find(
              (option) => String(option.label).toLowerCase() === searchValue.toLowerCase(),
            ) as Option,
          ])
        }

        setSearchValue('')
        if (searchRef.current) {
          searchRef.current.value = ''
        }

        return
      }

      if (searchValue.length > 0) {
        return
      }

      if (event.key === 'Backspace' || event.key === 'Delete') {
        const last = selected.filter((option: Option) => !option.disabled).pop()

        if (last) {
          setSelected(selected.filter((option: Option) => option.value !== last.value))
        }
      }
    }

    const handleTogglerKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
      if (!isOpen && (event.key === 'Enter' || event.key === 'ArrowDown')) {
        event.preventDefault()
        openDropdown()
        return
      }

      if (isOpen && dropdownMenuElement.current && event.key === 'ArrowDown') {
        event.preventDefault()
        const items = getOptionsList(dropdownMenuElement.current)
        const target = event.target as HTMLDivElement

        getNextActiveElement(
          items,
          target,
          event.key === 'ArrowDown',
          !items.includes(target),
        ).focus()
      }
    }

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

    const handleOnOptionClick = (option: Option) => {
      if (!multiple) {
        setSelected([option] as SelectedOption[])
        closeDropdown()
        setSearchValue('')
        if (searchRef.current) {
          searchRef.current.value = ''
        }

        return
      }

      if (option.custom && !userOptions.some((_option) => _option.value === option.value)) {
        setUserOptions([...userOptions, option])
      }

      if (clearSearchOnSelect || option.custom) {
        setSearchValue('')
        if (searchRef.current) {
          searchRef.current.value = ''
          searchRef.current.focus()
        }
      }

      if (selected.some((_option) => _option.value === option.value)) {
        setSelected(selected.filter((_option) => _option.value !== option.value))
      } else {
        setSelected([...selected, option] as SelectedOption[])
      }
    }

    const handleSelectAll = () => {
      setSelected(
        selectOptions(
          multiple,
          [...flattenedOptions.filter((option: Option) => !option.disabled), ...userOptions],
          selected,
        ),
      )
    }

    const handleDeselectAll = () => {
      setSelected(selected.filter((option) => option.disabled))
    }

    return (
      <CFormControlWrapper
        describedby={rest['aria-describedby']}
        feedback={feedback}
        feedbackInvalid={feedbackInvalid}
        feedbackValid={feedbackValid}
        id={id}
        invalid={invalid}
        label={label}
        text={text}
        tooltipFeedback={tooltipFeedback}
        valid={valid}
      >
        <CMultiSelectNativeSelect
          id={id}
          multiple={multiple}
          name={name}
          options={selected}
          required={required}
          value={
            multiple
              ? selected.map((option: SelectedOption) => option.value.toString())
              : selected.map((option: SelectedOption) => option.value)[0]
          }
          onChange={() => onChange && onChange(selected)}
          ref={nativeSelectRef}
        />
        <div
          className={classNames(
            'form-multi-select',
            {
              [`form-multi-select-${size}`]: size,
              disabled,
              'is-invalid': invalid,
              'is-valid': valid,
              show: isOpen,
            },
            className,
          )}
          onKeyDown={handleGlobalSearch}
          aria-expanded={isOpen}
          ref={ref}
        >
          <div
            className="form-multi-select-input-group"
            {...(!search && !disabled && { tabIndex: 0 })}
            onClick={() => !disabled && openDropdown()}
            onKeyDown={handleTogglerKeyDown}
            ref={dropdownRefElement}
          >
            <CMultiSelectSelection
              disabled={disabled}
              multiple={multiple}
              onRemove={(option) => !disabled && handleOnOptionClick(option)}
              placeholder={placeholder}
              search={search}
              selected={selected}
              selectionType={selectionType}
              selectionTypeCounterText={selectionTypeCounterText}
            >
              {search && (
                <input
                  type="text"
                  className="form-multi-select-search"
                  disabled={disabled}
                  autoComplete="off"
                  onChange={handleSearchChange}
                  onKeyDown={handleSearchKeyDown}
                  {...(selected.length === 0 && { placeholder: placeholder })}
                  {...(selected.length > 0 &&
                    selectionType === 'counter' && {
                      placeholder: `${selected.length} ${selectionTypeCounterText}`,
                    })}
                  {...(selected.length > 0 &&
                    !multiple && { placeholder: selected.map((option) => option.label)[0] })}
                  {...(multiple &&
                    selected.length > 0 &&
                    selectionType !== 'counter' && { size: searchValue.length + 2 })}
                  ref={searchRef}
                ></input>
              )}
              {!search && selected.length === 0 && (
                <span className="form-multi-select-placeholder">{placeholder}</span>
              )}
            </CMultiSelectSelection>
            <div className="form-multi-select-buttons">
              {!disabled && cleaner && selected.length > 0 && (
                <button
                  type="button"
                  className="form-multi-select-cleaner"
                  onClick={() => handleDeselectAll()}
                  aria-label={ariaCleanerLabel}
                ></button>
              )}
              <button
                type="button"
                className="form-multi-select-indicator"
                onClick={(event) => {
                  event.preventDefault()
                  event.stopPropagation()

                  if (!disabled) {
                    toggleDropdown()
                  }
                }}
                {...(disabled && { tabIndex: -1 })}
              ></button>
            </div>
          </div>
          <CConditionalPortal container={container} portal={portal}>
            <div
              className={classNames('form-multi-select-dropdown', {
                show: portal && isOpen,
              })}
              onKeyDown={handleGlobalSearch}
              role="menu"
              ref={dropdownMenuElement}
            >
              {multiple && selectAll && (
                <button
                  type="button"
                  className="form-multi-select-all"
                  onClick={() => handleSelectAll()}
                >
                  {selectAllLabel}
                </button>
              )}
              <CMultiSelectOptions
                loading={loading}
                onOptionOnClick={(option) => !disabled && handleOnOptionClick(option)}
                options={
                  filteredOptions.length === 0 && allowCreateOptions
                    ? userOption || []
                    : filteredOptions
                }
                optionsMaxHeight={optionsMaxHeight}
                optionsStyle={optionsStyle}
                optionsTemplate={optionsTemplate}
                optionsGroupsTemplate={optionsGroupsTemplate}
                searchNoResultsLabel={searchNoResultsLabel}
                selected={selected}
                virtualScroller={virtualScroller}
                visibleItems={visibleItems}
              />
            </div>
          </CConditionalPortal>
        </div>
      </CFormControlWrapper>
    )
  },
)

CMultiSelect.propTypes = {
  allowCreateOptions: PropTypes.bool,
  ariaCleanerLabel: PropTypes.string,
  className: PropTypes.string,
  cleaner: PropTypes.bool,
  clearSearchOnSelect: PropTypes.bool,
  container: PropTypes.any,
  disabled: PropTypes.bool,
  loading: PropTypes.bool,
  multiple: PropTypes.bool,
  name: PropTypes.string,
  onChange: PropTypes.func,
  onFilterChange: PropTypes.func,
  onHide: PropTypes.func,
  onShow: PropTypes.func,
  options: PropTypes.array.isRequired,
  optionsMaxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  optionsStyle: PropTypes.oneOf(['checkbox', 'text']),
  optionsTemplate: PropTypes.func,
  optionsGroupsTemplate: PropTypes.func,
  placeholder: PropTypes.string,
  portal: PropTypes.bool,
  required: PropTypes.bool,
  resetSelectionOnOptionsChange: PropTypes.bool,
  search: PropTypes.oneOfType([
    PropTypes.bool,
    PropTypes.oneOf<'external' | 'global'>(['external', 'global']),
    PropTypes.shape({
      external: PropTypes.bool.isRequired,
      global: PropTypes.bool.isRequired,
    }),
  ]),
  searchNoResultsLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  selectAll: PropTypes.bool,
  selectAllLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  selectionType: PropTypes.oneOf(['counter', 'tags', 'text']),
  selectionTypeCounterText: PropTypes.string,
  size: PropTypes.oneOf(['sm', 'lg']),
  virtualScroller: PropTypes.bool,
  visible: PropTypes.bool,
  visibleItems: PropTypes.number,
  ...CFormControlWrapper.propTypes,
}

CMultiSelect.displayName = 'CMultiSelect'
