"use client";

import { MILLISECONDS_PER_DAY } from "@capakraken/shared";
import { clsx } from "clsx";
import { useSession } from "next-auth/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAllocationHistory } from "~/hooks/useAllocationHistory.js";
import { useProjectDragContext } from "~/hooks/useProjectDragContext.js";
import { useTimelineDrag } from "~/hooks/useTimelineDrag.js";
import { useTimelineLayout } from "~/hooks/useTimelineLayout.js";
import { trpc } from "~/lib/trpc/client.js";
import { useInvalidateTimeline } from "~/hooks/useInvalidatePlanningViews.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import { FillOpenDemandModal } from "~/components/allocations/FillOpenDemandModal.js";
import { AllocationPopover } from "./AllocationPopover.js";
import { DemandPopover } from "./DemandPopover.js";
import { ResourceHoverCard } from "./ResourceHoverCard.js";
import type { TimelineDemandEntry } from "./TimelineContext.js";
import { BatchAssignPopover } from "./BatchAssignPopover.js";
import { FloatingActionBar } from "./FloatingActionBar.js";
import { NewAllocationPopover } from "./NewAllocationPopover.js";
import { ProjectPanel } from "./ProjectPanel.js";
import { ShiftPreviewTooltip } from "./ShiftPreviewTooltip.js";
import { TimelineHeader } from "./TimelineHeader.js";
import { TimelineToolbar } from "./TimelineToolbar.js";
import { addDays } from "./utils.js";
import { HEADER_DAY_HEIGHT, HEADER_MONTH_HEIGHT, LABEL_WIDTH } from "./timelineConstants.js";
import { formatDateShort } from "~/lib/format.js";
import {
  TimelineProvider,
  useTimelineContext,
  type TimelineAssignmentEntry,
} from "./TimelineContext.js";
import { TimelineResourcePanel } from "./TimelineResourcePanel.js";
import { TimelineProjectPanel, type OpenDemandAssignment } from "./TimelineProjectPanel.js";
import { ProjectColorLegend } from "./ProjectColorLegend.js";
import { useMultiSelectIntersection } from "~/hooks/useMultiSelectIntersection.js";
import type { TimelineVisualOverrides } from "./allocationVisualState.js";
import { SuccessToast } from "~/components/ui/SuccessToast.js";
import { useTimelineKeyboard } from "~/hooks/useTimelineKeyboard.js";
import { KeyboardShortcutOverlay } from "./KeyboardShortcutOverlay.js";
import { InlineAllocationEditor } from "./InlineAllocationEditor.js";

// ─── Entry point ────────────────────────────────────────────────────────────
// Two-layer mount: the outer shell creates drag state + project context,
// then wraps children with TimelineProvider. The inner content consumes context.

