import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
import { formatISODate, newDate, parseISODateString, newDateFromDate, todayInTz } from 'shared-utils/date-utils'
import type { IDateConstraints, TDateRangeMap } from 'shared-types/calendar'
import { isDateExcluded, isDayDisabled as checkDayDisabled } from 'shared-utils/calendar/date-validation'
import {
  convertSelectedToDates,
  updateRangeMap as calculateRangeMap,
  isRangeAllowed as checkRangeAllowed,
  addToSelection,
  removeFromSelection,
  toggleSelection,
  handleRangeSelection,
} from 'shared-utils/calendar/selection-manager'
import {
  shouldIgnoreKeyboardEvent,
  findNextSelectableDate as findNextDate,
  getKeyDirection,
} from 'shared-utils/calendar/keyboard-navigation'

import { DEFAULT_STRINGS, type IPktCalendar, type ICalendarState } from './types'

export function useCalendarState(props: IPktCalendar): ICalendarState {
  const {
    selected: selectedProp,
    multiple = false,
    maxMultiple = 0,
    range = false,
    earliest = null,
    latest = null,
    excludedates: excludedatesProp,
    excludeweekdays = [],
    weeknumbers = false,
    withcontrols = false,
    currentmonth: currentmonthProp,
    today: todayProp,
    strings: stringsProp,
    onDateSelected,
    onClose,
    id: idProp,
    className,
  } = props

  const generatedId = useId()
  const componentId = idProp ?? generatedId

  const strings = useMemo(() => ({ ...DEFAULT_STRINGS, ...stringsProp }), [stringsProp])

  const todayDate = useMemo(
    () => (todayProp ? parseISODateString(todayProp) : todayInTz()),
    [todayProp],
  )

  const excludedates = useMemo<Date[]>(() => {
    if (!excludedatesProp) return []
    return excludedatesProp.map((d) => (typeof d === 'string' ? parseISODateString(d) : d))
  }, [excludedatesProp])

  const isControlled = selectedProp !== undefined
  const [internalSelected, setInternalSelected] = useState<string[]>([])
  const activeSelected = isControlled ? selectedProp : internalSelected

  const _selected = useMemo(() => convertSelectedToDates(activeSelected), [activeSelected])

  const [year, setYear] = useState<number>(0)
  const [month, setMonth] = useState<number>(0)

  const [inRange, setInRange] = useState<TDateRangeMap>({})
  const [rangeHovered, setRangeHovered] = useState<Date | null>(null)

  const [focusedDate, setFocusedDate] = useState<string | null>(null)

  const calendarRef = useRef<HTMLDivElement>(null)
  const selectableDatesRef = useRef<{ currentDateISO: string; isDisabled: boolean; tabindex: string }[]>([])
  const tabIndexSetRef = useRef<number>(0)
  const currentmonthtouchedRef = useRef<boolean>(false)
  const initializedRef = useRef<boolean>(false)

  const dateConstraints = useMemo<IDateConstraints>(
    () => ({ earliest, latest, excludedates, excludeweekdays }),
    [earliest, latest, excludedates, excludeweekdays],
  )

  useEffect(() => {
    let effectiveMonth: Date | null = null

    if (currentmonthProp != null) {
      if (currentmonthProp instanceof Date) {
        effectiveMonth = newDateFromDate(currentmonthProp)
      } else if (typeof currentmonthProp === 'string') {
        effectiveMonth = parseISODateString(currentmonthProp)
      }
    }

    if (!effectiveMonth || isNaN(effectiveMonth.getTime())) {
      if (activeSelected.length > 0 && activeSelected[0] !== '') {
        const d = parseISODateString(activeSelected[activeSelected.length - 1])
        effectiveMonth = isNaN(d.getTime()) ? newDateFromDate(todayDate) : d
      } else {
        effectiveMonth = newDateFromDate(todayDate)
      }
    }

    if (!effectiveMonth || isNaN(effectiveMonth.getTime())) {
      effectiveMonth = newDateFromDate(todayDate)
    }

    // Clamp to latest/earliest so the calendar doesn't open on an entirely disabled month
    if (latest) {
      const latestDate = typeof latest === 'string' ? parseISODateString(latest) : latest
      if (!isNaN(latestDate.getTime()) && effectiveMonth > latestDate) {
        effectiveMonth = latestDate
      }
    }
    if (earliest) {
      const earliestDate = typeof earliest === 'string' ? parseISODateString(earliest) : earliest
      if (!isNaN(earliestDate.getTime()) && effectiveMonth < earliestDate) {
        effectiveMonth = earliestDate
      }
    }

    setYear(effectiveMonth.getFullYear())
    setMonth(effectiveMonth.getMonth())
    initializedRef.current = true
  }, [])

  // Sync when selected changes externally (controlled mode)
  useEffect(() => {
    if (!initializedRef.current) return
    if (currentmonthtouchedRef.current) return

    if (activeSelected.length > 0 && activeSelected[0] !== '') {
      const d = parseISODateString(activeSelected[activeSelected.length - 1])
      if (!isNaN(d.getTime())) {
        setYear(d.getFullYear())
        setMonth(d.getMonth())
      }
    }
  }, [activeSelected])

  // Sync range map when selected changes
  useEffect(() => {
    if (range && _selected.length === 2) {
      setInRange(calculateRangeMap(_selected[0], _selected[1]))
    } else if (!range || _selected.length < 2) {
      setInRange({})
    }
  }, [range, _selected])

  const updateSelected = useCallback(
    (newSelected: string[]) => {
      if (!isControlled) {
        setInternalSelected(newSelected)
      }
      onDateSelected?.(newSelected)
    },
    [isControlled, onDateSelected],
  )

  const close = useCallback(() => {
    onClose?.()
  }, [onClose])

  const changeMonth = useCallback((newYear: number, newMonth: number) => {
    setYear(typeof newYear === 'string' ? parseInt(newYear as unknown as string) : newYear)
    setMonth(typeof newMonth === 'string' ? parseInt(newMonth as unknown as string) : newMonth)
    tabIndexSetRef.current = 0
    setFocusedDate(null)
    selectableDatesRef.current = []
    currentmonthtouchedRef.current = true
  }, [])

  const prevMonth = useCallback(() => {
    const newMonth = month === 0 ? 11 : month - 1
    const newYear = month === 0 ? year - 1 : year
    changeMonth(newYear, newMonth)
  }, [year, month, changeMonth])

  const nextMonth = useCallback(() => {
    const newMonth = month === 11 ? 0 : month + 1
    const newYear = month === 11 ? year + 1 : year
    changeMonth(newYear, newMonth)
  }, [year, month, changeMonth])

  const isExcluded = useCallback((date: Date) => isDateExcluded(date, dateConstraints), [dateConstraints])

  const isDayDisabled = useCallback(
    (date: Date, isSelected: boolean) =>
      checkDayDisabled(date, isSelected, dateConstraints, {
        multiple,
        maxMultiple,
        selectedCount: activeSelected.length,
      }),
    [dateConstraints, multiple, maxMultiple, activeSelected.length],
  )

  const normalizeSelected = useCallback((): string[] => {
    if (typeof activeSelected === 'string') {
      return (activeSelected as string).split(',')
    }
    return activeSelected
  }, [activeSelected])

  const handleRangeHover = useCallback(
    (date: Date) => {
      if (
        !range ||
        _selected.length !== 1 ||
        !checkRangeAllowed(date, _selected, excludedates, excludeweekdays) ||
        _selected[0] >= date
      ) {
        setRangeHovered(null)
        return
      }
      setRangeHovered(date)
      setInRange(calculateRangeMap(_selected[0], date))
    },
    [range, _selected, excludedates, excludeweekdays],
  )

  const handleDateSelect = useCallback(
    (selectedDate: Date | null) => {
      if (!selectedDate) return

      let newSelected: string[]

      if (range) {
        newSelected = handleRangeSelection(selectedDate, normalizeSelected(), {
          excludedates,
          excludeweekdays,
        })

        if (!isControlled) {
          setInternalSelected(newSelected)
        }

        if (newSelected.length === 2) {
          onDateSelected?.(newSelected)
          close()
        } else if (newSelected.length === 1) {
          setInRange({})
          onDateSelected?.(newSelected)
        } else {
          onDateSelected?.(newSelected)
        }
      } else if (multiple) {
        newSelected = toggleSelection(selectedDate, normalizeSelected(), maxMultiple)
        updateSelected(newSelected)
      } else {
        const dateISO = formatISODate(selectedDate)
        if (activeSelected.includes(dateISO)) {
          newSelected = []
        } else {
          newSelected = [dateISO]
        }
        updateSelected(newSelected)
        close()
      }
    },
    [
      range,
      multiple,
      maxMultiple,
      normalizeSelected,
      activeSelected,
      excludedates,
      excludeweekdays,
      isControlled,
      updateSelected,
      close,
      onDateSelected,
    ],
  )

  const addToSelected = useCallback(
    (selectedDate: Date) => {
      const newSelected = addToSelection(selectedDate, normalizeSelected())
      if (!isControlled) {
        setInternalSelected(newSelected)
      }

      if (range && newSelected.length === 2) {
        onDateSelected?.(newSelected)
        close()
      }
    },
    [normalizeSelected, isControlled, range, onDateSelected, close],
  )

  const removeFromSelected = useCallback(
    (selectedDate: Date) => {
      const newSelected = removeFromSelection(selectedDate, normalizeSelected())
      if (!isControlled) {
        setInternalSelected(newSelected)
      }
    },
    [normalizeSelected, isControlled],
  )

  const toggleSelectedDate = useCallback(
    (selectedDate: Date) => {
      const newSelected = toggleSelection(selectedDate, normalizeSelected(), maxMultiple)
      if (!isControlled) {
        setInternalSelected(newSelected)
      }
    },
    [normalizeSelected, maxMultiple, isControlled],
  )

  const focusOnCurrentDate = useCallback(() => {
    const currentDateISO = formatISODate(newDateFromDate(todayDate))
    const el = calendarRef.current?.querySelector(`button[data-date="${currentDateISO}"]`)

    if (el instanceof HTMLButtonElement) {
      setFocusedDate(currentDateISO)
      el.focus()
      return
    }

    const firstSelectable = selectableDatesRef.current.find((x) => !x.isDisabled)
    if (firstSelectable) {
      const firstSelectableEl = calendarRef.current?.querySelector(
        `button[data-date="${firstSelectable.currentDateISO}"]`,
      )
      if (firstSelectableEl instanceof HTMLButtonElement) {
        setFocusedDate(firstSelectable.currentDateISO)
        firstSelectableEl.focus()
      }
    }
  }, [])

  const handleArrowKey = useCallback(
    (e: KeyboardEvent, direction: number) => {
      const target = e.target as HTMLElement
      if (shouldIgnoreKeyboardEvent(target)) return

      e.preventDefault()

      if (!focusedDate) {
        focusOnCurrentDate()
        return
      }

      const date = newDate(focusedDate)
      const nextDate = findNextDate(date, direction, calendarRef.current!.querySelector.bind(calendarRef.current!))

      if (nextDate) {
        const el = calendarRef.current!.querySelector(`button[data-date="${formatISODate(nextDate)}"]`)
        if (el instanceof HTMLButtonElement && !el.dataset.disabled) {
          setFocusedDate(formatISODate(nextDate))
          el.focus()
        }
      }
    },
    [focusedDate, focusOnCurrentDate],
  )

  const handleKeydown = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'Escape') {
        e.preventDefault()
        close()
        return
      }

      const direction = getKeyDirection(e.key)
      if (direction !== null) {
        handleArrowKey(e.nativeEvent, direction)
      }
    },
    [close, handleArrowKey],
  )

  const handleFocusOut = useCallback(
    (e: React.FocusEvent) => {
      const target = e.target as Element
      const relatedTarget = e.relatedTarget as Node | null

      if (calendarRef.current && calendarRef.current.contains(relatedTarget)) {
        return
      }

      if (target.classList.contains('pkt-hide')) {
        return
      }

      // Don't close if focus is coming from month/year picker controls
      // Safari iOS triggers focusout with null relatedTarget when select changes
      if (target.closest('.pkt-cal-month-picker')) {
        return
      }

      close()
    },
    [close],
  )

  return {
    componentId,
    strings,
    todayDate,
    year,
    month,
    activeSelected,
    _selected,
    inRange,
    rangeHovered,
    focusedDate,
    range,
    multiple,
    weeknumbers,
    withcontrols,
    earliest,
    latest,
    excludedates,
    excludeweekdays,
    className,
    dateConstraints,
    calendarRef,
    selectableDatesRef,
    tabIndexSetRef,
    prevMonth,
    nextMonth,
    changeMonth,
    handleDateSelect,
    addToSelected,
    removeFromSelected,
    toggleSelectedDate,
    handleRangeHover,
    isExcluded,
    isDayDisabled,
    focusOnCurrentDate,
    handleKeydown,
    handleFocusOut,
    close,
    setFocusedDate,
  }
}
