<template>
    <div ref="calendarEl" class="relative flex h-full flex-col rounded-lg border border-neutral-200 bg-white">
        <!-- Grid body with sticky header -->
        <div ref="gridBodyEl" class="relative min-h-0 flex-1 overflow-y-auto">
            <!-- Day headers (sticky inside scroll container) -->
            <div class="sticky top-0 z-20 flex border-b border-neutral-200 bg-white rounded-t-lg">
                <div class="w-16 shrink-0 border-r border-neutral-200" />
                <div
                    v-for="day in days"
                    :key="'header-' + day.dateStr"
                    class="flex-1 border-r border-neutral-100 py-2 text-center"
                    :class="{ 'bg-orange-50': day.holiday }"
                >
                    <div class="text-lg font-bold" :class="isToday(day.date) ? 'text-orange-500' : 'text-neutral-900'">
                        {{ day.dayNum }}
                    </div>
                    <div class="text-xs" :class="isToday(day.date) ? 'text-orange-500' : 'text-neutral-500'">
                        {{ day.label }}
                    </div>
                    <div
                        v-if="day.holiday"
                        class="flex items-center justify-center gap-0.5 truncate px-1 text-[10px] font-medium text-amber-600"
                        :title="day.holiday"
                    >
                        <Icon name="mdi:star-four-points-outline" size="10" class="flex-shrink-0" />
                        <span class="truncate">{{ day.holiday }}</span>
                    </div>
                    <div class="text-[10px] text-neutral-400">{{ day.totalFormatted }}</div>
                </div>
            </div>

            <!-- Columns -->
            <div class="relative flex">
            <!-- Hour labels -->
            <div class="w-16 shrink-0">
                <div
                    v-for="hour in hours"
                    :key="hour"
                    class="flex items-start justify-end border-r border-neutral-200 pr-2 text-xs text-neutral-400"
                    :style="{ height: `${hourHeight}px` }"
                >
                    {{ String(hour).padStart(2, '0') }} : 00
                </div>
            </div>

            <!-- Day columns -->
            <div
                v-for="(day, dayIndex) in days"
                :key="day.dateStr"
                :ref="(el) => { dayColumnEls[dayIndex] = el as HTMLElement }"
                class="relative flex-1 border-r border-neutral-100"
                :class="{ 'bg-orange-50': day.holiday }"
                @click="onClickGrid($event, day)"
                @contextmenu.prevent="onContextMenuGrid($event, day)"
            >
                <!-- Hour row lines -->
                <div
                    v-for="hour in hours"
                    :key="hour"
                    class="border-b border-neutral-100"
                    :style="{ height: `${hourHeight}px` }"
                />

                <!-- Time entry blocks with overlap columns -->
                <TimeEntryBlock
                    v-for="layout in layoutForDay(day.dateStr)"
                    :key="layout.entry.id"
                    :entry="layout.entry"
                    :hour-height="hourHeight"
                    :day-start-hour="0"
                    :is-drag-source="dragState?.entryId === layout.entry.id"
                    :column-index="layout.columnIndex"
                    :total-columns="layout.totalColumns"
                    @click="emit('editEntry', $event)"
                    @contextmenu="(ev, ent) => emit('contextmenu', ev, ent)"
                    @resize="(ent, newStart, newStop) => emit('resizeEntry', ent, newStart, newStop)"
                    @move-start="(payload) => onMoveStart(payload, dayIndex)"
                />

                <!-- Overflow indicators for dense groups -->
                <div
                    v-for="overflow in overflowsForDay(day.dateStr)"
                    :key="`overflow-${overflow.topPx}`"
                    class="absolute right-1 z-20 rounded bg-neutral-700 px-1.5 py-0.5 text-[10px] font-semibold text-white cursor-pointer hover:bg-neutral-600 transition"
                    :style="{ top: `${overflow.topPx}px` }"
                    @click.stop="openOverflowPopover(dayIndex, overflow)"
                >
                    +{{ overflow.count }}
                </div>

                <!-- Overflow popover -->
                <div
                    v-if="overflowPopover && overflowPopover.dayIndex === dayIndex"
                    class="absolute z-30 w-48 rounded-lg border border-neutral-200 bg-white p-2 shadow-xl"
                    :style="{ top: `${overflowPopover.topPx}px`, right: '4px' }"
                >
                    <div class="mb-1 flex items-center justify-between">
                        <span class="text-xs font-semibold text-neutral-600">{{ overflowPopover.entries.length }} entrées masquées</span>
                        <button class="text-neutral-400 hover:text-neutral-600 text-xs" @click="overflowPopover = null">&times;</button>
                    </div>
                    <div
                        v-for="entry in overflowPopover.entries"
                        :key="entry.id"
                        class="flex items-center gap-2 rounded px-1.5 py-1 cursor-pointer hover:bg-neutral-50 transition"
                        @click.stop="emit('editEntry', entry); overflowPopover = null"
                    >
                        <div
                            class="h-3 w-3 shrink-0 rounded-sm"
                            :style="{ backgroundColor: entry.project?.color ?? '#94a3b8' }"
                        />
                        <div class="min-w-0">
                            <div class="truncate text-xs font-medium text-neutral-800">{{ entry.title || $t('common.untitled') }}</div>
                            <div class="text-[10px] text-neutral-500">
                                {{ formatTime(entry.startedAt) }} – {{ entry.stoppedAt ? formatTime(entry.stoppedAt) : '...' }}
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Current time indicator -->
                <div
                    v-if="isToday(day.date)"
                    class="absolute left-0 right-0 z-10 pointer-events-none"
                    :style="{ top: `${currentTimeTopPx}px` }"
                >
                    <div class="relative flex items-center">
                        <div class="absolute -left-[5px] h-[10px] w-[10px] rounded-full bg-orange-500" />
                        <div class="h-[2px] w-full bg-orange-500" />
                    </div>
                </div>

                <!-- Drag ghost preview -->
                <div
                    v-if="dragState && dragState.targetDayIndex === dayIndex"
                    class="absolute left-1 right-1 rounded-md px-2 py-1 text-xs text-white shadow-lg pointer-events-none ring-2 ring-white/60 transition-[top] duration-75"
                    :style="{
                        top: `${dragState.ghostTopPx}px`,
                        height: `${dragState.ghostHeightPx}px`,
                        backgroundColor: dragState.color,
                        opacity: 0.75,
                    }"
                >
                    <div class="font-semibold truncate">{{ dragState.title }}</div>
                    <div class="text-[10px] opacity-90">{{ dragState.timeLabel }}</div>
                </div>
            </div>
        </div><!-- end columns flex -->
        </div><!-- end gridBodyEl -->
    </div>
