<template>
  <div class="h-full flex flex-col">
    <!-- Toolbar -->
    <div class="flex flex-wrap items-center justify-between gap-3 px-4 sm:px-6 py-3 border-b">
      <div class="flex flex-wrap items-center gap-3">
        <Select
          :model-value="episodeFilter === null ? 'all' : String(episodeFilter)"
          @update:model-value="handleEpisodeFilterChange"
        >
          <SelectTrigger class="w-48 h-8">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="all">All Episodes</SelectItem>
            <SelectItem v-for="ep in episodeOptions" :key="ep.id" :value="String(ep.id)">{{ ep.name }}</SelectItem>
          </SelectContent>
        </Select>

        <Button variant="outline" size="sm" class="h-8" @click="toggleAllGroups">
          {{ hasCollapsedGroups ? 'Expand All' : 'Collapse All' }}
        </Button>

        <div class="flex items-center gap-1 border rounded-md p-0.5">
          <Button
            v-for="option in GROUP_BY_OPTIONS"
            :key="option"
            :variant="groupBy === option ? 'secondary' : 'ghost'"
            size="sm"
            class="h-7 px-2 text-xs"
            @click="groupBy = option"
          >
            {{ groupByLabel(option) }}
          </Button>
        </div>

        <div class="flex items-center gap-1 border rounded-md p-0.5">
          <Button
            v-for="scale in SCALES"
            :key="scale"
            :variant="viewScale === scale ? 'secondary' : 'ghost'"
            size="sm"
            class="h-7 px-2 text-xs capitalize"
            @click="viewScale = scale"
          >
            {{ scale }}
          </Button>
        </div>

        <div class="flex items-center gap-1.5">
          <div class="w-36">
            <DatePicker v-model="manualRangeStart" placeholder="Start date" />
          </div>
          <span class="text-xs text-muted-foreground">to</span>
          <div class="w-36">
            <DatePicker v-model="manualRangeEnd" placeholder="End date" :min="manualRangeStart" />
          </div>
          <Button
            v-if="manualRangeStart || manualRangeEnd"
            variant="ghost"
            size="sm"
            class="h-8 px-2 text-xs"
            @click="manualRangeStart = ''; manualRangeEnd = ''"
          >
            Reset
          </Button>
        </div>
      </div>

      <div v-if="legendStatuses.length > 0" class="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
        <span v-for="status in legendStatuses" :key="status.id" class="flex items-center gap-1.5">
          <span class="h-2.5 w-2.5 rounded-sm flex-shrink-0" :style="{ backgroundColor: status.color }"></span>
          {{ status.name }}
        </span>
      </div>
    </div>

    <div v-if="isLoading" class="flex-1 flex items-center justify-center text-sm text-muted-foreground">
      Loading schedule...
    </div>
    <div v-else-if="error" class="flex-1 flex items-center justify-center text-sm text-destructive">
      {{ error }}
    </div>
    <div v-else class="flex-1 flex flex-col overflow-hidden">
      <div v-if="unscheduledCount > 0" class="flex-shrink-0 px-4 sm:px-6 py-2 text-xs text-muted-foreground border-b bg-muted/30">
        {{ unscheduledCount }} task{{ unscheduledCount === 1 ? '' : 's' }} without both a start date and deadline
        {{ unscheduledCount === 1 ? "isn't" : "aren't" }} shown on the chart.
      </div>

      <div v-if="rowGroups.length === 0" class="flex-1 p-12 text-center text-sm text-muted-foreground">
        No scheduled tasks to display yet. Set a start date and deadline on a task to see it here.
      </div>

      <div v-else class="flex-1 flex overflow-hidden">
        <!-- Frozen pane: Task Type/Task + Task Status columns, vertical scroll only -->
        <div
          ref="frozenPaneRef"
          class="no-scrollbar overflow-y-auto overflow-x-hidden flex-shrink-0 border-r"
          :style="{ width: FROZEN_WIDTH + 'px' }"
          @scroll="handleFrozenScroll"
        >
          <div class="flex sticky top-0 z-10 bg-background border-b" :style="{ height: HEADER_HEIGHT + 'px' }">
            <div class="border-r px-3 flex items-center text-xs font-medium text-muted-foreground flex-shrink-0" :style="{ width: LABEL_COLUMN_WIDTH + 'px' }">
              Task Type / Task
            </div>
            <div class="px-3 flex items-center text-xs font-medium text-muted-foreground flex-shrink-0" :style="{ width: STATUS_COLUMN_WIDTH + 'px' }">
              Task Status
            </div>
          </div>

          <div v-for="group in rowGroups" :key="group.key">
            <div
              class="flex items-center border-b bg-muted/40 cursor-pointer select-none"
              :style="{ height: GROUP_ROW_HEIGHT + 'px' }"
              @click="toggleGroup(group.key)"
            >
              <div class="border-r px-3 text-xs font-medium flex items-center gap-1 self-stretch flex-shrink-0" :style="{ width: LABEL_COLUMN_WIDTH + 'px' }">
                <component
                  :is="isCollapsed(group.key) ? ChevronRight : ChevronDown"
                  class="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
                />
                <span class="truncate">{{ group.label }}</span>
                <span class="text-muted-foreground flex-shrink-0">({{ group.tasks.length }})</span>
              </div>
              <div class="flex-shrink-0" :style="{ width: STATUS_COLUMN_WIDTH + 'px' }"></div>
            </div>

            <template v-if="!isCollapsed(group.key)">
              <div
                v-for="task in group.tasks"
                :key="task.id"
                class="group flex items-center border-b hover:bg-muted/30"
                :style="{ height: TASK_ROW_HEIGHT + 'px' }"
              >
                <div class="border-r pl-8 pr-3 text-xs truncate self-stretch flex items-center flex-shrink-0" :style="{ width: LABEL_COLUMN_WIDTH + 'px' }">
                  {{ taskRowLabel(task) }}
                </div>
                <div class="border-r px-3 flex items-center self-stretch flex-shrink-0" :style="{ width: STATUS_COLUMN_WIDTH + 'px' }">
                  <EditableTaskStatus
                    :task-id="task.id"
                    :status="task.status"
                    :project-id="projectId"
                    @status-updated="handleStatusUpdated"
                  />
                </div>
              </div>
            </template>
          </div>
        </div>

        <!-- Timeline pane: date axis + bars, scrolls both ways -->
        <div ref="timelinePaneRef" class="flex-1 overflow-auto" @scroll="handleTimelineScroll">
          <div class="relative" :style="{ width: chartWidth + 'px', minWidth: chartWidth + 'px' }">
            <!-- Date axis header: month row on top, date-number row below -->
            <div class="sticky top-0 z-10 bg-background border-b" :style="{ height: HEADER_HEIGHT + 'px' }">
              <div class="relative h-5 border-b">
                <div
                  v-for="marker in topAxisMarkers"
                  :key="'month-' + marker.left"
                  class="absolute top-0 bottom-0 border-l px-1.5 flex items-center overflow-hidden text-[10px] font-medium text-muted-foreground whitespace-nowrap"
                  :style="{ left: marker.left + 'px', width: marker.width + 'px' }"
                >
                  {{ marker.label }}
                </div>
              </div>
              <div class="relative h-6">
                <div
                  v-for="marker in axisMarkers"
                  :key="'day-' + marker.left"
                  class="absolute top-0 bottom-0 border-l flex items-center overflow-hidden text-[10px] text-muted-foreground px-1.5 whitespace-nowrap"
                  :style="{ left: marker.left + 'px', width: marker.width ? marker.width + 'px' : undefined }"
                >
                  {{ marker.label }}
                </div>
              </div>
            </div>

            <!-- Weekend shading -->
            <div
              v-for="col in weekendColumns"
              :key="col.left"
              class="absolute top-0 bottom-0 pointer-events-none"
              :style="{ left: col.left + 'px', width: pixelsPerDay + 'px', backgroundColor: 'hsl(var(--muted))' }"
            ></div>

            <!-- Rows -->
            <div v-for="group in rowGroups" :key="group.key">
              <div
                class="relative border-b bg-muted/40 cursor-pointer"
                :style="{ height: GROUP_ROW_HEIGHT + 'px' }"
                @click="toggleGroup(group.key)"
              >
                <div
                  v-if="group.barLeft !== null"
                  class="absolute top-1/2 -translate-y-1/2 h-1.5 rounded-full"
                  :style="{ left: group.barLeft + 'px', width: group.barWidth + 'px', backgroundColor: 'rgba(100, 116, 139, 0.5)' }"
                ></div>
              </div>

              <template v-if="!isCollapsed(group.key)">
                <div
                  v-for="task in group.tasks"
                  :key="task.id"
                  class="relative border-b hover:bg-muted/30"
                  :style="{ height: TASK_ROW_HEIGHT + 'px' }"
                >
                  <div
                    class="group absolute top-1/2 -translate-y-1/2 h-4 rounded hover:brightness-90 transition-[filter]"
                    :class="{ 'ring-2 ring-primary': isTaskActive(task) }"
                    :style="taskBarStyle(task)"
                    :title="taskBarTitle(task)"
                  >
                    <div
                      class="absolute inset-y-0 left-0 cursor-grab active:cursor-grabbing"
                      style="right: 6px;"
                      @mousedown="startDrag($event, task, 'move')"
                    ></div>
                    <div
                      class="absolute inset-y-0 left-0 w-1.5 cursor-ew-resize opacity-0 group-hover:opacity-100 rounded-l"
                      style="background-color: rgba(0, 0, 0, 0.2);"
                      @mousedown="startDrag($event, task, 'resize-start')"
                    ></div>
                    <div
                      class="absolute inset-y-0 right-0 w-1.5 cursor-ew-resize opacity-0 group-hover:opacity-100 rounded-r"
                      style="background-color: rgba(0, 0, 0, 0.2);"
                      @mousedown="startDrag($event, task, 'resize-end')"
                    ></div>
                    <span
                      class="absolute left-full top-1/2 -translate-y-1/2 ml-1.5 text-[10px] text-foreground whitespace-nowrap pointer-events-none transition-opacity"
                      :class="isTaskActive(task) ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'"
                    >
                      {{ taskRowLabel(task) }}
                    </span>
                  </div>
                  <!-- Submission date markers -->
                  <div
                    v-for="date in submissionDatesFor(task.id)"
                    :key="date"
                    class="absolute top-1/2 h-2 w-2 rounded-full bg-white border border-slate-500 pointer-events-none"
                    :style="{ left: dateToLeft(parseDate(date)) + 'px', transform: 'translate(-50%, -50%)' }"
                    :title="`Submitted ${formatDate(date)}`"
                  ></div>
                </div>
              </template>
            </div>

            <!-- Today marker -->
            <div
              v-if="todayLeft !== null"
              class="absolute top-0 bottom-0 w-px pointer-events-none"
              :style="{ left: todayLeft + 'px', backgroundColor: 'rgba(239, 68, 68, 0.7)' }"
            >
              <span class="absolute top-0 left-1/2 -translate-x-1/2 text-[9px] text-red-500 bg-background px-0.5 whitespace-nowrap">
                Today
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>

    <DetailPanelOverlay :visible="!!showPanel" v-model:mobile-open="showMobileDetail">
      <TaskDetailPanel
        v-if="selectedTask"
        :task-id="selectedTask.id"
        @close="closeDetailPanel"
        @task-updated="loadTasks"
      />
    </DetailPanelOverlay>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ChevronRight, ChevronDown } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { DatePicker } from '@/components/ui/date-picker'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import DetailPanelOverlay from '@/components/shared/DetailPanelOverlay.vue'
