import { bubble } from "@web3r/flowerkit/evt";
import { wait, getDebouncedFn } from "@web3r/flowerkit/fn";
import {
  useRef,
  useEffect,
  useState,
  useMemo,
  useCallback,
  useImperativeHandle,
} from "react";
import type {
  SyntheticEvent, MouseEvent, FC, ReactElement, FocusEvent, KeyboardEvent,
  Dispatch, SetStateAction,
  AnimationEventHandler, TouchEventHandler,
} from "react";
import type {
  ActionMeta, GroupBase, OnChangeValue, OptionsOrGroups,
} from "react-select";
import ReactSelect from "react-select";
import AsyncSelect from "react-select/async";
import { getDocument } from "ssr-window";
import type { IInputRef } from "#package/components/input";
import type { IComponentBaseProps } from "#package/types";
import { selectEmptyOptions } from "./config/constants";
import type {
  TOptionType,
  IGetOptionsModifyParams,
  IStateActions,
  ISelectRef,
  TOnResetParams,
  ISelect,
  TSelectCtx,
  TSelectValue,
  TSelectPackageAPI,
} from "./config/types";
import { SelectContext } from "./context";
import {
  getInitialValue,
  getOptions,
  isOptionsEqual,
  onOptionChange,
  getComponents,
  getStateClasses,
} from "./utils";
import { constants } from "../../constants";
import { getElRefHelper } from "../../utils/react/getElRefHelper";
import { useCN } from "../../utils/react/useCN";
import { useSSGRender } from "../../utils/react/useSSGRender";
import "./style.pcss";

/**
 * Компонент выпадающего списка
 * @returns {ReactElement}
 */
