<script setup>
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
import { useCalendarStore } from '@/stores/CalendarStore'
import CalendarHeader from '@/components/CalendarHeader.vue'
import CalendarWeek from '@/components/CalendarWeek.vue'
import HeaderControls from '@/components/HeaderControls.vue'
import {
  createScrollManager,
  createWeekColumnScrollManager,
  createMonthScrollManager,
} from '@/plugins/scrollManager'
import { daysInclusive, addDaysStr, MIN_YEAR, MAX_YEAR } from '@/utils/date'
import { toLocalString, fromLocalString, DEFAULT_TZ } from '@/utils/date'
import { addDays, differenceInWeeks } from 'date-fns'
import { createVirtualWeekManager } from '@/plugins/virtualWeeks'

const calendarStore = useCalendarStore()
const emit = defineEmits(['create-event', 'edit-event'])
const viewport = ref(null)
const viewportHeight = ref(600)
const rowHeight = ref(64)
const rowProbe = ref(null)
let rowProbeObserver = null
const baseDate = computed(() => new Date(1970, 0, 4 + calendarStore.config.first_day))
const selection = ref({ startDate: null, dayCount: 0 })
const isDragging = ref(false)
const dragAnchor = ref(null)
const DOUBLE_TAP_DELAY = 300
const pendingTap = ref({ date: null, time: 0, type: null })
const suppressMouseUntil = ref(0)
function normalizeDate(val) {
  if (typeof val === 'string') return val
  if (val && typeof val === 'object') {
    if (val.date) return String(val.date)
    if (val.startDate) return String(val.startDate)
  }
  return String(val)
}

function registerTap(rawDate, type) {
  const dateStr = normalizeDate(rawDate)
  const now = Date.now()
  const prev = pendingTap.value
  const delta = now - prev.time
  const isDouble =
    prev.date === dateStr && prev.type === type && delta <= DOUBLE_TAP_DELAY && delta >= 35
  if (isDouble) {
    pendingTap.value = { date: null, time: 0, type: null }
    return true
  }
  pendingTap.value = { date: dateStr, time: now, type }
  return false
}

const minVirtualWeek = computed(() => {
  const date = new Date(MIN_YEAR, 0, 1)
  const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7
  const firstDayOfWeek = addDays(date, -dayOffset)
  return differenceInWeeks(firstDayOfWeek, baseDate.value)
})

const maxVirtualWeek = computed(() => {
  const date = new Date(MAX_YEAR, 11, 31)
  const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7
  const firstDayOfWeek = addDays(date, -dayOffset)
  return differenceInWeeks(firstDayOfWeek, baseDate.value)
})

const totalVirtualWeeks = computed(() => {
  return maxVirtualWeek.value - minVirtualWeek.value + 1
})

const contentHeight = computed(() => {
  return totalVirtualWeeks.value * rowHeight.value
})

// Virtual weeks manager (after dependent refs exist)
const vwm = createVirtualWeekManager({
  calendarStore,
  viewport,
  viewportHeight,
  rowHeight,
  selection,
  baseDate,
  minVirtualWeek,
  maxVirtualWeek,
  contentHeight,
})
const visibleWeeks = vwm.visibleWeeks
const { scheduleWindowUpdate, resetWeeks, refreshEvents } = vwm

// Scroll managers (after scheduleWindowUpdate available)
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
const { scrollTop, setScrollTop, onScroll } = scrollManager
const weekColumnScrollManager = createWeekColumnScrollManager({
  viewport,
  viewportHeight,
  contentHeight,
  setScrollTop,
})
const { isWeekColDragging, handleWeekColMouseDown, handlePointerLockChange } =
  weekColumnScrollManager
const monthScrollManager = createMonthScrollManager({
  viewport,
  viewportHeight,
  contentHeight,
  setScrollTop,
})
const { handleMonthScrollPointerDown, handleMonthScrollTouchStart, handleMonthScrollWheel } =
  monthScrollManager

// Provide scroll refs to virtual week manager
vwm.attachScroll(scrollTop, setScrollTop)

const initialScrollTop = computed(() => {
  const nowDate = new Date(calendarStore.now)
  const targetWeekIndex = getWeekIndex(nowDate) - 3
  return (targetWeekIndex - minVirtualWeek.value) * rowHeight.value
})