import TaskDetailPanel from '@/components/task/TaskDetailPanel.vue'
import EditableTaskStatus from '@/components/task/EditableTaskStatus.vue'
import { taskService, type TaskListItem, type SubmissionDateInfo } from '@/services/task'
import { useTaskStatusesStore } from '@/stores/taskStatuses'
import { useDetailPanel } from '@/composables/useDetailPanel'
import { useToast } from '@/components/ui/toast/use-toast'

const props = defineProps<{
  projectId: number
}>()

const taskStatusesStore = useTaskStatusesStore()
const { toast } = useToast()

const {
  selectedEntity: selectedTask,
  showMobileDetail,
  showPanel,
  closeDetailPanel,
  selectEntity: selectTask
} = useDetailPanel<TaskListItem>({ sessionStorageKey: 'scheduleGantt.detailPanelEnabled' })

const isLoading = ref(false)
const error = ref<string | null>(null)
const tasks = ref<TaskListItem[]>([])
const submissionDates = ref<SubmissionDateInfo[]>([])
const episodeFilter = ref<number | null>(null)
const collapsedGroups = ref<Set<string>>(new Set())

const LABEL_COLUMN_WIDTH = 224 // matches w-56
const STATUS_COLUMN_WIDTH = 176 // matches w-44
const FROZEN_WIDTH = LABEL_COLUMN_WIDTH + STATUS_COLUMN_WIDTH
const HEADER_HEIGHT = 44
const GROUP_ROW_HEIGHT = 32
const TASK_ROW_HEIGHT = 36

