import moment, { type Moment } from 'moment'
import React, { useEffect, useReducer } from 'react'
import { type DateRange } from 'react-day-picker'

import Button from '../../Button/Button'
import DatePicker from '../../DatePicker/DatePicker'
import Icon from '../../Icons/Icon'
import Select from '../../Form/Select/Select'
import TextInput from '../../Form/TextInput'
import { compareToOptions } from './compareTo'
import styles from './comparison_date_ranges.module.scss'
import stylesDatePicker from '../../DatePicker/_datePicker.module.scss'
import { type AlertPropsWithDates } from '../Timeframe'
import { c } from '../../../translations/LibraryTranslationService'
import {
  adjustDateRangeWithRestrictions,
  getSelectedDateRange,
  isEmptyOrWhitespace,
  parseDateRangeString,
} from '../../DatePicker/DatePickerHelper'

type CompareToType = {
  id: number
  period: string
  getCompareToDates: (state: SelectionState) => {
    previousStartDate: PickerDate
    previousEndDate: PickerDate
  }
}

export type ComparisonDatePickerProps = {
  alertProps?: AlertPropsWithDates
  initial_firstRange_startDate?: PickerDate
  initial_firstRange_endDate?: PickerDate
  initial_secondRange_startDate?: PickerDate
  initial_secondRange_endDate?: PickerDate
  /** Toggle to determine if  the comparison date range option be visible */
  showComparisonRange?: boolean
  /** Toggle to determine if the `Compare To` dropdown selector is visible */
  showCompareToSelector?: boolean
  selectionStateCallout?: (
    /** selectionState will include all four dates, compareTo value & resetInputs boolean */
    selectionState: SelectionState,
    /** dispatch action to update the value of the selectionState */
    selectionStateDispatch: React.Dispatch<SelectionStateDispatchActions>,
  ) => void
  children?: (
    // TODO: look into removing this with toggle global_filter_side_drawer_change
    /** selectionState will include all four dates, compareTo value & resetInputs boolean */
    selectionState: SelectionState,
    /** dispatch action to update the value of the selectionState */
    selectionStateDispatch: React.Dispatch<SelectionStateDispatchActions>,
  ) => React.ReactNode
  /** Label for the first range */
  firstRangeLabel?: string
  /** Label for the second range */
  secondRangeLabel?: string
}

type DateRangeTypeNew = 'firstDateRange' | 'secondDateRange'