</template>

<script setup lang="ts">
import type { TimeEntry } from '~/modules/time-tracking/services/dto/time-entry'
import { useAbsenceService } from '~/modules/absence/services/absences'

const { t } = useI18n()
const absenceService = useAbsenceService()

const props = defineProps<{
    entries: TimeEntry[]
    startDate: Date
    viewMode: 'week' | 'day'
    stickyOffset?: number
}>()

const emit = defineEmits<{
    (e: 'editEntry', entry: TimeEntry): void
    (e: 'createEntry', startedAt: string): void
    (e: 'moveEntry', entry: TimeEntry, newStartedAt: string, newStoppedAt: string): void
    (e: 'resizeEntry', entry: TimeEntry, newStartedAt: string, newStoppedAt: string): void
    (e: 'contextmenu', event: MouseEvent, entry: TimeEntry | null): void
}>()

const hourHeight = 60
const hours = Array.from({ length: 24 }, (_, i) => i)
const dayLabels = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam']

const calendarEl = ref<HTMLElement | null>(null)
const gridBodyEl = ref<HTMLElement | null>(null)
const dayColumnEls = ref<HTMLElement[]>([])
const stickyOffset = computed(() => props.stickyOffset ?? 0)

// --- Current time indicator ---
const nowMinutes = ref(0)
let nowTimer: ReturnType<typeof setInterval> | undefined

function updateNowMinutes() {
    const now = new Date()
    nowMinutes.value = now.getHours() * 60 + now.getMinutes()
}

const currentTimeTopPx = computed(() => (nowMinutes.value / 60) * hourHeight)

updateNowMinutes()

onMounted(() => {
    nowTimer = setInterval(updateNowMinutes, 60_000)
})

onUnmounted(() => {
    clearInterval(nowTimer)
})

function getScrollParent(): HTMLElement | null {
    let el = calendarEl.value?.parentElement
    while (el) {
        if (el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'visible') return el
        el = el.parentElement
    }
    return null
}