// The frozen (Task Type/Task Status) pane and the timeline pane are two
// independently-scrolled elements so the horizontal scrollbar only ever
// spans the timeline. Vertical scroll position is kept in sync between them.
const frozenPaneRef = ref<HTMLElement | null>(null)
const timelinePaneRef = ref<HTMLElement | null>(null)
let syncingScroll = false

function handleFrozenScroll() {
  if (syncingScroll || !frozenPaneRef.value || !timelinePaneRef.value) return
  syncingScroll = true
  timelinePaneRef.value.scrollTop = frozenPaneRef.value.scrollTop
  syncingScroll = false
}

function handleTimelineScroll() {
  if (syncingScroll || !frozenPaneRef.value || !timelinePaneRef.value) return
  syncingScroll = true
  frozenPaneRef.value.scrollTop = timelinePaneRef.value.scrollTop
  syncingScroll = false
}

const SCALES = ['day', 'week', 'month'] as const
type ViewScale = typeof SCALES[number]
const viewScale = ref<ViewScale>('week')
const SCALE_PIXELS_PER_DAY: Record<ViewScale, number> = { day: 40, week: 22, month: 6 }
const pixelsPerDay = computed(() => SCALE_PIXELS_PER_DAY[viewScale.value])

const manualRangeStart = ref('')
const manualRangeEnd = ref('')