export function ComparisonDatePicker({
  alertProps,
  initial_firstRange_startDate,
  initial_firstRange_endDate,
  initial_secondRange_startDate,
  initial_secondRange_endDate,
  showCompareToSelector,
  showComparisonRange,
  selectionStateCallout,
  children,
  firstRangeLabel = c('firstRange'),
  secondRangeLabel = c('secondRange'),
}: ComparisonDatePickerProps): React.JSX.Element {
  const [activeField, setActiveField] = React.useState<string>('firstDateRange')
  const [isAutoFocus, setIsAutoFocus] = React.useState(true)

  const [selectionState, selectionStateDispatch] = useReducer(
    (prevState: SelectionState, payload: SelectionStateDispatchActions) =>
      rangeReducer(prevState, payload, setActiveField, () =>
        setIsAutoFocus(true),
      ),
    {
      firstRange_startDate:
        initial_firstRange_startDate ?? initialState().firstRange_startDate,
      firstRange_endDate:
        initial_firstRange_endDate ?? initialState().firstRange_endDate,
      secondRange_startDate:
        initial_secondRange_startDate ?? initialState().secondRange_startDate,
      secondRange_endDate:
        initial_secondRange_endDate ?? initialState().secondRange_endDate,
      selecting: initialState().selecting,
      showComparisonRange:
        showComparisonRange ?? initialState().showComparisonRange,
      compareTo: compareToOptions[0],
      resetInputs: true,
    },
  )

  const submitDateNew = (
    inputValue: DateRangeTypeNew,
    dateRange: DateRange,
  ) => {
    if (dateRange && dateRange.from) {
      if (inputValue === 'firstDateRange') {
        selectionStateDispatch({
          action: 'dateInputChanged',
          date: moment(dateRange.from),
          inputName: 'firstRange_startDate',
        })
        selectionStateDispatch({
          action: 'dateInputChanged',
          date: moment(dateRange.to),
          inputName: 'firstRange_endDate',
        })

        // Auto-focus to second range only on initial load when first range is complete
        if (
          isAutoFocus &&
          selectionState.showComparisonRange &&
          selectionState.compareTo === compareToOptions[0] &&
          dateRange.from &&
          dateRange.to
        ) {
          const fromDate = moment(dateRange.from)
          const toDate = moment(dateRange.to)

          if (!fromDate.isSame(toDate, 'day')) {
            setActiveField('secondDateRange')
            setIsAutoFocus(false)
          }
        }
      } else {
        selectionStateDispatch({
          action: 'dateInputChanged',
          date: moment(dateRange.from),
          inputName: 'secondRange_startDate',
        })
        selectionStateDispatch({
          action: 'dateInputChanged',
          date: moment(dateRange.to),
          inputName: 'secondRange_endDate',
        })
      }
    }
  }

  useEffect(() => {
    // The alert message might depend on the selected dates. So, we provide it though this callout.
    alertProps?.selectedDatesCallout?.({
      startDate: selectionState?.firstRange_startDate?.toString() ?? '',
      endDate: selectionState?.firstRange_endDate?.toString() ?? '',
      comparisonStartDate:
        selectionState?.secondRange_startDate?.toString() ?? '',
      comparisonEndDate: selectionState?.secondRange_endDate?.toString() ?? '',
    })
    // The actual State Update of the changed dates
    selectionStateCallout?.(selectionState, selectionStateDispatch)
  }, [alertProps, selectionState, selectionStateCallout])
  const [isDateValid1, setIsDateValid1] = React.useState(true)
  const [inputDate1, setInputDate1] = React.useState<string>('')
  const [isDateValid2, setIsDateValid2] = React.useState(true)
  const [inputDate2, setInputDate2] = React.useState<string>('')
  const dateRestrictions = { afterDate: moment().toDate() }
  const isEditingRef1 = React.useRef(false)
  const isEditingRef2 = React.useRef(false)

  const getFormattedDateValue = (
    dateRange: DateRange | undefined,
    isEditingRef: React.RefObject<boolean>,
    inputDate: string,
  ) => {
    if (isEditingRef.current) {
      return inputDate
    } else if (dateRange?.from) {
      return `${moment(getSelectedDateRange(dateRange, dateRestrictions)?.from).format('MM/DD/YYYY')} - ${moment(getSelectedDateRange(dateRange, dateRestrictions)?.to).format('MM/DD/YYYY')}`
    }
    return ''
  }

  const DateInput = ({
    labelText,
    dateRange,
    onDateChange,
    isDateValid,
    setInputDate,
    isEditingRef,
    inputDate,
    currentField,
    activeField,
  }: {
    labelText: string
    dateRange: DateRange | undefined
    onDateChange: (dateRange: DateRange | undefined) => void
    isDateValid: boolean
    setInputDate: (inputDate: string) => void
    isEditingRef: React.RefObject<boolean>
    inputDate: string
    currentField: string
    activeField: string
  }) => {
    const inputRef = React.useRef<HTMLInputElement>(null)

    return (
      <div className={stylesDatePicker.labelButton}>
        <div className={stylesDatePicker.inputBox}>
          <TextInput
            ref={inputRef}
            type='date-range'
            classType={`${stylesDatePicker.rangeWithLabel} ${!isDateValid ? 'error ' : ''} ${activeField === currentField ? styles.active : ''}`}
            hasError={!isDateValid}
            labelText={labelText}
            value={getFormattedDateValue(
              {
                from: dateRange?.from,
                to: dateRange?.to,
              },
              isEditingRef,
              inputDate,
            )}
            placeholder={c('validDateFormatForRangeDatePicker')}
            onFocus={() => {
              setActiveField(currentField)
            }}
            callout={(_, value) => {
              const stringValue = String(value)
              setInputDate(stringValue)
              if (isEmptyOrWhitespace(stringValue)) {
                onDateChange(undefined)
                return
              }
              const dateRange = parseDateRangeString(stringValue)
              if (dateRange) {
                const adjustedRange = adjustDateRangeWithRestrictions(
                  dateRange,
                  dateRestrictions,
                )
                onDateChange(adjustedRange)
              } else {
                isEditingRef.current = true
                isDateValid = false
                onDateChange(undefined)
              }
            }}
          />
        </div>
        <div
          className={`${stylesDatePicker.iconsContainer} ${
            stylesDatePicker.iconsContainerWithLabel
          }`}
        >
          <Button
            as='unstyled'
            onClick={() => {
              setActiveField(currentField)
            }}
          >
            <Icon icon='calendar' iconSize='16px' color={'dark-purple'} />
          </Button>
        </div>
      </div>
    )
  }

  return (
    <div className={styles.comparisonDateRanges}>
      {/* Time Period Comparisons Dropdown */}
      {showCompareToSelector && selectionState.showComparisonRange && (
        <div className={styles.compareToSelect}>
          <Select
            labelProps={{ label: c('compareTo') }}
            options={compareToOptions}
            selectedItem={selectionState.compareTo}
            optionKeyName='period'
            labelKeyName='period'
            onChange={(option) => {
              selectionStateDispatch({
                action: 'changeCompareTo',
                compareTo: option,
              })
            }}
            noMenuPortal
          />
        </div>
      )}

      <>
        <div className={styles.inputFields}>
          <div className={styles.inputField}>
            {DateInput({
              labelText: firstRangeLabel,
              dateRange: {
                from: selectionState.firstRange_startDate?.toDate(),
                to: selectionState.firstRange_endDate?.toDate(),
              },
              onDateChange: (dateRange: DateRange | undefined) => {
                if (dateRange) {
                  submitDateNew('firstDateRange', {
                    from: dateRange.from,
                    to: dateRange.to,
                  })
                  isEditingRef1.current = false
                  setInputDate1(inputDate1)
                  setIsDateValid1(true)
                }
              },
              isDateValid: isDateValid1,
              setInputDate: setInputDate1,
              isEditingRef: isEditingRef1,
              inputDate: inputDate1,
              currentField: 'firstDateRange',
              activeField,
            })}
          </div>

          {selectionState.showComparisonRange ? (
            <div className={styles.inputField}>
              {DateInput({
                labelText: secondRangeLabel,
                dateRange: {
                  from: selectionState.secondRange_startDate?.toDate(),
                  to: selectionState.secondRange_endDate?.toDate(),
                },
                onDateChange: (dateRange: DateRange | undefined) => {
                  if (dateRange) {
                    submitDateNew('secondDateRange', dateRange)
                    isEditingRef2.current = false
                    setInputDate2(inputDate2)
                    setIsDateValid2(true)
                  }
                },
                isDateValid: isDateValid2,
                setInputDate: setInputDate2,
                isEditingRef: isEditingRef2,
                inputDate: inputDate2,
                currentField: 'secondDateRange',
                activeField,
              })}
            </div>
          ) : (
            <></>
          )}
        </div>
        <DatePicker
          key={activeField}
          type='range'
          isExposed={true}
          labelText={
            activeField === 'firstDateRange'
              ? firstRangeLabel
              : secondRangeLabel
          }
          selectedDateRange={
            activeField === 'firstDateRange'
              ? {
                  from:
                    selectionState.firstRange_startDate &&
                    !isEditingRef1.current
                      ? selectionState.firstRange_startDate.toDate()
                      : undefined,
                  to:
                    selectionState.firstRange_endDate && !isEditingRef1.current
                      ? selectionState.firstRange_endDate.toDate()
                      : undefined,
                }
              : {
                  from:
                    selectionState.secondRange_startDate &&
                    !isEditingRef2.current
                      ? selectionState.secondRange_startDate.toDate()
                      : undefined,
                  to:
                    selectionState.secondRange_endDate && !isEditingRef2.current
                      ? selectionState.secondRange_endDate.toDate()
                      : undefined,
                }
          }
          onDateChange={(dateRange) => {
            if (dateRange && 'from' in dateRange) {
              if (activeField === 'firstDateRange') {
                submitDateNew('firstDateRange', dateRange)
                setIsDateValid1(true)
                isEditingRef1.current = false
                setInputDate1(inputDate1)
                setIsDateValid1(true)
              } else {
                submitDateNew('secondDateRange', dateRange)
                setIsDateValid2(true)
                isEditingRef2.current = false
                setInputDate2(inputDate2)
                setIsDateValid2(true)
              }
            }
          }}
          dateRestrictions={dateRestrictions}
        />
      </>

      {// TODO: look into removing this with toggle global_filter_side_drawer_change
      children?.(selectionState, selectionStateDispatch)}
    </div>
  )
}