export function TimelineView() {
  const { data: session, status: sessionStatus } = useSession();
  const mousePosRef = useRef({ x: 0, y: 0 });
  const role = sessionStatus === "authenticated"
    ? ((session.user as { role?: string } | undefined)?.role ?? "USER")
    : null;
  const isSelfServiceTimeline = role === "USER" || role === "VIEWER";
  const canManageTimeline = !isSelfServiceTimeline;

  const { push: pushHistory, pushBatch: pushBatchHistory, undo, redo, canUndo, canRedo } = useAllocationHistory();
  const pushHistoryRef = useRef(pushHistory);
  pushHistoryRef.current = pushHistory;
  const pushBatchHistoryRef = useRef(pushBatchHistory);
  pushBatchHistoryRef.current = pushBatchHistory;

  const [popover, setPopover] = useState<{
    allocationId: string;
    projectId: string;
    allocation?: TimelineAssignmentEntry | null;
    x: number;
    y: number;
    contextDate?: Date;
  } | null>(null);
  const [newAllocPopover, setNewAllocPopover] = useState<{
    resourceId: string;
    startDate: Date;
    endDate: Date;
    suggestedProjectId: string | null;
    anchorX: number;
    anchorY: number;
    /** Selection coordinates to keep the overlay visible while popover is open */
    selectionResourceId: string;
    selectionStart: Date;
    selectionEnd: Date;
  } | null>(null);

  // cellWidth placeholder — the real value comes from useTimelineLayout inside the content.
  // useTimelineDrag only needs cellWidth for pixel→day conversion during drag.
  // We start with 40 (day zoom default) and update via a ref.
  const cellWidthRef = useRef(40);

  const invalidateTimeline = useInvalidateTimeline();
  const batchShiftMutationOuter = trpc.timeline.batchShiftAllocations.useMutation({
    onSuccess: invalidateTimeline,
  });

  const [dragErrorToast, setDragErrorToast] = useState<string | null>(null);

  const {
    dragState,
    allocDragState,
    rangeState,
    multiSelectState,
    setMultiSelectState,
    optimisticAllocations,
    reconcileOptimisticAllocations,
    shiftPreview,
    isPreviewLoading,
    isApplying,
    isAllocSaving,
    onProjectBarMouseDown,
    onAllocMouseDown,
    onRowMouseDown,
    onCanvasMouseMove,
    onCanvasMouseUp,
    onCanvasMouseLeave,
    onCanvasRightMouseDown,
    clearMultiSelect,
    onProjectBarTouchStart,
    onAllocTouchStart,
    onRowTouchStart,
    onCanvasTouchMove,
    onCanvasTouchEnd,
  } = useTimelineDrag({
    cellWidthRef,
    onBlockClick: (info) => {
      setPopover({
        allocationId: info.allocationId,
        projectId: info.projectId,
        x: mousePosRef.current.x,
        y: mousePosRef.current.y,
      });
    },
    onRangeSelected: (info) => {
      setNewAllocPopover({
        resourceId: info.resourceId,
        startDate: info.startDate,
        endDate: info.endDate,
        suggestedProjectId: info.suggestedProjectId,
        anchorX: info.anchorX,
        anchorY: info.anchorY,
        selectionResourceId: info.resourceId,
        selectionStart: info.startDate,
        selectionEnd: info.endDate,
      });
    },
    onAllocationMoved: (snapshot) => {
      pushHistoryRef.current(snapshot);
    },
    onShiftClickAlloc: (allocationId: string) => {
      setMultiSelectState(prev => {
        const ids = new Set(prev.selectedAllocationIds);
        if (ids.has(allocationId)) {
          ids.delete(allocationId);
        } else {
          ids.add(allocationId);
        }
        return { ...prev, isSelecting: false, selectedAllocationIds: [...ids] };
      });
    },
    onMultiDragComplete: (daysDelta, mode, selectedIds) => {
      const ids = selectedIds ?? multiSelectState.selectedAllocationIds;
      if (ids.length > 0 && daysDelta !== 0) {
        pushBatchHistoryRef.current(ids, daysDelta, mode);
        batchShiftMutationOuter.mutate({ allocationIds: ids, daysDelta, mode });
        clearMultiSelect();
      }
    },
    onMutationError: (message) => setDragErrorToast(message),
  });

  const [openPanelProjectId, setOpenPanelProjectId] = useState<string | null>(null);
  const dragProjectId = dragState.isDragging ? dragState.projectId : null;
  const contextProjectId = canManageTimeline ? (dragProjectId ?? openPanelProjectId) : null;
  const { contextResourceIds, contextAllocations } = useProjectDragContext(contextProjectId, canManageTimeline);

  return (
    <>
    <SuccessToast
      show={dragErrorToast !== null}
      message={dragErrorToast ?? ""}
      variant="warning"
      onDone={() => setDragErrorToast(null)}
    />
    <TimelineProvider
      isDragging={dragState.isDragging}
      contextAllocations={contextAllocations as TimelineAssignmentEntry[]}
    >
      <TimelineViewContent
        mousePosRef={mousePosRef}
        cellWidthRef={cellWidthRef}
        dragState={dragState}
        allocDragState={allocDragState}
        rangeState={rangeState}
        multiSelectState={multiSelectState}
        setMultiSelectState={setMultiSelectState}
        optimisticAllocations={optimisticAllocations}
        reconcileOptimisticAllocations={reconcileOptimisticAllocations}
        onCanvasRightMouseDown={onCanvasRightMouseDown}
        clearMultiSelect={clearMultiSelect}
        shiftPreview={shiftPreview}
        isPreviewLoading={isPreviewLoading}
        isApplying={isApplying}
        isAllocSaving={isAllocSaving}
        onProjectBarMouseDown={onProjectBarMouseDown}
        onAllocMouseDown={onAllocMouseDown}
        onRowMouseDown={onRowMouseDown}
        onCanvasMouseMove={onCanvasMouseMove}
        onCanvasMouseUp={onCanvasMouseUp}
        onCanvasMouseLeave={onCanvasMouseLeave}
        onProjectBarTouchStart={onProjectBarTouchStart}
        onAllocTouchStart={onAllocTouchStart}
        onRowTouchStart={onRowTouchStart}
        onCanvasTouchMove={onCanvasTouchMove}
        onCanvasTouchEnd={onCanvasTouchEnd}
        contextResourceIds={contextResourceIds}
        popover={popover}
        setPopover={setPopover}
        newAllocPopover={newAllocPopover}
        setNewAllocPopover={setNewAllocPopover}
        openPanelProjectId={openPanelProjectId}
        setOpenPanelProjectId={setOpenPanelProjectId}
        canUndo={canUndo}
        canRedo={canRedo}
        isSelfServiceTimeline={isSelfServiceTimeline}
        undo={undo}
        redo={redo}
      />
    </TimelineProvider>
    </>
  );
}

// ─── Content (inside TimelineProvider — has context access) ─────────────────