function parseDate(dateStr: string): Date {
  return new Date(`${dateStr}T00:00:00Z`)
}

function toDateString(d: Date): string {
  return d.toISOString().slice(0, 10)
}

function addDays(d: Date, days: number): Date {
  const copy = new Date(d)
  copy.setUTCDate(copy.getUTCDate() + days)
  return copy
}

function daysBetween(a: Date, b: Date): number {
  return Math.round((b.getTime() - a.getTime()) / 86400000)
}

function formatTaskType(taskType: string): string {
  return taskType.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
}

function formatDate(dateStr?: string): string {
  if (!dateStr) return '?'
  return parseDate(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' })
}

async function loadTasks() {
  try {
    isLoading.value = true
    error.value = null
    tasks.value = await taskService.getTasks({ projectId: props.projectId, limit: 1000 })
  } catch (err: any) {
    console.error('Failed to load schedule tasks:', err)
    error.value = err.response?.data?.detail || 'Failed to load schedule'
  } finally {
    isLoading.value = false
  }
}

async function loadSubmissionDates() {
  try {
    submissionDates.value = await taskService.getSubmissionDates(props.projectId)
  } catch (err) {
    console.error('Failed to load submission dates:', err)
  }
}

// task_id -> deduplicated YYYY-MM-DD submission dates, for the white dot markers on each bar.
const submissionDatesByTask = computed(() => {
  const map = new Map<number, string[]>()
  for (const s of submissionDates.value) {
    const day = s.submitted_at.slice(0, 10)
    const existing = map.get(s.task_id)
    if (existing) {
      if (!existing.includes(day)) existing.push(day)
    } else {
      map.set(s.task_id, [day])
    }
  }
  return map
})

function submissionDatesFor(taskId: number): string[] {
  return submissionDatesByTask.value.get(taskId) || []
}

const episodeOptions = computed(() => {
  const map = new Map<number, string>()
  for (const t of tasks.value) {
    if (t.episode_id && t.episode_name && !map.has(t.episode_id)) {
      map.set(t.episode_id, t.episode_name)
    }
  }
  return Array.from(map.entries())
    .map(([id, name]) => ({ id, name }))
    .sort((a, b) => a.name.localeCompare(b.name))
})

function handleEpisodeFilterChange(value: unknown) {
  episodeFilter.value = value === 'all' ? null : Number(value)
}

const filteredTasks = computed(() => {
  if (episodeFilter.value === null) return tasks.value
  return tasks.value.filter(t => t.episode_id === episodeFilter.value)
})

const scheduledTasks = computed(() => filteredTasks.value.filter(t => t.start_date && t.deadline))
const unscheduledCount = computed(() => filteredTasks.value.length - scheduledTasks.value.length)

const autoWindowStart = computed<Date | null>(() => {
  if (scheduledTasks.value.length === 0) return null
  let min: Date | null = null
  for (const t of scheduledTasks.value) {
    const d = parseDate(t.start_date!)
    if (!min || d < min) min = d
  }
  if (!min) return null
  const padded = new Date(min)
  padded.setUTCDate(padded.getUTCDate() - 3)
  return padded
})

const autoWindowEnd = computed<Date | null>(() => {
  if (scheduledTasks.value.length === 0) return null
  let max: Date | null = null
  for (const t of scheduledTasks.value) {
    const d = parseDate(t.deadline!)
    if (!max || d > max) max = d
  }
  if (!max) return null
  const padded = new Date(max)
  padded.setUTCDate(padded.getUTCDate() + 3)
  return padded
})

const windowStart = computed<Date | null>(() => manualRangeStart.value ? parseDate(manualRangeStart.value) : autoWindowStart.value)
const windowEnd = computed<Date | null>(() => manualRangeEnd.value ? parseDate(manualRangeEnd.value) : autoWindowEnd.value)

const chartWidth = computed(() => {
  if (!windowStart.value || !windowEnd.value) return 0
  return Math.max(daysBetween(windowStart.value, windowEnd.value) * pixelsPerDay.value, 400)
})

function dateToLeft(date: Date): number {
  if (!windowStart.value) return 0
  return daysBetween(windowStart.value, date) * pixelsPerDay.value
}

// One bounded, window-clipped cell per calendar month touching [windowStart, windowEnd].
function computeMonthSegments(labelOptions: Intl.DateTimeFormatOptions): { left: number; width: number; label: string }[] {
  if (!windowStart.value || !windowEnd.value) return []
  const segments: { left: number; width: number; label: string }[] = []
  const rightBound = chartWidth.value
  const cursor = new Date(Date.UTC(windowStart.value.getUTCFullYear(), windowStart.value.getUTCMonth(), 1))
  while (cursor <= windowEnd.value) {
    const segStart = cursor < windowStart.value ? windowStart.value : cursor
    const nextMonth = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 1))
    const left = dateToLeft(segStart)
    const width = Math.max(Math.min(dateToLeft(nextMonth), rightBound) - left, 2)
    segments.push({ left, width, label: cursor.toLocaleDateString('en-US', labelOptions) })
    cursor.setUTCMonth(cursor.getUTCMonth() + 1)
  }
  return segments
}

