import {
  ChevronLeftIcon,
  ChevronRightIcon,
  ClockIcon,
  MapPinIcon,
  PlusIcon,
} from '@heroicons/react/20/solid'
import { useEffect, useRef, useState, type PointerEvent } from 'react'
import ButtonGroup from '../ButtonGroup'
import {
  timeToMinutes,
  workingHoursForDate,
  type CalendarCoreWorkingHours,
} from './calendarSettings'
import { toDateKey } from './dateUtils'

export type CalendarView = 'month' | 'week' | 'day' | 'year'
export type CalendarEventColor =
  | 'indigo'
  | 'blue'
  | 'emerald'
  | 'amber'
  | 'rose'
  | 'slate'

export type CalendarEvent = {
  id: string
  title: string
  start: string
  end: string
  allDay: boolean
  location: string
  description: string
  color: CalendarEventColor
}

type CalendarProps = {
  events: CalendarEvent[]
  coreWorkingHours: CalendarCoreWorkingHours
  selectedDate: Date
  view: CalendarView
  onSelectedDateChange: (date: Date) => void
  onViewChange: (view: CalendarView) => void
  onCreateEvent: (start: Date, end?: Date, allDay?: boolean) => void
  onEditEvent: (event: CalendarEvent) => void
}

const weekdayLabels = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']

const viewLabels: Record<CalendarView, string> = {
  day: 'Tag',
  week: 'Woche',
  month: 'Monat',
  year: 'Jahr',
}

const viewOptions = (Object.keys(viewLabels) as CalendarView[]).map((view) => ({
  id: view,
  label: viewLabels[view],
}))

const eventColors: Record<
  CalendarEventColor,
  { card: string; dot: string; text: string }
> = {
  indigo: {
    card:
      'bg-indigo-50 text-indigo-700 ring-indigo-200 hover:bg-indigo-100 dark:bg-indigo-500/15 dark:text-indigo-200 dark:ring-indigo-400/20 dark:hover:bg-indigo-500/25',
    dot: 'bg-indigo-500',
    text: 'text-indigo-600 dark:text-indigo-300',
  },
  blue: {
    card:
      'bg-blue-50 text-blue-700 ring-blue-200 hover:bg-blue-100 dark:bg-blue-500/15 dark:text-blue-200 dark:ring-blue-400/20 dark:hover:bg-blue-500/25',
    dot: 'bg-blue-500',
    text: 'text-blue-600 dark:text-blue-300',
  },
  emerald: {
    card:
      'bg-emerald-50 text-emerald-700 ring-emerald-200 hover:bg-emerald-100 dark:bg-emerald-500/15 dark:text-emerald-200 dark:ring-emerald-400/20 dark:hover:bg-emerald-500/25',
    dot: 'bg-emerald-500',
    text: 'text-emerald-600 dark:text-emerald-300',
  },
  amber: {
    card:
      'bg-amber-50 text-amber-800 ring-amber-200 hover:bg-amber-100 dark:bg-amber-500/15 dark:text-amber-200 dark:ring-amber-400/20 dark:hover:bg-amber-500/25',
    dot: 'bg-amber-500',
    text: 'text-amber-600 dark:text-amber-300',
  },
  rose: {
    card:
      'bg-rose-50 text-rose-700 ring-rose-200 hover:bg-rose-100 dark:bg-rose-500/15 dark:text-rose-200 dark:ring-rose-400/20 dark:hover:bg-rose-500/25',
    dot: 'bg-rose-500',
    text: 'text-rose-600 dark:text-rose-300',
  },
  slate: {
    card:
      'bg-slate-100 text-slate-700 ring-slate-200 hover:bg-slate-200 dark:bg-white/10 dark:text-slate-200 dark:ring-white/10 dark:hover:bg-white/15',
    dot: 'bg-slate-500',
    text: 'text-slate-600 dark:text-slate-300',
  },
}

function classNames(...classes: Array<string | false | null | undefined>) {
  return classes.filter(Boolean).join(' ')
}

function startOfDay(date: Date) {
  return new Date(date.getFullYear(), date.getMonth(), date.getDate())
}

function addDays(date: Date, amount: number) {
  const next = new Date(date)
  next.setDate(next.getDate() + amount)
  return startOfDay(next)
}

function addMonths(date: Date, amount: number) {
  const next = new Date(date.getFullYear(), date.getMonth() + amount, 1)
  return startOfDay(next)
}

function addYears(date: Date, amount: number) {
  return new Date(date.getFullYear() + amount, date.getMonth(), date.getDate())
}

function startOfWeek(date: Date) {
  const normalized = startOfDay(date)
  const mondayOffset = (normalized.getDay() + 6) % 7
  return addDays(normalized, -mondayOffset)
}

function isSameDay(left: Date, right: Date) {
  return toDateKey(left) === toDateKey(right)
}

function isToday(date: Date) {
  return isSameDay(date, new Date())
}

function eventsForDay(events: CalendarEvent[], date: Date) {
  const dayStart = startOfDay(date)
  const dayEnd = addDays(dayStart, 1)

  return events
    .filter((event) => {
      const eventStart = new Date(event.start)
      const eventEnd = new Date(event.end)
      return eventStart < dayEnd && eventEnd > dayStart
    })
    .sort(
      (left, right) =>
        Number(right.allDay) - Number(left.allDay) ||
        new Date(left.start).getTime() - new Date(right.start).getTime(),
    )
}

