import React, {
  useState, useEffect, useCallback,
} from 'react';
import cn from 'classnames';
import isValidDate from 'date-fns/isValid';
import isAfter from 'date-fns/isAfter';
import format from 'date-fns/format';
import parse from 'date-fns/parse';
import differenceInDays from 'date-fns/differenceInCalendarDays';
import getDate from 'date-fns/getDate';
import getMonth from 'date-fns/getMonth';
import getYear from 'date-fns/getYear';
import max from 'date-fns/max';
import min from 'date-fns/min';

import { Toggle } from '@veeqo/ui';
import {
  Label,
  DatePickerContainer,
  CalendarContainer,
  Calendar,
  ControlsContainer,
  DateRangeLabel,
  Select,
  SelectButton,
  SelectItemsContainer,
  SelectItem,
  DateRangeInputsContainer,
  DateRangeInputContainer,
  DateRangeInputsDivider,
  DateRangeInput,
  ComparePeriodContainer,
  ComparePeriodLabel, SubmitButton, ButtonContainer,
} from './styled';

import { ComparePeriods, DatePickerProps, DateRange } from './types';
import { comparePeriodsList, todayDate } from './constants';
import { dateRanges, dateRangesByTimezone } from './dateRanges';

type ClassNamesReturnPayload = {
  container?: string;
  calendarContainer?: string;
  calendar?: string;
  controls?: string;
  dateRangeSelect?: string;
  dateRangeButton?: string;
  dateRangeItemsContainer?: string;
  dateRangeItem?: string;
  startDate?: string;
  endDate?: string;
  comparePeriodSwitch?: string;
  comparePeriodSelect?: string;
  comparePeriodButton?: string;
  comparePeriodItemsContainer?: string;
  comparePeriodItem?: string;
};

const generateClassNames = (prefix?: string): ClassNamesReturnPayload => ({
  container: prefix ? `${prefix}-container` : undefined,
  calendarContainer: prefix ? `${prefix}-calendar-container` : undefined,
  calendar: prefix ? `${prefix}-calendar` : undefined,
  controls: prefix ? `${prefix}-controls-container` : undefined,
  dateRangeSelect: prefix ? `${prefix}-date-range-select` : undefined,
  dateRangeButton: prefix ? `${prefix}-date-range-button` : undefined,
  dateRangeItemsContainer: prefix ? `${prefix}-date-range-items-container` : undefined,
  dateRangeItem: prefix ? `${prefix}-date-range-item` : undefined,
  startDate: prefix ? `${prefix}-start-date-input` : undefined,
  endDate: prefix ? `${prefix}-end-date-input` : undefined,
  comparePeriodSwitch: prefix ? `${prefix}-compare-period-switch` : undefined,
  comparePeriodSelect: prefix ? `${prefix}-compare-period-select` : undefined,
  comparePeriodButton: prefix ? `${prefix}-compare-period-button` : undefined,
  comparePeriodItemsContainer: prefix ? `${prefix}-compare-period-items-container` : undefined,
  comparePeriodItem: prefix ? `${prefix}-compare-period-item` : undefined,
});

const isValid = (date: Date) => isValidDate(date) && isAfter(date, new Date('1/1/1000'));

const isEqualDates = (startDate: Date, endDate: Date) => (
  getDate(startDate) === getDate(endDate)
  && getMonth(startDate) === getMonth(endDate)
  && getYear(startDate) === getYear(endDate)
);

