import { isValidDate } from "@web3r/flowerkit/date";
import { bubble } from "@web3r/flowerkit/evt";
import { wait } from "@web3r/flowerkit/fn";
import { isWithinInterval } from "date-fns";
import type { ReactNode, FC } from "react";
import {
  memo,
  useCallback,
  useEffect,
  useState,
  useRef,
  useMemo,
  useImperativeHandle,
} from "react";
import * as Picker from "react-datepicker";
import type { DatePicker as DatePickerInstance, ReactDatePickerCustomHeaderProps, DatePickerProps } from "react-datepicker";
import type { IComponentBaseProps } from "#package/types";
import {
  defaultLocales, ruLocale, todayMinus100Years, todayPlus30Years,
} from "./config/constants";
import type {
  IDatePicker,
  TNativeDatepickerEvent,
  TStateActions,
  TValidDateSingleStamp,
  TValidDateUnionStamp,
  TValidDateRangeStamp,
  TDatePickerInputProps,
  TDatePickerCustomHeaderProps,
  TDatePickerCallbackArg,
  TDatePickerCustomHeaderSlotProps,
  TCustomMaskArg, IDatePickerRef,
} from "./config/types";
import { DatePickerContext } from "./context";
import {
  getDateFormat,
  getDateInstance,
  getAsyncFn,
  getIsValidRange,
  getClearDate,
  getDateWithLocalTime,
  getStrFromStamp,
  getDefaultMask,
} from "./lib";
import { DatePickerCustomHeader } from "./ui/DatePickerCustomHeader";
import { DatePickerCustomInput } from "./ui/DatePickerCustomInput";
import { constants } from "../../constants";
import { getElRefHelper } from "../../utils/react/getElRefHelper";
import { useCN } from "../../utils/react/useCN";
import { useListeners } from "../../utils/react/useListeners";
import { useSSGRender } from "../../utils/react/useSSGRender";
import type { IResetEventParams, IInputRef } from "../input";
import { Input } from "../input";
import "./style.pcss";

const PickerApp = Picker.default;

const MemoHeader = memo((props: TDatePickerCustomHeaderProps) => <DatePickerCustomHeader {...props} />);
MemoHeader.displayName = "DatePickerCustomHeader";

const MemoInput = memo((props: TDatePickerInputProps) => <DatePickerCustomInput {...props} />);
MemoInput.displayName = "DatePickerCustomInput";

/**
 * Компонент всплывающего календаря
 * @returns {ReactNode}
 */