const initialState = (showComparisonRange = false): SelectionState => ({
  compareTo: compareToOptions[0],
  firstRange_endDate: null,
  firstRange_startDate: null,
  resetInputs: false,
  secondRange_endDate: null,
  secondRange_startDate: null,
  selecting: 'firstRange_startDate',
  showComparisonRange,
})

function shouldResetInputs(state: SelectionState): boolean {
  /** Determine if a mouse click on the calendar should reset all date inputs */
  let shouldReset = false

  // only showing single date range and both dates are selected
  if (
    !state.showComparisonRange &&
    state.firstRange_startDate &&
    state.firstRange_endDate
  )
    shouldReset = true

  // showing both comparison date ranges and all dates are selected (Custom Range)
  if (
    state.showComparisonRange &&
    state.firstRange_startDate &&
    state.firstRange_endDate &&
    state.secondRange_startDate &&
    state.secondRange_endDate
  )
    shouldReset = true

  // showing both comparison date ranges with `CompareTo` time selected and first date range is selected
  if (
    state.showComparisonRange &&
    state.compareTo.id !== 0 &&
    state.firstRange_startDate &&
    state.firstRange_endDate
  )
    shouldReset = true

  return shouldReset
}

const validStartDate = (
  startDate: PickerDate,
  endDate?: PickerDate,
): boolean => {
  return Boolean(endDate && startDate?.isSameOrBefore(endDate, 'day'))
}