// Scroll to current hour on mount
onMounted(() => {
    nextTick(() => {
        if (!gridBodyEl.value) return
        const now = new Date()
        const currentMinutes = now.getHours() * 60 + now.getMinutes()
        const scrollTarget = (currentMinutes / 60) * hourHeight - gridBodyEl.value.clientHeight / 3
        gridBodyEl.value.scrollTop = Math.max(0, scrollTarget)
    })
})

// --- Public holidays (computed server-side, shared with the absence calendar) ---
const holidays = ref<Record<string, string>>({})

async function loadHolidays() {
    const count = props.viewMode === 'week' ? 7 : 1
    const start = new Date(props.startDate)
    const end = new Date(start)
    end.setDate(end.getDate() + count - 1)
    try {
        holidays.value = await absenceService.getPublicHolidays(toDateStr(start), toDateStr(end))
    } catch {
        holidays.value = {}
    }
}

watch(() => [props.startDate, props.viewMode], loadHolidays, { immediate: true })

// --- Days computation ---
const days = computed(() => {
    const count = props.viewMode === 'week' ? 7 : 1
    const result = []
    for (let i = 0; i < count; i++) {
        const d = new Date(props.startDate)
        d.setDate(d.getDate() + i)
        const dateStr = toDateStr(d)
        const dayEntries = props.entries.filter((e) => toDateStr(new Date(e.startedAt)) === dateStr)
        const totalMs = dayEntries.reduce((sum, e) => {
            if (!e.stoppedAt) return sum
            return sum + (new Date(e.stoppedAt).getTime() - new Date(e.startedAt).getTime())
        }, 0)
        const totalH = Math.floor(totalMs / 3600000)
        const totalM = Math.floor((totalMs % 3600000) / 60000)
        const totalS = Math.floor((totalMs % 60000) / 1000)

        result.push({
            date: new Date(d),
            dateStr,
            dayNum: d.getDate(),
            label: dayLabels[d.getDay()],
            holiday: holidays.value[dateStr] ?? null,
            totalFormatted: `${String(totalH).padStart(2, '0')}:${String(totalM).padStart(2, '0')}:${String(totalS).padStart(2, '0')}`,
        })
    }
    return result
})

function toDateStr(d: Date): string {
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}

function isToday(d: Date): boolean {
    return toDateStr(d) === toDateStr(new Date())
}

function entriesForDay(dateStr: string): TimeEntry[] {
    return props.entries.filter((e) => toDateStr(new Date(e.startedAt)) === dateStr)
}

// --- Overlap layout computation ---
const MAX_VISIBLE_COLUMNS = 4

interface EntryLayout {
    entry: TimeEntry
    columnIndex: number
    totalColumns: number
}

interface OverflowIndicator {
    topPx: number
    count: number
    hiddenEntries: TimeEntry[]
}

function getEntryMinutes(entry: TimeEntry): { start: number; end: number } {
    const s = new Date(entry.startedAt)
    const startMin = s.getHours() * 60 + s.getMinutes()
    const e = entry.stoppedAt ? new Date(entry.stoppedAt) : new Date()
    const endMin = e.getHours() * 60 + e.getMinutes()
    return { start: startMin, end: Math.max(endMin, startMin + 15) }
}

