import React, {
  FC,
  ReactNode,
  RefObject,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import classNames from "classnames";
import { format, isAfter, isBefore, parse } from "date-fns";
import { Box } from "../Box";
import { bemHOF } from "../../utilities/bem";
import { Button, BUTTON_ICON_POSITION, BUTTON_VARIANT } from "../Button";
import { MenuRenderer } from "../MenuRenderer";
import { PopoverMenu } from "../PopoverMenu";
import { Icon, ICON_TYPE } from "../Icon";
import { Flex } from "../Flex";
import { Text } from "../Text";
import { Heading } from "../Heading";
import { CustomDateRangeInput } from "./CustomDateRangeInput";
import { DATE_FORMAT, parseDate } from "../../utilities";
import { DatePicker } from "../DatePicker";
import { isValidDate } from "../../utilities/isValidDate";

const cn = bemHOF("DateRangePicker");

export interface DateRangePickerValueWithDates {
  startDate: Date;
  endDate: Date;
}

export interface DateRangePickerPreset {
  name: string;
  startDate: Date;
  endDate: Date;
}

export type DateRangePickerValue =
  | DateRangePickerValueWithDates
  | null
  | DateRangePickerPreset;

export interface DateRangePickerProps {
  /**
   * Accepts a `DateRangePickerPreset` (`{ name: string; startDate: Date; endDate: Date}`),
   * a custom value (same as a preset but without a `name`), or `null`
   */
  value: DateRangePickerValue;
  /**
   * Called every time a new date range is selected
   */
  onChange: (newValue: DateRangePickerValue) => void;
  /**
   * These are the options that appear above "Custom" when the `DateRangePicker` is opened.
   * Each preset object should include a `name`, `startDate`, and `endDate`.
   */
  presets: DateRangePickerPreset[];
  /**
   * This determines how the menu aligns with the dropdown control. If right aligned, the
   * custom date selection UI will appear to the left of the list of presets.
   */
  align?: "left" | "right";
  /**
   * The earliest custom date that can be selected
   */
  minDate?: Date;
  /**
   * The latest custom date that can be selected
   */
  maxDate?: Date;
  /**
   * Classes to be added to the outermost element
   */
  className?: string;
}

interface DateRangePickerPresetProps {
  isSelected: boolean;
  children: ReactNode;
  onClick: () => void;
}

const RangePickerPreset = ({
  isSelected,
  children,
  onClick,
}: DateRangePickerPresetProps) => {
  const classes = classNames(
    cn({ e: "preset" }),
    isSelected && cn({ e: "preset", m: "isSelected" }),
  );
  return (
    <button type="button" className={classes} onClick={onClick}>
      <Heading.H4>{children}</Heading.H4>
    </button>
  );
};

const CUSTOM_PRESET = "Custom";

const getMatchingPreset = (
  value: DateRangePickerValue,
  presets: DateRangePickerPreset[],
) => {
  if (!value || !("name" in value)) return null;
  return presets.find((p) => p.name === value.name);
};

const focusInput = (ref: RefObject<HTMLInputElement>) => {
  const input = ref.current;
  const { activeElement } = document;
  if (input && input !== activeElement) {
    input.focus();
  }
};

export const DateRangePicker: FC<DateRangePickerProps> = ({
  className,
  onChange,
  align = "left",
  presets,
  value,
  minDate,
  maxDate,
  ...rest
}) => {
  const placement = align === "left" ? "bottom-start" : "bottom-end";
  const [selectingCustomDate, setSelectingCustomDate] = useState(
    !getMatchingPreset(value, presets) && value !== null,
  );
  const [
    customStartDateInputVal,
    setCustomStartDateInputVal,
  ] = useState<string>("");
  const [customStartDateError, setCustomStartDateError] = useState("");
  const [customEndDateInputVal, setCustomEndDateInputVal] = useState<string>(
    "",
  );
  const [customEndDateError, setCustomEndDateError] = useState("");
  const [customSelectingWhichDate, setCustomSelectingWhichDate] = useState<
    "start" | "end"
  >("start");

  const startInput = useRef<HTMLInputElement>(null);
  const endInput = useRef<HTMLInputElement>(null);

  const handleClose = () => {
    setCustomStartDateError("");
    if (selectingCustomDate && value && "name" in value) {
      setSelectingCustomDate(false);
    }
  };

  const selectedPreset = useMemo(() => {
    if (!value) return null;
    if (selectingCustomDate) return CUSTOM_PRESET;
    const matchingPreset = getMatchingPreset(value, presets);
    return matchingPreset ? matchingPreset.name : CUSTOM_PRESET;
  }, [presets, value, selectingCustomDate]);

  const customDateInputsValid = useMemo(() => {
    const parsedStart = parseDate(customStartDateInputVal);
    const parsedEnd = parseDate(customEndDateInputVal);
    return !!(parsedStart && parsedEnd);
  }, [customStartDateInputVal, customEndDateInputVal]);

  const displayText = useMemo(() => {
    if (!value) return "Select Date Range";
    const matchingPreset = getMatchingPreset(value, presets);
    if (selectingCustomDate && matchingPreset) return CUSTOM_PRESET;
    return selectedPreset === CUSTOM_PRESET
      ? `${format(value.startDate, DATE_FORMAT)} to ${format(
          value.endDate,
          DATE_FORMAT,
        )}`
      : selectedPreset;
  }, [selectedPreset, value, selectingCustomDate, presets]);

  useEffect(() => {
    if (!selectingCustomDate || customDateInputsValid) return;
    const inputToFocus =
      customSelectingWhichDate === "start" ? startInput : endInput;
    focusInput(inputToFocus);
  }, [customSelectingWhichDate, selectingCustomDate, customDateInputsValid]);

  const validDates = useMemo(() => {
    let start = null;
    let end = null;

    // Parse start date
    if (customStartDateInputVal) {
      const parsed = parse(customStartDateInputVal, DATE_FORMAT, new Date());
      start = isValidDate(parsed) ? parsed : null;
    }

    // Parse end date
    if (customEndDateInputVal) {
      const parsed = parse(customEndDateInputVal, DATE_FORMAT, new Date());
      end = isValidDate(parsed) ? parsed : null;
    }

    // Start date before minDate or after maxDate
    if (
      start &&
      ((minDate && isBefore(start, minDate)) ||
        (maxDate && isAfter(start, maxDate)))
    ) {
      setCustomStartDateError("Outside allowed date range");
      start = null;
    }

    // End date before minDate or after maxDate
    if (
      end &&
      ((minDate && isBefore(end, minDate)) ||
        (maxDate && isAfter(end, maxDate)))
    ) {
      setCustomEndDateError("Outside allowed date range");
      end = null;
    }

    // Start date after endDate
    if (start && end && isAfter(start, end)) {
      setCustomEndDateError("End date cannot be before start date");
      end = null;
    }

    // If start is a Date, remove any error
    if (start) {
      setCustomStartDateError("");
    }

    // If end is a Date, remove any error
    if (end) {
      setCustomEndDateError("");
    }

    return { start, end };
  }, [customStartDateInputVal, customEndDateInputVal, minDate, maxDate]);

  const handleDatePickerChange = ([newStartDate, newEndDate]: [Date, Date]) => {
    if (!newEndDate) focusInput(endInput);
    const newStartString = newStartDate
      ? format(newStartDate, DATE_FORMAT)
      : "";
    const newEndString = newEndDate ? format(newEndDate, DATE_FORMAT) : "";
    setCustomStartDateInputVal(newStartString);
    setCustomEndDateInputVal(newEndString);
  };

  const handleDatePickerSelect = (date: Date) => {
    // Handles situation where an end date is provided
    // without a start date
    if (!validDates.start && validDates.end) {
      setCustomStartDateInputVal(format(date, DATE_FORMAT));
    }
  };

  const customApplyBtnDisabled =
    !validDates.start ||
    !validDates.end ||
    !!customStartDateError ||
    !!customEndDateError;

  return (
    <MenuRenderer
      className={classNames(cn(), className)}
      placement={placement}
      onClose={handleClose}
      hasPortal
      trigger={(triggerProps) => (
        <Button
          variant={BUTTON_VARIANT.SECONDARY}
          icon={ICON_TYPE.CARET_DOWN}
          iconPosition={BUTTON_ICON_POSITION.RIGHT}
          className={cn({ e: "trigger" })}
          {...triggerProps}
        >
          <Flex className={cn({ e: "trigger-inner" })}>
            <Icon
              icon={ICON_TYPE.CALENDAR}
              className={cn({ e: "trigger-icon" })}
            />{" "}
            <Text as="span" className={cn({ e: "trigger-text" })}>
              {displayText}
            </Text>
          </Flex>
        </Button>
      )}
      {...rest}
    >
      {({ listProps, onClose }) => (
        <PopoverMenu {...listProps}>
          <Flex
            className={cn({
              e: "popover-inner-wrapper",
              m: `${align}Aligned`,
            })}
          >
            <Flex className={cn({ e: "presets-wrapper" })}>
              {presets.map((p) => (
                <RangePickerPreset
                  key={p.name}
                  onClick={() => {
                    onChange(p);
                    if (onClose) {
                      onClose();
                    }
                  }}
                  isSelected={p.name === selectedPreset}
                >
                  {p.name}
                </RangePickerPreset>
              ))}
              <RangePickerPreset
                onClick={() => {
                  if (selectingCustomDate) return;
                  setSelectingCustomDate(true);
                }}
                isSelected={selectedPreset === CUSTOM_PRESET}
              >
                Custom
              </RangePickerPreset>
            </Flex>
            {selectedPreset === CUSTOM_PRESET ? (
              <Box
                className={classNames(
                  cn({ e: "custom-wrapper" }),
                  cn({ e: "custom-wrapper", m: `${align}Aligned` }),
                )}
              >
                <Flex className={cn({ e: "custom-date-inputs-wrapper" })}>
                  <CustomDateRangeInput
                    ref={startInput}
                    value={customStartDateInputVal}
                    setValue={setCustomStartDateInputVal}
                    error={customStartDateError}
                    setError={setCustomStartDateError}
                    ariaLabel="Start Date"
                    onFocus={() => setCustomSelectingWhichDate("start")}
                  />
                  <Text as="span" className={cn({ e: "custom-dates-to" })}>
                    to
                  </Text>
                  <CustomDateRangeInput
                    ref={endInput}
                    value={customEndDateInputVal}
                    setValue={setCustomEndDateInputVal}
                    error={customEndDateError}
                    setError={setCustomEndDateError}
                    ariaLabel="End Date"
                    onFocus={() => setCustomSelectingWhichDate("end")}
                  />
                </Flex>
                <Box className={cn({ e: "custom-date-calendars-wrapper" })}>
                  <DatePicker
                    monthsShown={2}
                    inline
                    onChange={handleDatePickerChange}
                    onSelect={handleDatePickerSelect}
                    selectsRange
                    selected={validDates.start}
                    startDate={validDates.start}
                    endDate={validDates.end}
                    minDate={minDate}
                    maxDate={maxDate}
                  />
                </Box>
                <Flex className={cn({ e: "custom-date-footer" })}>
                  <Button
                    disabled={customApplyBtnDisabled}
                    onClick={() => {
                      if (onClose) {
                        onClose();
                      }
                      const startDate = parseDate(customStartDateInputVal);
                      const endDate = parseDate(customEndDateInputVal);
                      if (startDate && endDate) {
                        onChange({ startDate, endDate });
                      }
                    }}
                  >
                    Apply
                  </Button>
                </Flex>
              </Box>
            ) : null}
          </Flex>
        </PopoverMenu>
      )}
    </MenuRenderer>
  );
};