const Select: FC<ISelect & IComponentBaseProps> = (props): ReactElement => {

  const {
    baseClass = "formSelect",
    extraClasses = {},
    extraAttrs = {},
    name = "",
    id,
    options = selectEmptyOptions,
    getOptionsModify,
    onChange,
    onKeyDown,
    onClick,
    onReset,
    onMount,
    onUnmount,
    onBlur,
    onMenuToggle,
    onSelectChangeRequest,
    onOptionsSearchRequest,
    form,
    value,
    placeholder = "",
    tabIndex = -1,
    isLoading = false,
    isRequired = false,
    isDisabled = false,
    isSearchable = false,
    isClearable = false,
    isCheckboxView = false,
    isCountView = false,
    isAsync = false,
    isMulti = false,
    isAutoScroll = false,
    loadOptionsDebounce = 500,
    formatOptionLabel = ({ label, data = {} }: TOptionType) => `${data?.prefix ?? ""}${label ?? ""}`,
    formatCount,
    customComponents = {},
    closeMenuOnSelect,
    closeMenuOnScroll = false,
    menuShouldBlockScroll = false,
    autoFocus = false,
    openMenuOnFocus = true,
    backspaceRemovesValue = false,
    openMenuOnClick = true,
    blurInputOnSelect,
    escapeClearsValue = true,
    menuShouldScrollIntoView = false,
    captureMenuScroll = false,
    hideSelectedOptions = false,
    icons = {
      loading: "loading",
      dropdown: "dropdown",
      clear: "clear",
      remove: "remove",
    },
    styles = {
      clearIndicator: () => {},
      clearValue: () => {},
      container: () => {},
      control: () => {},
      dropdownIndicator: () => {},
      group: () => {},
      groupHeading: () => {},
      indicatorsContainer: () => {},
      indicatorSeparator: () => {},
      input: () => {},
      loadingIndicator: () => {},
      loadingMessage: () => {},
      menu: () => {},
      menuList: () => {},
      menuPortal: () => {},
      multiValue: () => {},
      multiValueLabel: () => {},
      multiValueRemove: () => {},
      noOptionsMessage: () => {},
      option: () => {},
      placeholder: () => {},
      singleValue: () => {},
      valueContainer: () => {},
    },
    isOpen,
    ref,
  } = props || {};

  const { getCN } = useCN(baseClass);

  const selectRef = useRef<TSelectPackageAPI>(null);
  const inputRef = useRef<IInputRef | null>(null);
  const containerRef = useRef<HTMLDivElement | null>(null);

  const normalizedValue = useMemo<TSelectValue<typeof isMulti>>(() => getInitialValue({ value, isMulti, options }), [ value, isMulti, options ]);

  const originalValue = useRef<TSelectValue<typeof isMulti>>(normalizedValue);
  const originalOptions = useRef(options);

  const stateClasses = useMemo(() => getStateClasses(getCN), [ getCN ]) as Record<string, string>;

  // main state values
  const [ selectValue, setSelectValue ] = useState<TSelectValue<typeof isMulti>>(normalizedValue);
  const [ selectOptions, setSelectOptions ] = useState<TOptionType[]>(options);
  const [ selectDisabled, setSelectDisabled ] = useState<boolean | undefined>(isDisabled);
  const [ selectLoading, setSelectLoading ] = useState<boolean | undefined>(isLoading);
  const [ selectRequired, setSelectRequired ] = useState<boolean | undefined>(isRequired);
  const isMenuControlled = typeof isOpen === "boolean";
  const [ selectIsOpen, setSelectIsOpen ] = useState<boolean>(Boolean(isOpen));
  const [ isMenuManaged, setIsMenuManaged ] = useState<boolean>(isMenuControlled);

  // prev state values
  const prevControlledOpen = useRef<boolean | undefined>(undefined);
  const prevValue = useRef<TSelectValue<typeof isMulti>>(selectValue);
  const prevOptions = useRef<TOptionType[]>(selectOptions);
  const lastSelectedValueRef = useRef<string | null>(null);
  const animationRef = useRef<boolean>(false);
  const prevStateClassesRef = useRef<Record<string, string>>(stateClasses);

  const getSelectValueSnapshot = useCallback((optionsSnapshot: TOptionType[]): TSelectValue<typeof isMulti> => {
    const refValue = selectRef.current?.getValue() ?? selectEmptyOptions;
    // eslint-disable-next-line no-nested-ternary
    const stateValue = Array.isArray(selectValue) ? selectValue : (selectValue ? [ selectValue ] : selectEmptyOptions);
    const sourceValue = refValue.length ? refValue : stateValue;

    const valueByOptionsRef = sourceValue.map((item) => {
      if (!item?.value) {
        return item;
      }
      return optionsSnapshot.find((option) => option?.value === item.value) ?? item;
    });

    if (isMulti) {
      return valueByOptionsRef as TSelectValue<typeof isMulti>;
    }
    return (valueByOptionsRef[0] ?? null) as TSelectValue<typeof isMulti>;
  }, [ isMulti, selectValue ]);

  const applyOptionsModify = useCallback((type: IGetOptionsModifyParams["type"]) => {
    if (typeof getOptionsModify !== "function") {
      return;
    }
    setSelectOptions((prev) => {
      const optionsSnapshot = [ ...prev ];
      const valueSnapshot = getSelectValueSnapshot(optionsSnapshot);
      const valueForModify = type === "close"
        && isMulti
        && Array.isArray(valueSnapshot)
        && !!lastSelectedValueRef.current
        ? (() => {
          const latest = valueSnapshot.find((item) => item?.value === lastSelectedValueRef.current);
          if (!latest) {
            return valueSnapshot;
          }
          return [ latest, ...valueSnapshot.filter((item) => item !== latest) ] as TSelectValue<typeof isMulti>;
        })()
        : valueSnapshot;
      const nextOptions = getOptionsModify({
        options: optionsSnapshot,
        value: valueForModify,
        prevOptions: [ ...prev ],
        type,
      });
      if (!Array.isArray(nextOptions)) {
        return prev;
      }

      const isSameByRef = nextOptions.length === prev.length
        && nextOptions.every((item, index) => item === prev[index]);
      return isSameByRef ? prev : nextOptions;
    });
  }, [ getOptionsModify, getSelectValueSnapshot, isMulti ]);

  const onMenuOpen = useCallback(() => {
    if (animationRef.current) {
      return;
    }
    const containerEl = getElRefHelper(containerRef);
    if (containerEl) {
      wait(50).finally(() => {
        containerEl.classList.add(stateClasses.isOpen);
      });
    }
    applyOptionsModify("open");
    if (typeof onMenuToggle === "function") {
      onMenuToggle("open");
    }
  }, [ applyOptionsModify, onMenuToggle ]);

  const onMenuClose = useMemo(() => getDebouncedFn(() => {
    const container = getElRefHelper(containerRef);
    const valueInput = getElRefHelper(inputRef);
    if (container) {
      container.classList.remove(stateClasses.isOpen);
      if (valueInput) {
        bubble(valueInput, "blur", {
          container,
        });
      }
    }
    applyOptionsModify("close");
    if (typeof onMenuToggle === "function") {
      onMenuToggle("close");
    }
  }, 300), [ applyOptionsModify, onMenuToggle ]);

  const handleMenuOpen = useCallback(() => {
    if (!isMenuControlled) {
      setIsMenuManaged(true);
    }
    setSelectIsOpen(true);
    onMenuOpen();
  }, [ isMenuControlled, onMenuOpen ]);

  const handleMenuClose = useCallback(() => {
    if (animationRef.current) {
      return;
    }
    if (!isMenuControlled) {
      setIsMenuManaged(true);
    }
    setSelectIsOpen(false);
    onMenuClose();
  }, [ isMenuControlled, onMenuClose ]);

  const onContainerClick = useCallback(
    (e: MouseEvent<HTMLDivElement>) => {
      if (typeof onClick === "function") {
        onClick({
          event: e.nativeEvent,
          stateActions,
          ref: selectRef,
          getData: () => ({
            value: selectValue,
            options: selectOptions,
            original: {
              value,
              options,
            },
          }),
        });
      }
    }, [ selectOptions, onClick, selectValue, value, options ]
  );

  const handleValidate = (e: CustomEvent) => {
    const containerEl = getElRefHelper(containerRef);
    if (containerEl) {
      containerEl.classList.toggle(constants.stateClasses.invalid, !e.detail.isValid);
    }
  };

  const setSelectIsOpenState = useCallback<Dispatch<SetStateAction<boolean | undefined>>>((nextState) => {
    setSelectIsOpen((prev) => {
      const resolvedState = typeof nextState === "function"
        ? (nextState as (prevState: boolean | undefined) => boolean | undefined)(prev)
        : nextState;
      return Boolean(resolvedState);
    });
  }, []);

  const stateActions = useMemo<IStateActions>(
    () => ({
      setSelectValue,
      setSelectOptions,
      setSelectDisabled,
      setSelectLoading,
      setSelectRequired,
      setSelectIsOpen: setSelectIsOpenState,
    }), [ setSelectIsOpenState ]);

  const loadOptions = useMemo(() => getDebouncedFn((
    inputValue: string,
    callback: (options: OptionsOrGroups<TOptionType, GroupBase<TOptionType>>) => void
  ): void => {
    if (onOptionsSearchRequest) {
      void getOptions({
        value: inputValue,
        callback,
        onOptionsSearchRequest,
        stateActions,
        name,
      });
    }
  }, loadOptionsDebounce), [ onOptionsSearchRequest, stateActions, name, loadOptionsDebounce ]);

  const handleReset = useCallback(
    (e: SyntheticEvent) => {
      const originalOpts = originalOptions.current;
      const originalVal = originalValue.current;
      if (typeof onReset === "function") {
        onReset({
          event: e,
          stateActions,
          ref: selectRef,
          getData: () => ({
            value: selectValue,
            options: selectOptions,
            original: {
              value: originalVal,
              options: originalOpts,
            },
          }),
        } as TOnResetParams);
      } else {
        setSelectOptions(originalOpts);
        setSelectValue(originalVal);
      }
    }, [ onReset, selectValue, selectOptions ]
  );

  const handleChange = useCallback(
    async (data: OnChangeValue<TOptionType, typeof isMulti>, actionData: ActionMeta<TOptionType>) => {
      if (isMulti) {
        if (actionData?.action === "select-option" && actionData.option?.value) {
          lastSelectedValueRef.current = actionData.option.value;
        } else if (!Array.isArray(data) || !data.length) {
          lastSelectedValueRef.current = null;
        } else {
          const tail = data[data.length - 1];
          lastSelectedValueRef.current = tail?.value ?? null;
        }
      }
      await onOptionChange<typeof isMulti>({
        actionData,
        data,
        onSelectChangeRequest,
        stateActions,
        selectRef,
        onChange,
        name,
        selectOptions,
        inputRef,
      });
    }, [ selectOptions, onChange, onSelectChangeRequest, name, isMulti ]
  );

  const handleBlur = useCallback(
    (e: FocusEvent<HTMLInputElement>) => {
      if (typeof onBlur === "function") {
        onBlur({
          event: e.nativeEvent,
          stateActions,
          ref: selectRef,
          getData: () => ({
            value: selectValue,
            options: selectOptions,
            original: {
              value,
              options,
            },
          }),
        });
      }
    }, [ onBlur, selectOptions, selectValue, value, options ]
  );

  const handleKeyDown = useCallback(async (event: KeyboardEvent<HTMLInputElement>) => {
    if (typeof onKeyDown === "function") {
      onKeyDown({
        event: event.nativeEvent,
        stateActions,
        getData: () => ({
          value: selectValue,
          options: selectOptions,
          isLoading: selectLoading || false,
        }),
        ref: selectRef,
      });
    } else {
      const inputValue = (event.currentTarget as HTMLInputElement | null)?.value ?? "";
      if (
        event?.code?.toLowerCase() === "backspace" &&
        backspaceRemovesValue &&
        !!selectValue &&
        !isCountView &&
        !inputValue.length
      ) {
        event.preventDefault();
        if (isMulti) {
          const nextValues = (selectValue as TOptionType[]).slice(0, -1);
          const newValue = nextValues.length ? nextValues : null;
          setSelectValue(newValue);
          await handleChange(newValue, { action: "remove-value" } as ActionMeta<TOptionType>);
        } else {
          setSelectValue(null);
          if (isAsync) {
            setSelectOptions(selectEmptyOptions);
          }
          await handleChange(null, { action: "clear" } as ActionMeta<TOptionType>);
        }
      }
    }

  }, [ onKeyDown, stateActions, selectValue, selectOptions, selectLoading, backspaceRemovesValue, isCountView, isMulti, isAsync, handleChange ]);

  const resolvedCloseMenuOnSelect = closeMenuOnSelect ?? !isMulti;
  const resolvedBlurInputOnSelect = blurInputOnSelect ?? !isMulti;

  const baseProps = useMemo(() => {
    const passThrough = {
      options: selectOptions,
      isOptionDisabled: (option: TOptionType) => !!option?.data?.isDisabled || false,
      onChange: handleChange,
      onKeyDown: handleKeyDown,
      value: selectValue,
      inputId: id || name,
      closeMenuOnSelect: resolvedCloseMenuOnSelect,
      blurInputOnSelect: resolvedBlurInputOnSelect,
      isDisabled: selectDisabled || selectLoading,
      menuShouldScrollIntoView,
      isLoading: selectLoading,
      isMulti,
      className: getCN("inner"),
      classNamePrefix: getCN(),
      onBlur: handleBlur,
      name,
      form,
      placeholder,
      tabIndex,
      isSearchable,
      isClearable,
      menuIsOpen: (isMenuControlled || isMenuManaged) ? selectIsOpen : undefined,
      onMenuOpen: handleMenuOpen,
      onMenuClose: handleMenuClose,
      formatOptionLabel,
      closeMenuOnScroll,
      menuShouldBlockScroll,
      autoFocus,
      openMenuOnFocus,
      backspaceRemovesValue,
      openMenuOnClick,
      escapeClearsValue,
      captureMenuScroll,
      hideSelectedOptions,
      styles,
    };
    return Object.fromEntries(
      Object.entries(passThrough).filter(([ _, v ]) => v !== undefined)
    );
  }, [
    selectValue, handleMenuOpen, handleMenuClose, selectDisabled, selectLoading,
    selectOptions, handleChange, handleKeyDown, handleBlur, name, selectIsOpen,
    id, isMulti, form, placeholder, tabIndex, isSearchable,
    isClearable, formatOptionLabel,
    resolvedCloseMenuOnSelect, closeMenuOnScroll, menuShouldBlockScroll, autoFocus,
    openMenuOnFocus, backspaceRemovesValue, openMenuOnClick, resolvedBlurInputOnSelect,
    escapeClearsValue, menuShouldScrollIntoView, captureMenuScroll,
    hideSelectedOptions, styles, isMenuControlled, isMenuManaged,
  ]);

  const components = useMemo(() => getComponents<typeof isMulti>(customComponents), [ customComponents ]);

  const className = getCN("", {
    isOpen: !!selectIsOpen,
    isValue: Array.isArray(selectValue) ? !!selectValue.length : !!selectValue,
    isSearchable,
    isCheckboxView,
    isAsync,
    isMulti,
    isCountView,
    ...extraClasses,
  });

  const ctx = useMemo<TSelectCtx>(
    () => ({
      getCN,
      icons,
      isAsync,
      isSearchable,
      isCheckboxView,
      name,
      form,
      isAutoScroll,
      stateActions,
      inputRef,
      handleValidate,
      handleReset,
      getOptionsModify,
      selectValue,
      isRequired: selectRequired,
      isDisabled: selectDisabled,
      isCountView,
      isMulti,
      formatCount,
      formatOptionLabel,
    }), [ icons, isAsync, isSearchable, isCheckboxView, name, form,
      isAutoScroll, stateActions, inputRef, handleValidate, handleReset,
      getOptionsModify, selectValue, selectRequired, selectDisabled, isCountView,
      isMulti, formatCount, formatOptionLabel ]);

  const onAnimationStart = useCallback<AnimationEventHandler>((e) => {
    const el = getElRefHelper(containerRef);
    if (!!el && !!e.target && (e.target as HTMLElement).classList.contains(getCN("menu"))) {
      animationRef.current = true;
      el.classList.add(stateClasses.isTransition);
      // fallback
      wait(1000).finally(() => {
        animationRef.current = false;
        el.classList.remove(stateClasses.isTransition);
      });
    }
  }, [ getCN, stateClasses.isTransition ]);

  const onAnimationEnd = useCallback<AnimationEventHandler>((e) => {
    const el = getElRefHelper(containerRef);
    if (!!el && !!e.target && (e.target as HTMLElement).classList.contains(getCN("menu"))) {
      animationRef.current = false;
      el.classList.remove(stateClasses.isTransition);
    }
  }, [ getCN, stateClasses.isTransition ]);

  const onTouchEnd = useCallback<TouchEventHandler>((e) => {
    e.stopPropagation();
  }, []);

  useImperativeHandle(
    ref, () => ({
      el: getElRefHelper(containerRef) as HTMLDivElement | null,
      ref: selectRef,
      utils: null,
      getData: () => ({
        value: selectValue,
        options: selectOptions,
        isLoading: !!selectLoading,
      }),
      stateActions,
    }), [ selectValue, selectOptions, selectLoading, stateActions ]
  );

  useEffect(() => {
    if (typeof onMount === "function") {
      onMount();
    }
    return () => {
      if (typeof onUnmount === "function") {
        onUnmount();
      }
    };
  }, []);

  useEffect(() => {
    if (prevValue.current !== selectValue) {
      const valueInput = getElRefHelper(inputRef);
      if (valueInput) {
        bubble(valueInput, "change", {
          type: "select",
          refs: { selectRef, inputRef },
          data: {
            action: "change",
            name,
            value: selectValue,
            options: selectOptions,
          },
          stateActions,
        });
      }
      console.debug(`[Select] Value was changed for select with id/name "${id || name}"`, selectValue);
      prevValue.current = selectValue;
    }
  }, [ selectValue, selectOptions, stateActions, id, name ]);

  useEffect(() => {
    if (!isOptionsEqual(options, prevOptions.current)) {
      setSelectOptions(options);
      if (value !== undefined) {
        setSelectValue(options.length ? normalizedValue : null);
      }
      prevOptions.current = options;
    }
  }, [ options, normalizedValue, value ]);

  useEffect(() => {
    if (value !== undefined) {
      setSelectValue((prev) => (prev !== normalizedValue ? normalizedValue : prev));
    }
  }, [ value, normalizedValue ]);

  useEffect(() => {
    const containerEl = getElRefHelper(containerRef);
    if (containerEl) {
      const prevStateClasses = prevStateClassesRef.current;
      containerEl.classList.remove(
        prevStateClasses.isRequired, prevStateClasses.isDisabled, prevStateClasses.isLoading
      );
      containerEl.classList.toggle(stateClasses.isRequired, !!selectRequired);
      containerEl.classList.toggle(stateClasses.isDisabled, !!selectDisabled);
      containerEl.classList.toggle(stateClasses.isLoading, !!selectLoading);
      prevStateClassesRef.current = stateClasses;
    }
  }, [ stateClasses, selectRequired, selectDisabled, selectLoading ]);

  useEffect(() => {
    if (isRequired !== selectRequired) {
      setSelectRequired(isRequired);
    }
  }, [ isRequired ]);

  useEffect(() => {
    if (isDisabled !== selectDisabled) {
      setSelectDisabled(isDisabled);
    }
  }, [ isDisabled ]);

  useEffect(() => {
    if (isLoading !== selectLoading) {
      setSelectLoading(isLoading);
    }
  }, [ isLoading ]);

  useEffect(() => {
    if (isMenuControlled) {
      setSelectIsOpen(Boolean(isOpen));
    }
  }, [ isMenuControlled, isOpen ]);

  useEffect(() => {
    if (!isMenuControlled) {
      return;
    }
    if (prevControlledOpen.current === undefined) {
      prevControlledOpen.current = selectIsOpen;
      if (selectIsOpen) {
        onMenuOpen();
      }
      return;
    }
    if (prevControlledOpen.current !== selectIsOpen) {
      selectIsOpen ? onMenuOpen() : onMenuClose();
      prevControlledOpen.current = selectIsOpen;
    }
  }, [ isMenuControlled, selectIsOpen, onMenuOpen, onMenuClose ]);

  useEffect(() => {
    if (!selectIsOpen) {
      return;
    }
    const onDocumentClick = (e: MouseEvent) => {
      const target = e.target;
      const containerEl = getElRefHelper(containerRef);
      const isInsideSelect = (
        (!!containerEl && !!target && containerEl.contains(target as Node))
        || (target as HTMLElement).getAttribute("role") === "option"
      );
      if (!!target && getDocument().documentElement.contains(target as HTMLElement) && !isInsideSelect) {
        handleMenuClose();
        selectRef.current?.blur();
      }
    };
    getDocument().addEventListener("click", onDocumentClick);
    return () => {
      getDocument().removeEventListener("click", onDocumentClick);
    };
  }, [ selectIsOpen, getCN, isMenuControlled, handleMenuClose ]);

  useEffect(() => {
    if (normalizedValue !== selectValue && typeof getOptionsModify === "function") {
      applyOptionsModify("change");
    }
  }, [ selectValue, normalizedValue, getOptionsModify, applyOptionsModify ]);

  const SSGComponent = useSSGRender("data-js-select", props);

  if (SSGComponent) {
    return SSGComponent;
  }

  return (
    <SelectContext value={ctx}>
      <div
        onClick={onContainerClick}
        onAnimationStart={onAnimationStart}
        onAnimationEnd={onAnimationEnd}
        onTouchEnd={onTouchEnd}
        ref={containerRef}
        className={className}
        {...extraAttrs}
      >
        {isAsync ? (
          <AsyncSelect<TOptionType> ref={selectRef} loadOptions={loadOptions} components={components} {...baseProps} />
        ) : (
          <ReactSelect<TOptionType> ref={selectRef} components={components} {...baseProps} />
        )}
      </div>
    </SelectContext>
  );
};

export type {
  ISelect, ISelectRef, TOptionType, TSelectPackageAPI, TSelectValue,
};
export { Select };