function formatTime(value: string | Date) {
  return new Intl.DateTimeFormat('de-DE', {
    hour: '2-digit',
    minute: '2-digit',
  }).format(typeof value === 'string' ? new Date(value) : value)
}

function dateAtMinutes(date: Date, minutes: number) {
  const result = startOfDay(date)
  result.setMinutes(minutes)
  return result
}

function addMinutes(date: Date, minutes: number) {
  return new Date(date.getTime() + minutes * 60_000)
}

function normalizeTimeRange(anchor: Date, focus: Date) {
  const start = anchor <= focus ? anchor : focus
  const lastSlot = anchor <= focus ? focus : anchor
  return {
    start,
    end: addMinutes(lastSlot, 30),
  }
}

function formatSlotTime(minutes: number) {
  return `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(
    minutes % 60,
  ).padStart(2, '0')}`
}

function titleForView(date: Date, view: CalendarView) {
  if (view === 'day') {
    return new Intl.DateTimeFormat('de-DE', {
      weekday: 'long',
      day: '2-digit',
      month: 'long',
      year: 'numeric',
    }).format(date)
  }

  if (view === 'week') {
    const first = startOfWeek(date)
    const last = addDays(first, 6)
    const firstLabel = new Intl.DateTimeFormat('de-DE', {
      day: '2-digit',
      month: first.getMonth() === last.getMonth() ? undefined : 'short',
    }).format(first)
    const lastLabel = new Intl.DateTimeFormat('de-DE', {
      day: '2-digit',
      month: 'short',
      year: 'numeric',
    }).format(last)
    return `${firstLabel} - ${lastLabel}`
  }

  return new Intl.DateTimeFormat('de-DE', {
    month: view === 'month' ? 'long' : undefined,
    year: 'numeric',
  }).format(date)
}

