<script setup lang="ts">
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue'
import { Calendar, ChevronLeft, ChevronRight } from 'lucide-vue-next'
import { getLocaleCode } from '../utils/locale'
import { getFixedPositionMapping } from '../utils/dropdown'
import { useFocusTrap } from '../utils/focusTrap'

interface Props {
  modelValue: string
  placeholder?: string
  showTime?: boolean
  hour?: number
  minute?: number
}

const props = withDefaults(defineProps<Props>(), {
  placeholder: 'Select date',
  showTime: false,
  hour: 0,
  minute: 0,
})

const emit = defineEmits<{
  'update:modelValue': [value: string]
  'update:hour': [value: number]
  'update:minute': [value: number]
}>()

const { activate: activateTrap, deactivate: deactivateTrap } = useFocusTrap()

const isOpen = ref(false)
const triggerRef = ref<HTMLButtonElement | null>(null)
const panelRef = ref<HTMLDivElement | null>(null)
const panelStyle = ref<Record<string, string>>({})

// The month/year currently displayed in the calendar
const viewYear = ref(new Date().getFullYear())
const viewMonth = ref(new Date().getMonth()) // 0-indexed

// ── Formatting ──────────────────────────────────────────────────────
const displayText = computed(() => {
  if (!props.modelValue) return null
  const [y, m, d] = props.modelValue.split('-').map(Number)
  const date = new Date(y, m - 1, d)
  const datePart = date.toLocaleDateString(getLocaleCode(), {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
  })
  if (props.showTime) {
    const hh = String(props.hour).padStart(2, '0')
    const mm = String(props.minute).padStart(2, '0')
    return `${datePart} ${hh}:${mm}`
  }
  return datePart
})

// ── Reduced motion check ────────────────────────────────────────────
const scrollBehavior = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' as const : 'smooth' as const

// ── Time wheel ──────────────────────────────────────────────────────
const WHEEL_ITEM_H = 36
const WHEEL_VISIBLE = 5
const WHEEL_HEIGHT = WHEEL_ITEM_H * WHEEL_VISIBLE // 180px
const WHEEL_PAD = WHEEL_ITEM_H * 2 // 72px spacer (2 items above/below center)

const internalHour = ref(props.hour)
const internalMinute = ref(props.minute)
const hourWheelRef = ref<HTMLDivElement | null>(null)
const minuteWheelRef = ref<HTMLDivElement | null>(null)

watch(() => props.hour, (v) => { internalHour.value = v })
watch(() => props.minute, (v) => { internalMinute.value = v })

// Debounced scroll handler to read the current value
let hourScrollTimer: ReturnType<typeof setTimeout> | null = null
let minuteScrollTimer: ReturnType<typeof setTimeout> | null = null

function onHourScroll() {
  if (hourScrollTimer) clearTimeout(hourScrollTimer)
  hourScrollTimer = setTimeout(() => {
    if (!hourWheelRef.value) return
    const index = Math.round(hourWheelRef.value.scrollTop / WHEEL_ITEM_H)
    const clamped = Math.min(23, Math.max(0, index))
    if (internalHour.value !== clamped) {
      internalHour.value = clamped
      emit('update:hour', clamped)
    }
  }, 60)
}

function onMinuteScroll() {
  if (minuteScrollTimer) clearTimeout(minuteScrollTimer)
  minuteScrollTimer = setTimeout(() => {
    if (!minuteWheelRef.value) return
    const index = Math.round(minuteWheelRef.value.scrollTop / WHEEL_ITEM_H)
    const clamped = Math.min(59, Math.max(0, index))
    if (internalMinute.value !== clamped) {
      internalMinute.value = clamped
      emit('update:minute', clamped)
    }
  }, 60)
}

// Mouse wheel: one item per tick
function onHourWheel(e: WheelEvent) {
  e.preventDefault()
  if (!hourWheelRef.value) return
  const dir = e.deltaY > 0 ? 1 : -1
  const cur = Math.round(hourWheelRef.value.scrollTop / WHEEL_ITEM_H)
  const next = Math.min(23, Math.max(0, cur + dir))
  hourWheelRef.value.scrollTo({ top: next * WHEEL_ITEM_H, behavior: scrollBehavior })
}

