import { multiselect, type MultiSelectSlots, type SlotsToClasses } from '@trail-ui/theme';
import React, {
  useState,
  useRef,
  useEffect,
  forwardRef,
  type ForwardedRef,
  type KeyboardEvent,
  type ChangeEvent,
  type RefObject,
  useMemo,
} from 'react';
import { CheckIcon, ChevronDownIcon, ErrorIcon } from '@trail-ui/icons';
import Overlay, { Positions } from '../overlay/overlay';
import { Input, Label } from '../multiselect/tw-field';
import { Text } from '../multiselect/tw-text';
import { Button } from 'react-aria-components';

// Types for handling both string and object options
export type BaseOption = {
  label: string;
  value: string;
  [key: string]: any;
};

// Constants
const Keys = {
  Backspace: 'Backspace',
  Clear: 'Clear',
  Down: 'ArrowDown',
  End: 'End',
  Enter: 'Enter',
  Escape: 'Escape',
  Home: 'Home',
  Left: 'ArrowLeft',
  PageDown: 'PageDown',
  PageUp: 'PageUp',
  Right: 'ArrowRight',
  Space: ' ',
  Tab: 'Tab',
  Up: 'ArrowUp',
} as const;

// Define specific action types
const MenuActions = {
  Close: 'Close',
  CloseSelect: 'CloseSelect',
  First: 'First',
  Last: 'Last',
  Next: 'Next',
  Open: 'Open',
  Previous: 'Previous',
  Select: 'Select',
  Space: 'Space',
  Type: 'Type',
} as const;

// Types
type MenuActionType = (typeof MenuActions)[keyof typeof MenuActions];
type NavigationAction =
  | typeof MenuActions.First
  | typeof MenuActions.Last
  | typeof MenuActions.Next
  | typeof MenuActions.Previous;

export interface CustomSelectProps {
  /**
   * @property {BaseOption[]} options - The list of options available for selection.
   */
  options: BaseOption[];
  /**
   * @property {string | number} [value] - The controlled value of the component
   */
  value: string | number;
  /**
   * @property {string} [label] - An optional label for the Select component.
   * @default 'Select items'
   */
  label?: string;
  /**
   * @property {string} [id] - An optional id for the Select component.
   */
  id?: string;
  /**
   * @property {string} [labelKey] - The key to use for the label of each option.
   */
  labelKey?: string;
  /**
   * @property {string} [valueKey] - The key to use for the value of each option.
   */
  valueKey?: string;
  /**
   * @param selectedItems  - A callback function that is called when the selected items change.
   * @returns
   */
  onChange: (selectedItems: string | number) => void;
  /**
   * @property {() => void} [onBlur] - A callback function that is called when the Select component loses focus.
   */
  onBlur?: () => void;
  /**
   * @property {BaseOption} [defaultValue] - An optional array of default selected items.
   */
  defaultValue?: BaseOption;
  /**
   * @property {SlotsToClasses<MultiSelectSlots>} [classNames] - An optional object of classes to style the Select component.
   */
  classNames?: SlotsToClasses<MultiSelectSlots>;
  /**
   * @property {string} [errorMessage] - An optional error message to display below the Select component.
   */
  errorMessage?: string;
  /**
   * @property {string} [description] - An optional description to display below the Select component.
   */
  description?: string;
  /**
   * @property {string} [placeholder] - An optional placeholder to display in the input field.
   */
  placeholder?: string;
  /**
   * @property {React.ReactNode} [errorIcon] - An optional icon to display next to the error message.
   */
  errorIcon?: React.ReactNode;
  /**
   * @property {string} [errorId] - The id of the error message for accessibility.
   */
  errorId?: string;
  /**
   * @property {boolean} [isRequired] - Whether the Select component is required.
   */
  isRequired?: boolean;
  /**
   * @property {boolean} [isDisabled] - Whether the Select component is disabled.
   */
  isDisabled?: boolean;
  /**
   * @property {boolean} [isInvalid] - Whether the Select component is invalid.
   */
  isInvalid?: boolean;
  /**
   * @property {string} [name] - The name of the form field
   */
  name?: string;
  /**
   * @property {string} [form] - The id of the form this field belongs to
   */
  form?: string;
  /**
   * @property {number} [maxOptionsBeforeConversionToComboBox] - the component will behave as select component if the options.length is less than this value and as a combox once the value exceeds this number
   * note - the isCombox property overrides the behaviour of this feild
   * @default 6
   */
  maxOptionsBeforeConversionToComboBox?: number;
  /**
   * @property {boolean} [isCombobox] - if set to true the components behaves as a combobox, if set to false it will behave as a listbox, if no value is passed it will act as a select if the options.length is less than maxOptionsBeforeConversionToComboBox and as a comboxbox after that
   * @default undefined
   */
  isCombobox?: boolean;
}