function MonthView({
  events,
  selectedDate,
  onSelectedDateChange,
  onCreateEvent,
  onEditEvent,
  onOpenDay,
}: Omit<CalendarProps, 'view' | 'onViewChange' | 'coreWorkingHours'> & {
  onOpenDay: (date: Date) => void
}) {
  const monthStart = new Date(
    selectedDate.getFullYear(),
    selectedDate.getMonth(),
    1,
  )
  const gridStart = startOfWeek(monthStart)
  const days = Array.from({ length: 42 }, (_, index) =>
    addDays(gridStart, index),
  )
  const selectedEvents = eventsForDay(events, selectedDate)

  const gridRef = useRef<HTMLDivElement>(null)
  const dragRef = useRef<{
    pointerId: number
    anchorIndex: number
    focusIndex: number
  } | null>(null)
  const [daySelection, setDaySelection] = useState<{
    startIndex: number
    endIndex: number
  } | null>(null)
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)

  // Spiegel der aktuellen Props für den nativen Wheel-Listener, der nur einmal
  // registriert wird und sonst veraltete Werte einfangen würde.
  const selectedDateRef = useRef(selectedDate)
  selectedDateRef.current = selectedDate
  const onSelectedDateChangeRef = useRef(onSelectedDateChange)
  onSelectedDateChangeRef.current = onSelectedDateChange

  // Mausrad über dem Raster wechselt den Monat. Die 250-ms-Sperre verhindert,
  // dass ein einzelner Scroll-Schub (Trackpad-Momentum) mehrere Monate springt.
  useEffect(() => {
    const grid = gridRef.current
    if (!grid) {
      return
    }

    let lastSwitch = 0
    function handleWheel(event: WheelEvent) {
      if (
        event.deltaY === 0 ||
        Math.abs(event.deltaX) > Math.abs(event.deltaY)
      ) {
        return
      }

      event.preventDefault()
      const now = Date.now()
      if (now - lastSwitch < 250) {
        return
      }

      lastSwitch = now
      onSelectedDateChangeRef.current(
        addMonths(selectedDateRef.current, event.deltaY > 0 ? 1 : -1),
      )
    }

    grid.addEventListener('wheel', handleWheel, { passive: false })
    return () => grid.removeEventListener('wheel', handleWheel)
  }, [])

  function getDayIndexFromPointer(
    clientX: number,
    clientY: number,
    clampToGrid = false,
  ) {
    const grid = gridRef.current
    if (!grid) {
      return null
    }

    const bounds = grid.getBoundingClientRect()
    const rawX = clientX - bounds.left
    const rawY = clientY - bounds.top

    if (
      !clampToGrid &&
      (rawX < 0 || rawX >= bounds.width || rawY < 0 || rawY >= bounds.height)
    ) {
      return null
    }

    const relativeX = Math.min(bounds.width - 1, Math.max(0, rawX))
    const relativeY = Math.min(bounds.height - 1, Math.max(0, rawY))
    const column = Math.min(6, Math.floor((relativeX / bounds.width) * 7))
    const row = Math.min(5, Math.floor((relativeY / bounds.height) * 6))

    return row * 7 + column
  }

  function handleGridPointerDown(event: PointerEvent<HTMLDivElement>) {
    if (event.button !== 0) {
      return
    }

    if (
      event.target instanceof HTMLElement &&
      event.target.closest('[data-calendar-event], [data-calendar-skip-drag]')
    ) {
      return
    }

    const index = getDayIndexFromPointer(event.clientX, event.clientY)
    if (index === null || !gridRef.current) {
      return
    }

    event.preventDefault()
    dragRef.current = {
      pointerId: event.pointerId,
      anchorIndex: index,
      focusIndex: index,
    }
    setHoveredIndex(null)
    setDaySelection({ startIndex: index, endIndex: index })
    gridRef.current.setPointerCapture(event.pointerId)
  }

  function handleGridPointerMove(event: PointerEvent<HTMLDivElement>) {
    const drag = dragRef.current

    if (drag) {
      const index = getDayIndexFromPointer(event.clientX, event.clientY, true)
      if (index === null || index === drag.focusIndex) {
        return
      }

      drag.focusIndex = index
      setDaySelection({
        startIndex: Math.min(drag.anchorIndex, index),
        endIndex: Math.max(drag.anchorIndex, index),
      })
      return
    }

    if (event.pointerType !== 'mouse') {
      return
    }

    const index = getDayIndexFromPointer(event.clientX, event.clientY)
    setHoveredIndex((current) => (current === index ? current : index))
  }

  function finishGridSelection(event: PointerEvent<HTMLDivElement>) {
    const drag = dragRef.current
    if (!drag) {
      return
    }

    const index =
      getDayIndexFromPointer(event.clientX, event.clientY, true) ??
      drag.focusIndex
    const startIndex = Math.min(drag.anchorIndex, index)
    const endIndex = Math.max(drag.anchorIndex, index)

    if (gridRef.current?.hasPointerCapture(drag.pointerId)) {
      gridRef.current.releasePointerCapture(drag.pointerId)
    }

    dragRef.current = null
    setDaySelection(null)

    if (startIndex === endIndex) {
      onOpenDay(days[startIndex])
      return
    }

    onSelectedDateChange(days[startIndex])
    onCreateEvent(days[startIndex], days[endIndex], true)
  }

  function cancelGridSelection() {
    const drag = dragRef.current

    if (drag && gridRef.current?.hasPointerCapture(drag.pointerId)) {
      gridRef.current.releasePointerCapture(drag.pointerId)
    }

    dragRef.current = null
    setDaySelection(null)
  }

  return (
    <div className="flex min-h-0 flex-1 flex-col">
      <div className="grid grid-cols-7 border-y border-gray-200 bg-gray-50 text-center text-xs font-semibold text-gray-600 dark:border-white/10 dark:bg-white/5 dark:text-gray-300">
        {weekdayLabels.map((weekday) => (
          <div key={weekday} className="py-2">
            {weekday}
          </div>
        ))}
      </div>

      <div
        ref={gridRef}
        onPointerDown={handleGridPointerDown}
        onPointerMove={handleGridPointerMove}
        onPointerUp={finishGridSelection}
        onPointerCancel={cancelGridSelection}
        onPointerLeave={() => {
          if (!dragRef.current) {
            setHoveredIndex(null)
          }
        }}
        className="grid min-h-[32rem] flex-1 grid-cols-7 grid-rows-6 gap-px bg-gray-200 select-none dark:bg-white/10"
      >
        {days.map((day, index) => {
          const dayEvents = eventsForDay(events, day)
          const currentMonth = day.getMonth() === selectedDate.getMonth()
          const selected = isSameDay(day, selectedDate)
          const inSelection =
            daySelection !== null &&
            index >= daySelection.startIndex &&
            index <= daySelection.endIndex

          return (
            <div
              key={toDateKey(day)}
              role="button"
              tabIndex={0}
              className={classNames(
                'group relative min-h-20 cursor-pointer bg-white p-1.5 text-gray-900 shadow-[inset_0_1px_0_rgb(255_255_255)] outline-none transition hover:bg-indigo-50/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 sm:min-h-24 sm:p-2 dark:bg-gray-900 dark:text-white dark:shadow-none dark:hover:bg-indigo-500/5',
                !currentMonth &&
                  'bg-gray-100/70 text-gray-300 opacity-55 saturate-50 hover:bg-gray-100 dark:bg-gray-950/70 dark:text-gray-600 dark:opacity-45 dark:hover:bg-gray-950',
                selected && 'relative ring-2 ring-inset ring-indigo-500',
                inSelection && 'relative z-0 ring-2 ring-inset ring-indigo-400',
              )}
              onKeyDown={(event) => {
                if (event.key === 'Enter' || event.key === ' ') {
                  event.preventDefault()
                  onOpenDay(day)
                }
              }}
            >
              {inSelection && (
                <span
                  aria-hidden="true"
                  className="pointer-events-none absolute inset-0 -z-10 bg-indigo-500/20 dark:bg-indigo-400/15"
                />
              )}
              <div className="flex items-center">
                {daySelection === null && hoveredIndex === index && (
                  <button
                    type="button"
                    data-calendar-skip-drag
                    onPointerDown={(pointerEvent) =>
                      pointerEvent.stopPropagation()
                    }
                    onClick={(clickEvent) => {
                      clickEvent.stopPropagation()
                      onCreateEvent(day)
                    }}
                    className="hidden min-w-0 items-center gap-1 truncate rounded border border-dashed border-indigo-400 bg-indigo-50/95 px-1.5 py-0.5 text-[11px] font-medium text-indigo-600 shadow-sm transition hover:bg-indigo-100 sm:flex dark:border-indigo-400/60 dark:bg-indigo-500/25 dark:text-indigo-100 dark:hover:bg-indigo-500/40"
                  >
                    <PlusIcon className="size-3 shrink-0" />
                    <span className="truncate">Neuer Termin</span>
                  </button>
                )}
                <button
                  type="button"
                  onClick={() => onOpenDay(day)}
                  className={classNames(
                    'ml-auto flex size-7 shrink-0 items-center justify-center rounded-full text-sm font-medium',
                    isToday(day)
                      ? 'bg-indigo-600 text-white dark:bg-indigo-500'
                      : selected
                        ? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
                        : currentMonth
                          ? 'font-semibold text-gray-900 hover:bg-indigo-50 hover:text-indigo-700 dark:text-white dark:hover:bg-indigo-500/15 dark:hover:text-indigo-300'
                          : 'text-gray-400 hover:bg-gray-200/70 dark:text-gray-600 dark:hover:bg-white/5',
                  )}
                >
                  {day.getDate()}
                </button>
              </div>

              <div className="mt-1 hidden space-y-1 sm:block">
                {dayEvents.slice(0, 3).map((event) => (
                  <button
                    key={event.id}
                    type="button"
                    data-calendar-event
                    onPointerDown={(pointerEvent) =>
                      pointerEvent.stopPropagation()
                    }
                    onClick={(clickEvent) => {
                      clickEvent.stopPropagation()
                      onSelectedDateChange(day)
                      onEditEvent(event)
                    }}
                    onDoubleClick={(doubleClickEvent) =>
                      doubleClickEvent.stopPropagation()
                    }
                    className={classNames(
                      'flex w-full items-center gap-1 truncate rounded px-1.5 py-0.5 text-left text-xs font-medium ring-1 ring-inset',
                      eventColors[event.color].card,
                      !currentMonth && 'opacity-45 grayscale',
                    )}
                  >
                    <span className="truncate">{event.title}</span>
                    {!event.allDay && (
                      <span className="ml-auto hidden shrink-0 xl:inline">
                        {formatTime(event.start)}
                      </span>
                    )}
                  </button>
                ))}
                {dayEvents.length > 3 && (
                  <button
                    type="button"
                    data-calendar-skip-drag
                    onClick={() => onOpenDay(day)}
                    className="px-1 text-xs font-medium text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-300"
                  >
                    + {dayEvents.length - 3} weitere
                  </button>
                )}
              </div>

              {dayEvents.length > 0 && (
                <div className="mt-auto flex flex-wrap justify-end gap-1 pt-2 sm:hidden">
                  {dayEvents.slice(0, 4).map((event) => (
                    <span
                      key={event.id}
                      className={classNames(
                        'size-1.5 rounded-full',
                        eventColors[event.color].dot,
                      )}
                    />
                  ))}
                </div>
              )}
            </div>
          )
        })}
      </div>

      <div className="border-t border-gray-200 bg-white p-4 sm:hidden dark:border-white/10 dark:bg-gray-900">
        <div className="flex items-center justify-between">
          <h3 className="text-sm font-semibold text-gray-900 dark:text-white">
            {new Intl.DateTimeFormat('de-DE', {
              weekday: 'long',
              day: '2-digit',
              month: 'long',
            }).format(selectedDate)}
          </h3>
          <button
            type="button"
            onClick={() => onCreateEvent(selectedDate)}
            className="inline-flex size-8 items-center justify-center rounded-full bg-indigo-600 text-white"
          >
            <PlusIcon className="size-4" />
          </button>
        </div>
        {selectedEvents.length === 0 ? (
          <p className="mt-3 text-sm text-gray-500 dark:text-gray-400">
            Keine Termine an diesem Tag.
          </p>
        ) : (
          <ol className="mt-3 space-y-2">
            {selectedEvents.map((event) => (
              <li key={event.id}>
                <EventListButton event={event} onClick={onEditEvent} />
              </li>
            ))}
          </ol>
        )}
      </div>
    </div>
  )
}