function onMinuteWheel(e: WheelEvent) {
  e.preventDefault()
  if (!minuteWheelRef.value) return
  const dir = e.deltaY > 0 ? 1 : -1
  const cur = Math.round(minuteWheelRef.value.scrollTop / WHEEL_ITEM_H)
  const next = Math.min(59, Math.max(0, cur + dir))
  minuteWheelRef.value.scrollTo({ top: next * WHEEL_ITEM_H, behavior: scrollBehavior })
}

// Keyboard support for time wheels
function onWheelKeydown(e: KeyboardEvent, type: 'hour' | 'minute') {
  if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
  e.preventDefault()
  const dir = e.key === 'ArrowUp' ? -1 : 1
  if (type === 'hour') {
    const next = Math.min(23, Math.max(0, internalHour.value + dir))
    internalHour.value = next
    emit('update:hour', next)
    if (hourWheelRef.value) {
      hourWheelRef.value.scrollTo({ top: next * WHEEL_ITEM_H, behavior: scrollBehavior })
    }
  } else {
    const next = Math.min(59, Math.max(0, internalMinute.value + dir))
    internalMinute.value = next
    emit('update:minute', next)
    if (minuteWheelRef.value) {
      minuteWheelRef.value.scrollTo({ top: next * WHEEL_ITEM_H, behavior: scrollBehavior })
    }
  }
}

// Click-and-drag support
let dragEl: HTMLElement | null = null
let dragStartY = 0
let dragStartScrollTop = 0

function onWheelPointerDown(e: PointerEvent) {
  const el = e.currentTarget as HTMLElement
  dragEl = el
  dragStartY = e.clientY
  dragStartScrollTop = el.scrollTop
  el.setPointerCapture(e.pointerId)
}

function onWheelPointerMove(e: PointerEvent) {
  if (!dragEl) return
  e.preventDefault()
  const delta = dragStartY - e.clientY
  dragEl.scrollTop = dragStartScrollTop + delta
}

function onWheelPointerUp(e: PointerEvent) {
  if (!dragEl) return
  const el = dragEl
  dragEl = null
  el.releasePointerCapture(e.pointerId)
  // Snap to nearest item
  const index = Math.round(el.scrollTop / WHEEL_ITEM_H)
  el.scrollTo({ top: index * WHEEL_ITEM_H, behavior: scrollBehavior })
}

function scrollWheelsToTime() {
  if (hourWheelRef.value) {
    hourWheelRef.value.scrollTop = internalHour.value * WHEEL_ITEM_H
  }
  if (minuteWheelRef.value) {
    minuteWheelRef.value.scrollTop = internalMinute.value * WHEEL_ITEM_H
  }
}

const viewMonthLabel = computed(() => {
  const date = new Date(viewYear.value, viewMonth.value, 1)
  return date.toLocaleDateString(getLocaleCode(), { month: 'long', year: 'numeric' })
})

// ── Today helpers ───────────────────────────────────────────────────
function todayString(): string {
  const now = new Date()
  return formatDate(now.getFullYear(), now.getMonth(), now.getDate())
}

function formatDate(y: number, m: number, d: number): string {
  const mm = String(m + 1).padStart(2, '0')
  const dd = String(d).padStart(2, '0')
  return `${y}-${mm}-${dd}`
}

// ── Calendar grid ───────────────────────────────────────────────────
interface DayCell {
  date: number
  month: number   // 0-indexed
  year: number
  isCurrentMonth: boolean
  dateString: string
}

const dayCells = computed<DayCell[]>(() => {
  const y = viewYear.value
  const m = viewMonth.value

  const firstDayOfWeek = new Date(y, m, 1).getDay()
  const startOffset = (firstDayOfWeek + 6) % 7

  const daysInMonth = new Date(y, m + 1, 0).getDate()
  const daysInPrevMonth = new Date(y, m, 0).getDate()

  const cells: DayCell[] = []

  const prevMonth = m === 0 ? 11 : m - 1
  const prevYear = m === 0 ? y - 1 : y
  for (let i = startOffset - 1; i >= 0; i--) {
    const d = daysInPrevMonth - i
    cells.push({
      date: d,
      month: prevMonth,
      year: prevYear,
      isCurrentMonth: false,
      dateString: formatDate(prevYear, prevMonth, d),
    })
  }

  for (let d = 1; d <= daysInMonth; d++) {
    cells.push({
      date: d,
      month: m,
      year: y,
      isCurrentMonth: true,
      dateString: formatDate(y, m, d),
    })
  }

  const nextMonth = m === 11 ? 0 : m + 1
  const nextYear = m === 11 ? y + 1 : y
  let nextDay = 1
  while (cells.length < 42) {
    cells.push({
      date: nextDay,
      month: nextMonth,
      year: nextYear,
      isCurrentMonth: false,
      dateString: formatDate(nextYear, nextMonth, nextDay),
    })
    nextDay++
  }

  return cells
})