function computeRowHeight() {
  if (rowProbe.value) {
    const h = rowProbe.value.getBoundingClientRect().height || 64
    rowHeight.value = Math.round(h)
    return rowHeight.value
  }
  const el = document.createElement('div')
  el.style.position = 'absolute'
  el.style.visibility = 'hidden'
  el.style.height = 'var(--row-h)'
  document.body.appendChild(el)
  const h = el.getBoundingClientRect().height || 64
  el.remove()
  rowHeight.value = Math.round(h)
  return rowHeight.value
}
function measureFromProbe() {
  if (!rowProbe.value) return
  const h = rowProbe.value.getBoundingClientRect().height
  if (!h) return
  const newH = Math.round(h)
  if (newH !== rowHeight.value) {
    const oldH = rowHeight.value
    // Anchor: keep the same top virtual week visible.
    const topVirtualWeek = Math.floor(scrollTop.value / oldH) + minVirtualWeek.value
    rowHeight.value = newH
    const newScrollTop = (topVirtualWeek - minVirtualWeek.value) * newH
    setScrollTop(newScrollTop, 'row-height-change')
    resetWeeks('row-height-change')
  }
}

const { getWeekIndex, getFirstDayForVirtualWeek, goToToday, handleHeaderYearChange } = vwm

// createWeek logic moved to virtualWeeks plugin

// goToToday now provided by manager

function clearSelection() {
  selection.value = { startDate: null, dayCount: 0 }
}

function startDrag(dateStr) {
  dateStr = normalizeDate(dateStr)
  if (calendarStore.config.select_days === 0) return
  isDragging.value = true
  dragAnchor.value = dateStr
  selection.value = { startDate: dateStr, dayCount: 1 }
  addGlobalTouchListeners()
}

function updateDrag(dateStr) {
  if (!isDragging.value) return
  const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr)
  selection.value = { startDate, dayCount }
}

function endDrag(dateStr) {
  if (!isDragging.value) return
  isDragging.value = false
  const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr)
  selection.value = { startDate, dayCount }
}

function finalizeDragAndCreate() {
  if (!isDragging.value) return
  isDragging.value = false
  const eventData = createEventFromSelection()
  if (eventData) {
    clearSelection()
    emit('create-event', eventData)
  }
  removeGlobalTouchListeners()
}

// Build a minimal event creation payload from current selection
// Returns null if selection is invalid or empty.
function createEventFromSelection() {
  const sel = selection.value || {}
  if (!sel.startDate || !sel.dayCount || sel.dayCount <= 0) return null
  return {
    startDate: sel.startDate,
    dayCount: sel.dayCount,
  }
}

function getDateUnderPoint(x, y) {
  const el = document.elementFromPoint(x, y)
  let cur = el
  while (cur) {
    if (cur.dataset && cur.dataset.date) return cur.dataset.date
    cur = cur.parentElement
  }
  return getDateFromCoordinates(x, y)
}

function onGlobalTouchMove(e) {
  if (!isDragging.value) return
  const t = e.touches && e.touches[0]
  if (!t) return
  e.preventDefault()
  const dateStr = getDateUnderPoint(t.clientX, t.clientY)
  if (dateStr) updateDrag(dateStr)
}

function onGlobalTouchEnd(e) {
  if (!isDragging.value) {
    removeGlobalTouchListeners()
    return
  }
  const t = (e.changedTouches && e.changedTouches[0]) || (e.touches && e.touches[0])
  if (t) {
    const dateStr = getDateUnderPoint(t.clientX, t.clientY)
    if (dateStr) {
      const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr)
      selection.value = { startDate, dayCount }
    }
  }
  finalizeDragAndCreate()
}

function addGlobalTouchListeners() {
  window.addEventListener('touchmove', onGlobalTouchMove, { passive: false })
  window.addEventListener('touchend', onGlobalTouchEnd, { passive: false })
  window.addEventListener('touchcancel', onGlobalTouchEnd, { passive: false })
}

function removeGlobalTouchListeners() {
  window.removeEventListener('touchmove', onGlobalTouchMove)
  window.removeEventListener('touchend', onGlobalTouchEnd)
  window.removeEventListener('touchcancel', onGlobalTouchEnd)
}

// Fallback hit-test if elementFromPoint doesn't find a day cell (e.g., moving between rows).
function getDateFromCoordinates(clientX, clientY) {
  if (!viewport.value) return null
  const vpRect = viewport.value.getBoundingClientRect()
  const yOffset = clientY - vpRect.top + viewport.value.scrollTop
  if (yOffset < 0) return null
  const rowIndex = Math.floor(yOffset / rowHeight.value)
  const virtualWeek = minVirtualWeek.value + rowIndex
  if (virtualWeek < minVirtualWeek.value || virtualWeek > maxVirtualWeek.value) return null
  const sampleWeek = viewport.value.querySelector('.week-row')
  if (!sampleWeek) return null
  const labelEl = sampleWeek.querySelector('.week-label')
  const wrRect = sampleWeek.getBoundingClientRect()
  const labelRight = labelEl ? labelEl.getBoundingClientRect().right : wrRect.left
  const daysAreaRight = wrRect.right
  const daysWidth = daysAreaRight - labelRight
  if (clientX < labelRight || clientX > daysAreaRight) return null
  const col = Math.min(6, Math.max(0, Math.floor(((clientX - labelRight) / daysWidth) * 7)))
  const firstDay = getFirstDayForVirtualWeek(virtualWeek)
  const targetDate = addDays(firstDay, col)
  return toLocalString(targetDate, DEFAULT_TZ)
}