function EventListButton({
  event,
  onClick,
}: {
  event: CalendarEvent
  onClick: (event: CalendarEvent) => void
}) {
  return (
    <button
      type="button"
      onClick={() => onClick(event)}
      className={classNames(
        'flex w-full items-start gap-3 rounded-xl p-3 text-left ring-1 ring-inset transition',
        eventColors[event.color].card,
      )}
    >
      <span
        className={classNames(
          'mt-1 size-2 shrink-0 rounded-full',
          eventColors[event.color].dot,
        )}
      />
      <span className="min-w-0 flex-1">
        <span className="block truncate text-sm font-semibold">
          {event.title}
        </span>
        <span className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs opacity-80">
          <span className="inline-flex items-center gap-1">
            <ClockIcon className="size-3.5" />
            {event.allDay
              ? 'Ganztägig'
              : `${formatTime(event.start)} - ${formatTime(event.end)}`}
          </span>
          {event.location && (
            <span className="inline-flex items-center gap-1 truncate">
              <MapPinIcon className="size-3.5" />
              {event.location}
            </span>
          )}
        </span>
      </span>
    </button>
  )
}

function TimeGridView({
  events,
  coreWorkingHours,
  selectedDate,
  days,
  onSelectedDateChange,
  onCreateEvent,
  onEditEvent,
}: Omit<CalendarProps, 'view' | 'onViewChange'> & { days: Date[] }) {
  const timeGridRef = useRef<HTMLDivElement>(null)
  const interactionRef = useRef<{
    pointerId: number
    anchor: Date
    focus: Date
  } | null>(null)
  const [hoveredSlot, setHoveredSlot] = useState<{
    dayKey: string
    minutes: number
  } | null>(null)
  const [timeSelection, setTimeSelection] = useState<{
    anchor: Date
    focus: Date
  } | null>(null)
  const hours = Array.from({ length: 24 }, (_, index) => index)
  const allDayEvents = days.flatMap((day) =>
    eventsForDay(events, day).filter((event) => event.allDay),
  )
  const selectedRange = timeSelection
    ? normalizeTimeRange(timeSelection.anchor, timeSelection.focus)
    : null
  const gridTemplateColumns = `3.5rem repeat(${days.length}, minmax(7rem, 1fr))`
  const showHoverSlotTopEdge = days.length === 1

  function getSlotFromPointer(
    clientX: number,
    clientY: number,
    clampToGrid = false,
  ) {
    const grid = timeGridRef.current
    if (!grid) {
      return null
    }

    const bounds = grid.getBoundingClientRect()
    const timeGutterWidth = 56
    const daysWidth = bounds.width - timeGutterWidth
    const rawX = clientX - bounds.left - timeGutterWidth
    const rawY = clientY - bounds.top

    if (
      !clampToGrid &&
      (rawX < 0 || rawX >= daysWidth || rawY < 0 || rawY >= bounds.height)
    ) {
      return null
    }

    const relativeX = Math.min(daysWidth - 1, Math.max(0, rawX))
    const relativeY = Math.min(bounds.height - 1, Math.max(0, rawY))
    const dayIndex = Math.min(
      days.length - 1,
      Math.floor((relativeX / daysWidth) * days.length),
    )
    const minutes =
      Math.min(47, Math.floor((relativeY / bounds.height) * 48)) * 30

    return dateAtMinutes(days[dayIndex], minutes)
  }

  function handleTimeGridPointerDown(event: PointerEvent<HTMLDivElement>) {
    if (event.button !== 0) {
      return
    }

    if (
      event.target instanceof HTMLElement &&
      event.target.closest('[data-calendar-event]')
    ) {
      return
    }

    const slot = getSlotFromPointer(event.clientX, event.clientY)
    if (!slot || !timeGridRef.current) {
      return
    }

    event.preventDefault()
    interactionRef.current = {
      pointerId: event.pointerId,
      anchor: slot,
      focus: slot,
    }
    setHoveredSlot(null)
    setTimeSelection({ anchor: slot, focus: slot })
    timeGridRef.current.setPointerCapture(event.pointerId)
  }

  function handleTimeGridPointerMove(event: PointerEvent<HTMLDivElement>) {
    const interaction = interactionRef.current
    const slot = getSlotFromPointer(
      event.clientX,
      event.clientY,
      Boolean(interaction),
    )

    if (!slot) {
      setHoveredSlot(null)
      return
    }

    if (interaction) {
      interaction.focus = slot
      setTimeSelection({
        anchor: interaction.anchor,
        focus: slot,
      })
      return
    }

    setHoveredSlot({
      dayKey: toDateKey(slot),
      minutes: slot.getHours() * 60 + slot.getMinutes(),
    })
  }

  function finishTimeGridSelection(event: PointerEvent<HTMLDivElement>) {
    const interaction = interactionRef.current
    if (!interaction) {
      return
    }

    const focus =
      getSlotFromPointer(event.clientX, event.clientY, true) ??
      interaction.focus
    const range = normalizeTimeRange(interaction.anchor, focus)

    if (timeGridRef.current?.hasPointerCapture(interaction.pointerId)) {
      timeGridRef.current.releasePointerCapture(interaction.pointerId)
    }

    interactionRef.current = null
    setTimeSelection(null)
    setHoveredSlot(null)
    onSelectedDateChange(range.start)
    onCreateEvent(range.start, range.end)
  }

  function cancelTimeGridSelection() {
    const interaction = interactionRef.current

    if (
      interaction &&
      timeGridRef.current?.hasPointerCapture(interaction.pointerId)
    ) {
      timeGridRef.current.releasePointerCapture(interaction.pointerId)
    }

    interactionRef.current = null
    setTimeSelection(null)
    setHoveredSlot(null)
  }

  return (
    <div className="flex min-h-0 flex-1 flex-col bg-white dark:bg-gray-900">
      <div className="min-h-0 flex-1 overflow-auto">
        <div className="min-w-max">
          <div className="sticky top-0 z-20">
            <div
              className="grid border-b border-gray-200 bg-white dark:border-white/10 dark:bg-gray-900"
              style={{ gridTemplateColumns }}
            >
              <div className="bg-white dark:bg-gray-900" />
              {days.map((day) => (
                <button
                  key={toDateKey(day)}
                  type="button"
                  onClick={() => onSelectedDateChange(day)}
                  className={classNames(
                    'border-l border-gray-200 px-2 py-3 text-center dark:border-white/10',
                    isSameDay(day, selectedDate) &&
                      'bg-indigo-50 dark:bg-indigo-500/10',
                  )}
                >
                  <span className="block text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
                    {new Intl.DateTimeFormat('de-DE', {
                      weekday: 'short',
                    }).format(day)}
                  </span>
                  <span
                    className={classNames(
                      'mx-auto mt-1 flex size-8 items-center justify-center rounded-full text-sm font-semibold',
                      isToday(day)
                        ? 'bg-indigo-600 text-white dark:bg-indigo-500'
                        : 'text-gray-900 dark:text-white',
                    )}
                  >
                    {day.getDate()}
                  </span>
                </button>
              ))}
            </div>

            {allDayEvents.length > 0 && (
              <div
                className="grid border-b border-gray-200 bg-gray-50 dark:border-white/10 dark:bg-white/5"
                style={{ gridTemplateColumns }}
              >
                <div className="px-2 py-2 text-right text-[10px] font-medium uppercase text-gray-400">
                  Ganz
                </div>
                {days.map((day) => (
                  <div
                    key={toDateKey(day)}
                    className="min-h-10 space-y-1 border-l border-gray-200 p-1 dark:border-white/10"
                  >
                    {eventsForDay(events, day)
                      .filter((event) => event.allDay)
                      .map((event) => (
                        <button
                          key={event.id}
                          type="button"
                          onClick={() => onEditEvent(event)}
                          className={classNames(
                            'w-full truncate rounded px-2 py-1 text-left text-xs font-semibold ring-1 ring-inset',
                            eventColors[event.color].card,
                          )}
                        >
                          {event.title}
                        </button>
                      ))}
                  </div>
                ))}
              </div>
            )}
          </div>

          <div
            ref={timeGridRef}
            onPointerDown={handleTimeGridPointerDown}
            onPointerMove={handleTimeGridPointerMove}
            onPointerUp={finishTimeGridSelection}
            onPointerCancel={cancelTimeGridSelection}
            onPointerLeave={() => {
              if (!interactionRef.current) {
                setHoveredSlot(null)
              }
            }}
            className="grid w-full"
            style={{ gridTemplateColumns }}
          >
            <div className="relative h-[96rem]">
              {hours.map((hour) => (
                <div
                  key={hour}
                  className="absolute right-2 -translate-y-2 text-[11px] text-gray-400 dark:text-gray-500"
                  style={{ top: `${(hour / 24) * 100}%` }}
                >
                  {String(hour).padStart(2, '0')}:00
                </div>
              ))}
            </div>

            {days.map((day) => {
              const timedEvents = eventsForDay(events, day).filter(
                (event) => !event.allDay,
              )
              const coreHours = workingHoursForDate(coreWorkingHours, day)
              const coreStartMinutes = coreHours
                ? timeToMinutes(coreHours.start)
                : 0
              const coreEndMinutes = coreHours
                ? timeToMinutes(coreHours.end)
                : 0
              const dayStart = startOfDay(day)
              const dayEnd = addDays(dayStart, 1)
              const selectionStart =
                selectedRange && selectedRange.end > dayStart
                  ? new Date(
                      Math.max(
                        selectedRange.start.getTime(),
                        dayStart.getTime(),
                      ),
                    )
                  : null
              const selectionEnd =
                selectedRange && selectedRange.start < dayEnd
                  ? new Date(
                      Math.min(selectedRange.end.getTime(), dayEnd.getTime()),
                    )
                  : null
              const hasSelection =
                selectionStart &&
                selectionEnd &&
                selectionEnd > selectionStart
              const selectionStartMinutes = selectionStart
                ? (selectionStart.getTime() - dayStart.getTime()) / 60_000
                : 0
              const selectionDurationMinutes =
                selectionStart && selectionEnd
                  ? (selectionEnd.getTime() - selectionStart.getTime()) / 60_000
                  : 0

              return (
                <div
                  key={toDateKey(day)}
                  data-calendar-time-column
                  className="relative h-[96rem] cursor-crosshair border-l border-gray-200 bg-gray-100/80 dark:border-white/10 dark:bg-gray-950/70"
                >
                  {coreHours?.enabled &&
                    coreEndMinutes > coreStartMinutes && (
                      <div
                        aria-label={`Kernarbeitszeit ${coreHours.start} bis ${coreHours.end}`}
                        className="pointer-events-none absolute inset-x-0 z-0 border-y border-gray-200 bg-white shadow-[inset_0_1px_0_rgb(255_255_255)] dark:border-white/10 dark:bg-gray-900 dark:shadow-none"
                        style={{
                          top: `${(coreStartMinutes / 1440) * 100}%`,
                          height: `${
                            ((coreEndMinutes - coreStartMinutes) / 1440) * 100
                          }%`,
                        }}
                      />
                    )}
                  <div
                    aria-hidden="true"
                    className="pointer-events-none absolute inset-0 z-[1]"
                  >
                    {Array.from({ length: 49 }, (_, index) => (
                      <span
                        key={index}
                        className={classNames(
                          'absolute inset-x-0 border-t',
                          index % 2 === 0
                            ? 'border-gray-200 dark:border-white/10'
                            : 'border-gray-100 dark:border-white/5',
                        )}
                        style={{ top: `${(index / 48) * 100}%` }}
                      />
                    ))}
                  </div>
                  {hasSelection && (
                    <div
                      aria-hidden="true"
                      className="pointer-events-none absolute right-1 left-1 z-[6] flex items-start overflow-hidden rounded-lg border-2 border-indigo-500 bg-indigo-100/90 px-2 py-1 text-xs font-semibold text-indigo-800 shadow-sm dark:border-indigo-400 dark:bg-indigo-500/30 dark:text-indigo-100"
                      style={{
                        top: `${(selectionStartMinutes / 1440) * 100}%`,
                        height: `${Math.max(
                          2.1,
                          (selectionDurationMinutes / 1440) * 100,
                        )}%`,
                      }}
                    >
                      <span className="truncate">
                        {selectionStart &&
                          selectionEnd &&
                          `${formatTime(selectionStart)} – ${formatTime(
                            selectionEnd,
                          )}`}
                      </span>
                    </div>
                  )}
                  {!timeSelection &&
                    hoveredSlot?.dayKey === toDateKey(day) && (
                      <div
                        aria-hidden="true"
                        className={classNames(
                          'pointer-events-none absolute right-1 left-1 z-[5] flex items-center gap-1.5 overflow-hidden rounded-lg border border-dashed border-indigo-400 bg-indigo-50/90 px-2 text-xs font-semibold text-indigo-700 shadow-sm dark:border-indigo-400/70 dark:bg-indigo-500/15 dark:text-indigo-200',
                          showHoverSlotTopEdge &&
                            'before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-10 before:border-t before:border-dashed before:border-indigo-400 dark:before:border-indigo-400/70',
                        )}
                        style={{
                          top: `${(hoveredSlot.minutes / 1440) * 100}%`,
                          height: `${(30 / 1440) * 100}%`,
                        }}
                      >
                        <PlusIcon className="size-3.5 shrink-0" />
                        <span className="truncate">
                          {formatSlotTime(hoveredSlot.minutes)} Neuer Termin
                        </span>
                      </div>
                    )}
                  {timedEvents.map((event) => {
                    const eventStart = new Date(event.start)
                    const eventEnd = new Date(event.end)
                    const segmentStart = new Date(
                      Math.max(eventStart.getTime(), dayStart.getTime()),
                    )
                    const segmentEnd = new Date(
                      Math.min(eventEnd.getTime(), dayEnd.getTime()),
                    )
                    const startMinutes =
                      (segmentStart.getTime() - dayStart.getTime()) / 60_000
                    const durationMinutes = Math.max(
                      30,
                      (segmentEnd.getTime() - segmentStart.getTime()) / 60_000,
                    )

                    return (
                      <button
                        key={event.id}
                        data-calendar-event
                        type="button"
                        onPointerDown={(pointerEvent) =>
                          pointerEvent.stopPropagation()
                        }
                        onClick={(clickEvent) => {
                          clickEvent.stopPropagation()
                          onEditEvent(event)
                        }}
                        onDoubleClick={(doubleClickEvent) =>
                          doubleClickEvent.stopPropagation()
                        }
                        onMouseEnter={() => setHoveredSlot(null)}
                        onPointerMove={(moveEvent) =>
                          moveEvent.stopPropagation()
                        }
                        className={classNames(
                          'absolute right-1 left-1 z-10 overflow-hidden rounded-lg p-2 text-left text-xs ring-1 ring-inset transition',
                          eventColors[event.color].card,
                        )}
                        style={{
                          top: `${(startMinutes / 1440) * 100}%`,
                          height: `${Math.max(
                            2.1,
                            (durationMinutes / 1440) * 100,
                          )}%`,
                        }}
                      >
                        <span className="block truncate font-semibold">
                          {event.title}
                        </span>
                        <span className="mt-0.5 block truncate opacity-80">
                          {formatTime(segmentStart)} - {formatTime(segmentEnd)}
                          {event.location ? ` · ${event.location}` : ''}
                        </span>
                      </button>
                    )
                  })}
                </div>
              )
            })}
          </div>
        </div>
      </div>
    </div>
  )
}