const dayHeaders = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']

// ── Positioning ─────────────────────────────────────────────────────
function updatePosition() {
  if (!triggerRef.value) return
  const rect = triggerRef.value.getBoundingClientRect()
  const { scaleX, scaleY, offsetX, offsetY } = getFixedPositionMapping()
  const gap = 4

  const panelWidth = props.showTime ? 390 : 280
  const estW = panelWidth * scaleX
  const vpW = window.innerWidth

  let leftVP = rect.left
  if (leftVP + estW > vpW - gap) {
    leftVP = vpW - estW - gap
  }
  if (leftVP < gap) leftVP = gap

  panelStyle.value = {
    position: 'fixed',
    top: `${(rect.bottom + gap - offsetY) / scaleY}px`,
    left: `${(leftVP - offsetX) / scaleX}px`,
    width: `${panelWidth}px`,
    zIndex: '9999',
  }
}

// ── Open / Close ────────────────────────────────────────────────────
function toggle() {
  if (isOpen.value) {
    close()
  } else {
    open()
  }
}

function open() {
  if (props.modelValue) {
    const [y, m] = props.modelValue.split('-').map(Number)
    viewYear.value = y
    viewMonth.value = m - 1
  } else {
    const now = new Date()
    viewYear.value = now.getFullYear()
    viewMonth.value = now.getMonth()
  }

  isOpen.value = true
  updatePosition()

  nextTick(() => {
    document.addEventListener('click', onClickOutside, true)
    document.addEventListener('scroll', onScrollOrResize, true)
    window.addEventListener('resize', onScrollOrResize)

    if (props.showTime) {
      scrollWheelsToTime()
    }
    if (panelRef.value) activateTrap(panelRef.value)
  })
}

function close() {
  deactivateTrap()
  isOpen.value = false
  document.removeEventListener('click', onClickOutside, true)
  document.removeEventListener('scroll', onScrollOrResize, true)
  window.removeEventListener('resize', onScrollOrResize)
}

// ── Month navigation ────────────────────────────────────────────────
function prevMonthNav() {
  if (viewMonth.value === 0) {
    viewMonth.value = 11
    viewYear.value--
  } else {
    viewMonth.value--
  }
}

function nextMonthNav() {
  if (viewMonth.value === 11) {
    viewMonth.value = 0
    viewYear.value++
  } else {
    viewMonth.value++
  }
}

// ── Selection ───────────────────────────────────────────────────────
function selectDay(cell: DayCell) {
  if (!cell.isCurrentMonth) return
  emit('update:modelValue', cell.dateString)
  if (!props.showTime) {
    close()
  }
}

function selectToday() {
  emit('update:modelValue', todayString())
  if (!props.showTime) {
    close()
  }
}

// ── Event handlers ──────────────────────────────────────────────────
function onClickOutside(e: MouseEvent) {
  const target = e.target as Node
  if (
    triggerRef.value?.contains(target) ||
    panelRef.value?.contains(target)
  ) {
    return
  }
  close()
}

function onScrollOrResize() {
  if (isOpen.value) {
    updatePosition()
  }
}

// ── Sync view when modelValue changes externally ────────────────────
watch(
  () => props.modelValue,
  (val) => {
    if (val && isOpen.value) {
      const [y, m] = val.split('-').map(Number)
      viewYear.value = y
      viewMonth.value = m - 1
    }
  }
)

// ── Cleanup ─────────────────────────────────────────────────────────
onBeforeUnmount(() => {
  document.removeEventListener('click', onClickOutside, true)
  document.removeEventListener('scroll', onScrollOrResize, true)
  window.removeEventListener('resize', onScrollOrResize)
})
</script>

