import { useTheme } from '@wise/components-theming';
import { clsx } from 'clsx';
import { useState, useEffect, useRef, useMemo, useId } from 'react';
import { useIntl } from 'react-intl';

import Button, { ButtonProps } from '../button';
import Chevron from '../chevron';
import { Position, Size } from '../common';
import BottomSheet from '../common/bottomSheet';
import { stopPropagation } from '../common/domHelpers';
import { useLayout } from '../common/hooks';
import Panel from '../common/panel';
import Drawer from '../drawer';
import { useInputAttributes } from '../inputs/contexts';

import messages from './Select.messages';
import Option from './option';
import SearchBox from './searchBox';

const DEFAULT_SEARCH_VALUE = '';
const DEFAULT_OPTIONS_PAGE_SIZE = 1000;

const includesString = (string1: string, string2: string) =>
  string1.toLowerCase().includes(string2.toLowerCase());

export interface SelectOptionItem {
  value: any;
  label?: React.ReactNode;
  icon?: React.ReactNode;
  currency?: string;
  note?: React.ReactNode;
  secondary?: React.ReactNode;
}

export interface SelectItem extends Partial<SelectOptionItem> {
  header?: React.ReactNode;
  separator?: boolean;
  disabled?: boolean;
  searchStrings?: string[];
}

export interface SelectItemWithPlaceholder extends SelectItem {
  placeholder?: string;
}

function defaultFilterFunction(option: SelectItemWithPlaceholder, searchValue: string) {
  if (isPlaceholderOption(option)) {
    return true;
  }
  const { label, note, secondary, currency, searchStrings } = option;
  return (
    (typeof label === 'string' && includesString(label, searchValue)) ||
    (typeof note === 'string' && includesString(note, searchValue)) ||
    (typeof secondary === 'string' && includesString(secondary, searchValue)) ||
    (!!currency && includesString(currency, searchValue)) ||
    (!!searchStrings && searchStrings.some((string) => includesString(string, searchValue)))
  );
}

function isActionableOption(option: SelectItem) {
  return !option.header && !option.separator && !option.disabled;
}

function isHeaderOption(option: SelectItem | null) {
  return option != null && 'header' in option;
}

function isSeparatorOption(option: SelectItem | null) {
  return option != null && 'separator' in option;
}

function clamp(from: number, to: number, value: number) {
  return Math.max(Math.min(to, value), from);
}

/**
 * No option or placeholder option is selected
 */
const DEFAULT_SELECTED_OPTION = null;

function isPlaceholderOption(option: SelectItemWithPlaceholder | null) {
  return option === DEFAULT_SELECTED_OPTION || 'placeholder' in option;
}

function isSearchableOption(option: SelectItemWithPlaceholder | null) {
  return !isHeaderOption(option) && !isSeparatorOption(option) && !isPlaceholderOption(option);
}

const getUniqueIdForOption = (parentId: string | undefined, option: SelectItem | null) => {
  if (option == null) {
    return undefined;
  }

  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
  const uniqueOptionId =
    option.value || (typeof option.label === 'string' ? option.label.replace(/\s/g, '') : '');

  return `option-${parentId ?? ''}-${uniqueOptionId}`;
};