const DatePicker: FC<IDatePicker & IComponentBaseProps> = (props): ReactNode => {

  const {
    baseClass = "datePicker",
    extraAttrs = {},
    extraClasses = {},
    inputExtraClasses = {},
    onChange,
    onFocus,
    onReset,
    children,
    value,
    name,
    id,
    form,
    placeholder,
    min = todayMinus100Years,
    max = todayPlus30Years,
    isDisabled = false,
    isRequired = false,
    isTime = false,
    dateFormat = "dd.MM.yyyy",
    timeFormat = "HH:mm",
    getDateValidation = (value: TValidDateUnionStamp) => true,
    getTimeValidation = (value: TValidDateSingleStamp) => true,
    getDateInputValue = (value: TValidDateUnionStamp, isTime: boolean) => getStrFromStamp(value, isTime),
    timeIntervals = 15,
    isRange = false,
    locale = ruLocale,
    locales = defaultLocales,
    pickerCustomization = {},
    onCtrlChange,
    slotHeader,
    isFastNav = false,
    iconPrev = "prev",
    iconNext = "next",
    onMount,
    onUnmount,
    getInputMask = (props: TCustomMaskArg) => getDefaultMask(props),
    isAutoFix = false,
    onUserInputChange,
    onValidate,
    ref,
  } = props || {};

  const { getCN } = useCN(baseClass);

  // Состояние промежутка дат
  const [ isRanged, setIsRanged ] = useState<boolean>(() => (isRange && getIsValidRange(value)) || getIsValidRange(value) || isRange && !value);

  // Состояние выбранной даты (может быть null)
  const [ date, setDate ] = useState<TValidDateSingleStamp>(() => getIsValidRange(value) ? getDateInstance((value as [ Date, Date ])[0]) : getDateInstance(value));

  // Конечная дата (может быть null)
  const [ endDate, setEndDate ] = useState<TValidDateSingleStamp>(() => getIsValidRange(value) ? getDateInstance((value as [ Date, Date ])[1]) : null);


  // Если календарь со временем, то максимум и минимум без часов
  const [ minDate, setMinDate ] = useState<TValidDateSingleStamp>(() => isTime ? getDateInstance(min) : getClearDate(getDateInstance(min)));
  const [ maxDate, setMaxDate ] = useState<TValidDateSingleStamp>(() => isTime ? getDateInstance(max) : getClearDate(getDateInstance(max)));

  // Прочие состояния
  const [ currentLocale, setCurrentLocale ] = useState<IDatePicker["locale"]>(locale);
  const [ isDisabledState, setIsDisabledState ] = useState<IDatePicker["isDisabled"]>(isDisabled);

  // Агрегатор состояний
  const stateActions = useMemo<TStateActions>(() => ({
    setInputDisabledState: setIsDisabledState,
    setValue: setDate,
    setEndDate,
    setMinDate,
    setMaxDate,
    setCurrentLocale,
  }), []);

  // Референсы
  const inputRef = useRef<IInputRef>(null);
  const pickerRef = useRef<DatePickerInstance | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  // Предыдущие значения
  const prevMinDate = useRef<TValidDateSingleStamp>(minDate);
  const prevMaxDate = useRef<TValidDateSingleStamp>(maxDate);
  const prevIsDisabled = useRef<IDatePicker["isDisabled"]>(isDisabledState);
  const prevLocale = useRef<IDatePicker["locale"]>(locale);
  const prevIsRanged = useRef<IDatePicker["isRange"]>(isRanged);
  const originalEndDateValue = useRef<TValidDateSingleStamp>(getIsValidRange(value) ? getDateInstance((value as [ Date, Date ])[1]) : null);
  const prevDate = useRef<TValidDateSingleStamp>(date);
  const originalDateValue = useRef<TValidDateSingleStamp>(getIsValidRange(value) ? getDateInstance((value as [ Date, Date ])[0]) : getDateInstance(value));
  const prevEndDate = useRef<TValidDateSingleStamp>(endDate);

  // Формат вывода
  const currentDateFormat = useMemo<string>(() => getDateFormat(isTime, dateFormat, timeFormat), [ isTime, timeFormat, dateFormat ]);

  // Значение для поля
  const dateInputValue = useMemo<string>(() => getDateInputValue(!!endDate ? [ date, endDate ] : date, isTime), [ date, endDate, isTime ]);

  const prevInputValue = useRef(dateInputValue);

  const setInputBlur = () => {
    const container = getElRefHelper(containerRef);
    const input = getElRefHelper(inputRef);
    if (container && input) {
      wait(200).finally(() => {
        bubble(input, "blur", {
          container,
        });
      });
    }
  };

  /**
   * Callback для ручного ввода поля
   */
  const handleUserInputChange = useCallback((event?: TNativeDatepickerEvent) => {
    if (event) {
      if (typeof onUserInputChange === "function") {
        onUserInputChange({
          event,
          stateActions,
          data: {
            name,
            value: date,
            dateInputValue,
          },
          ref: pickerRef,
        });
      } else {
        event.preventDefault();
      }
    }
  }, [ onUserInputChange, dateInputValue, date, name ]);

  const handleChange = useCallback(async (value: TValidDateUnionStamp, e?: TNativeDatepickerEvent) => {

    // Проверка на предыдущую дату
    const getComparedDate = (val: TValidDateUnionStamp) => {
      if (isRanged) { // range
        return val;
      } else {
        if (isValidDate(val)) {
          return isTime ? (val as Date).getTime() : getClearDate(new Date(val as Date)).getTime();
        } else {
          return val;
        }
      }
    };

    const oldDate = getComparedDate(prevDate.current);
    const newDate = getComparedDate(value);

    if (oldDate !== newDate) {

      // Штамп с локальным временем пользователя
      const stamp = isValidDate(value) && !isTime ? getDateWithLocalTime(value as Date) : value;

      if (typeof onChange === "function") {
        const event = new CustomEvent("change", {
          bubbles: true,
          cancelable: true,
          detail: {
            type: "date" as const,
            nativeEvent: e,
            value: stamp,
            name,
            target: getElRefHelper(inputRef) as HTMLInputElement,
          },
        });
        onChange({
          event,
          data: {
            name,
            value: stamp,
            dateInputValue,
          },
          ref: pickerRef,
          stateActions,
        });
      } else {
        const fn = (value: TValidDateSingleStamp) => getAsyncFn(() => getDateValidation(value));
        if (isRanged) {
          const [ start, end ] = stamp as TValidDateRangeStamp;
          const isStartDateValid = await fn(start);
          const isEndDateValid = await fn(end);
          if (isStartDateValid && isEndDateValid) {
            setDate(start);
            setEndDate(end);
          } else {
            setDate(null);
            setEndDate(null);
          }
        } else {
          const isValueValid = await fn(stamp as TValidDateSingleStamp);
          if (isValueValid) {
            setDate(stamp as TValidDateSingleStamp);
          } else {
            setDate(null);
          }
        }
      }
    }

    setInputBlur();

  }, [ onChange, getDateValidation, isRanged, isTime ]);

  const handleFocus = useCallback((event?: TNativeDatepickerEvent) => {
    if (typeof onFocus === "function" && event) {
      onFocus({
        event,
        data: {
          name,
          value: date,
          dateInputValue,
        },
        ref: pickerRef,
        stateActions,
      });
    }
  }, [ onFocus, name, date, dateInputValue ]);

  const handleLocaleChange = (e: CustomEvent<{ lang: string; }>) => {
    const lang = e.detail.lang;
    setCurrentLocale(lang);
  };

  const handleReset = useCallback((event: IResetEventParams) => {
    if (typeof onReset === "function") {
      onReset({
        event: event.event,
        data: {
          name,
          value: date,
          dateInputValue,
        },
        ref: pickerRef,
        stateActions,
      });
    } else {
      setDate(originalDateValue.current);
      setEndDate(originalEndDateValue.current);
      if (pickerRef.current) {
        pickerRef.current.setOpen(false);
      }
    }
  }, [ onReset, date, dateInputValue ]);

  const handleValidate = useCallback((e: CustomEvent<{ isValid: boolean; }>) => {
    if (typeof onValidate === "function") {
      onValidate({
        event: e,
        stateActions,
      });
    } else {
      containerRef?.current?.classList.toggle(constants.stateClasses.invalid, !e.detail.isValid);
    }
  }, [ onValidate ]);

  useEffect(() => {
    const isRanged = getIsValidRange(value);
    const newDate = isRanged ? getDateInstance((value as TValidDateRangeStamp)[0]) : getDateInstance(value);
    const newEndDate = isRanged ? getDateInstance((value as TValidDateRangeStamp)[1]) : null;
    const newMinDate = getDateInstance(min);
    const newMaxDate = getDateInstance(max);

    if (newDate !== prevDate.current) {
      setDate(newDate);
    }

    const processedMinDate = isTime ? newMinDate : getClearDate(newMinDate);
    if (processedMinDate !== prevMinDate.current) {
      setMinDate(processedMinDate);
      prevMinDate.current = processedMinDate;
    }

    const processedMaxDate = isTime ? newMaxDate : getClearDate(newMaxDate);
    if (processedMaxDate !== prevMaxDate.current) {
      setMaxDate(processedMaxDate);
      prevMaxDate.current = processedMaxDate;
    }

    if (isDisabled !== prevIsDisabled.current) {
      setIsDisabledState(isDisabled);
      prevIsDisabled.current = isDisabled;
    }

    if (locale !== prevLocale.current) {
      setCurrentLocale(locale);
      prevLocale.current = locale;
    }

    const newIsRanged = isRanged || isRange;
    if (newIsRanged !== prevIsRanged.current) {
      setIsRanged(newIsRanged);
      prevIsRanged.current = newIsRanged;
    }

    if (newEndDate !== prevEndDate.current) {
      setEndDate(newEndDate);
      prevEndDate.current = newEndDate;
    }
  }, [ min, max, value, isDisabled, locale, isRange, isTime ]);

  useEffect(() => {
    // add locales
    (locales || []).forEach(({ name, data }) => {
      if (name && data) {
        Picker.registerLocale(name, data);
      } else {
        console.error(`[DatePicker] Missing prop "name" or "data" for locale object`);
      }
    });

    if (typeof onMount === "function") {
      onMount();
    }
    return () => {
      if (typeof onUnmount === "function") {
        onUnmount();
      }
    };

  }, []);

  useEffect(() => {
    if (dateInputValue !== prevInputValue.current) {
      const inputNode = getElRefHelper(inputRef);
      if (inputNode) {
        const setBubble = () => {
          bubble(inputNode, "change", {
            data: {
              name,
              value: date,
              endDate,
              dateInputValue,
            },
            ref: pickerRef,
            stateActions,
          });
        };
        if (isRanged) {
          if (endDate) {
            setBubble();
          }
        } else {
          setBubble();
        }
      }
      prevInputValue.current = dateInputValue;
    }
  }, [ dateInputValue ]);

  useEffect(() => {
    if (date !== prevDate.current) {
      prevDate.current = date;
    }
  }, [ date ]);

  useListeners({
    listeners: {
      [constants.localisationBubbles.localisationChange]: handleLocaleChange,
    },
  });

  useImperativeHandle(ref, (): IDatePickerRef => ({
    el: containerRef.current,
    ref: pickerRef,
    utils: null,
    getData: () => ({
      rawValue: isRanged ? [ date, endDate ] : date,
      value: dateInputValue,
    }),
    stateActions,
  }), [ isRanged, date, endDate, dateInputValue ]);

  const isDateTimeInRange = (currentDate: Date) => {
    switch (true) {
      case typeof getTimeValidation === "function":
        return getTimeValidation(currentDate);
      case !!(minDate && maxDate):
        return isWithinInterval(currentDate, { start: minDate, end: maxDate });
      default:
        return true;
    }
  };

  const renderCustomHeader = useCallback((props: ReactDatePickerCustomHeaderProps) => <MemoHeader pickerRef={pickerRef} iconPrev={iconPrev} iconNext={iconNext}
    maxDate={maxDate!} minDate={minDate!} name={name} dateInputValue={dateInputValue}
    isFastNav={isFastNav} isTime={isTime}
    timeFormat={timeFormat} dateFormat={dateFormat}
    onCtrlChange={onCtrlChange}
    slotHeader={slotHeader} {...props} />, [ iconPrev, iconNext, isFastNav, isTime, slotHeader, onCtrlChange, maxDate, minDate, timeFormat, dateFormat, name, dateInputValue ]);

  const requiredValue = useMemo(() => {
    if (typeof isRequired === "boolean" && isRequired) {
      return isRanged ? "dateRange" : "date";
    } else {
      return isRequired;
    }
  }, [ isRequired, isRanged ]);

  const classNames = {
    isRange: isRanged,
    isTime,
    isRequired,
    ...extraClasses,
  };

  const extraInputAttrs = {
    "data-js-component-input": "datePicker",
    "data-js-input-validation-bubble": "true",
    ...isRanged && {
      "data-js-datepicker-from": getDateInputValue(date, isTime),
      "data-js-datepicker-to": getDateInputValue(endDate, isTime),
    },
  };

  const context = useMemo(() => ({
    getCN,
  }), [ getCN ]);

  const safePickerCustomization = pickerCustomization as Omit<DatePickerProps, "onChange" | "selectsMultiple" | "formatMultipleDates">;

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

  if (SSGComponent) {
    return SSGComponent;
  }

  return (
    <DatePickerContext value={context}>
      <div ref={containerRef} data-js-observer-exclude={""} className={getCN("", classNames)} {...extraAttrs}>
        <Input id={id} onValidate={handleValidate} isDisabled={isDisabledState} onReset={handleReset}
          extraAttrs={extraInputAttrs} ref={inputRef} type={"hidden"} name={name} value={dateInputValue}
          isRequired={requiredValue} form={form} />
        {/* @ts-expect-error (some dynamic props) */}
        <PickerApp
          timeIntervals={timeIntervals}
          timeCaption={""}
          onChangeRaw={handleUserInputChange}
          timeFormat={timeFormat}
          showTimeSelect={isTime}
          dateFormat={currentDateFormat}
          id={name}
          disabled={isDisabledState}
          maxDate={maxDate!}
          minDate={minDate!}
          filterTime={isDateTimeInRange}
          placeholderText={placeholder}
          customInput={(
            <MemoInput isAutoFix={isAutoFix} placeholder={placeholder} timeIntervals={timeIntervals}
              isTime={isTime} handleChange={handleChange} pickerRef={pickerRef}
              format={currentDateFormat} date={date!} mask={isRanged ? null : getInputMask}
              minDate={minDate!} maxDate={maxDate!} isDisabled={isDisabledState}
              extraClasses={inputExtraClasses}>
              {children}
            </MemoInput>
          )}
          locale={currentLocale}
          selected={date}
          onChange={handleChange}
          onFocus={handleFocus}
          startDate={date}
          endDate={endDate}
          selectsRange={isRanged}
          renderCustomHeader={renderCustomHeader}
          ref={pickerRef}
          {...safePickerCustomization}
        />
      </div>
    </DatePickerContext>
  );

};

export type {
  IDatePicker,
  IDatePickerRef,
  TCustomMaskArg,
  TDatePickerCallbackArg,
  TDatePickerCustomHeaderSlotProps,
  TValidDateRangeStamp,
  TValidDateSingleStamp,
  TValidDateUnionStamp,
};

export { DatePicker };