<template>
  <div class="relative">
    <!-- Trigger button -->
    <button
      ref="triggerRef"
      type="button"
      @click="toggle"
      :aria-expanded="isOpen"
      aria-haspopup="dialog"
      class="w-full flex items-center justify-between gap-2 px-3 py-2 bg-bg-inset border border-border-subtle rounded-xl text-[0.8125rem] text-left cursor-pointer transition-colors"
      :style="
        isOpen
          ? {
              borderColor: 'var(--color-accent)',
              boxShadow: '0 0 0 2px var(--color-accent-muted)',
              outline: 'none',
            }
          : {}
      "
    >
      <span
        :class="displayText ? 'text-text-primary' : 'text-text-tertiary'"
        class="truncate"
      >
        {{ displayText ?? placeholder }}
      </span>
      <Calendar
        aria-hidden="true"
        class="w-4 h-4 text-text-secondary shrink-0"
        :stroke-width="2"
      />
    </button>

    <!-- Calendar popover -->
    <Teleport to="#app">
      <Transition name="dropdown">
      <div
        v-if="isOpen"
        ref="panelRef"
        :style="panelStyle"
        role="dialog"
        aria-modal="true"
        aria-label="Date picker"
        @keydown.escape.prevent="close"
        class="bg-bg-surface border border-border-visible rounded-xl shadow-[0_4px_24px_rgba(0,0,0,0.4)] overflow-hidden"
      >
        <!-- Month/year header -->
        <div class="flex items-center justify-between px-3 py-2.5">
          <button
            type="button"
            @click="prevMonthNav"
            aria-label="Previous month"
            v-tooltip="'Previous month'"
            class="p-1 rounded-lg hover:bg-bg-elevated transition-colors cursor-pointer text-text-secondary hover:text-text-primary"
          >
            <ChevronLeft aria-hidden="true" class="w-4 h-4" :stroke-width="2" />
          </button>
          <span class="text-[0.8125rem] font-medium text-text-primary select-none">
            {{ viewMonthLabel }}
          </span>
          <button
            type="button"
            @click="nextMonthNav"
            aria-label="Next month"
            v-tooltip="'Next month'"
            class="p-1 rounded-lg hover:bg-bg-elevated transition-colors cursor-pointer text-text-secondary hover:text-text-primary"
          >
            <ChevronRight aria-hidden="true" class="w-4 h-4" :stroke-width="2" />
          </button>
        </div>

        <!-- Calendar + Time wheels side by side -->
        <div class="flex">
          <!-- Calendar column -->
          <div class="flex-1 min-w-0">
            <!-- Day-of-week headers -->
            <div class="grid grid-cols-7 px-2" role="row">
              <div
                v-for="header in dayHeaders"
                :key="header"
                role="columnheader"
                class="text-center text-[0.6875rem] font-medium text-text-tertiary py-1 select-none"
              >
                {{ header }}
              </div>
            </div>

            <!-- Day grid -->
            <div class="grid grid-cols-7 px-2 pb-2" role="grid" aria-label="Calendar days">
              <button
                v-for="(cell, index) in dayCells"
                :key="index"
                type="button"
                :disabled="!cell.isCurrentMonth"
                @click="selectDay(cell)"
                :aria-label="new Date(cell.year, cell.month, cell.date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })"
                :aria-selected="cell.isCurrentMonth ? cell.dateString === modelValue : undefined"
                class="relative flex items-center justify-center h-8 w-full text-[0.75rem] rounded-lg transition-colors select-none"
                :class="[
                  !cell.isCurrentMonth
                    ? 'text-text-tertiary/40 cursor-default'
                    : cell.dateString === modelValue
                      ? 'bg-accent text-bg-base font-medium cursor-pointer'
                      : cell.dateString === todayString()
                        ? 'ring-1 ring-accent text-accent-text cursor-pointer hover:bg-bg-elevated'
                        : 'text-text-primary cursor-pointer hover:bg-bg-elevated',
                ]"
              >
                {{ cell.date }}
              </button>
            </div>
          </div>

          <!-- Time wheels column (to the right of calendar) -->
          <div v-if="showTime" class="border-l border-border-subtle flex flex-col items-center justify-center px-3">
            <div class="flex items-center gap-1.5">
              <!-- Hour wheel -->
              <div
                class="relative overflow-hidden rounded-lg"
                :style="{ height: WHEEL_HEIGHT + 'px', width: '42px' }"
              >
                <!-- Highlight band (behind scroll content via DOM order) -->
                <div
                  class="absolute inset-x-0 rounded-lg pointer-events-none bg-bg-elevated border border-border-subtle"
                  :style="{ top: WHEEL_PAD + 'px', height: WHEEL_ITEM_H + 'px' }"
                />
                <!-- Scrollable wheel -->
                <div
                  ref="hourWheelRef"
                  tabindex="0"
                  role="spinbutton"
                  :aria-valuenow="internalHour"
                  aria-valuemin="0"
                  aria-valuemax="23"
                  aria-label="Hour"
                  class="absolute inset-0 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden cursor-grab active:cursor-grabbing"
                  style="touch-action: none; -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%); mask-image: linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%);"
                  @scroll="onHourScroll"
                  @wheel.prevent="onHourWheel"
                  @keydown="onWheelKeydown($event, 'hour')"
                  @pointerdown.prevent="onWheelPointerDown"
                  @pointermove="onWheelPointerMove"
                  @pointerup="onWheelPointerUp"
                >
                  <div :style="{ height: WHEEL_PAD + 'px' }" class="shrink-0" />
                  <div
                    v-for="h in 24"
                    :key="h"
                    class="shrink-0 flex items-center justify-center text-[0.875rem] font-mono select-none"
                    :style="{ height: WHEEL_ITEM_H + 'px' }"
                    :class="internalHour === h - 1 ? 'text-text-primary font-semibold' : 'text-text-tertiary'"
                  >
                    {{ String(h - 1).padStart(2, '0') }}
                  </div>
                  <div :style="{ height: WHEEL_PAD + 'px' }" class="shrink-0" />
                </div>
              </div>

              <span class="text-text-secondary text-sm font-mono font-semibold select-none">:</span>

              <!-- Minute wheel -->
              <div
                class="relative overflow-hidden rounded-lg"
                :style="{ height: WHEEL_HEIGHT + 'px', width: '42px' }"
              >
                <!-- Highlight band -->
                <div
                  class="absolute inset-x-0 rounded-lg pointer-events-none bg-bg-elevated border border-border-subtle"
                  :style="{ top: WHEEL_PAD + 'px', height: WHEEL_ITEM_H + 'px' }"
                />
                <!-- Scrollable wheel -->
                <div
                  ref="minuteWheelRef"
                  tabindex="0"
                  role="spinbutton"
                  :aria-valuenow="internalMinute"
                  aria-valuemin="0"
                  aria-valuemax="59"
                  aria-label="Minute"
                  class="absolute inset-0 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden cursor-grab active:cursor-grabbing"
                  style="touch-action: none; -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%); mask-image: linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%);"
                  @scroll="onMinuteScroll"
                  @wheel.prevent="onMinuteWheel"
                  @keydown="onWheelKeydown($event, 'minute')"
                  @pointerdown.prevent="onWheelPointerDown"
                  @pointermove="onWheelPointerMove"
                  @pointerup="onWheelPointerUp"
                >
                  <div :style="{ height: WHEEL_PAD + 'px' }" class="shrink-0" />
                  <div
                    v-for="m in 60"
                    :key="m"
                    class="shrink-0 flex items-center justify-center text-[0.875rem] font-mono select-none"
                    :style="{ height: WHEEL_ITEM_H + 'px' }"
                    :class="internalMinute === m - 1 ? 'text-text-primary font-semibold' : 'text-text-tertiary'"
                  >
                    {{ String(m - 1).padStart(2, '0') }}
                  </div>
                  <div :style="{ height: WHEEL_PAD + 'px' }" class="shrink-0" />
                </div>
              </div>
            </div>
          </div>
        </div>

        <!-- Today shortcut -->
        <div class="border-t border-border-subtle px-3 py-2">
          <button
            type="button"
            @click="selectToday"
            class="w-full text-center text-[0.75rem] text-accent-text hover:text-accent cursor-pointer transition-colors py-0.5"
          >
            Today
          </button>
        </div>
      </div>
      </Transition>
    </Teleport>
  </div>
</template>
