import { ChevronDownIcon, ChevronUpIcon, ErrorIcon } from '@trail-ui/icons';
import { clsx } from '@trail-ui/shared-utils';
import {
  type SelectSlots,
  type SelectVariantProps,
  type SlotsToClasses,
  select,
} from '@trail-ui/theme';
import {
  type ForwardedRef,
  type ReactElement,
  type ReactNode,
  cloneElement,
  forwardRef,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import type { Placement } from 'react-aria';
import type { SelectProps as AriaSelectProps, PopoverProps } from 'react-aria-components';
import {
  Select as AriaSelect,
  Button,
  Label,
  Popover,
  SelectValue,
  Text,
} from 'react-aria-components';
import { ListBox, type ListBoxProps } from '../listbox';
import { Spinner, type SpinnerProps } from '../spinner';

export interface SelectProps<T extends object>
  extends Omit<AriaSelectProps<T>, 'children'>,
    SelectVariantProps {
  /**
   * The label of the select.
   */
  label?: ReactNode;
  /**
   * The id of the label of the select.
   */
  labelId?: string;
  /**
   * The id of the selected option.
   */
  optionId?: string;
  /**
   * The description of the select.
   */
  description?: ReactNode;
  /**
   * The id of the error message of the select.
   */
  errorId?: string;
  /**
   * The error icon of the select.
   */
  errorIcon?: ReactNode;
  /**
   * The error message of the select.
   */
  errorMessage?: ReactNode;
  /**
   * The placement of the popover.
   */
  placement?: Placement;
  /**
   * The items of the select.
   */
  items?: Iterable<T>;
  /**
   * The children of the select.
   */
  children: React.ReactNode | ((item: T) => React.ReactNode);
  /**
   * The icon that represents the select open state. Usually a chevron icon.
   */
  selectorIcon?: ReactNode;
  /**
   * Element to be rendered in the left side of the select.
   */
  startContent?: React.ReactNode;
  /**
   * Element to be rendered in the right side of the select.
   */
  endContent?: ReactNode;
  /**
   * Whether the select is loading.
   */
  isLoading?: boolean;
  /**
   * Props to be passed to the spinner component.
   *
   * @default { size: "sm" , color: "current" }
   */
  spinnerProps?: Partial<SpinnerProps>;
  /**
   * Props to be passed to the popover component.
   *
   * @default { placement: "bottom", triggerScaleOnOpen: false, offset: 5 }
   */
  popoverProps?: Partial<PopoverProps>;
  /**
   * Props to be passed to the listbox component.
   *
   * @default { disableAnimation: false }
   */
  listBoxProps?: Partial<ListBoxProps<T>>;
  /**
   * Classes object to style the select and its children.
   */
  classNames?: SlotsToClasses<SelectSlots>;
  /**
   * The current selected value
   */
  selectedItem?: string;
  /**
   * The current item that has focus in the dropdown options
   */
  currentFocusedItem?: string;
}

function Select<T extends object>(props: SelectProps<T>, ref: ForwardedRef<HTMLDivElement>) {
  const [isOpen, setIsOpen] = useState(false);
  const searchRef = useRef<HTMLInputElement>(null);
  const targetRef = useRef<HTMLDivElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);

  const {
    children,
    className,
    classNames,
    label,
    labelId,
    optionId,
    description,
    errorId,
    errorIcon = (
      <ErrorIcon
        className="h-4 w-4 text-red-800 dark:text-red-600"
        role="img"
        aria-label="Error"
        aria-hidden={false}
      />
    ),
    errorMessage,
    items,
    placement = 'bottom',
    isLoading = false,
    selectorIcon = <ChevronDownIcon height={24} width={24} />,
    spinnerProps,
    popoverProps,
    listBoxProps,
    ...otherProps
  } = props;

  // biome-ignore lint/correctness/useExhaustiveDependencies: the useeffect should only be triggred on isOpen change
  useEffect(() => {
    requestAnimationFrame(() => {
      searchRef?.current?.focus();
    });
    if (!isOpen) {
      buttonRef.current?.setAttribute(
        'aria-activedescendant',
        props.selectedItem ? `listbox-id-option-${props.selectedItem}` : '',
      );
    }
  }, [isOpen]);

  const slots = useMemo(() => select(), []);
  const baseStyles = clsx(classNames?.base, className);

  const clonedIcon = cloneElement(selectorIcon as ReactElement, {
    'aria-hidden': true,
    className: slots.selectorIcon({ class: classNames?.selectorIcon }),
  });

  const renderIndicator = useMemo(() => {
    if (isLoading) {
      return (
        <Spinner
          aria-hidden
          color="current"
          size="sm"
          {...spinnerProps}
          className={slots.spinner({ class: classNames?.spinner })}
        />
      );
    }

    return clonedIcon;
  }, [isLoading, clonedIcon, spinnerProps, slots, classNames?.spinner]);

  useEffect(() => {
    if (buttonRef.current) buttonRef.current.setAttribute('role', 'combobox');
  }, []);

  useEffect(() => {
    if (buttonRef.current)
      buttonRef.current.setAttribute(
        'aria-activedescendant',
        props.currentFocusedItem ? `listbox-id-option-${props.currentFocusedItem}` : '',
      );
  }, [props.currentFocusedItem]);

  return (
    <>
      <AriaSelect
        ref={ref}
        isOpen={isOpen}
        onOpenChange={setIsOpen}
        {...otherProps}
        className={slots.base({ class: baseStyles })}
      >
        {label && (
          <Label id={labelId} className={slots.label({ class: classNames?.label })}>
            {label}
          </Label>
        )}
        <div className={slots.mainWrapper({ class: classNames?.mainWrapper })}>
          <Button
            id={optionId}
            ref={buttonRef}
            aria-labelledby={labelId ? labelId : optionId}
            aria-expanded={isOpen}
            aria-controls="listbox-id"
            className={slots.trigger({ class: classNames?.trigger })}
            {...(errorMessage && { 'aria-describedby': errorId })}
            {...(props.isRequired && { 'aria-required': true })}
          >
            <SelectValue className={slots.value({ class: classNames?.value })} />
            <span className={`${isOpen ? 'rotate-[180deg]' : ''}`}>{renderIndicator}</span>
          </Button>

          {errorMessage ? (
            <Text
              id={errorId}
              slot="errorMessage"
              aria-live="polite"
              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 ref={targetRef} />
        </div>
        <Popover
          placement={placement}
          className={slots.popover({ class: classNames?.popover })}
          isNonModal={true}
          // biome-ignore lint/a11y/useValidAriaValues: need to pass empty string to clear aria-labelledby property
          aria-labelledby=""
          UNSTABLE_portalContainer={targetRef.current || undefined}
          {...popoverProps}
        >
          <>
            <div
              role="button"
              className="z-2 fixed left-[0] top-[0] h-[100vh] w-[100vw]"
              onClick={() => setIsOpen(false)}
              onKeyDown={() => {}}
              aria-label="Close dropdown"
              tabIndex={-1}
            />
            <ListBox id="listbox-id" items={items} {...listBoxProps}>
              {children}
            </ListBox>
          </>
        </Popover>
      </AriaSelect>
    </>
  );
}

const _Select = forwardRef(Select) as <T extends object>(
  props: SelectProps<T> & { ref?: ForwardedRef<HTMLDivElement> },
) => ReactElement;

export { _Select as Select };
