import { addDays, isAfter, isBefore, endOfDay, isEqual } from 'date-fns'
import { DateRange, Matcher } from 'react-day-picker'

import { DisableCalendarDates, RangeContext } from '../CalendarTypes'

type Props = {
  range: DateRange | undefined
  newDisableCalendarDates?: DisableCalendarDates
  setCalendarRange: (range: DateRange | undefined) => void
  setCalendarHasError?: (arg: boolean) => void
  calendarRange?: DateRange
  overlappingDate?: DateRange[]
  calendarHasError?: boolean
  rangeContext?: RangeContext
}

export const calendarSelectionRules = ({
  range,
  newDisableCalendarDates,
  setCalendarRange,
  calendarRange,
  overlappingDate,
  setCalendarHasError,
  rangeContext,
}: Props) => {
  // Get and parse needed data
  const rangeFrom = range?.from ? endOfDay(range.from) : null
  const rangeTo = range?.to ? endOfDay(range.to) : null
  const calendarRangeFrom = calendarRange?.from
    ? endOfDay(calendarRange.from)
    : null
  const calendarRangeTo = calendarRange?.to ? endOfDay(calendarRange.to) : null

  const rangeContextFrom = rangeContext?.from
    ? endOfDay(rangeContext.from)
    : null
  const rangeContextTo = rangeContext?.to ? endOfDay(rangeContext.to) : null

  // When some dates are not available for selection, the earliest date will be available only for "end"
  // and the latest date will only be available for "start"
  const rangeFromDate =
    !range && calendarRangeFrom && calendarRangeTo
      ? calendarRangeFrom
      : rangeFrom
  const overlappingDateFrom = overlappingDate?.length
    ? overlappingDate.find((date) =>
        !!(date.from && rangeFromDate)
          ? isEqual(endOfDay(date.from), rangeFromDate)
          : false
      )
    : null

  const overlappingDateTo = overlappingDate?.length
    ? overlappingDate.find((date) =>
        !!(date.from && rangeTo) ? isEqual(endOfDay(date.from), rangeTo) : false
      )
    : null
  //----------

  const checkOutRange = newDisableCalendarDates?.availableDates?.length
    ? newDisableCalendarDates.availableDates.find((checkInDate) =>
        !!(checkInDate.checkIn && rangeFrom)
          ? isEqual(endOfDay(checkInDate.checkIn), rangeFrom)
          : false
      )
    : null

  // Calendar selection rules: The cases are handled sequentially, starting from simple selections to more complex contextual selections.
  // The rules are processed in a specific order, so one case is handled before another.
  // Changing the order will cause the rules to break or not work properly.

  if (
    (calendarRangeFrom &&
      overlappingDateFrom &&
      rangeFrom &&
      !isEqual(rangeFrom, calendarRangeFrom)) ||
    !range ||
    (calendarRangeFrom &&
      calendarRangeTo &&
      overlappingDateTo &&
      rangeTo &&
      !isEqual(rangeTo, calendarRangeTo))
  ) {
    // 1. If dates overlap, clear the selection.
    return setCalendarRange(undefined)
  }

  if (
    // 2. If selected "start" is after "range context end", clear selection and display error.
    // Also, if there are unavailable dates between the rangeContext "start" and "end", do not allow a fully overlapping selection.
    (rangeFrom && rangeContextTo && isAfter(rangeFrom, rangeContextTo)) ||
    // In case of current rangeTo is after contextRange and user clicks the new check-in
    (calendarRangeFrom &&
      calendarRangeTo &&
      rangeContextTo &&
      rangeTo &&
      // If rangeTo has been adjusted, the current click was at the end
      // In this case we can verify if the new click (check-in-to-be) goes over the context range end
      !isEqual(rangeTo, calendarRangeTo) &&
      isAfter(rangeTo, rangeContextTo))
  ) {
    setCalendarHasError && setCalendarHasError(true)
    return setCalendarRange(undefined)
  }

  if (rangeFrom && rangeTo && isEqual(rangeFrom, rangeTo)) {
    // 3. If "start" is selected and the same date is clicked again, keep the current "start" selection.
    return setCalendarRange({ from: rangeFrom, to: undefined })
  }

  if (calendarRangeFrom && calendarRangeTo && !range) {
    // 4. If "calendarRange" has dates selected and the same "start" date is clicked, the "range" will become undefined.
    // Set the current selection (calendarRange) to the initial "start" of calendarRange.
    return setCalendarRange({ from: calendarRangeFrom, to: undefined })
  }

  // 5. Handle gap selection backwards and forwards
  if (
    // In the range context case, every date selection that is after selected "start" last checkout,
    // clear selection and prompt error
    rangeContext &&
    rangeTo &&
    checkOutRange?.lastCheckOut &&
    isAfter(rangeTo, endOfDay(checkOutRange.lastCheckOut)) &&
    // Handle case of existing range that span over the end of the context range
    ((calendarRangeTo && !isEqual(rangeTo, calendarRangeTo)) ||
      !calendarRangeTo) &&
    isAfter(rangeTo, endOfDay(rangeContext.to))
  ) {
    setCalendarHasError && setCalendarHasError(true)
    return setCalendarRange(undefined)
  }

  if (
    // Enforce on every date selection before current "start", mark as "start" selection
    rangeFrom &&
    calendarRangeFrom &&
    isBefore(rangeFrom, calendarRangeFrom)
  ) {
    return setCalendarRange({ from: rangeFrom, to: undefined })
  }

  if (
    // Every date selection that is after selected "start" last checkout, mark as "start"
    rangeTo &&
    checkOutRange?.lastCheckOut &&
    isAfter(rangeTo, endOfDay(checkOutRange.lastCheckOut))
  ) {
    if (overlappingDateTo) {
      return setCalendarRange(undefined)
    }
    return setCalendarRange({ from: rangeTo, to: undefined })
  }

  if (
    calendarRangeFrom &&
    rangeFrom &&
    !isEqual(calendarRangeFrom, rangeFrom)
  ) {
    // 6. If "calendarRange" has selected dates and the new selection "start" differs from the current "calendarRange start", reset and make a new "start" selection.
    return setCalendarRange({ from: rangeFrom, to: undefined })
  } else if (calendarRangeTo && rangeTo && !isEqual(calendarRangeTo, rangeTo)) {
    // 7. If "calendarRange" has selected dates and the new selection "end" differs from the current "calendarRange end", reset and make a new "start" selection.

    return setCalendarRange({ from: rangeTo, to: undefined })
  }

  if (
    // 8. If selected "end" is before "context start", mark as "start" and display error
    rangeFrom &&
    rangeTo &&
    rangeContextFrom &&
    isBefore(rangeTo, rangeContextFrom)
  ) {
    setCalendarHasError && setCalendarHasError(true)
    return setCalendarRange({ from: rangeTo, to: undefined })
  }

  setCalendarRange(range)
}