const axisMarkers = computed<{ left: number; width?: number; label: string }[]>(() => {
  if (!windowStart.value || !windowEnd.value) return []

  if (viewScale.value === 'month') {
    return computeMonthSegments({ month: 'short', timeZone: 'UTC' })
  }

  const markers: { left: number; label: string }[] = []
  const cursor = new Date(windowStart.value)
  while (cursor <= windowEnd.value) {
    markers.push({
      left: dateToLeft(cursor),
      label: String(cursor.getUTCDate())
    })
    cursor.setUTCDate(cursor.getUTCDate() + 1)
  }
  return markers
})

// Coarser grouping row shown above axisMarkers: month spans (day/week scale) or year spans (month scale).
const topAxisMarkers = computed(() => {
  if (!windowStart.value || !windowEnd.value) return []

  if (viewScale.value === 'month') {
    const markers: { left: number; width: number; label: string }[] = []
    const rightBound = chartWidth.value
    const cursor = new Date(Date.UTC(windowStart.value.getUTCFullYear(), 0, 1))
    while (cursor <= windowEnd.value) {
      const segStart = cursor < windowStart.value ? windowStart.value : cursor
      const nextYear = new Date(Date.UTC(cursor.getUTCFullYear() + 1, 0, 1))
      const left = dateToLeft(segStart)
      const width = Math.max(Math.min(dateToLeft(nextYear), rightBound) - left, 2)
      markers.push({ left, width, label: String(cursor.getUTCFullYear()) })
      cursor.setUTCFullYear(cursor.getUTCFullYear() + 1)
    }
    return markers
  }

  return computeMonthSegments({ month: 'long', year: 'numeric', timeZone: 'UTC' })
})