function computeOverlapLayout(dayEntries: TimeEntry[]): { layouts: EntryLayout[]; overflows: OverflowIndicator[] } {
    if (dayEntries.length === 0) return { layouts: [], overflows: [] }

    // Sort by start time, then by duration (longest first)
    const sorted = [...dayEntries].sort((a, b) => {
        const aM = getEntryMinutes(a)
        const bM = getEntryMinutes(b)
        if (aM.start !== bM.start) return aM.start - bM.start
        return (bM.end - bM.start) - (aM.end - aM.start)
    })

    // Group overlapping entries into clusters
    const clusters: TimeEntry[][] = []
    let currentCluster: TimeEntry[] = []
    let clusterEnd = 0

    for (const entry of sorted) {
        const { start, end } = getEntryMinutes(entry)
        if (currentCluster.length === 0 || start < clusterEnd) {
            currentCluster.push(entry)
            clusterEnd = Math.max(clusterEnd, end)
        } else {
            clusters.push(currentCluster)
            currentCluster = [entry]
            clusterEnd = end
        }
    }
    if (currentCluster.length > 0) clusters.push(currentCluster)

    const layouts: EntryLayout[] = []
    const overflows: OverflowIndicator[] = []

    for (const cluster of clusters) {
        // Assign columns within this cluster
        const colEnds: number[] = []

        const clusterAssignments: { entry: TimeEntry; col: number }[] = []

        for (const entry of cluster) {
            const { start, end } = getEntryMinutes(entry)
            // Find first column where this entry fits
            let placed = false
            for (let c = 0; c < colEnds.length; c++) {
                if (colEnds[c]! <= start) {
                    colEnds[c] = end
                    clusterAssignments.push({ entry, col: c })
                    placed = true
                    break
                }
            }
            if (!placed) {
                clusterAssignments.push({ entry, col: colEnds.length })
                colEnds.push(end)
            }
        }

        const totalColumns = Math.min(colEnds.length, MAX_VISIBLE_COLUMNS)
        let hasOverflow = false

        for (const { entry, col } of clusterAssignments) {
            if (col < MAX_VISIBLE_COLUMNS) {
                layouts.push({
                    entry,
                    columnIndex: col,
                    totalColumns,
                })
            } else {
                hasOverflow = true
            }
        }

        if (hasOverflow) {
            const hidden = clusterAssignments.filter((a) => a.col >= MAX_VISIBLE_COLUMNS)
            const firstEntry = cluster[0]!
            const { start } = getEntryMinutes(firstEntry)
            overflows.push({
                topPx: (start / 60) * hourHeight,
                count: hidden.length,
                hiddenEntries: hidden.map((a) => a.entry),
            })
        }
    }

    return { layouts, overflows }
}

const layoutCache = computed(() => {
    const cache = new Map<string, { layouts: EntryLayout[]; overflows: OverflowIndicator[] }>()
    for (const day of days.value) {
        const dayEntries = entriesForDay(day.dateStr)
        cache.set(day.dateStr, computeOverlapLayout(dayEntries))
    }
    return cache
})

function layoutForDay(dateStr: string): EntryLayout[] {
    return layoutCache.value.get(dateStr)?.layouts ?? []
}

function overflowsForDay(dateStr: string): OverflowIndicator[] {
    return layoutCache.value.get(dateStr)?.overflows ?? []
}

// --- Overflow popover ---
interface OverflowPopoverState {
    dayIndex: number
    topPx: number
    entries: TimeEntry[]
}

const overflowPopover = ref<OverflowPopoverState | null>(null)

function openOverflowPopover(dayIndex: number, overflow: OverflowIndicator) {
    overflowPopover.value = {
        dayIndex,
        topPx: overflow.topPx,
        entries: overflow.hiddenEntries,
    }
}

function formatTime(iso: string): string {
    const d = new Date(iso)
    return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}

function getSnappedMinutesFromY(y: number): number {
    return Math.max(0, Math.min(23 * 60 + 45, Math.round((y / hourHeight) * 60 / 15) * 15))
}

function formatMinutes(totalMinutes: number): string {
    const h = Math.floor(totalMinutes / 60)
    const m = totalMinutes % 60
    return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
}

// --- Click to create ---
let dragEndTime = 0

function onClickGrid(event: MouseEvent, day: { date: Date; dateStr: string }) {
    // Suppress click right after drag end
    if (Date.now() - dragEndTime < 200) return

    const target = event.currentTarget as HTMLElement
    const rect = target.getBoundingClientRect()
    const y = event.clientY - rect.top
    const minutes = getSnappedMinutesFromY(y)
    const h = Math.floor(minutes / 60)
    const m = minutes % 60
    const d = new Date(day.date)
    d.setHours(h, m, 0, 0)
    emit('createEntry', d.toISOString())
}

function onContextMenuGrid(event: MouseEvent, _day: { date: Date; dateStr: string }) {
    emit('contextmenu', event, null)
}

// --- Drag to move ---
interface DragState {
    entryId: number
    entry: TimeEntry
    title: string
    color: string
    durationMinutes: number
    ghostHeightPx: number
    offsetY: number
    targetDayIndex: number
    ghostTopPx: number
    snappedMinutes: number
    timeLabel: string
}

const dragState = ref<DragState | null>(null)
let autoScrollActive = false
let lastMouseEvent: MouseEvent | null = null