function TimelineViewContent({
  mousePosRef,
  cellWidthRef,
  dragState,
  allocDragState,
  rangeState,
  multiSelectState,
  setMultiSelectState,
  optimisticAllocations,
  reconcileOptimisticAllocations,
  onCanvasRightMouseDown,
  clearMultiSelect,
  shiftPreview,
  isPreviewLoading,
  isApplying,
  isAllocSaving,
  onProjectBarMouseDown,
  onAllocMouseDown,
  onRowMouseDown,
  onCanvasMouseMove,
  onCanvasMouseUp,
  onCanvasMouseLeave,
  onProjectBarTouchStart,
  onAllocTouchStart,
  onRowTouchStart,
  onCanvasTouchMove,
  onCanvasTouchEnd,
  contextResourceIds,
  popover,
  setPopover,
  newAllocPopover,
  setNewAllocPopover,
  openPanelProjectId,
  setOpenPanelProjectId,
  canUndo,
  canRedo,
  isSelfServiceTimeline,
  undo,
  redo,
}: {
  mousePosRef: React.RefObject<{ x: number; y: number }>;
  cellWidthRef: React.RefObject<number>;
  dragState: ReturnType<typeof useTimelineDrag>["dragState"];
  allocDragState: ReturnType<typeof useTimelineDrag>["allocDragState"];
  rangeState: ReturnType<typeof useTimelineDrag>["rangeState"];
  multiSelectState: ReturnType<typeof useTimelineDrag>["multiSelectState"];
  setMultiSelectState: ReturnType<typeof useTimelineDrag>["setMultiSelectState"];
  optimisticAllocations: TimelineVisualOverrides;
  reconcileOptimisticAllocations: ReturnType<typeof useTimelineDrag>["reconcileOptimisticAllocations"];
  onCanvasRightMouseDown: ReturnType<typeof useTimelineDrag>["onCanvasRightMouseDown"];
  clearMultiSelect: ReturnType<typeof useTimelineDrag>["clearMultiSelect"];
  shiftPreview: ReturnType<typeof useTimelineDrag>["shiftPreview"];
  isPreviewLoading: boolean;
  isApplying: boolean;
  isAllocSaving: boolean;
  onProjectBarMouseDown: ReturnType<typeof useTimelineDrag>["onProjectBarMouseDown"];
  onAllocMouseDown: ReturnType<typeof useTimelineDrag>["onAllocMouseDown"];
  onRowMouseDown: ReturnType<typeof useTimelineDrag>["onRowMouseDown"];
  onCanvasMouseMove: ReturnType<typeof useTimelineDrag>["onCanvasMouseMove"];
  onCanvasMouseUp: ReturnType<typeof useTimelineDrag>["onCanvasMouseUp"];
  onCanvasMouseLeave: ReturnType<typeof useTimelineDrag>["onCanvasMouseLeave"];
  onProjectBarTouchStart: ReturnType<typeof useTimelineDrag>["onProjectBarTouchStart"];
  onAllocTouchStart: ReturnType<typeof useTimelineDrag>["onAllocTouchStart"];
  onRowTouchStart: ReturnType<typeof useTimelineDrag>["onRowTouchStart"];
  onCanvasTouchMove: ReturnType<typeof useTimelineDrag>["onCanvasTouchMove"];
  onCanvasTouchEnd: ReturnType<typeof useTimelineDrag>["onCanvasTouchEnd"];
  contextResourceIds: string[];
  popover: {
    allocationId: string;
    projectId: string;
    allocation?: TimelineAssignmentEntry | null;
    x: number;
    y: number;
    contextDate?: Date;
  } | null;
  setPopover: React.Dispatch<React.SetStateAction<typeof popover>>;
  newAllocPopover: {
    resourceId: string;
    startDate: Date;
    endDate: Date;
    suggestedProjectId: string | null;
    anchorX: number;
    anchorY: number;
    selectionResourceId: string;
    selectionStart: Date;
    selectionEnd: Date;
  } | null;
  setNewAllocPopover: React.Dispatch<React.SetStateAction<typeof newAllocPopover>>;
  openPanelProjectId: string | null;
  setOpenPanelProjectId: React.Dispatch<React.SetStateAction<string | null>>;
  canUndo: boolean;
  canRedo: boolean;
  isSelfServiceTimeline: boolean;
  undo: () => Promise<void>;
  redo: () => Promise<void>;
}) {
  const ctx = useTimelineContext();
  const {
    resources,
    projectGroups,
    allocsByResource,
    openDemandsByProject,
    viewStart,
    viewEnd,
    viewDays,
    visibleAssignments,
    visibleDemands,
    setViewStart,
    setViewDays,
    filters,
    setFilters,
    filterOpen,
    setFilterOpen,
    viewMode,
    setViewMode,
    today,
    isLoading,
    isInitialLoading,
    isEntriesError,
    totalAllocCount,
  } = ctx;

  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLDivElement>(null);

  // Tooltip DOM refs
  const dragTooltipRef = useRef<HTMLDivElement>(null);
  const allocTooltipRef = useRef<HTMLDivElement>(null);
  const rangeHintRef = useRef<HTMLDivElement>(null);
  const multiDragTooltipRef = useRef<HTMLDivElement>(null);
  const [openDemandToAssign, setOpenDemandToAssign] = useState<OpenDemandAssignment | null>(null);
  const [demandPopover, setDemandPopover] = useState<{
    demand: TimelineDemandEntry;
    x: number;
    y: number;
  } | null>(null);
  const [showBatchAssign, setShowBatchAssign] = useState(false);
  const [resourceHover, setResourceHover] = useState<{
    resourceId: string;
    anchorEl: HTMLElement;
  } | null>(null);
  const resourceHoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const previousViewModeRef = useRef(viewMode);

  const invalidateTimelineInner = useInvalidateTimeline();
  const batchDeleteMutation = trpc.allocation.batchDelete.useMutation({
    onSuccess: () => {
      invalidateTimelineInner();
      clearMultiSelect();
    },
  });

  // ─── Batch-delete handler — shared by keyboard shortcut and action bar ─────
  const handleBatchDelete = useCallback(() => {
    if (multiSelectState.selectedAllocationIds.length === 0) return;
    const msg = `Delete ${multiSelectState.selectedAllocationIds.length} allocation(s)? This cannot be undone.`;
    if (window.confirm(msg)) {
      batchDeleteMutation.mutate({ ids: multiSelectState.selectedAllocationIds });
    }
  }, [batchDeleteMutation, multiSelectState.selectedAllocationIds]);

  const { CELL_WIDTH, dates, totalCanvasWidth, toLeft, toWidth, gridLines, monthGroups, xToDate } =
    useTimelineLayout(viewStart, viewDays, filters.zoom, filters.showWeekends, today);

  const { showShortcuts, setShowShortcuts } = useTimelineKeyboard({
    scrollContainerRef,
    cellWidth: CELL_WIDTH,
    selectedAllocationIds: multiSelectState.selectedAllocationIds,
    onDeleteSelected: handleBatchDelete,
  });

  const [inlineEditTarget, setInlineEditTarget] = useState<{
    allocationId: string;
    startDate: Date;
    endDate: Date;
    hoursPerDay: number;
    barRect: DOMRect;
  } | null>(null);

  const hasActivePointerOverlay =
    dragState.isDragging || allocDragState.isActive || rangeState.isSelecting || multiSelectState.isMultiDragging;

  useEffect(() => {
    if (optimisticAllocations.size === 0) return;
    reconcileOptimisticAllocations([...visibleAssignments, ...visibleDemands]);
  }, [
    optimisticAllocations,
    reconcileOptimisticAllocations,
    visibleAssignments,
    visibleDemands,
  ]);

  useEffect(() => {
    if (!hasActivePointerOverlay) return;
    setPopover(null);
    setDemandPopover(null);
    setResourceHover(null);
  }, [hasActivePointerOverlay]);

  useEffect(() => {
    if (previousViewModeRef.current === viewMode) {
      return;
    }

    previousViewModeRef.current = viewMode;
    setPopover(null);
    setDemandPopover(null);
    setNewAllocPopover(null);
    setResourceHover(null);
  }, [viewMode, setNewAllocPopover]);

  useEffect(() => {
    if (!isInitialLoading) return;
    setPopover(null);
    setDemandPopover(null);
    setNewAllocPopover(null);
    setResourceHover(null);
  }, [isInitialLoading, setNewAllocPopover]);

  // ─── Keep selection overlay visible while popover is open ───────────────────
  const effectiveRangeState: typeof rangeState = rangeState.isSelecting
    ? rangeState
    : newAllocPopover
      ? {
          isSelecting: true,
          resourceId: newAllocPopover.selectionResourceId,
          startDate: newAllocPopover.selectionStart,
          currentDate: newAllocPopover.selectionEnd,
          suggestedProjectId: newAllocPopover.suggestedProjectId,
          startClientX: 0,
        }
      : rangeState;

  // ─── Auto-suggest project for resource-view range select ───────────────────
  const enrichedSuggestedProjectId = useMemo(() => {
    if (!newAllocPopover) return null;
    // Already has a suggestion (e.g. from project view)
    if (newAllocPopover.suggestedProjectId) return newAllocPopover.suggestedProjectId;
    // Resource view: find the project with the most hours in this resource's row
    const allocs = allocsByResource.get(newAllocPopover.resourceId);
    if (!allocs || allocs.length === 0) return null;
    const projectHours = new Map<string, number>();
    for (const alloc of allocs) {
      projectHours.set(alloc.projectId, (projectHours.get(alloc.projectId) ?? 0) + alloc.hoursPerDay);
    }
    let maxPid: string | null = null;
    let maxH = 0;
    for (const [pid, h] of projectHours) {
      if (h > maxH) { maxH = h; maxPid = pid; }
    }
    return maxPid;
  }, [newAllocPopover, allocsByResource]);

  // Keep cellWidthRef in sync so the drag hook uses the correct value.
  cellWidthRef.current = CELL_WIDTH;

  // ─── Native mousemove listener — updates tooltips without React state ─────
  useEffect(() => {
    if (!hasActivePointerOverlay) return;
    const el = canvasRef.current;
    if (!el) return;
    const handler = (e: MouseEvent) => {
      const x = e.clientX;
      const y = e.clientY;
      mousePosRef.current = { x, y };
      if (dragTooltipRef.current) {
        dragTooltipRef.current.style.left = `${x + 12}px`;
        dragTooltipRef.current.style.top = `${y - 8}px`;
      }
      if (allocTooltipRef.current) {
        allocTooltipRef.current.style.left = `${x + 14}px`;
        allocTooltipRef.current.style.top = `${y - 36}px`;
      }
      if (rangeHintRef.current) {
        rangeHintRef.current.style.left = `${x + 12}px`;
        rangeHintRef.current.style.top = `${y - 28}px`;
      }
      if (multiDragTooltipRef.current) {
        multiDragTooltipRef.current.style.left = `${x + 14}px`;
        multiDragTooltipRef.current.style.top = `${y - 36}px`;
      }
    };
    // During multi-drag, listen on document (cursor may leave canvas)
    const target: EventTarget = multiSelectState.isMultiDragging ? document : el;
    target.addEventListener("mousemove", handler as EventListener, { passive: true });
    return () => target.removeEventListener("mousemove", handler as EventListener);
  }, [hasActivePointerOverlay, isLoading, mousePosRef, multiSelectState.isMultiDragging]); // eslint-disable-line react-hooks/exhaustive-deps

  // ─── Shift+wheel → horizontal scroll ──────────────────────────────────────
  useEffect(() => {
    const el = scrollContainerRef.current;
    if (!el) return;
    const handler = (e: WheelEvent) => {
      if (e.shiftKey && e.deltaY !== 0 && e.deltaX === 0) {
        e.preventDefault();
        el.scrollLeft += e.deltaY;
      }
    };
    el.addEventListener("wheel", handler, { passive: false });
    return () => el.removeEventListener("wheel", handler);
  }, [isLoading]); // eslint-disable-line react-hooks/exhaustive-deps

  // ─── Keyboard undo/redo ───────────────────────────────────────────────────
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      const isMac = navigator.platform.toUpperCase().includes("MAC");
      const modKey = isMac ? e.metaKey : e.ctrlKey;
      if (!modKey) return;
      if (e.key === "z" && !e.shiftKey) {
        e.preventDefault();
        void undo();
      }
      if ((e.key === "z" && e.shiftKey) || e.key === "y") {
        e.preventDefault();
        void redo();
      }
    };
    window.addEventListener("keydown", handler);
    return () => window.removeEventListener("keydown", handler);
  }, [undo, redo]);

  // ─── ESC to close overlays (topmost first) ─────────────────────────────────
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key !== "Escape") return;
      if (multiSelectState.selectedAllocationIds.length > 0 || multiSelectState.selectedResourceIds.length > 0) {
        e.preventDefault();
        clearMultiSelect();
        return;
      }
      if (demandPopover) {
        e.preventDefault();
        setDemandPopover(null);
      } else if (popover) {
        e.preventDefault();
        setPopover(null);
      } else if (newAllocPopover) {
        e.preventDefault();
        setNewAllocPopover(null);
      } else if (openDemandToAssign) {
        e.preventDefault();
        setOpenDemandToAssign(null);
      } else if (openPanelProjectId) {
        e.preventDefault();
        setOpenPanelProjectId(null);
      }
    };
    window.addEventListener("keydown", handler);
    return () => window.removeEventListener("keydown", handler);
  }, [demandPopover, popover, newAllocPopover, openDemandToAssign, openPanelProjectId, setPopover, setNewAllocPopover, setOpenPanelProjectId, multiSelectState.selectedAllocationIds.length, multiSelectState.selectedResourceIds.length, clearMultiSelect]);

  // ─── Resource hover card — event delegation on label columns ──────────────
  useEffect(() => {
    if (hasActivePointerOverlay) {
      if (resourceHoverTimerRef.current) {
        clearTimeout(resourceHoverTimerRef.current);
        resourceHoverTimerRef.current = null;
      }
      setResourceHover(null);
      return;
    }

    const canvas = canvasRef.current;
    if (!canvas) return;

    const HOVER_DELAY = 400;

    function onMouseOver(e: MouseEvent) {
      if (hasActivePointerOverlay) return;
      const target = (e.target as HTMLElement).closest<HTMLElement>("[data-resource-hover-id]");
      if (!target) return;
      const rid = target.dataset.resourceHoverId;
      if (!rid) return;

      // Clear any pending hide
      if (resourceHoverTimerRef.current) {
        clearTimeout(resourceHoverTimerRef.current);
        resourceHoverTimerRef.current = null;
      }

      // If already showing this resource, skip
      if (resourceHover?.resourceId === rid) return;

      resourceHoverTimerRef.current = setTimeout(() => {
        resourceHoverTimerRef.current = null;
        setResourceHover({ resourceId: rid, anchorEl: target });
      }, HOVER_DELAY);
    }

    function onMouseOut(e: MouseEvent) {
      if (hasActivePointerOverlay) return;
      const related = e.relatedTarget as HTMLElement | null;
      // Don't close if moving into another resource-hover target or the hover card itself
      if (related?.closest?.("[data-resource-hover-id]") || related?.closest?.("[data-resource-hover-card]")) return;

      if (resourceHoverTimerRef.current) {
        clearTimeout(resourceHoverTimerRef.current);
        resourceHoverTimerRef.current = null;
      }
      // Small delay before hiding to allow moving into hover card
      resourceHoverTimerRef.current = setTimeout(() => {
        resourceHoverTimerRef.current = null;
        setResourceHover(null);
      }, 150);
    }

    canvas.addEventListener("mouseover", onMouseOver, { passive: true });
    canvas.addEventListener("mouseout", onMouseOut, { passive: true });
    return () => {
      canvas.removeEventListener("mouseover", onMouseOver);
      canvas.removeEventListener("mouseout", onMouseOut);
      if (resourceHoverTimerRef.current) {
        clearTimeout(resourceHoverTimerRef.current);
        resourceHoverTimerRef.current = null;
      }
    };
  }, [resourceHover?.resourceId, isInitialLoading, hasActivePointerOverlay]); // eslint-disable-line react-hooks/exhaustive-deps

  // ─── Scroll-left tracking for horizontal virtualization ────────────────────
  // Updated via RAF so React state only updates after a frame, not on every
  // pixel of scroll. The ref gives instant reads inside event handlers.
  const scrollLeftRef = useRef(0);
  const scrollRafRef = useRef<number | null>(null);
  const [scrollLeft, setScrollLeft] = useState(0);


  // ─── Navigation callbacks for TimelineToolbar ────────────────────────────
  const handleNavigateBack = useCallback(
    () => setViewStart((v) => addDays(v, -28)),
    [setViewStart],
  );
  const handleNavigateToday = useCallback(
    () => setViewStart(addDays(today, -30)),
    [setViewStart, today],
  );
  const handleNavigateForward = useCallback(
    () => setViewStart((v) => addDays(v, 28)),
    [setViewStart],
  );
  const handleUndo = useCallback(() => { void undo(); }, [undo]);
  const handleRedo = useCallback(() => { void redo(); }, [redo]);

  // ─── Scroll handler — extends date range and tracks scroll offset ─────────
  const handleContainerScroll = useCallback(() => {
    const el = scrollContainerRef.current;
    if (!el) return;
    const distanceFromRight = el.scrollWidth - el.scrollLeft - el.clientWidth;
    if (distanceFromRight < CELL_WIDTH * 40) {
      setViewDays((d) => d + 120);
    }
    scrollLeftRef.current = el.scrollLeft;
    if (scrollRafRef.current === null) {
      scrollRafRef.current = requestAnimationFrame(() => {
        scrollRafRef.current = null;
        setScrollLeft(scrollLeftRef.current);
      });
    }
  }, [CELL_WIDTH, setViewDays]);

  // ─── Canvas mousemove — only forwards event when drag overlay is active ───
  const handleMouseMove = useCallback(
    (e: React.MouseEvent) => {
      if (!hasActivePointerOverlay) return;
      onCanvasMouseMove(e);
    },
    [hasActivePointerOverlay, onCanvasMouseMove],
  );

  // ─── openAllocationPopoverAt — routed to demand or allocation popover ─────
  const openAllocationPopoverAt = useCallback(
    (
      info: { allocationId: string; projectId: string; contextDate?: Date },
      anchorX: number,
      anchorY: number,
    ) => {
      if (hasActivePointerOverlay) return;
      const demands = openDemandsByProject.get(info.projectId);
      const demand = demands?.find((d) => d.id === info.allocationId);
      if (demand) {
        setDemandPopover({ demand, x: anchorX, y: anchorY });
        return;
      }
      const allocation = visibleAssignments.find((entry) => (
        entry.id === info.allocationId
        || entry.entityId === info.allocationId
        || entry.sourceAllocationId === info.allocationId
        || getPlanningEntryMutationId(entry) === info.allocationId
      )) ?? null;
      setPopover({
        allocationId: info.allocationId,
        projectId: info.projectId,
        allocation,
        x: anchorX,
        y: anchorY,
        ...(info.contextDate ? { contextDate: info.contextDate } : {}),
      });
    },
    [hasActivePointerOverlay, openDemandsByProject, visibleAssignments],
  );

  // ─── onOpenDemandClick for project panel — guards against overlay-active ──
  const handleOpenDemandClick = useCallback(
    (demand: TimelineDemandEntry, anchorX: number, anchorY: number) => {
      if (hasActivePointerOverlay) return;
      setDemandPopover({ demand, x: anchorX, y: anchorY });
    },
    [hasActivePointerOverlay],
  );

  // ─── onInlineEdit for resource panel — opens inline allocation editor ─────
  const handleInlineEdit = useCallback(
    (id: string, vals: { startDate: Date; endDate: Date; hoursPerDay: number }, rect: DOMRect) => {
      setInlineEditTarget({ allocationId: id, ...vals, barRect: rect });
    },
    [],
  );

  // ─── FloatingActionBar callbacks ────────────────────────────────────────────
  const handleShowBatchAssign = useCallback(() => setShowBatchAssign(true), []);

  // ─── Stable panel event handlers — self-service gets a typed no-op so the
  //     memo() on ResourcePanel/ProjectPanel is not defeated by new fn refs.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const stableNoop = useCallback((..._args: any[]) => undefined, []);
  const panelOnAllocMouseDown = (isSelfServiceTimeline ? stableNoop : onAllocMouseDown) as typeof onAllocMouseDown;
  const panelOnAllocTouchStart = (isSelfServiceTimeline ? stableNoop : onAllocTouchStart) as typeof onAllocTouchStart;
  const panelOnRowMouseDown = (isSelfServiceTimeline ? stableNoop : onRowMouseDown) as typeof onRowMouseDown;
  const panelOnRowTouchStart = (isSelfServiceTimeline ? stableNoop : onRowTouchStart) as typeof onRowTouchStart;
  const panelOnAllocationContextMenu = (isSelfServiceTimeline ? stableNoop : openAllocationPopoverAt) as typeof openAllocationPopoverAt;
  const panelOnProjectBarMouseDown = (isSelfServiceTimeline ? stableNoop : onProjectBarMouseDown) as typeof onProjectBarMouseDown;
  const panelOnProjectBarTouchStart = (isSelfServiceTimeline ? stableNoop : onProjectBarTouchStart) as typeof onProjectBarTouchStart;
  const panelOnOpenPanel = (isSelfServiceTimeline ? stableNoop : setOpenPanelProjectId) as typeof setOpenPanelProjectId;
  const panelOnOpenDemandClick = (isSelfServiceTimeline ? stableNoop : handleOpenDemandClick) as typeof handleOpenDemandClick;

  // ─── Multi-select intersection computation ────────────────────────────────
  useMultiSelectIntersection({
    multiSelectState,
    setMultiSelectState,
    clearMultiSelect,
    canvasRef,
    viewMode,
    resources,
    allocsByResource,
    projectGroups,
    openDemandsByProject,
    dates,
    today,
    CELL_WIDTH,
    toLeft,
    toWidth,
  });

  return (
    <div className="relative flex flex-1 flex-col gap-4 min-h-0">
      {/* Toolbar */}
      <TimelineToolbar
        viewMode={viewMode}
        onViewModeChange={setViewMode}
        isViewModeSwitchDisabled={isInitialLoading}
        filters={filters}
        onFiltersChange={setFilters}
        filterOpen={filterOpen}
        onFilterOpenChange={setFilterOpen}
        resourceCount={resources.length}
        projectCount={projectGroups.length}
        totalAllocCount={totalAllocCount}
        onNavigateBack={handleNavigateBack}
        onNavigateToday={handleNavigateToday}
        onNavigateForward={handleNavigateForward}
        canUndo={canUndo}
        canRedo={canRedo}
        onUndo={handleUndo}
        onRedo={handleRedo}
      />

      {/* Project color legend */}
      <ProjectColorLegend />

      {/* Scrollable canvas */}
      <div
        ref={scrollContainerRef}
        onScroll={handleContainerScroll}
        className="app-surface relative z-0 flex-1 overflow-auto"
      >
        {isEntriesError ? (
          <div className="flex flex-col items-center justify-center gap-3 py-24 text-sm text-red-600 dark:text-red-400">
            <span>Failed to load timeline data. Please try refreshing the page.</span>
          </div>
        ) : isInitialLoading ? (
          <div className="flex items-center justify-center py-24 text-sm text-gray-500 dark:text-gray-400">
            Loading timeline...
          </div>
        ) : (
          <div style={{ minWidth: LABEL_WIDTH + totalCanvasWidth }}>
            <TimelineHeader
              monthGroups={monthGroups}
              dates={dates}
              CELL_WIDTH={CELL_WIDTH}
              LABEL_WIDTH={LABEL_WIDTH}
              HEADER_MONTH_HEIGHT={HEADER_MONTH_HEIGHT}
              HEADER_DAY_HEIGHT={HEADER_DAY_HEIGHT}
              zoom={filters.zoom}
              viewMode={viewMode}
              today={today}
            />

            {/* Canvas rows */}
            <div
              ref={canvasRef}
              onMouseMove={handleMouseMove}
              onMouseUp={(e) => void onCanvasMouseUp(e)}
              onMouseLeave={onCanvasMouseLeave}
              onMouseDown={(e) => {
                if (!isSelfServiceTimeline && e.button === 2) {
                  onCanvasRightMouseDown(e);
                }
              }}
              onContextMenu={(e) => e.preventDefault()}
              onTouchMove={(e) => {
                if (!hasActivePointerOverlay) return;
                onCanvasTouchMove(e);
              }}
              onTouchEnd={(e) => void onCanvasTouchEnd(e)}
              className={clsx(
                (dragState.isDragging || allocDragState.isActive || multiSelectState.isMultiDragging) && "cursor-grabbing select-none",
                rangeState.isSelecting && "cursor-crosshair select-none",
                multiSelectState.isSelecting && "cursor-crosshair select-none",
              )}
            >
              {viewMode === "resource" && (
                <TimelineResourcePanel
                  scrollContainerRef={scrollContainerRef}
                  dragState={dragState}
                  allocDragState={allocDragState}
                  rangeState={effectiveRangeState}
                  shiftPreview={shiftPreview}
                  contextResourceIds={contextResourceIds}
                  onAllocMouseDown={panelOnAllocMouseDown}
                  onAllocTouchStart={panelOnAllocTouchStart}
                  onRowMouseDown={panelOnRowMouseDown}
                  onRowTouchStart={panelOnRowTouchStart}
                  onAllocationContextMenu={panelOnAllocationContextMenu}
                  multiSelectState={multiSelectState}
                  optimisticAllocations={optimisticAllocations}
                  suppressHoverInteractions={hasActivePointerOverlay}
                  {...(!isSelfServiceTimeline ? { onInlineEdit: handleInlineEdit } : {})}
                  scrollLeft={scrollLeft}
                  containerWidth={scrollContainerRef.current?.clientWidth ?? 1200}
                  CELL_WIDTH={CELL_WIDTH}
                  dates={dates}
                  totalCanvasWidth={totalCanvasWidth}
                  toLeft={toLeft}
                  toWidth={toWidth}
                  gridLines={gridLines}
                  xToDate={xToDate}
                />
              )}

              {viewMode === "project" && (
                <TimelineProjectPanel
                  scrollContainerRef={scrollContainerRef}
                  dragState={dragState}
                  allocDragState={allocDragState}
                  rangeState={effectiveRangeState}
                  multiSelectState={multiSelectState}
                  onProjectBarMouseDown={panelOnProjectBarMouseDown}
                  onProjectBarTouchStart={panelOnProjectBarTouchStart}
                  onAllocMouseDown={panelOnAllocMouseDown}
                  onAllocTouchStart={panelOnAllocTouchStart}
                  onRowMouseDown={panelOnRowMouseDown}
                  onRowTouchStart={panelOnRowTouchStart}
                  onOpenPanel={panelOnOpenPanel}
                  onOpenDemandClick={panelOnOpenDemandClick}
                  onAllocationContextMenu={panelOnAllocationContextMenu}
                  optimisticAllocations={optimisticAllocations}
                  suppressHoverInteractions={hasActivePointerOverlay}
                  CELL_WIDTH={CELL_WIDTH}
                  dates={dates}
                  totalCanvasWidth={totalCanvasWidth}
                  toLeft={toLeft}
                  toWidth={toWidth}
                  gridLines={gridLines}
                  xToDate={xToDate}
                />
              )}
            </div>
          </div>
        )}
      </div>

      {/* Multi-select rectangle overlay */}
      {multiSelectState.isSelecting && (
        <div
          className="fixed border-2 border-sky-500 bg-sky-500/10 pointer-events-none z-30 rounded"
          style={{
            left: Math.min(multiSelectState.startX, multiSelectState.currentX),
            top: Math.min(multiSelectState.startY, multiSelectState.currentY),
            width: Math.abs(multiSelectState.currentX - multiSelectState.startX),
            height: Math.abs(multiSelectState.currentY - multiSelectState.startY),
          }}
        />
      )}

      {/* Saving indicators */}
      {(isApplying || isAllocSaving) && (
        <div className="pointer-events-none absolute inset-0 z-50 flex items-center justify-center rounded-2xl bg-white/50 dark:bg-gray-950/50">
          <div className="app-surface px-5 py-3 text-sm font-medium text-gray-700 dark:text-gray-200">
            {isApplying ? "Applying shift…" : "Saving…"}
          </div>
        </div>
      )}

      {/* Drag preview tooltip */}
      {dragState.isDragging && dragState.daysDelta !== 0 && (
        <div
          ref={dragTooltipRef}
          className="fixed z-50 pointer-events-none"
          style={{ left: mousePosRef.current.x + 12, top: mousePosRef.current.y - 8 }}
        >
          <ShiftPreviewTooltip
            preview={
              shiftPreview ?? {
                valid: true,
                deltaCents: 0,
                wouldExceedBudget: false,
                budgetUtilizationAfter: 0,
                conflictCount: 0,
                errors: [],
                warnings: [],
              }
            }
            projectName={dragState.projectName ?? ""}
            newStartDate={dragState.currentStartDate ?? today}
            newEndDate={dragState.currentEndDate ?? today}
            isLoading={isPreviewLoading}
          />
        </div>
      )}

      {/* Alloc drag tooltip */}
      {allocDragState.isActive &&
        allocDragState.daysDelta !== 0 &&
        allocDragState.currentStartDate &&
        allocDragState.currentEndDate && (
          <div
            ref={allocTooltipRef}
            className="fixed z-40 bg-gray-800 text-white text-xs px-2.5 py-1.5 rounded-lg pointer-events-none shadow-lg space-y-0.5"
            style={{ left: mousePosRef.current.x + 14, top: mousePosRef.current.y - 36 }}
          >
            <div className="font-semibold">{allocDragState.projectName}</div>
            <div className="opacity-80">
              {formatDateShort(allocDragState.currentStartDate)}
              {" – "}
              {formatDateShort(allocDragState.currentEndDate)}
            </div>
          </div>
        )}

      {/* Range-select hint */}
      {rangeState.isSelecting && rangeState.startDate && rangeState.currentDate && (
        <div
          ref={rangeHintRef}
          className="fixed z-40 bg-brand-700 text-white text-xs px-2 py-1 rounded-lg pointer-events-none shadow"
          style={{ left: mousePosRef.current.x + 12, top: mousePosRef.current.y - 28 }}
        >
          {(() => {
            const end = rangeState.currentDate;
            const [s, e] =
              rangeState.startDate <= end
                ? [rangeState.startDate, end]
                : [end, rangeState.startDate];
            const days = Math.round((e.getTime() - s.getTime()) / MILLISECONDS_PER_DAY) + 1;
            return `${days} day${days !== 1 ? "s" : ""}`;
          })()}
        </div>
      )}

      {/* Multi-drag tooltip */}
      {multiSelectState.isMultiDragging && multiSelectState.multiDragDaysDelta !== 0 && (
        <div
          ref={multiDragTooltipRef}
          className="fixed z-50 bg-sky-700 text-white text-xs px-2.5 py-1.5 rounded-lg pointer-events-none shadow-lg font-medium"
          style={{ left: mousePosRef.current.x + 14, top: mousePosRef.current.y - 36 }}
        >
          {multiSelectState.multiDragMode === "resize-start" ? "Start " : multiSelectState.multiDragMode === "resize-end" ? "End " : ""}
          {multiSelectState.multiDragDaysDelta > 0 ? "+" : ""}
          {multiSelectState.multiDragDaysDelta}d
          {" "}
          ({multiSelectState.selectedAllocationIds.length} allocations)
        </div>
      )}

      {/* Allocation / Demand popover (click path) */}
      {!isSelfServiceTimeline && !hasActivePointerOverlay && popover && (() => {
        // Check if clicked allocation is actually a demand
        const clickedDemand = openDemandsByProject.get(popover.projectId)?.find((d) => d.id === popover.allocationId);
        if (clickedDemand) {
          return (
            <DemandPopover
              demand={clickedDemand}
              onClose={() => setPopover(null)}
              onOpenPanel={(pid) => {
                setPopover(null);
                setOpenPanelProjectId(pid);
              }}
              onFillDemand={(d) => {
                setPopover(null);
                setOpenDemandToAssign({
                  id: d.id,
                  projectId: d.projectId,
                  roleId: d.roleId,
                  role: d.role,
                  headcount: d.requestedHeadcount,
                  startDate: new Date(d.startDate),
                  endDate: new Date(d.endDate),
                  hoursPerDay: d.hoursPerDay,
                  ...(d.roleEntity !== undefined ? { roleEntity: d.roleEntity } : {}),
                  ...(d.project !== undefined ? { project: d.project } : {}),
                });
              }}
              anchorX={popover.x}
              anchorY={popover.y}
              ignoreScrollContainers={[scrollContainerRef]}
            />
          );
        }
        return (
          <AllocationPopover
            allocationId={popover.allocationId}
            projectId={popover.projectId}
            initialAllocation={popover.allocation ?? null}
            onClose={() => setPopover(null)}
            onOpenPanel={(pid) => {
              setPopover(null);
              setOpenPanelProjectId(pid);
            }}
            anchorX={popover.x}
            anchorY={popover.y}
            ignoreScrollContainers={[scrollContainerRef]}
            {...(popover.contextDate ? { contextDate: popover.contextDate } : {})}
          />
        );
      })()}

      {/* Demand popover */}
      {!isSelfServiceTimeline && !hasActivePointerOverlay && demandPopover && (
        <DemandPopover
          demand={demandPopover.demand}
          onClose={() => setDemandPopover(null)}
          onOpenPanel={(pid) => {
            setDemandPopover(null);
            setOpenPanelProjectId(pid);
          }}
          onFillDemand={(d) => {
            setDemandPopover(null);
            setOpenDemandToAssign({
              id: d.id,
              projectId: d.projectId,
              roleId: d.roleId,
              role: d.role,
              headcount: d.requestedHeadcount,
              startDate: new Date(d.startDate),
              endDate: new Date(d.endDate),
              hoursPerDay: d.hoursPerDay,
              ...(d.roleEntity !== undefined ? { roleEntity: d.roleEntity } : {}),
              ...(d.project !== undefined ? { project: d.project } : {}),
            });
          }}
          anchorX={demandPopover.x}
          anchorY={demandPopover.y}
          ignoreScrollContainers={[scrollContainerRef]}
        />
      )}

      {/* New allocation popover */}
      {!isSelfServiceTimeline && newAllocPopover && (
        <NewAllocationPopover
          resourceId={newAllocPopover.resourceId}
          startDate={newAllocPopover.startDate}
          endDate={newAllocPopover.endDate}
          suggestedProjectId={enrichedSuggestedProjectId}
          anchorX={newAllocPopover.anchorX}
          anchorY={newAllocPopover.anchorY}
          onClose={() => setNewAllocPopover(null)}
          onCreated={() => setNewAllocPopover(null)}
          ignoreScrollContainers={[scrollContainerRef]}
        />
      )}

      {/* Project side panel */}
      {!isSelfServiceTimeline && openPanelProjectId && (
        <ProjectPanel projectId={openPanelProjectId} onClose={() => setOpenPanelProjectId(null)} />
      )}

      {/* Open-demand assignment modal */}
      {!isSelfServiceTimeline && openDemandToAssign && (
        <FillOpenDemandModal
          allocation={openDemandToAssign}
          onClose={() => setOpenDemandToAssign(null)}
          onSuccess={() => setOpenDemandToAssign(null)}
        />
      )}

      {/* Multi-select floating action bar */}
      <FloatingActionBar
        selectedAllocationCount={multiSelectState.selectedAllocationIds.length}
        selectedResourceCount={multiSelectState.selectedResourceIds.length}
        onDelete={handleBatchDelete}
        onAssign={handleShowBatchAssign}
        onClear={clearMultiSelect}
        isDeleting={batchDeleteMutation.isPending}
      />

      {/* Batch assign popover */}
      {showBatchAssign && multiSelectState.dateRange && (
        <BatchAssignPopover
          resourceIds={multiSelectState.selectedResourceIds}
          startDate={multiSelectState.dateRange.start}
          endDate={multiSelectState.dateRange.end}
          onClose={() => setShowBatchAssign(false)}
          onCreated={() => {
            setShowBatchAssign(false);
            clearMultiSelect();
          }}
        />
      )}

      {/* Resource hover card */}
      {!hasActivePointerOverlay && resourceHover && (
        <ResourceHoverCard
          resourceId={resourceHover.resourceId}
          anchorEl={resourceHover.anchorEl}
          onClose={() => setResourceHover(null)}
        />
      )}

      {/* Inline allocation editor */}
      {inlineEditTarget && (
        <InlineAllocationEditor
          allocationId={inlineEditTarget.allocationId}
          initialStartDate={inlineEditTarget.startDate}
          initialEndDate={inlineEditTarget.endDate}
          initialHoursPerDay={inlineEditTarget.hoursPerDay}
          barRect={inlineEditTarget.barRect}
          onClose={() => setInlineEditTarget(null)}
          onSaved={() => setInlineEditTarget(null)}
        />
      )}

      {/* Keyboard shortcut overlay */}
      {showShortcuts && <KeyboardShortcutOverlay onClose={() => setShowShortcuts(false)} />}

      {/* Keyboard shortcut hint button */}
      <button
        type="button"
        onClick={() => setShowShortcuts((prev) => !prev)}
        title="Keyboard shortcuts (?)"
        className="fixed bottom-6 right-6 z-40 rounded-full w-8 h-8 flex items-center justify-center bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-sm font-medium"
      >
        ?
      </button>
    </div>
  );
}