const validEndDate = (endDate: PickerDate, startDate?: PickerDate): boolean => {
  return Boolean(startDate && endDate?.isSameOrAfter(startDate, 'day'))
}

function rangeReducer(
  prevState: SelectionState,
  payload: SelectionStateDispatchActions,
  setActiveField: (activeField: string) => void,
  onReset?: () => void,
): SelectionState {
  switch (payload.action) {
    /** MOUSE CLICK / CALENDAR DATE ENTRY (MultiRangeDatePicker) */
    case 'calendarDateSelected': {
      const newState: SelectionState = {
        ...(prevState.resetInputs // if resetInputs is true, reset all inputs
          ? initialState(prevState.showComparisonRange) // clear the selected dates to the initial state, plus `showComparisonRange` value so it isn't lost
          : prevState), // otherwise, keep the previously selected dates
        [prevState.selecting]: payload.date, // Update the selected date value
        compareTo: prevState.compareTo, // Keep the `Compare To` value the same
      }

      /** START DATE VALIDATION */
      // validate the date selections such that the selected start date is before the selected end date
      if (
        !validStartDate(
          newState.firstRange_startDate,
          newState.firstRange_endDate,
        )
      ) {
        newState.firstRange_endDate = null
      } else if (
        !validEndDate(
          newState.firstRange_endDate,
          newState.firstRange_startDate,
        )
      ) {
        newState.firstRange_endDate = null
      } else if (
        newState.secondRange_startDate &&
        !validStartDate(
          newState.secondRange_startDate,
          newState.secondRange_endDate,
        )
      ) {
        newState.secondRange_endDate = null
      } else if (
        newState.secondRange_endDate &&
        !validEndDate(
          newState.secondRange_endDate,
          newState.secondRange_startDate,
        )
      ) {
        newState.secondRange_endDate = null
      }

      //  if a `Compare To` value is selected && start/end dates set, set the secondRange values accordingly
      if (newState.firstRange_startDate && newState.firstRange_endDate) {
        const { previousStartDate, previousEndDate } =
          prevState.compareTo.getCompareToDates(newState)

        newState.secondRange_startDate =
          previousStartDate ?? newState.secondRange_startDate
        newState.secondRange_endDate =
          previousEndDate ?? newState.secondRange_endDate
      }

      // determine next date to select
      newState.selecting = getNextSelectionState(newState, newState.selecting)

      // if setting the final date, then reset the inputs on the next date selection
      newState.resetInputs = shouldResetInputs(newState)

      return newState
    }

    /** MANUAL DATE ENTRY */
    case 'dateInputChanged': {
      const newState: SelectionState = {
        ...prevState,
        [payload.inputName]: payload.date,
      }
      /** BEGIN INPUT DATE VALIDATION
       *     if a start date is selected, validate that the corresponding end date is the same day or later
       *     if an end date is selected, validate that the corresponding start date is the same day or earlier
       */
      if (
        payload.inputName === 'firstRange_startDate' &&
        !validStartDate(
          newState.firstRange_startDate,
          newState.firstRange_endDate,
        )
      ) {
        // start date can't come after end date, so reset end date
        newState.firstRange_endDate = null
      }

      if (
        payload.inputName === 'firstRange_endDate' &&
        !validEndDate(
          newState.firstRange_endDate,
          newState.firstRange_startDate,
        )
      ) {
        // end date can't come before start date, so reset end date
        newState.firstRange_endDate = null
      }

      if (
        payload.inputName === 'secondRange_startDate' &&
        !validStartDate(
          newState.secondRange_startDate,
          newState.secondRange_endDate,
        )
      ) {
        // start date can't come after end date, so reset end date
        newState.secondRange_endDate = null
      }

      if (
        payload.inputName === 'secondRange_endDate' &&
        !validEndDate(
          newState.secondRange_endDate,
          newState.secondRange_startDate,
        )
      ) {
        // end date can't come before start date, so reset end date
        newState.secondRange_endDate = null
      }
      /** END INPUT DATE VALIDATION */

      //  if a `Compare To` value is selected && start/end dates set, set the secondRange values accordingly
      if (newState.firstRange_startDate && newState.firstRange_endDate) {
        const { previousStartDate, previousEndDate } =
          prevState.compareTo.getCompareToDates(newState)

        newState.secondRange_startDate =
          previousStartDate ?? newState.secondRange_startDate
        newState.secondRange_endDate =
          previousEndDate ?? newState.secondRange_endDate
      }

      // determine next date to select
      newState.selecting = getNextSelectionState(newState, newState.selecting)

      // if setting the final date, then reset the inputs on the next date selection
      newState.resetInputs = shouldResetInputs(newState)

      return newState
    }

    /** RESET DATE VALUES */
    case 'resetInputs': {
      const newState: SelectionState = {
        ...prevState,
        ...initialState(),
        showComparisonRange: prevState.showComparisonRange,
      }
      setActiveField('firstDateRange')
      // Reset initial load flag so auto-focus can work again
      onReset?.()

      return newState
    }

    /** UPDATED `COMPARE TO` VALUES ON STATE */
    case 'changeCompareTo': {
      // update the comparison range with the new `Compare To` value (if applicable)
      const newState: SelectionState = {
        ...prevState,
        compareTo: payload.compareTo,
      }
      // check if first start/end date are set, if so, update the second range with the new `Compare To` value
      if (newState.firstRange_startDate && newState.firstRange_endDate) {
        const { previousStartDate, previousEndDate } =
          newState.compareTo.getCompareToDates(newState)

        newState.secondRange_startDate =
          previousStartDate ?? newState.secondRange_startDate
        newState.secondRange_endDate =
          previousEndDate ?? newState.secondRange_endDate
      }

      return newState
    }

    /** CHANGE INPUT SELECTION FROM MANUAL INPUT */
    case 'selectedDateInputChange': {
      return { ...prevState, selecting: payload.inputName, resetInputs: false }
    }

    default:
      throw new Error(c('errorComparisonDatePickerReducer'))
  }
}