function onMoveStart(payload: { entry: TimeEntry; offsetY: number }, sourceDayIndex: number) {
    const entry = payload.entry
    const startMinutes = new Date(entry.startedAt).getHours() * 60 + new Date(entry.startedAt).getMinutes()
    const endMinutes = entry.stoppedAt
        ? new Date(entry.stoppedAt).getHours() * 60 + new Date(entry.stoppedAt).getMinutes()
        : startMinutes + 60
    const durationMinutes = endMinutes - startMinutes

    dragState.value = {
        entryId: entry.id,
        entry,
        title: entry.title || t('common.untitled'),
        color: entry.project?.color ?? '#94a3b8',
        durationMinutes,
        ghostHeightPx: Math.max((durationMinutes / 60) * hourHeight, 20),
        offsetY: payload.offsetY,
        targetDayIndex: sourceDayIndex,
        ghostTopPx: (startMinutes / 60) * hourHeight,
        snappedMinutes: startMinutes,
        timeLabel: `${formatMinutes(startMinutes)} – ${formatMinutes(endMinutes)}`,
    }

    document.body.style.userSelect = 'none'
    document.body.style.cursor = 'grabbing'
    document.addEventListener('mousemove', onDragMove)
    document.addEventListener('mouseup', onDragEnd)
}

function updateDragPosition(event: MouseEvent) {
    if (!dragState.value) return

    // Find which column the cursor is over
    let targetDayIndex = dragState.value.targetDayIndex
    for (let i = 0; i < dayColumnEls.value.length; i++) {
        const el = dayColumnEls.value[i]
        if (!el) continue
        const rect = el.getBoundingClientRect()
        if (event.clientX >= rect.left && event.clientX <= rect.right) {
            targetDayIndex = i
            break
        }
    }

    // Calculate Y position in the target column
    const targetCol = dayColumnEls.value[targetDayIndex]
    if (!targetCol) return
    const colRect = targetCol.getBoundingClientRect()
    const y = event.clientY - colRect.top - dragState.value.offsetY
    const snappedMinutes = getSnappedMinutesFromY(y)
    const endMinutes = snappedMinutes + dragState.value.durationMinutes

    dragState.value.targetDayIndex = targetDayIndex
    dragState.value.snappedMinutes = snappedMinutes
    dragState.value.ghostTopPx = (snappedMinutes / 60) * hourHeight
    dragState.value.timeLabel = `${formatMinutes(snappedMinutes)} – ${formatMinutes(endMinutes)}`
}

function onDragMove(event: MouseEvent) {
    if (!dragState.value) return
    event.preventDefault()
    lastMouseEvent = event
    updateDragPosition(event)

    // Start auto-scroll if not running
    if (!autoScrollActive) {
        autoScrollActive = true
        requestAnimationFrame(autoScrollLoop)
    }
}

function autoScrollLoop() {
    const scrollParent = getScrollParent()
    if (!autoScrollActive || !lastMouseEvent || !scrollParent || !dragState.value) {
        autoScrollActive = false
        return
    }

    const rect = scrollParent.getBoundingClientRect()
    const edgeSize = 60
    const maxSpeed = 10

    const distFromTop = lastMouseEvent.clientY - rect.top
    const distFromBottom = rect.bottom - lastMouseEvent.clientY

    let scrolled = false
    if (distFromTop < edgeSize && distFromTop > 0) {
        scrollParent.scrollTop -= maxSpeed * (1 - distFromTop / edgeSize)
        scrolled = true
    } else if (distFromBottom < edgeSize && distFromBottom > 0) {
        scrollParent.scrollTop += maxSpeed * (1 - distFromBottom / edgeSize)
        scrolled = true
    }

    // Update ghost position if we scrolled (scroll changes coordinate mapping)
    if (scrolled && lastMouseEvent) {
        updateDragPosition(lastMouseEvent)
    }

    requestAnimationFrame(autoScrollLoop)
}

function onDragEnd() {
    document.removeEventListener('mousemove', onDragMove)
    document.removeEventListener('mouseup', onDragEnd)
    document.body.style.userSelect = ''
    document.body.style.cursor = ''
    autoScrollActive = false
    lastMouseEvent = null

    if (!dragState.value) return

    const state = dragState.value
    const targetDay = days.value[state.targetDayIndex]

    if (targetDay) {
        const h = Math.floor(state.snappedMinutes / 60)
        const m = state.snappedMinutes % 60
        const newStart = new Date(targetDay.date)
        newStart.setHours(h, m, 0, 0)
        const newStop = new Date(newStart.getTime() + state.durationMinutes * 60000)

        emit('moveEntry', state.entry, newStart.toISOString(), newStop.toISOString())
    }

    dragState.value = null
    dragEndTime = Date.now()
}
</script>