function YearView({
  events,
  selectedDate,
  onOpenDay,
}: Pick<CalendarProps, 'events' | 'selectedDate'> & {
  onOpenDay: (date: Date) => void
}) {
  const months = Array.from(
    { length: 12 },
    (_, month) => new Date(selectedDate.getFullYear(), month, 1),
  )

  return (
    <div className="min-h-0 flex-1 overflow-auto bg-gray-50 p-4 sm:p-6 dark:bg-gray-950">
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
        {months.map((month) => {
          const gridStart = startOfWeek(month)
          const days = Array.from({ length: 42 }, (_, index) =>
            addDays(gridStart, index),
          )

          return (
            <section
              key={month.getMonth()}
              className="rounded-2xl bg-white p-4 shadow-sm ring-1 ring-gray-200 dark:bg-gray-900 dark:ring-white/10"
            >
              <h2 className="text-sm font-semibold capitalize text-gray-900 dark:text-white">
                {new Intl.DateTimeFormat('de-DE', { month: 'long' }).format(
                  month,
                )}
              </h2>
              <div className="mt-4 grid grid-cols-7 text-center text-[10px] font-semibold uppercase text-gray-400">
                {weekdayLabels.map((weekday) => (
                  <div key={weekday}>{weekday.slice(0, 1)}</div>
                ))}
              </div>
              <div className="mt-2 grid grid-cols-7 gap-y-1 text-center text-xs">
                {days.map((day) => {
                  const inMonth = day.getMonth() === month.getMonth()
                  const dayEvents = eventsForDay(events, day)
                  const selected = isSameDay(day, selectedDate)

                  return (
                    <button
                      key={toDateKey(day)}
                      type="button"
                      onClick={() => onOpenDay(day)}
                      className={classNames(
                        'relative mx-auto flex size-8 items-center justify-center rounded-full',
                        !inMonth && 'text-gray-300 dark:text-gray-700',
                        inMonth &&
                          'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10',
                        isToday(day) &&
                          'bg-indigo-600 font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500',
                        selected &&
                          !isToday(day) &&
                          'bg-gray-900 text-white dark:bg-white dark:text-gray-900',
                      )}
                    >
                      {day.getDate()}
                      {dayEvents.length > 0 && (
                        <span
                          className={classNames(
                            'absolute bottom-0.5 size-1 rounded-full',
                            isToday(day) || selected
                              ? 'bg-white dark:bg-gray-900'
                              : eventColors[dayEvents[0].color].dot,
                          )}
                        />
                      )}
                    </button>
                  )
                })}
              </div>
            </section>
          )
        })}
      </div>
    </div>
  )
}