function calculateSelection(anchorStr, otherStr) {
  const limit = calendarStore.config.select_days
  const anchorDate = fromLocalString(anchorStr, DEFAULT_TZ)
  const otherDate = fromLocalString(otherStr, DEFAULT_TZ)
  const forward = otherDate >= anchorDate
  const span = daysInclusive(anchorStr, otherStr)

  if (span <= limit) {
    const startDate = forward ? anchorStr : otherStr
    return { startDate, dayCount: span }
  }

  if (forward) {
    return { startDate: anchorStr, dayCount: limit }
  } else {
    const startDate = addDaysStr(anchorStr, -(limit - 1), DEFAULT_TZ)
    return { startDate, dayCount: limit }
  }
}

// ---------------- Week label column drag scrolling ----------------
function getWeekLabelRect() {
  // Prefer header year label width as stable reference
  const headerYear = document.querySelector('.calendar-header .year-label')
  if (headerYear) return headerYear.getBoundingClientRect()
  const weekLabel = viewport.value?.querySelector('.week-row .week-label')
  return weekLabel ? weekLabel.getBoundingClientRect() : null
}

onMounted(() => {
  computeRowHeight()
  calendarStore.updateCurrentDate()

  if (viewport.value) {
    viewportHeight.value = viewport.value.clientHeight
    setScrollTop(initialScrollTop.value, 'initial-mount')
    viewport.value.addEventListener('scroll', onScroll)
    // Capture mousedown in viewport to allow dragging via week label column
    viewport.value.addEventListener('mousedown', handleWeekColMouseDown, true)
  }
  document.addEventListener('pointerlockchange', handlePointerLockChange)

  const timer = setInterval(() => {
    calendarStore.updateCurrentDate()
  }, 60000)

  // Initial incremental build (no existing weeks yet)
  scheduleWindowUpdate('init')

  if (window.ResizeObserver && rowProbe.value) {
    rowProbeObserver = new ResizeObserver(() => {
      measureFromProbe()
    })
    rowProbeObserver.observe(rowProbe.value)
  }

  onBeforeUnmount(() => {
    clearInterval(timer)
  })
})

onBeforeUnmount(() => {
  if (viewport.value) {
    viewport.value.removeEventListener('scroll', onScroll)
    viewport.value.removeEventListener('mousedown', handleWeekColMouseDown, true)
  }
  if (rowProbeObserver && rowProbe.value) {
    try {
      rowProbeObserver.unobserve(rowProbe.value)
      rowProbeObserver.disconnect()
    } catch (e) {}
  }
  document.removeEventListener('pointerlockchange', handlePointerLockChange)
})

const handleDayMouseDown = (d) => {
  d = normalizeDate(d)
  if (Date.now() < suppressMouseUntil.value) return
  if (registerTap(d, 'mouse')) startDrag(d)
}
const handleDayMouseEnter = (d) => updateDrag(normalizeDate(d))
const handleDayMouseUp = (d) => {
  d = normalizeDate(d)
  if (Date.now() < suppressMouseUntil.value && !isDragging.value) return
  if (!isDragging.value) return
  endDrag(d)
  const ev = createEventFromSelection()
  if (ev) {
    clearSelection()
    emit('create-event', ev)
  }
}
const handleDayTouchStart = (d) => {
  d = normalizeDate(d)
  suppressMouseUntil.value = Date.now() + 800
  if (registerTap(d, 'touch')) startDrag(d)
}

const handleEventClick = (payload) => {
  emit('edit-event', payload)
}

// header year change delegated to manager

// Heuristic: rotate month label (180deg) only for predominantly Latin text.
// We explicitly avoid locale detection; rely solely on characters present.
// Disable rotation if any CJK Unified Ideograph or Compatibility Ideograph appears.
function shouldRotateMonth(label) {
  if (!label) return false
  return /\p{Script=Latin}/u.test(label)
}

