import mergeProps from 'merge-props';
import { useCallback, useEffect, useRef, useState, useDeferredValue } from 'react';
import { Listbox as ListboxBase } from '@headlessui/react';
import { Breakpoint } from '@transferwise/neptune-tokens';
import { useScreenSize } from '../../common/hooks/useScreenSize';
import { useEffectEvent } from '../../common/hooks/useEffectEvent';
import { useInputAttributes } from '../contexts';

import { SelectInputBottomSheet } from './BottomSheet';
import { SelectInputPopover } from './Popover';
import { SelectInputOptions } from './Options';
import { DefaultRenderTrigger } from './DefaultRenderTrigger';

import {
  SelectInputOptionContentWithinTriggerContext,
  SelectInputTriggerButtonPropsContext,
} from './SelectInput.contexts';
import { searchableString, sortByRelevance } from './SelectInput.utils';
import { SelectInputProps } from './SelectInput.types';

const noop = () => {};

/**
 * SelectInput component allows users to select an option from a dropdown list.
 * Supports filtering, multiple selection, and customization.
 */
export function SelectInput<T = string, M extends boolean = false>({
  id: idProp,
  parentId,
  name,
  multiple,
  placeholder,
  autocomplete,
  items,
  defaultValue,
  value: controlledValue,
  compareValues,
  renderValue = String,
  renderFooter,
  renderTrigger = DefaultRenderTrigger,
  filterable,
  filterPlaceholder,
  sortFilteredOptions,
  disabled,
  size = 'md',
  className,
  UNSAFE_triggerButtonProps,
  triggerRef: externalTriggerRef,
  onFilterChange = noop,
  onChange,
  onOpen,
  onClose,
  onClear,
}: SelectInputProps<T, M>) {
  const inputAttributes = useInputAttributes({ nonLabelable: true });
  const id = idProp ?? inputAttributes.id;

  const [open, setOpen] = useState(false);

  const initialized = useRef(false);
  const handleClose = useEffectEvent(onClose ?? (() => {}));
  const handleOpen = useEffectEvent(onOpen ?? (() => {}));

  useEffect(() => {
    if (initialized.current) {
      if (open) {
        handleOpen?.();
      } else {
        handleClose?.();
      }
    } else {
      initialized.current = true;
    }
  }, [open]);

  const [filterQuery, _setFilterQuery] = useState('');
  const deferredFilterQuery = useDeferredValue(filterQuery);
  const previousFilterQueryRef = useRef(filterQuery);

  const setFilterQuery = useCallback(
    (query: string) => {
      _setFilterQuery(query);
      if (query !== previousFilterQueryRef.current) {
        onFilterChange({
          query,
          queryNormalized: query ? searchableString(query) : null,
        });
        previousFilterQueryRef.current = query;
      }
    },
    [onFilterChange],
  );

  const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
  const searchInputRef = useRef<HTMLInputElement>(null);
  const listboxRef = useRef<HTMLDivElement>(null);
  const controllerRef = filterable ? searchInputRef : listboxRef;

  const screenSm = useScreenSize(Breakpoint.SMALL);
  const OptionsOverlay = screenSm ? SelectInputPopover : SelectInputBottomSheet;

  /**
   * Attempts to resolve the `listbox` label
   * @see https://storybook.wise.design/?path=/docs/forms-selectinput-accessibility--docs#labelling
   */
  const getListBoxLabelProps = (): {
    listBoxLabel?: string;
    listBoxLabelledBy?: string;
  } => {
    if (UNSAFE_triggerButtonProps?.['aria-label']) {
      return {
        listBoxLabel: UNSAFE_triggerButtonProps['aria-label'],
      };
    }

    if (UNSAFE_triggerButtonProps?.['aria-labelledby']) {
      return {
        listBoxLabelledBy: UNSAFE_triggerButtonProps['aria-labelledby'],
      };
    }

    if (inputAttributes['aria-labelledby']) {
      return {
        listBoxLabelledBy: inputAttributes['aria-labelledby'],
      };
    }

    return {};
  };

  return (
    <ListboxBase
      name={name}
      multiple={multiple}
      defaultValue={defaultValue as M extends true ? T[] : T}
      value={controlledValue as M extends true ? T[] : T}
      by={compareValues}
      disabled={disabled}
      onChange={
        ((value) => {
          if (!multiple) {
            setOpen(false);
          }
          onChange?.(value);
        }) satisfies SelectInputProps<T, M>['onChange']
      }
    >
      {({ disabled: uiDisabled, value }) => {
        const placeholderShown =
          multiple && Array.isArray(value) ? value.length === 0 : value == null;
        return (
          <OptionsOverlay
            placement="bottom-start"
            open={open}
            renderTrigger={({ ref, getInteractionProps }) => (
              <SelectInputTriggerButtonPropsContext.Provider
                // eslint-disable-next-line react/jsx-no-constructed-context-values
                value={{
                  ref: (node) => {
                    ref(node);
                    if (externalTriggerRef) {
                      // eslint-disable-next-line no-param-reassign
                      externalTriggerRef.current = node;
                    } else {
                      internalTriggerRef.current = node;
                    }
                  },
                  size,
                  ...inputAttributes,
                  ...UNSAFE_triggerButtonProps,
                  id,
                  ...mergeProps(
                    {
                      onClick: () => {
                        setOpen((prev) => !prev);
                      },
                      onKeyDown: (event: React.KeyboardEvent) => {
                        if (
                          event.key === ' ' ||
                          event.key === 'Enter' ||
                          event.key === 'ArrowDown' ||
                          event.key === 'ArrowUp'
                        ) {
                          setOpen((prev) => !prev);
                        }
                      },
                    },
                    getInteractionProps(),
                  ),
                }}
              >
                {renderTrigger({
                  content: !placeholderShown ? (
                    <SelectInputOptionContentWithinTriggerContext.Provider value>
                      {multiple && Array.isArray(value)
                        ? (value as readonly NonNullable<T>[])
                            .map((option) => renderValue(option, true))
                            .filter((node) => node != null)
                            .join(', ')
                        : renderValue(value as NonNullable<T>, true)}
                    </SelectInputOptionContentWithinTriggerContext.Provider>
                  ) : (
                    placeholder
                  ),
                  placeholderShown,
                  clear:
                    onClear != null
                      ? () => {
                          onClear();
                          (externalTriggerRef?.current ?? internalTriggerRef.current)?.focus({
                            preventScroll: true,
                          });
                        }
                      : undefined,
                  disabled: uiDisabled,
                  size,
                  className,
                })}
              </SelectInputTriggerButtonPropsContext.Provider>
            )}
            initialFocusRef={controllerRef}
            size={filterable ? 'lg' : 'md'}
            padding="none"
            onClose={() => {
              setOpen(false);
            }}
            onCloseEnd={() => {
              setFilterQuery('');
            }}
          >
            <SelectInputOptions
              id={id ? `${id}Search` : undefined}
              parentId={parentId}
              items={items}
              compareValues={compareValues}
              renderValue={renderValue}
              renderFooter={renderFooter}
              filterable={filterable}
              filterPlaceholder={filterPlaceholder}
              sortFilteredOptions={sortFilteredOptions}
              searchInputRef={searchInputRef}
              listboxRef={listboxRef}
              filterQuery={deferredFilterQuery}
              autocomplete={autocomplete}
              name={name}
              onFilterChange={setFilterQuery}
              onAutocompleteSelect={(matchedValue) => {
                onChange?.(matchedValue as M extends true ? T[] : T);
                if (!multiple) {
                  setOpen(false);
                }
              }}
              {...getListBoxLabelProps()}
            />
          </OptionsOverlay>
        );
      }}
    </ListboxBase>
  );
}

// Attach sortByRelevance to the component for convenience
SelectInput.sortByRelevance = sortByRelevance;