function getActionFromKey(key: string, menuOpen: boolean): MenuActionType | undefined {
  if (!menuOpen && key === Keys.Down) return MenuActions.Open;
  if (!menuOpen && key === Keys.Enter) return MenuActions.Open;
  if (key === Keys.Down) return MenuActions.Next;
  if (key === Keys.Up) return MenuActions.Previous;
  if (key === Keys.Home) return MenuActions.First;
  if (key === Keys.End) return MenuActions.Last;
  if (key === Keys.Escape) return MenuActions.Close;
  if (key === Keys.Enter) return MenuActions.CloseSelect;
  if (key === Keys.Backspace || key === Keys.Clear || key.length === 1) return MenuActions.Type;
  return undefined;
}

const CustomSelect = forwardRef(
  (
    {
      options,
      label,
      labelKey = 'value',
      valueKey = 'id',
      id = 'select',
      onChange,
      onBlur,
      defaultValue,
      classNames,
      errorMessage,
      description,
      errorIcon = (
        <ErrorIcon
          className="h-4 w-4 text-red-800 dark:text-red-600"
          role="img"
          aria-label="Error"
          aria-hidden={false}
        />
      ),
      errorId,
      isRequired,
      isDisabled,
      isInvalid,
      placeholder = 'Select items',
      value: selectedValue,
      form,
      name,
      maxOptionsBeforeConversionToComboBox = 6,
      isCombobox,
      ...otherprops
    }: CustomSelectProps,
    ref: ForwardedRef<HTMLDivElement>,
  ) => {
    // State
    const [isOpen, setIsOpen] = useState(false);
    const [searchValue, setSearchValue] = useState('');
    const [activeIndex, setActiveIndex] = useState(-1);
    const [value, setValue] = useState<BaseOption>({ label: '', value: '' });

    // Refs
    const listboxRef = useRef<HTMLUListElement>(null);
    const inputRef = useRef<HTMLInputElement>(null);
    const localRef = useRef<HTMLDivElement>(null);
    const buttonRef = useRef<HTMLButtonElement>(null);
    const optionRefs = useRef<Array<RefObject<HTMLLIElement>>>([]);
    const hiddenInputRef = useRef<HTMLInputElement>(null);
    const refElementForOverlay = useRef<HTMLDivElement>(null);

    // Styles
    const slots = useMemo(() => multiselect(), []);
    const isComponentCombobox = useMemo(() => {
      if (isCombobox !== undefined) {
        return isCombobox;
      } else {
        return options.length > maxOptionsBeforeConversionToComboBox;
      }
    }, [isCombobox, options, maxOptionsBeforeConversionToComboBox]);

    // Helpers
    const getOptionLabel = (option: BaseOption): string => {
      if (typeof option === 'string') return option;
      return (option as BaseOption)[labelKey] || (option as BaseOption).label || '';
    };

    const getOptionValue = (option: BaseOption): string => {
      if (typeof option === 'string') return option;
      return (option as BaseOption)[valueKey] || (option as BaseOption).value || '';
    };

    const resetIndex = () => setActiveIndex(-1);

    // Filter options based on search input
    const filteredOptions = useMemo(
      () =>
        options.filter((option) => {
          return getOptionLabel(option)
            .toString()
            .toLowerCase()
            .includes(searchValue.toLowerCase());
        }),
      [searchValue, options],
    );

    const toggleDropdown = () => setIsOpen((prevState) => !prevState);

    const toggleItem = (option: BaseOption) => {
      if (isDisabled) return;

      onChange(option.value);
      setSearchValue('');
      toggleDropdown();

      if (inputRef.current) {
        inputRef.current.focus();
        inputRef.current.value = getOptionLabel(option);
      }

      if (buttonRef.current) {
        buttonRef.current.focus();
      }

      // Update hidden input
      if (hiddenInputRef.current) {
        hiddenInputRef.current.value = getOptionLabel(option);
      }
    };

    // Event handlers
    const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
      if (isDisabled) return;
      setSearchValue(e.target.value);
      if (!isOpen) toggleDropdown();
      if (activeIndex === -1) {
        setActiveIndex(0);
      }
    };

    function getUpdatedIndex(current: number, action: NavigationAction): number {
      switch (action) {
        case MenuActions.First:
          return 0;
        case MenuActions.Last:
          return filteredOptions.length;
        case MenuActions.Previous:
          return (activeIndex - 1 + filteredOptions.length) % filteredOptions.length;
        case MenuActions.Next:
          return (activeIndex + 1) % filteredOptions.length;
        default:
          return current;
      }
    }

    const handleKeyDown = (
      e: KeyboardEvent<HTMLLIElement | HTMLInputElement | HTMLOrSVGElement>,
    ) => {
      const action = getActionFromKey(e.key, isOpen);

      if (isDisabled) {
        e.preventDefault();
        e.stopPropagation();
        return;
      }

      if (e.key === Keys.Tab) {
        if (isOpen) {
          toggleDropdown();
          resetIndex();
          setSearchValue('');
        }
        return;
      }

      if (!action) return;

      if (MenuActions.Type !== action) e.preventDefault();

      switch (action) {
        case MenuActions.Next:
        case MenuActions.Last:
        case MenuActions.First:
        case MenuActions.Previous:
          const nextIndex = getUpdatedIndex(activeIndex, action);
          setActiveIndex(nextIndex);
          optionRefs.current[nextIndex].current?.focus();
          break;
        case MenuActions.Type:
          if (inputRef.current) inputRef.current.focus();
          break;
        case MenuActions.CloseSelect:
          if (activeIndex >= 0) {
            optionRefs.current[activeIndex].current?.click();
          }
          break;
        case MenuActions.Close:
          handleOnClick({ detail: 1 } as React.MouseEvent);
          setSearchValue('');
          resetIndex();
          break;
        case MenuActions.Open:
          handleOnClick({ detail: 1 } as React.MouseEvent);
          break;
      }
    };

    const handleOnClick = (e?: React.MouseEvent) => {
      //Whenever enter button is clicked on a button element, the onKeyPress and onClick event both are fired
      //This causes a problem that toggleDropdown() is called twice, so the dropdown is closed as soon as it opens
      //The on click event has a property e.detail, if e.detail === 1, it means that the event was triggered due to a onClick and not enter button click
      //Therefore a check is added here, when user clicks enter, in onClck evnet details is 0, so this toggleDropdown is not called, but in onKeyPressEvent when handleOnClick is called inside handleKeyDown we are explicitely passing e.detail as 1, to open the dropdown.
      //In this way on enter click the toggleDropdown is called only once
      if (!isDisabled && e?.detail === 1) {
        toggleDropdown();
        if (isOpen) {
          resetIndex();
        }
      }
    };

    // Click outside handler
    useEffect(() => {
      const handleClickOutside = (event: MouseEvent) => {
        if (localRef.current && !localRef.current.contains(event.target as Node)) {
          setIsOpen(false);
        }
      };

      document.addEventListener('mousedown', handleClickOutside);
      return () => document.removeEventListener('mousedown', handleClickOutside);
    }, []);

    // Sync the forwarded ref with our local ref
    useEffect(() => {
      if (!ref) return;

      if (typeof ref === 'function') {
        ref(localRef.current);
      } else {
        ref.current = localRef.current;
      }
    }, [ref]);

    useEffect(() => {
      const index = options.findIndex((item) => item.value === selectedValue);
      setValue(index !== -1 ? options[index] : { label: '', value: '' });
    }, [selectedValue, options]);

    useEffect(() => {
      if (isOpen) {
        buttonRef.current?.setAttribute('role', 'combobox');
        optionRefs.current = filteredOptions.map(
          (_, i) => optionRefs.current[i] || React.createRef<HTMLLIElement>(),
        );
        if (filteredOptions.length > 1 && !isCombobox) {
          setActiveIndex(0);
        }
      }
    }, [isOpen]);

    // // Reset active index when filtered options change
    useEffect(() => {
      if (filteredOptions.length > 0) {
        resetIndex();
      }
    }, [filteredOptions]);

    useEffect(() => {
      if (isOpen && activeIndex >= 0) {
        requestAnimationFrame(() => {
          // if (!isCombobox) optionRefs.current[activeIndex].current?.focus();
          optionRefs.current[activeIndex]?.current?.scrollIntoView({ block: 'nearest' });
        });
      }

      //Add and remove araia-activedesendant on button
      if (isOpen)
        buttonRef.current?.setAttribute('aria-activedescendant', `${id}-option-${activeIndex}`);
      else buttonRef.current?.removeAttribute('aria-activedescendant');
    }, [activeIndex]);

    return (
      <div
        ref={localRef}
        className="group relative flex w-full max-w-xs flex-col transition-opacity duration-200 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 motion-reduce:transition-none"
      >
        <Label
          id={`${id}-label`}
          isDisabled={isDisabled}
          requiredHint={isRequired}
          className={slots.label({ class: classNames?.label })}
        >
          {label}
        </Label>

        <div ref={refElementForOverlay} className="relative">
          <input
            ref={hiddenInputRef}
            type="hidden"
            name={name}
            value={getOptionValue(value)}
            form={form}
            required={isRequired}
          />

          <div className={slots.control({ class: classNames?.control })}>
            <div className="flex flex-1 items-center gap-2">
              {isComponentCombobox ? (
                <Input
                  ref={inputRef}
                  autoComplete="off"
                  aria-autocomplete="list"
                  aria-labelledby={`${id}-label`}
                  onBlur={onBlur}
                  className={slots.input({ class: classNames?.input })}
                  onChange={handleInputChange}
                  onKeyDown={handleKeyDown}
                  onClick={handleOnClick}
                  placeholder={!getOptionValue(value) ? `${placeholder}` : ''}
                  aria-expanded={isOpen}
                  aria-controls={isOpen ? 'select-combobox' : undefined}
                  aria-activedescendant={
                    isOpen && activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined
                  }
                  disabled={isDisabled}
                  required={isRequired && !getOptionValue(value) && !searchValue}
                  aria-invalid={isInvalid}
                  aria-required={isRequired}
                  role="combobox"
                  aria-describedby={errorId || ''}
                  aria-haspopup="listbox"
                  {...otherprops}
                />
              ) : (
                <Button
                  ref={buttonRef}
                  onKeyDown={handleKeyDown}
                  onClick={handleOnClick}
                  className={slots.input({ class: classNames?.label }) + ` text-left`}
                  aria-expanded={isOpen}
                  aria-controls={isOpen ? 'select-listbox' : undefined}
                  aria-activedescendant={
                    isOpen && activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined
                  }
                  aria-invalid={isInvalid}
                  aria-required={isRequired}
                  aria-describedby={errorId || ''}
                  aria-labelledby={`${id}-label`}
                  isDisabled={isDisabled}
                  aria-haspopup="listbox"
                  {...otherprops}
                >
                  {getOptionLabel(value) || placeholder}
                </Button>
              )}
              <ChevronDownIcon
                height={24}
                width={24}
                onKeyDown={handleKeyDown}
                onClick={(e) => {
                  handleOnClick(e);
                  inputRef.current?.focus();
                  buttonRef.current?.focus();
                }}
                className={`${isOpen ? 'rotate-180' : ''} cursor-pointer`}
              />
            </div>
          </div>

          {errorMessage ? (
            <Text
              id={errorId}
              slot="errorMessage"
              elementType="div"
              className={slots.errorMessage({
                class: classNames?.errorMessage,
              })}
            >
              {errorIcon}
              <span>{errorMessage}</span>
            </Text>
          ) : description ? (
            <Text
              slot="description"
              elementType="div"
              className={slots.description({ class: classNames?.description })}
            >
              <span>{description}</span>
            </Text>
          ) : null}
        </div>
        {isOpen && filteredOptions.length > 0 && (
          <Overlay
            referenceElement={refElementForOverlay}
            position={Positions.bottom}
            style={{ maxHeight: 240 }}
            offset={{ x: 8, y: 8 }}
          >
            <ul
              ref={listboxRef}
              role="listbox"
              id={`select-${isComponentCombobox ? 'combobox ' : 'listbox'}`}
              aria-label={label}
              className="absolute z-30 max-h-60 w-full overflow-auto rounded-md bg-neutral-50 py-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
            >
              {filteredOptions.map((option, index) => {
                const isActive = index === activeIndex;

                return (
                  <li
                    key={getOptionValue(option)}
                    ref={optionRefs.current[index]}
                    role="option"
                    id={`${id}-option-${index}`}
                    aria-selected={getOptionValue(value) === getOptionValue(option)}
                    tabIndex={isActive ? 0 : -1}
                    className={`relative flex cursor-pointer justify-between border-l-[3px] p-2 hover:bg-purple-50 focus:bg-purple-50 ${isActive ? 'border-purple-600 bg-purple-50' : 'border-transparent'} ${getOptionValue(value) === getOptionValue(option) ? 'font-medium' : ''}`}
                    onClick={() => toggleItem(option)}
                    onKeyDown={handleKeyDown}
                    onMouseEnter={() => setActiveIndex(index)}
                  >
                    <span className={`block truncate text-sm text-neutral-900`}>
                      {getOptionLabel(option)}
                    </span>
                    {getOptionValue(value) === getOptionValue(option) && (
                      <span className="flex items-center pl-1.5">
                        <CheckIcon className="h-5 w-5 text-purple-600" />
                      </span>
                    )}
                  </li>
                );
              })}
            </ul>
          </Overlay>
        )}
      </div>
    );
  },
);

CustomSelect.displayName = 'CustomSelectComponent';

export default CustomSelect;