// Watch first day changes (e.g., first_day config update) to adjust scroll
// Keep roughly same visible date when first_day setting changes.
watch(
  () => calendarStore.config.first_day,
  () => {
    const currentTopVW = Math.floor(scrollTop.value / rowHeight.value) + minVirtualWeek.value
    const currentTopDate = getFirstDayForVirtualWeek(currentTopVW)
    requestAnimationFrame(() => {
      const newTopWeekIndex = getWeekIndex(currentTopDate)
      const newScroll = (newTopWeekIndex - minVirtualWeek.value) * rowHeight.value
      setScrollTop(newScroll, 'first-day-change')
      resetWeeks('first-day-change')
    })
  },
)

// Watch lightweight mutation counter only (not deep events map) and rebuild lazily
watch(
  () => calendarStore.events,
  () => {
    refreshEvents('events')
  },
  { deep: true },
)

// Reflect selection & events by rebuilding day objects in-place
watch(
  () => [selection.value.startDate, selection.value.dayCount],
  () => refreshEvents('selection'),
)

// Rebuild if viewport height changes (e.g., resize)
window.addEventListener('resize', () => {
  if (viewport.value) viewportHeight.value = viewport.value.clientHeight
  measureFromProbe()
  scheduleWindowUpdate('resize')
})
</script>

<template>
  <div class="calendar-view-root">
    <div ref="rowProbe" class="row-height-probe" aria-hidden="true"></div>
    <div class="wrap">
      <HeaderControls @go-to-today="goToToday" />
      <CalendarHeader
        :scroll-top="scrollTop"
        :row-height="rowHeight"
        :min-virtual-week="minVirtualWeek"
        @year-change="handleHeaderYearChange"
      />
      <div class="calendar-container">
        <div class="calendar-viewport" ref="viewport">
          <!-- Main calendar content (weeks and days) -->
          <div class="main-calendar-area">
            <div class="calendar-content" :style="{ height: contentHeight + 'px' }">
              <CalendarWeek
                v-for="week in visibleWeeks"
                :key="week.virtualWeek"
                :week="week"
                :dragging="isDragging"
                :style="{ top: week.top + 'px' }"
                @day-mousedown="handleDayMouseDown"
                @day-mouseenter="handleDayMouseEnter"
                @day-mouseup="handleDayMouseUp"
                @day-touchstart="handleDayTouchStart"
                @event-click="handleEventClick"
              />
            </div>
          </div>
          <!-- Month column area -->
          <div class="month-column-area">
            <!-- Month labels -->
            <div class="month-labels-container" :style="{ height: contentHeight + 'px' }">
              <template v-for="monthWeek in visibleWeeks" :key="monthWeek.virtualWeek + '-month'">
                <div
                  v-if="monthWeek && monthWeek.monthLabel"
                  class="month-label"
                  :class="monthWeek.monthLabel?.monthClass"
                  :style="{
                    height: `calc(var(--row-h) * ${monthWeek.monthLabel?.weeksSpan || 1})`,
                    top: (monthWeek.top || 0) + 'px',
                  }"
                  @pointerdown="handleMonthScrollPointerDown"
                  @touchstart.prevent="handleMonthScrollTouchStart"
                  @wheel="handleMonthScrollWheel"
                >
                  <span :class="{ bottomup: shouldRotateMonth(monthWeek.monthLabel?.text) }">{{
                    monthWeek.monthLabel?.text || ''
                  }}</span>
                </div>
              </template>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.calendar-view-root {
  display: contents;
}
.wrap {
  height: 100vh;
  display: flex;
  flex-direction: column;
}

header {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.75rem 0.5rem 0.25rem 0.5rem;
}
header h1 {
  margin: 0;
  padding: 0;
  font-size: 1.6rem;
  font-weight: 600;
}

.calendar-container {
  flex: 1;
  display: flex;
  position: relative;
  /* Prevent text selection in calendar */
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

.calendar-viewport {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  display: grid;
  grid-template-columns: 1fr var(--month-w);
}

.main-calendar-area {
  position: relative;
  overflow: hidden;
}

.calendar-content {
  position: relative;
  width: 100%;
}

.month-column-area {
  position: relative;
  cursor: ns-resize;
}

.month-labels-container {
  position: relative;
  width: 100%;
  height: 100%;
}

.month-label {
  position: absolute;
  left: 0;
  width: 100%;
  background-image: linear-gradient(to bottom, rgba(186, 186, 186, 0.3), rgba(186, 186, 186, 0.2));
  font-size: 2em;
  font-weight: 700;
  color: var(--muted);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 15;
  overflow: hidden;
  cursor: ns-resize;
  user-select: none;
  touch-action: none;
}

.month-label > span {
  display: inline-block;
  white-space: nowrap;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  transform-origin: center;
  pointer-events: none;
}

.bottomup {
  transform: rotate(180deg);
}

.row-height-probe {
  position: absolute;
  visibility: hidden;
  height: var(--row-h);
  pointer-events: none;
}
</style>
