import { useCallback, useEffect, useState } from "react";
import type { DateRange, DayEventHandler } from "react-day-picker";

type DateSelectionMode = "single" | "multiple" | "range";

type DateSelectionState<T extends DateSelectionMode> = T extends "single"
  ? Date | undefined
  : T extends "multiple"
    ? Date[] | undefined
    : T extends "range"
      ? DateRange | undefined
      : never;

function getInitialState<T extends DateSelectionMode>(
  mode: T,
): DateSelectionState<T> {
  switch (mode) {
    case "single":
      return {} as DateSelectionState<T>;
    case "multiple":
      return [] as Date[] | undefined as DateSelectionState<T>;
    case "range":
      return { from: undefined, to: undefined } as DateSelectionState<T>;
    default: {
      const _exhaustiveCheck: never = mode;
      throw new Error(`Unsupported mode: ${_exhaustiveCheck as string}`);
    }
  }
}

function useCalendar<T extends DateSelectionMode>({
  mode,
  date,
  onDateChange,
  isDateDisabled,
}: {
  mode: T;
  date?: DateSelectionState<T>;
  onDateChange?: (date: DateSelectionState<T>) => void;
  isDateDisabled?: (date: Date) => boolean;
}) {
  const [selection, setSelection] = useState<DateSelectionState<T>>(
    () => date ?? getInitialState(mode),
  );
  const [numberOfMonths, setNumberOfMonths] = useState<number>(2);
  const getInitialMonth = (date: DateSelectionState<T> | undefined): Date => {
    if (!date) return new Date();

    switch (mode) {
      case "single":
        return (date as Date) ?? new Date();
      case "multiple": {
        const dates = date as Date[];
        return dates?.[0] ?? new Date();
      }
      case "range": {
        const range = date as DateRange;
        return range?.from ?? new Date();
      }
      default: {
        const _exhaustiveCheck: never = mode;
        throw new Error(`Unsupported mode: ${_exhaustiveCheck as string}`);
      }
    }
  };

  const [month, setMonth] = useState<Date>(() => getInitialMonth(date));
  const clearSelection = useCallback(() => {
    setSelection(getInitialState(mode));
    if (onDateChange) {
      onDateChange(getInitialState(mode));
    }
  }, [mode, onDateChange]);

  useEffect(() => {
    const updateNumberOfMonths = () =>
      setNumberOfMonths(window.innerWidth < 768 ? 1 : 2);

    updateNumberOfMonths();
    window.addEventListener("resize", updateNumberOfMonths);

    return () => window.removeEventListener("resize", updateNumberOfMonths);
  }, []);

  const onSelectionChange = useCallback(
    (date: DateSelectionState<T>) => {
      setSelection(date);
      if (onDateChange) {
        onDateChange(date);
      }
    },
    [onDateChange],
  );

  const rangeContainsDisabledDates = (start: Date, end: Date): boolean => {
    if (!isDateDisabled) return false;

    const current = new Date(start);

    while (current <= end) {
      if (isDateDisabled(current)) {
        return true;
      }
      current.setDate(current.getDate() + 1);
    }

    return false;
  };

  const findLastValidDate = (start: Date, end: Date): Date => {
    if (!isDateDisabled) return end;

    const current = new Date(start);
    let lastValid = new Date(start);

    while (current <= end) {
      if (!isDateDisabled(current)) {
        lastValid = new Date(current);
      }

      current.setDate(current.getDate() + 1);

      if (current <= end && isDateDisabled?.(current)) {
        return lastValid;
      }
    }

    return end;
  };
  const handleRangeSelection = useCallback(
    (day: Date) => {
      const currentRange = selection as DateRange;

      // No selection yet or complete range already selected
      if (!currentRange?.from || currentRange.to) {
        // If we already have a complete range
        if (currentRange?.from && currentRange?.to) {
          // Clicked before current start - create new range from clicked day to current end
          if (day < currentRange.from) {
            if (rangeContainsDisabledDates(day, currentRange.to)) {
              const lastValid = findLastValidDate(day, currentRange.to);
              onSelectionChange({
                from: day,
                to: lastValid,
              } as DateSelectionState<T>);
            } else {
              onSelectionChange({
                from: day,
                to: currentRange.to,
              } as DateSelectionState<T>);
            }
            return;
          }
          // Clicked after current end - create new range from current start to clicked day
          else if (day > currentRange.to) {
            if (rangeContainsDisabledDates(currentRange.from, day)) {
              const lastValid = findLastValidDate(currentRange.from, day);
              onSelectionChange({
                from: currentRange.from,
                to: lastValid,
              } as DateSelectionState<T>);
            } else {
              onSelectionChange({
                from: currentRange.from,
                to: day,
              } as DateSelectionState<T>);
            }
            return;
          }
          // Clicked within current range - start new selection from clicked day
          else {
            onSelectionChange({
              from: day,
              to: undefined,
            } as DateSelectionState<T>);
            return;
          }
        }
        // No range yet or only start date selected - set clicked day as start
        else {
          onSelectionChange({
            from: day,
            to: undefined,
          } as DateSelectionState<T>);
          return;
        }
      }

      // We have a start date but no end date yet
      if (currentRange.from) {
        // Clicked before current start - swap start/end
        if (day < currentRange.from) {
          if (rangeContainsDisabledDates(day, currentRange.from)) {
            const lastValid = findLastValidDate(day, currentRange.from);
            onSelectionChange({
              from: day,
              to: lastValid,
            } as DateSelectionState<T>);
          } else {
            onSelectionChange({
              from: day,
              to: currentRange.from,
            } as DateSelectionState<T>);
          }
        }
        // Clicked after current start - set as end date
        else {
          const nextDay = new Date(currentRange.from);
          nextDay.setDate(nextDay.getDate() + 1);

          if (isDateDisabled?.(nextDay) && day >= nextDay) {
            onSelectionChange({
              from: currentRange.from,
              to: currentRange.from,
            } as DateSelectionState<T>);
            return;
          }

          if (rangeContainsDisabledDates(currentRange.from, day)) {
            const lastValid = findLastValidDate(currentRange.from, day);
            onSelectionChange({
              from: currentRange.from,
              to: lastValid,
            } as DateSelectionState<T>);
          } else {
            onSelectionChange({
              from: currentRange.from,
              to: day,
            } as DateSelectionState<T>);
          }
        }
      }
    },
    [selection, isDateDisabled, onSelectionChange],
  );

  const handleDayClick: DayEventHandler<React.MouseEvent> = useCallback(
    (day, modifiers) => {
      if (modifiers.disabled) return;

      switch (mode) {
        case "single":
          onSelectionChange(day as DateSelectionState<T>);
          break;
        case "multiple": {
          const dates = [...((selection as Date[]) || [])];
          const index = dates.findIndex(
            (d) => d.toDateString() === day.toDateString(),
          );
          if (index >= 0) {
            dates.splice(index, 1);
          } else {
            dates.push(day);
          }
          onSelectionChange(dates as DateSelectionState<T>);
          break;
        }
        case "range":
          handleRangeSelection(day);
          break;
        default: {
          const _exhaustiveCheck: never = mode;
          throw new Error(`Unsupported mode: ${_exhaustiveCheck as string}`);
        }
      }
    },
    [mode, selection, onSelectionChange, handleRangeSelection],
  );

  const defaultDisabledDays = { before: new Date() };

  return {
    mode,
    selected: selection,
    onSelect: onSelectionChange,
    handleClearDates: clearSelection,
    month,
    onMonthChange: setMonth,
    numberOfMonths,
    showPreviousMonth: false,
    disabled: defaultDisabledDays,
    onDayClick: handleDayClick,
    isDateDisabled,
  };
}

export { useCalendar };
export type { DateRange, DateSelectionMode, DateSelectionState };