export interface SelectProps {
  placeholder?: string;
  id?: string;
  required?: boolean;
  disabled?: boolean;
  inverse?: boolean;
  dropdownRight?: `${Size.EXTRA_SMALL | Size.SMALL | Size.MEDIUM | Size.LARGE | Size.EXTRA_LARGE}`;
  dropdownWidth?: `${Size.SMALL | Size.MEDIUM | Size.LARGE}`;
  /** @default 'md' */
  size?: `${Size.SMALL | Size.MEDIUM | Size.LARGE}`;
  /** @default true */
  block?: boolean;
  selected?: SelectOptionItem;
  /**
   * Search toggle
   * if `true` default search functionality being enabled (not case sensitive search in option labels & currency props)
   * if `function` you can define your own search function to implement custom search experience. This search function used while filtering the options array.
   */
  search?: boolean | ((option: SelectItemWithPlaceholder, searchValue: string) => boolean);
  options: SelectItem[];
  /** @default '' */
  searchValue?: string;
  searchPlaceholder?: string;
  /** @default {} */
  classNames?: Record<string, string>;
  dropdownUp?: boolean;
  buttonProps?: Extract<ButtonProps, { as?: 'button' }>;
  dropdownProps?: React.ComponentPropsWithoutRef<'ul'>;
  onChange: (value: SelectItem | typeof DEFAULT_SELECTED_OPTION) => void;
  onFocus?: React.FocusEventHandler<HTMLDivElement>;
  onBlur?: React.FocusEventHandler<HTMLDivElement>;
  /**
   * To have full control of your search value and response use `onSearchChange` function combined with `searchValue` and custom filtering on the options array.
   * DO NOT USE TOGETHER WITH `search` PROPERTY
   */
  onSearchChange?: (value: string) => void;
}

const defaultClassNames = {};

/**
 * @deprecated Use `SelectInput` instead (https://neptune.wise.design/blog/2023-11-28-adopting-our-new-selectinput)
 */