function getNextSelectionState(
  selectionState: SelectionState,
  selecting: SelectionState['selecting'],
): SelectionState['selecting'] {
  const allSelected =
    selectionState.firstRange_startDate &&
    selectionState.firstRange_endDate &&
    selectionState.secondRange_startDate &&
    selectionState.secondRange_endDate

  switch (selecting) {
    case 'firstRange_startDate':
      if (!selectionState.firstRange_endDate) {
        return 'firstRange_endDate'
      }
      if (
        !selectionState.secondRange_startDate &&
        selectionState.showComparisonRange &&
        selectionState.compareTo.id === 0
      ) {
        return 'secondRange_startDate'
      }
      if (
        !selectionState.secondRange_endDate &&
        selectionState.showComparisonRange &&
        selectionState.compareTo.id === 0
      ) {
        return 'secondRange_endDate'
      }
      if (allSelected) {
        return 'firstRange_startDate'
      }
      return 'firstRange_endDate'
    case 'firstRange_endDate':
      if (
        !selectionState.secondRange_startDate &&
        selectionState.showComparisonRange &&
        selectionState.compareTo.id === 0
      ) {
        return 'secondRange_startDate'
      }
      if (
        !selectionState.secondRange_endDate &&
        selectionState.showComparisonRange &&
        selectionState.compareTo.id === 0
      ) {
        return 'secondRange_endDate'
      }
      if (!selectionState.firstRange_startDate || allSelected) {
        return 'firstRange_startDate'
      }
      return selectionState.showComparisonRange &&
        selectionState.compareTo.id === 0
        ? 'secondRange_startDate'
        : 'firstRange_startDate'
    case 'secondRange_startDate':
      if (!selectionState.secondRange_endDate) {
        return 'secondRange_endDate'
      }
      if (!selectionState.firstRange_startDate) {
        return 'firstRange_startDate'
      }
      if (!selectionState.firstRange_endDate) {
        return 'firstRange_endDate'
      }
      return 'firstRange_startDate'
    case 'secondRange_endDate':
      if (!selectionState.secondRange_startDate) {
        return 'secondRange_startDate'
      }
      if (!selectionState.firstRange_startDate) {
        return 'firstRange_startDate'
      }
      if (!selectionState.firstRange_endDate) {
        return 'firstRange_endDate'
      }
      return 'firstRange_startDate'

    default:
      throw new Error("state doesn't exist")
  }
}

export type PickerDate = Moment | null
export type SelectionStateBase = {
  firstRange_startDate: PickerDate
  firstRange_endDate: PickerDate
  secondRange_startDate: PickerDate
  secondRange_endDate: PickerDate
  showComparisonRange?: boolean
}
export type SelectionState = SelectionStateBase & {
  compareTo: CompareToType
  selecting: keyof SelectionStateBase
  resetInputs?: boolean
}
export type SelectionStateDispatchActions =
  | {
      action: 'calendarDateSelected'
      date: Moment | null
    }
  | {
      action: 'dateInputChanged'
      date: Moment
      inputName: keyof SelectionStateBase
    }
  | {
      action: 'resetInputs'
      originalDates: SelectionStateBase
    }
  | {
      action: 'changeCompareTo'
      compareTo: CompareToType
    }
  | {
      action: 'selectedDateInputChange'
      inputName: keyof SelectionStateBase
    }