const weekendColumns = computed(() => {
  if (!windowStart.value || !windowEnd.value) return []
  const columns: { left: number }[] = []
  const cursor = new Date(windowStart.value)
  while (cursor <= windowEnd.value) {
    const day = cursor.getUTCDay()
    if (day === 0 || day === 6) {
      columns.push({ left: dateToLeft(cursor) })
    }
    cursor.setUTCDate(cursor.getUTCDate() + 1)
  }
  return columns
})

const todayLeft = computed(() => {
  if (!windowStart.value || !windowEnd.value) return null
  const now = new Date()
  const todayUtc = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()))
  if (todayUtc < windowStart.value || todayUtc > windowEnd.value) return null
  return dateToLeft(todayUtc)
})

const GROUP_BY_OPTIONS = ['taskType', 'shot'] as const
type GroupBy = typeof GROUP_BY_OPTIONS[number]
const groupBy = ref<GroupBy>('taskType')

function groupByLabel(mode: GroupBy): string {
  return mode === 'taskType' ? 'Task Type' : 'Shot'
}

function entityName(task: TaskListItem): string {
  return task.shot_name || task.asset_name || task.name
}

// The row label shows whichever dimension ISN'T already the group header:
// task type when grouped by shot, and the shot/asset name when grouped by task type.
function taskRowLabel(task: TaskListItem): string {
  return groupBy.value === 'taskType' ? entityName(task) : formatTaskType(task.task_type)
}

interface RowGroup {
  key: string
  label: string
  tasks: TaskListItem[]
  barLeft: number | null
  barWidth: number | null
}

const rowGroups = computed<RowGroup[]>(() => {
  const byKey = new Map<string, TaskListItem[]>()
  for (const t of scheduledTasks.value) {
    const key = groupBy.value === 'taskType' ? t.task_type : entityName(t)
    if (!byKey.has(key)) byKey.set(key, [])
    byKey.get(key)!.push(t)
  }

  return Array.from(byKey.entries())
    .sort((a, b) => a[0].localeCompare(b[0]))
    .map(([key, groupTasks]) => {
      const sorted = [...groupTasks].sort((a, b) => (a.start_date || '').localeCompare(b.start_date || ''))
      let barLeft: number | null = null
      let barWidth: number | null = null
      if (windowStart.value) {
        let minStart: Date | null = null
        let maxEnd: Date | null = null
        for (const t of sorted) {
          const s = parseDate(t.start_date!)
          const e = parseDate(t.deadline!)
          if (!minStart || s < minStart) minStart = s
          if (!maxEnd || e > maxEnd) maxEnd = e
        }
        barLeft = dateToLeft(minStart!)
        barWidth = Math.max(dateToLeft(maxEnd!) - barLeft, 4)
      }
      const label = groupBy.value === 'taskType' ? formatTaskType(key) : key
      return { key, label, tasks: sorted, barLeft, barWidth }
    })
})

function isCollapsed(key: string): boolean {
  return collapsedGroups.value.has(key)
}

function toggleGroup(key: string) {
  const next = new Set(collapsedGroups.value)
  if (next.has(key)) next.delete(key)
  else next.add(key)
  collapsedGroups.value = next
}

const hasCollapsedGroups = computed(() => collapsedGroups.value.size > 0)

function toggleAllGroups() {
  collapsedGroups.value = hasCollapsedGroups.value
    ? new Set()
    : new Set(rowGroups.value.map(g => g.key))
}

watch(groupBy, () => {
  collapsedGroups.value = new Set()
})

function statusColor(task: TaskListItem): string {
  const status = taskStatusesStore.getStatusById(props.projectId, task.status)
  return status?.color || '#94A3B8'
}

function isTaskActive(task: TaskListItem): boolean {
  return selectedTask.value?.id === task.id
}

function handleStatusUpdated(taskId: number, newStatus: string) {
  const task = tasks.value.find(t => t.id === taskId)
  if (task) task.status = newStatus
}

// --- Drag to reschedule ---

interface DragState {
  taskId: number
  mode: 'move' | 'resize-start' | 'resize-end'
  startX: number
  originalStart: Date
  originalEnd: Date
  currentStart: Date
  currentEnd: Date
}

const dragState = ref<DragState | null>(null)