export default function Select({
  placeholder,
  id,
  required,
  disabled,
  inverse,
  dropdownWidth,
  size = 'md',
  block = true,
  selected,
  search,
  onChange,
  onFocus,
  onBlur,
  options: defaultOptions,
  onSearchChange,
  searchValue: initSearchValue = '',
  searchPlaceholder,
  classNames: classNamesProp = defaultClassNames,
  dropdownUp,
  dropdownProps,
  buttonProps,
}: SelectProps) {
  const inputAttributes = useInputAttributes();

  const { formatMessage } = useIntl();
  const s = (className: string) => classNamesProp[className] || className;
  const [open, setOpen] = useState(false);
  const [searchValue, setSearchValue] = useState(DEFAULT_SEARCH_VALUE);
  const [keyboardFocusedOptionIndex, setKeyboardFocusedOptionIndex] = useState(-1);
  const keyboardFocusedReference = useRef<HTMLLIElement>(null);
  const previousKeyboardFocusedOptionIndex = useRef(-1);
  const [numberOfOptionsShown, setNumberOfOptionsShown] = useState(DEFAULT_OPTIONS_PAGE_SIZE);
  const searchBoxReference = useRef<HTMLInputElement>(null);
  const selectReference = useRef<HTMLDivElement>(null);
  const dropdownButtonReference = useRef<HTMLButtonElement>(null);
  const optionsListReference = useRef<HTMLUListElement>(null);
  const isSearchEnabled = !!onSearchChange || !!search;
  const isDropdownAutoWidth = dropdownWidth == null;

  const options = useMemo(() => {
    if (!search || !searchValue) {
      return defaultOptions;
    }

    return defaultOptions.filter(isSearchableOption).filter((option) => {
      if (typeof search === 'function') {
        return search(option, searchValue);
      }
      return defaultFilterFunction(option, searchValue);
    });
  }, [defaultOptions, search, searchValue]);

  const selectableOptions = useMemo(() => options.filter(isActionableOption), [options]);
  const focusedOption = selectableOptions[keyboardFocusedOptionIndex];

  const fallbackButtonId = useId();
  const computedId = id || inputAttributes.id || fallbackButtonId;
  const listboxId = `${computedId}-listbox`;
  const searchBoxId = `${computedId}-searchbox`;

  const { isMobile } = useLayout();

  useEffect(() => {
    let cancelled = false;

    if (keyboardFocusedOptionIndex >= 0) {
      requestAnimationFrame(() => {
        if (!cancelled) {
          if (isSearchEnabled) {
            keyboardFocusedReference.current?.scrollIntoView?.({ block: 'center' });
          } else {
            keyboardFocusedReference.current?.focus();
          }
        }
      });

      return () => {
        cancelled = true;
      };
    }
  }, [keyboardFocusedOptionIndex, isSearchEnabled]);

  const handleOnClick = () => {
    setOpen(true);
  };

  const handleTouchStart: React.TouchEventHandler<HTMLDivElement> = (event) => {
    if (event.currentTarget === event.target && open) {
      handleCloseOptions();
    }
  };

  const handleOnFocus: React.FocusEventHandler<HTMLDivElement> = (event) => {
    onFocus?.(event);
  };

  const handleOnBlur: React.FocusEventHandler<HTMLDivElement> = (event) => {
    const { nativeEvent } = event;
    if (nativeEvent) {
      const elementReceivingFocus = nativeEvent.relatedTarget;
      const select = event.currentTarget;
      if (
        select &&
        elementReceivingFocus instanceof Node &&
        select.contains(elementReceivingFocus)
      ) {
        return;
      }
    }

    onBlur?.(event);
  };

  const handleSearchChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
    setNumberOfOptionsShown(DEFAULT_OPTIONS_PAGE_SIZE);
    setSearchValue(event.target.value);
    onSearchChange?.(event.target.value);
  };

  const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => {
    switch (event.key) {
      case 'ArrowUp':
      case 'ArrowDown':
        if (open) {
          moveFocusWithDifference(event.key === 'ArrowUp' ? -1 : 1);
        } else {
          setOpen(true);
        }
        stopPropagation(event);
        break;
      case ' ':
        if (event.target !== searchBoxReference.current) {
          if (open) {
            selectKeyboardFocusedOption();
          } else {
            setOpen(true);
          }
          stopPropagation(event);
        }
        break;
      case 'Enter':
        if (open) {
          selectKeyboardFocusedOption();
        } else {
          setOpen(true);
        }
        stopPropagation(event);
        break;
      case 'Escape':
        handleCloseOptions();
        stopPropagation(event);
        break;
      case 'Tab':
        if (open) {
          selectKeyboardFocusedOption();
        }
        break;
      default:
        break;
    }
  };

  function selectKeyboardFocusedOption() {
    if (keyboardFocusedOptionIndex >= 0 && selectableOptions.length > 0) {
      selectOption(selectableOptions[keyboardFocusedOptionIndex]);
    }
  }

  function moveFocusWithDifference(difference: number) {
    const selectedOptionIndex = selectableOptions.reduce((optionIndex, current, index) => {
      if (optionIndex >= 0) {
        return optionIndex;
      }
      if (isOptionSelected(selected, current)) {
        return index;
      }
      return -1;
    }, -1);
    const previousFocusedIndex = previousKeyboardFocusedOptionIndex.current;
    let indexToStartMovingFrom = previousFocusedIndex;
    if (previousFocusedIndex < 0) {
      if (selectedOptionIndex < 0) {
        setKeyboardFocusedOptionIndex(0);
      } else {
        indexToStartMovingFrom = selectedOptionIndex;
      }
    }

    const unClampedNewIndex = indexToStartMovingFrom + difference;
    const newIndex = clamp(0, selectableOptions.length - 1, unClampedNewIndex);

    setKeyboardFocusedOptionIndex(newIndex);
  }

  useEffect(() => {
    if (open) {
      if (!isMobile || searchValue) {
        if (isSearchEnabled && searchBoxReference.current) {
          searchBoxReference.current.focus();
        }
        if (
          !isSearchEnabled &&
          optionsListReference.current &&
          previousKeyboardFocusedOptionIndex.current < 0
        ) {
          optionsListReference.current.focus();
        }
      }

      previousKeyboardFocusedOptionIndex.current = keyboardFocusedOptionIndex;
    } else {
      previousKeyboardFocusedOptionIndex.current = -1;
    }
  }, [open, searchValue, isSearchEnabled, isMobile, keyboardFocusedOptionIndex]);

  const handleCloseOptions = () => {
    setOpen(false);
    setKeyboardFocusedOptionIndex(-1);
    if (dropdownButtonReference.current) {
      dropdownButtonReference.current.focus();
    }
  };

  function createSelectHandlerForOption(option: SelectItemWithPlaceholder) {
    return (event: React.SyntheticEvent) => {
      stopPropagation(event);
      selectOption(option);
    };
  }

  function selectOption(option: SelectItemWithPlaceholder) {
    onChange(isPlaceholderOption(option) ? DEFAULT_SELECTED_OPTION : option);
    handleCloseOptions();
  }

  function renderOptionsList({ className = '' } = {}) {
    const dropdownClass = clsx(
      s('np-dropdown-menu'),
      {
        [s('np-dropdown-menu-desktop')]: !isMobile,
        [s(`np-dropdown-menu-${dropdownWidth}`)]: !isMobile && !isDropdownAutoWidth,
      },
      s(className),
    );

    const showPlaceholder = !required && !isSearchEnabled && Boolean(placeholder);
    return (
      <ul
        ref={optionsListReference}
        id={listboxId}
        role="listbox"
        aria-orientation="vertical"
        aria-activedescendant={getUniqueIdForOption(id, selected ?? null)}
        tabIndex={-1}
        className={dropdownClass}
        {...dropdownProps}
      >
        {showPlaceholder && <PlaceHolderOption />}
        {isSearchEnabled && (
          <SearchBox
            ref={searchBoxReference}
            id={searchBoxId}
            classNames={classNamesProp}
            value={initSearchValue || searchValue}
            placeholder={searchPlaceholder || formatMessage(messages.searchPlaceholder)}
            focusedOptionId={getUniqueIdForOption(id, focusedOption)}
            onChange={handleSearchChange}
            onClick={stopPropagation}
          />
        )}
        {options.slice(0, numberOfOptionsShown).map(renderOption)}
        {numberOfOptionsShown < options.length && <ShowMoreOption />}
      </ul>
    );
  }

  function ShowMoreOption() {
    function handleOnClick(event: React.SyntheticEvent<HTMLLIElement>) {
      stopPropagation(event);
      setNumberOfOptionsShown(numberOfOptionsShown + DEFAULT_OPTIONS_PAGE_SIZE);
    }
    return (
      /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */
      <li
        className={clsx(s('clickable'), s('border-bottom'), s('show-more'))}
        onClick={handleOnClick}
        onKeyPress={handleOnClick}
      >
        {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
        <a>...</a>
      </li>
    );
  }

  function PlaceHolderOption() {
    const placeholderOption = { placeholder };
    return (
      /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */
      <li
        className={clsx(s('clickable'), s('border-bottom'))}
        onClick={createSelectHandlerForOption(placeholderOption)}
        onKeyPress={createSelectHandlerForOption(placeholderOption)}
      >
        {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
        <a>{placeholder}</a>
      </li>
    );
  }

  function SeparatorOption() {
    return <li className={s('np-separator')} aria-hidden />;
  }

  function HeaderOption({ children }: { children?: React.ReactNode }) {
    return (
      <li // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
        className={clsx(s('np-dropdown-header'), s('np-text-title-group'))}
        onClick={stopPropagation}
        onKeyPress={stopPropagation}
      >
        {children}
      </li>
    );
  }

  function isOptionSelected(selected: SelectOptionItem | undefined, option: SelectItem) {
    return selected?.value === option?.value;
  }

  const renderOption = (option: SelectItem, index: number) => {
    const separatorOption = option;
    if (isSeparatorOption(separatorOption) && separatorOption?.separator) {
      return <SeparatorOption key={index} />;
    }
    const headerOption = option;
    if (isHeaderOption(headerOption) && headerOption.header) {
      return <HeaderOption key={index}>{headerOption.header}</HeaderOption>;
    }

    const isActive = isOptionSelected(selected, option);
    const selectOption = option;
    const isFocusedWithKeyboard =
      !selectOption.disabled &&
      keyboardFocusedOptionIndex === getIndexWithoutHeadersForIndexWithHeaders(index);

    const className = clsx(
      s('np-dropdown-item'),
      selectOption.disabled ? [s('disabled')] : s('clickable'),
      {
        [s('active')]: isActive,
        [s('np-dropdown-item--focused')]: isFocusedWithKeyboard,
      },
    );

    const handleOnClick = selectOption.disabled
      ? stopPropagation
      : createSelectHandlerForOption(selectOption);

    return (
      <li
        ref={isFocusedWithKeyboard ? keyboardFocusedReference : undefined}
        key={index}
        id={getUniqueIdForOption(id, option)}
        aria-selected={isActive}
        aria-disabled={option.disabled}
        role="option"
        tabIndex={-1}
        className={className}
        onClick={handleOnClick}
        onKeyPress={handleOnClick}
      >
        {/* @ts-expect-error options needs DOM refactoring */}
        <a disabled={selectOption.disabled}>
          <Option
            label={undefined}
            value={undefined}
            {...selectOption}
            classNames={classNamesProp}
          />
        </a>
      </li>
    );
  };

  function getIndexWithoutHeadersForIndexWithHeaders(index: number) {
    return options.reduce((sum, option, currentIndex) => {
      if (currentIndex < index && isActionableOption(option)) {
        return sum + 1;
      }
      return sum;
    }, 0);
  }

  const hasActiveOptions = !!defaultOptions.length;
  if (open && (initSearchValue || searchValue)) {
    if (hasActiveOptions && keyboardFocusedOptionIndex < 0) {
      setKeyboardFocusedOptionIndex(0);
    }
    if (!hasActiveOptions && keyboardFocusedOptionIndex >= 0) {
      setKeyboardFocusedOptionIndex(-1);
    }
  }

  return (
    <div // eslint-disable-line jsx-a11y/no-static-element-interactions
      ref={selectReference}
      className={clsx(s('np-select'), block ? s('btn-block') : null, s('btn-group'))}
      onKeyDown={handleKeyDown}
      onTouchMove={handleTouchStart}
      onFocus={handleOnFocus}
      onBlur={handleOnBlur}
    >
      <Button
        ref={dropdownButtonReference}
        {...inputAttributes}
        id={computedId}
        block={block}
        size={size}
        htmlType="button"
        className={clsx(
          s('np-dropdown-toggle'),
          s('np-text-body-large'),
          inverse ? s('np-dropdown-toggle-navy') : null,
        )}
        // reset Button's styles
        type={null}
        priority={null}
        disabled={disabled}
        aria-controls={listboxId}
        aria-expanded={open}
        onClick={handleOnClick}
        {...buttonProps}
      >
        {selected ? (
          <Option label={undefined} {...selected} classNames={classNamesProp} selected />
        ) : (
          <span className={s('form-control-placeholder')}>{placeholder}</span>
        )}
        <Chevron
          // disabled={disabled}
          className={clsx(
            s('tw-icon'),
            s('tw-chevron-up-icon'),
            s('tw-chevron'),
            s('bottom'),
            s('np-select-chevron'),
          )}
        />
      </Button>
      {isMobile ? (
        isSearchEnabled ? (
          <Drawer
            open={open}
            headerTitle={searchPlaceholder || formatMessage(messages.searchPlaceholder)}
            onClose={handleCloseOptions}
          >
            {renderOptionsList()}
          </Drawer>
        ) : (
          <BottomSheet open={open} onClose={handleCloseOptions}>
            {renderOptionsList()}
          </BottomSheet>
        )
      ) : (
        <Panel
          open={open}
          flip={false}
          altAxis
          anchorRef={selectReference}
          anchorWidth={isDropdownAutoWidth}
          position={dropdownUp ? Position.TOP : Position.BOTTOM}
          onClose={handleCloseOptions}
        >
          {renderOptionsList({ className: 'p-a-1' })}
        </Panel>
      )}
    </div>
  );
}