export default function Calendar({
  events,
  coreWorkingHours,
  selectedDate,
  view,
  onSelectedDateChange,
  onViewChange,
  onCreateEvent,
  onEditEvent,
}: CalendarProps) {
  function openDay(date: Date) {
    onSelectedDateChange(date)
    onViewChange('day')
  }

  function move(amount: number) {
    if (view === 'day') {
      onSelectedDateChange(addDays(selectedDate, amount))
      return
    }
    if (view === 'week') {
      onSelectedDateChange(addDays(selectedDate, amount * 7))
      return
    }
    if (view === 'month') {
      onSelectedDateChange(addMonths(selectedDate, amount))
      return
    }
    onSelectedDateChange(addYears(selectedDate, amount))
  }

  const weekDays = Array.from({ length: 7 }, (_, index) =>
    addDays(startOfWeek(selectedDate), index),
  )

  return (
    <div className="flex h-full min-h-0 flex-col bg-white dark:bg-gray-900">
      <header className="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-gray-200 px-4 py-3 sm:px-6 dark:border-white/10">
        <div className="min-w-0">
          <p className="text-xs font-medium uppercase tracking-wide text-indigo-600 dark:text-indigo-400">
            Kalender
          </p>
          <h1 className="truncate text-lg font-semibold capitalize text-gray-950 dark:text-white">
            {titleForView(selectedDate, view)}
          </h1>
        </div>

        <div className="flex items-center gap-2">
          <div className="inline-flex rounded-lg bg-white shadow-xs ring-1 ring-gray-300 dark:bg-white/5 dark:ring-white/10">
            <button
              type="button"
              onClick={() => move(-1)}
              className="inline-flex size-9 items-center justify-center rounded-l-lg text-gray-500 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-white"
            >
              <span className="sr-only">Zurück</span>
              <ChevronLeftIcon className="size-5" />
            </button>
            <button
              type="button"
              onClick={() => onSelectedDateChange(startOfDay(new Date()))}
              className="border-x border-gray-300 px-3 text-sm font-semibold text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:text-gray-200 dark:hover:bg-white/10"
            >
              Heute
            </button>
            <button
              type="button"
              onClick={() => move(1)}
              className="inline-flex size-9 items-center justify-center rounded-r-lg text-gray-500 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-white"
            >
              <span className="sr-only">Weiter</span>
              <ChevronRightIcon className="size-5" />
            </button>
          </div>

          <ButtonGroup
            ariaLabel="Kalenderansicht wechseln"
            size="lg"
            value={view}
            items={viewOptions}
            onChange={(nextView) => onViewChange(nextView as CalendarView)}
          />

          <button
            type="button"
            onClick={() => onCreateEvent(selectedDate)}
            className="inline-flex h-9 items-center gap-1.5 rounded-lg bg-indigo-600 px-3 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
          >
            <PlusIcon className="size-4" />
            <span className="hidden sm:inline">Termin</span>
          </button>
        </div>
      </header>

      {view === 'month' && (
        <MonthView
          events={events}
          selectedDate={selectedDate}
          onSelectedDateChange={onSelectedDateChange}
          onCreateEvent={onCreateEvent}
          onEditEvent={onEditEvent}
          onOpenDay={openDay}
        />
      )}
      {view === 'week' && (
        <TimeGridView
          events={events}
          coreWorkingHours={coreWorkingHours}
          selectedDate={selectedDate}
          days={weekDays}
          onSelectedDateChange={onSelectedDateChange}
          onCreateEvent={onCreateEvent}
          onEditEvent={onEditEvent}
        />
      )}
      {view === 'day' && (
        <TimeGridView
          events={events}
          coreWorkingHours={coreWorkingHours}
          selectedDate={selectedDate}
          days={[selectedDate]}
          onSelectedDateChange={onSelectedDateChange}
          onCreateEvent={onCreateEvent}
          onEditEvent={onEditEvent}
        />
      )}
      {view === 'year' && (
        <YearView
          events={events}
          selectedDate={selectedDate}
          onOpenDay={openDay}
        />
      )}
    </div>
  )
}