function startDrag(event: MouseEvent, task: TaskListItem, mode: DragState['mode']) {
  event.preventDefault()
  event.stopPropagation()
  const originalStart = parseDate(task.start_date!)
  const originalEnd = parseDate(task.deadline!)
  dragState.value = {
    taskId: task.id,
    mode,
    startX: event.clientX,
    originalStart,
    originalEnd,
    currentStart: originalStart,
    currentEnd: originalEnd
  }
  window.addEventListener('mousemove', handleDragMove)
  window.addEventListener('mouseup', handleDragEnd)
}

function handleDragMove(event: MouseEvent) {
  const drag = dragState.value
  if (!drag) return

  const deltaX = event.clientX - drag.startX
  const deltaDays = Math.round(deltaX / pixelsPerDay.value)

  let newStart = drag.originalStart
  let newEnd = drag.originalEnd

  if (drag.mode === 'move') {
    newStart = addDays(drag.originalStart, deltaDays)
    newEnd = addDays(drag.originalEnd, deltaDays)
  } else if (drag.mode === 'resize-start') {
    newStart = addDays(drag.originalStart, deltaDays)
    if (newStart >= drag.originalEnd) newStart = addDays(drag.originalEnd, -1)
  } else if (drag.mode === 'resize-end') {
    newEnd = addDays(drag.originalEnd, deltaDays)
    if (newEnd <= drag.originalStart) newEnd = addDays(drag.originalStart, 1)
  }

  dragState.value = { ...drag, currentStart: newStart, currentEnd: newEnd }
}

async function handleDragEnd() {
  window.removeEventListener('mousemove', handleDragMove)
  window.removeEventListener('mouseup', handleDragEnd)

  const drag = dragState.value
  dragState.value = null
  if (!drag) return

  const changed = daysBetween(drag.originalStart, drag.currentStart) !== 0 || daysBetween(drag.originalEnd, drag.currentEnd) !== 0

  if (!changed) {
    // No movement - treat as a click on the bar body
    if (drag.mode === 'move') openTask(drag.taskId)
    return
  }

  const task = tasks.value.find(t => t.id === drag.taskId)
  if (!task) return

  const previousStart = task.start_date
  const previousDeadline = task.deadline
  const newStartStr = toDateString(drag.currentStart)
  const newEndStr = toDateString(drag.currentEnd)

  task.start_date = newStartStr
  task.deadline = newEndStr

  try {
    await taskService.updateTask(drag.taskId, { start_date: newStartStr, deadline: newEndStr })
  } catch (err: any) {
    console.error('Failed to reschedule task:', err)
    task.start_date = previousStart
    task.deadline = previousDeadline
    toast({
      title: 'Error',
      description: err.response?.data?.detail || 'Failed to reschedule task',
      variant: 'destructive'
    })
  }
}

function taskBarStyle(task: TaskListItem) {
  const isDragging = dragState.value?.taskId === task.id
  const start = isDragging ? dragState.value!.currentStart : parseDate(task.start_date!)
  const end = isDragging ? dragState.value!.currentEnd : parseDate(task.deadline!)
  const left = dateToLeft(start)
  const width = Math.max(dateToLeft(end) - left, 6)
  return {
    left: `${left}px`,
    width: `${width}px`,
    backgroundColor: statusColor(task)
  }
}

function taskBarTitle(task: TaskListItem): string {
  return `${task.name}: ${formatDate(task.start_date)} – ${formatDate(task.deadline)}`
}

const legendStatuses = computed(() => taskStatusesStore.getAllStatusOptions(props.projectId) || [])

function openTask(taskId: number) {
  const task = tasks.value.find(t => t.id === taskId)
  if (task) selectTask(task)
}

onMounted(() => {
  loadTasks()
  loadSubmissionDates()
  taskStatusesStore.fetchProjectStatuses(props.projectId)
})

onUnmounted(() => {
  window.removeEventListener('mousemove', handleDragMove)
  window.removeEventListener('mouseup', handleDragEnd)
})

watch(() => props.projectId, () => {
  episodeFilter.value = null
  collapsedGroups.value = new Set()
  manualRangeStart.value = ''
  manualRangeEnd.value = ''
  groupBy.value = 'taskType'
  loadTasks()
  loadSubmissionDates()
  taskStatusesStore.fetchProjectStatuses(props.projectId)
})
</script>

<style scoped>
/* Frozen pane scrolls vertically (kept in sync with the timeline pane) but
   shouldn't show its own scrollbar - the timeline pane's scrollbar is enough. */
.no-scrollbar {
  scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
  display: none;
}
</style>