const DatePicker = ({
  className,
  e2eClassName,
  startDate,
  endDate,
  minDate,
  maxDate,
  dateFormat = 'MM/dd/yyyy',
  showComparePeriod = false,
  vertical = false,
  defaultComparePeriod = 'Previous week',
  timeZone,
  onDateChange,
  onComparePeriodChange,
  submitButtonText,
  onSubmitButtonClick,
}: DatePickerProps) => {
  const classNames = generateClassNames(className);
  const e2eClassNames = generateClassNames(e2eClassName);

  // Date() => MM/dd/yyyy
  const formatDate = useCallback((date: Date) => format(date, dateFormat), [dateFormat]);

  // MM/dd/yyyy => Date()
  const parseDate = (date: string, currentDate: Date | null = null) => {
    let parsedDate = parse(date, dateFormat, maxDate || todayDate);
    if (!isValid(parsedDate)) {
      if (date.length > 0) {
        parsedDate = parse(date, dateFormat.slice(0, date.length), maxDate || todayDate);
      }
      if (!isValid(parsedDate)) {
        parsedDate = currentDate || maxDate || todayDate;
      }
    }
    return parsedDate;
  };

  /**
   * Ensures that date falls between the valid range of [minDate, maxDate].
   * @param date Date to test against the valid range.
   * @returns - date if date is already in the valid range.
   *          - minDate if date is before minDate.
   *          - maxDate if date is after maxDate.
   */
  const getNearestDateBetweenValidRange = (date: Date) => {
    let nearestValidDate = new Date(date);
    if (minDate) {
      nearestValidDate = max([nearestValidDate, minDate]);
    }
    nearestValidDate = min([nearestValidDate, maxDate || todayDate]);
    return nearestValidDate;
  };

  const [startDateInputValue, setStartDateInputValue] = useState(formatDate(startDate));
  const [endDateInputValue, setEndDateInputValue] = useState(
    endDate !== null ? formatDate(endDate) : formatDate(startDate),
  );

  const [isDateRangeOpen, setIsDateRangeOpen] = useState(false);

  const [isComparePeriodVisible, setisComparePeriodVisible] = useState(showComparePeriod && defaultComparePeriod !== 'None');
  const [isComparePeriodOpen, setisComparePeriodOpen] = useState(false);
  const [currentComparePeriod, setCurrentComparePeriod] = useState(defaultComparePeriod);

  const currentDateRanges = timeZone ? dateRangesByTimezone(timeZone) : dateRanges;

  useEffect(() => {
    setStartDateInputValue(formatDate(startDate));

    // null in endDate can be when the start date is selected but the end date isn't
    if (endDate !== null) {
      setEndDateInputValue(formatDate(endDate));
    } else {
      setEndDateInputValue(formatDate(startDate));
    }
  }, [startDate, endDate, formatDate]);

  const onInputBlur = (start: string, end: string | null) => {
    if (!end) {
      const dateRange: DateRange = [getNearestDateBetweenValidRange(parseDate(start, startDate)),
        null];

      if (isEqualDates(dateRange[0], startDate) && endDate === null) return null;

      return onDateChange(dateRange);
    }

    let dateRange: DateRange = [parseDate(start, startDate), parseDate(end, endDate)];

    if (differenceInDays(dateRange[1], dateRange[0]) <= 0) {
      dateRange = [parseDate(end, startDate), parseDate(end, endDate)];
    }

    // Ensure that dateRange is in the valid range of [minDate, maxDate]
    dateRange = [getNearestDateBetweenValidRange(dateRange[0]),
      getNearestDateBetweenValidRange(dateRange[1])];

    if (
      isEqualDates(dateRange[0], startDate)
      && isEqualDates(dateRange[1], endDate || startDate)
    ) return null;

    return onDateChange(dateRange);
  };

  const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
    const eventKey = event.key;
    if (eventKey === 'Enter') {
      event.currentTarget.blur();
    }
  };

  const onChangeDateRange = (dateRange: any) => {
    onDateChange([dateRange.startDate, dateRange.endDate]);
    setIsDateRangeOpen(false);
  };

  let currentDateRange = Object.values(currentDateRanges).find((dateRange) => {
    const isStart = isEqualDates(startDate, dateRange.startDate);
    const isEnd = isEqualDates(endDate || startDate, dateRange.endDate);
    return isStart && isEnd;
  });

  if (!currentDateRange) {
    currentDateRange = {
      label: 'Custom',
      startDate,
      endDate: endDate || startDate,
    };
  }

  return (
    <DatePickerContainer
      vertical={vertical}
      className={cn(classNames?.container, e2eClassNames?.container)}
    >
      <CalendarContainer
        vertical={vertical}
        className={cn(classNames?.calendarContainer, e2eClassNames?.calendarContainer)}
      >
        <Calendar
          className={cn(classNames?.calendar, e2eClassNames?.calendar)}
          selected={startDate}
          startDate={startDate}
          endDate={endDate}
          onChange={onDateChange}
          dateFormat={dateFormat}
          monthsShown={vertical ? 1 : 2}
          minDate={minDate}
          maxDate={maxDate || currentDateRanges.Today.endDate}
          selectsRange
          inline
        />
      </CalendarContainer>
      <ControlsContainer
        vertical={vertical}
        className={cn(classNames?.controls, e2eClassNames?.controls)}
      >
        <DateRangeLabel variant="bodyBold">Date range:</DateRangeLabel>
        <Select
          shouldShowDropdown={isDateRangeOpen}
          className={cn(classNames?.dateRangeSelect, e2eClassNames?.dateRangeSelect)}
          onClickAway={() => setIsDateRangeOpen(false)}
          timeout={200}
          vertical={vertical}
          cta={(
            <SelectButton
              className={cn(classNames?.dateRangeButton, e2eClassNames?.dateRangeButton)}
              dropdown
              onClick={() => setIsDateRangeOpen(!isDateRangeOpen)}
              size="sm"
            >
              {currentDateRange.label}
            </SelectButton>
          )}
        >
          <SelectItemsContainer
            className={cn(
              classNames?.dateRangeItemsContainer,
              e2eClassNames?.dateRangeItemsContainer,
            )}
          >
            {Object.values(currentDateRanges).map((dateRange) => (
              <SelectItem
                className={cn(classNames?.dateRangeItem, e2eClassNames?.dateRangeItem)}
                key={dateRange.label}
                variant="body"
                onClick={() => onChangeDateRange(dateRange)}
              >
                {dateRange.label}
              </SelectItem>
            ))}
          </SelectItemsContainer>
        </Select>
        <DateRangeInputsContainer>
          <DateRangeInputContainer>
            <Label variant="bodyBold">Start date:</Label>
            <DateRangeInput
              className={cn(classNames?.startDate, e2eClassNames?.startDate)}
              value={startDateInputValue}
              onChange={(date) => setStartDateInputValue(date)}
              onBlur={((e: React.FormEvent<HTMLInputElement>) => {
                onInputBlur(e.currentTarget.value, endDate ? formatDate(endDate) : null);
              }) as any}
              onKeyDown={onInputKeyDown}
            />
          </DateRangeInputContainer>
          <DateRangeInputsDivider variant="bodyBold">–</DateRangeInputsDivider>
          <DateRangeInputContainer>
            <Label variant="bodyBold">End date:</Label>
            <DateRangeInput
              className={cn(classNames?.endDate, e2eClassNames?.endDate)}
              value={endDateInputValue}
              onChange={(date) => setEndDateInputValue(date)}
              onBlur={((e: React.ChangeEvent<HTMLInputElement>) => {
                onInputBlur(formatDate(startDate), e.currentTarget.value);
              }) as any}
              onKeyDown={onInputKeyDown}
            />
          </DateRangeInputContainer>
        </DateRangeInputsContainer>
        {showComparePeriod ? (
          <ComparePeriodContainer>
            <Toggle
              className={cn(classNames?.comparePeriodSwitch, e2eClassNames?.comparePeriodSwitch)}
              onClick={() => {
                setisComparePeriodVisible(!isComparePeriodVisible);
                if (!isComparePeriodVisible && onComparePeriodChange) {
                  onComparePeriodChange(currentComparePeriod);
                }
                if (isComparePeriodVisible && onComparePeriodChange) {
                  onComparePeriodChange('None');
                }
              }}
              switched={isComparePeriodVisible}
              small
            />
            <ComparePeriodLabel variant="bodyBold">Compare previous period</ComparePeriodLabel>
          </ComparePeriodContainer>
        ) : null}
        {isComparePeriodVisible ? (
          <Select
            shouldShowDropdown={isComparePeriodOpen}
            className={cn(classNames?.comparePeriodSelect, e2eClassNames?.comparePeriodSelect)}
            onClickAway={() => setisComparePeriodOpen(false)}
            timeout={200}
            vertical={vertical}
            cta={(
              <SelectButton
                className={cn(classNames?.comparePeriodButton, e2eClassNames?.comparePeriodButton)}
                dropdown
                onClick={() => setisComparePeriodOpen(!isComparePeriodOpen)}
                size="sm"
              >
                {currentComparePeriod}
              </SelectButton>
            )}
          >
            <SelectItemsContainer
              className={cn(
                classNames?.comparePeriodItemsContainer,
                e2eClassNames?.comparePeriodItemsContainer,
              )}
            >
              {comparePeriodsList.map((prevPeriod: ComparePeriods) => (
                <SelectItem
                  className={cn(classNames?.comparePeriodItem, e2eClassNames?.comparePeriodItem)}
                  key={prevPeriod}
                  variant="body"
                  onClick={() => {
                    setisComparePeriodOpen(false);
                    setCurrentComparePeriod(prevPeriod);
                    if (onComparePeriodChange) onComparePeriodChange(prevPeriod);
                  }}
                >
                  {prevPeriod}
                </SelectItem>
              ))}
            </SelectItemsContainer>
          </Select>
        ) : null}
        {submitButtonText && (
        <ButtonContainer vertical={vertical}>
          <SubmitButton
            variant="primary"
            size="sm"
            marginLeft={vertical ? '0px' : 'auto'}
            width="35%"
            onClick={() => {
              setIsDateRangeOpen(false);
              onSubmitButtonClick!([startDate, endDate || startDate]);
            }}
          >
            {submitButtonText}
          </SubmitButton>
        </ButtonContainer>
        )}
      </ControlsContainer>
    </DatePickerContainer>
  );
};

DatePicker.dateRanges = dateRanges;
DatePicker.dateRangesByTimezone = dateRangesByTimezone;

export default DatePicker;
