"use client";

import { clsx } from "clsx";
import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import {
  useTimelineContext,
  type TimelineAssignmentEntry,
} from "./TimelineContext.js";
import {
  applyPointerOffsetPreviewRect,
  applyVisualOverrides,
  getDragPointerOffset,
  type TimelineVisualOverrides,
} from "./allocationVisualState.js";
import { ConflictOverlay } from "./ConflictOverlay.js";
import { computeSubLanes } from "./utils.js";
import { heatmapBgColor } from "./heatmapUtils.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import {
  TimelineTooltip,
  type HeatmapHoverData,
  type VacationHoverData,
} from "./TimelineTooltip.js";
import {
  ROW_HEIGHT,
  SUB_LANE_HEIGHT,
  LABEL_WIDTH,
} from "./timelineConstants.js";
import { getProjectColor } from "~/lib/project-colors.js";
import type {
  DragState,
  AllocDragState,
  RangeState,
  ShiftPreviewData,
  MultiSelectState,
} from "~/hooks/useTimelineDrag.js";
import type { HeatmapColorScheme } from "~/hooks/useAppPreferences.js";
import {
  buildVacationBlocksByResource,
  renderVacationBlocks,
  renderRangeOverlay,
  renderOverbookingBlink,
  type VacationBlockInfo,
} from "./renderHelpers.js";
import {
  cancelHoverFrame,
  scheduleVacationHoverUpdate,
  updateTooltipPosition,
} from "./timelineHover.js";
import { buildResourceHeatmapHover } from "./timelineHeatmap.js";
import {
  buildResourceCapacitySeries,
  type ResourceCapacitySeries,
} from "./timelineCapacity.js";
import {
  buildAllocationWorkingDaySegments,
  isAllocationScheduledOnDate,
  toLocalDateKey,
} from "./timelineAvailability.js";

// ─── Props ──────────────────────────────────────────────────────────────────

interface TimelineResourcePanelProps {
  scrollContainerRef: React.RefObject<HTMLDivElement | null>;
  dragState: DragState;
  allocDragState: AllocDragState;
  rangeState: RangeState;
  shiftPreview: ShiftPreviewData | null;
  contextResourceIds: string[];
  onAllocMouseDown: (e: React.MouseEvent, info: AllocMouseDownInfo) => void;
  onAllocTouchStart: (e: React.TouchEvent, info: AllocMouseDownInfo) => void;
  onRowMouseDown: (e: React.MouseEvent, info: RowMouseDownInfo) => void;
  onRowTouchStart: (e: React.TouchEvent, info: RowMouseDownInfo) => void;
  onAllocationContextMenu: (
    info: { allocationId: string; projectId: string; contextDate?: Date },
    anchorX: number,
    anchorY: number,
  ) => void;
  multiSelectState: MultiSelectState;
  optimisticAllocations: TimelineVisualOverrides;
  suppressHoverInteractions: boolean;
  // Layout from useTimelineLayout
  CELL_WIDTH: number;
  dates: Date[];
  totalCanvasWidth: number;
  toLeft: (date: Date) => number;
  toWidth: (start: Date, end: Date) => number;
  gridLines: React.ReactNode;
  xToDate: (clientX: number, rect: DOMRect) => Date;
}

export interface AllocMouseDownInfo {
  mode: "move" | "resize-start" | "resize-end";
  allocationId: string;
  mutationAllocationId: string;
  projectId: string;
  projectName: string;
  resourceId: string | null;
  startDate: Date;
  endDate: Date;
  allocationStartDate?: Date;
  allocationEndDate?: Date;
  scope?: "allocation" | "segment";
}

export interface RowMouseDownInfo {
  resourceId: string;
  startDate: Date;
  suggestedProjectId?: string;
}

// ─── Component ──────────────────────────────────────────────────────────────

function TimelineResourcePanelInner({
  scrollContainerRef,
  dragState,
  allocDragState,
  rangeState,
  shiftPreview,
  contextResourceIds,
  onAllocMouseDown,
  onAllocTouchStart,
  onRowMouseDown,
  onRowTouchStart,
  onAllocationContextMenu,
  multiSelectState,
  optimisticAllocations,
  suppressHoverInteractions,
  CELL_WIDTH,
  dates,
  totalCanvasWidth,
  toLeft,
  toWidth,
  gridLines,
  xToDate,
}: TimelineResourcePanelProps) {
  const {
    resources,
    allocsByResource,
    vacationsByResource,
    filters,
    viewStart,
    viewEnd,
    displayMode,
    heatmapScheme,
    blinkOverbookedDays,
    activeFilterCount,
  } = useTimelineContext();

  // ─── Heatmap hover state ────────────────────────────────────────────────────
  const heatmapRafRef = useRef<number | null>(null);
  const lastHeatmapDayRef = useRef<number>(-1);
  const vacationHoverRafRef = useRef<number | null>(null);
  const hoveredVacationKeyRef = useRef<string | null>(null);
  const pendingHeatmapRef = useRef<{
    clientX: number;
    rect: DOMRect;
    allocs: TimelineAssignmentEntry[];
  } | null>(null);
  const heatmapTooltipRef = useRef<HTMLDivElement | null>(null);
  const vacationTooltipRef = useRef<HTMLDivElement | null>(null);
  const heatmapTooltipPosRef = useRef({ left: 0, top: 0 });
  const vacationTooltipPosRef = useRef({ left: 0, top: 0 });

  const [heatmapHover, setHeatmapHover] = useState<HeatmapHoverData | null>(null);
  const [vacationHover, setVacationHover] = useState<VacationHoverData | null>(null);

  // ─── Virtual row list ────────────────────────────────────────────────────────
  const rowVirtualizer = useVirtualizer({
    count: resources.length,
    getScrollElement: () => scrollContainerRef.current,
    estimateSize: () => ROW_HEIGHT,
    overscan: 5,
  });
  const virtualItems = rowVirtualizer.getVirtualItems();
  const totalRowHeight = rowVirtualizer.getTotalSize();

  const visualAllocsByResource = useMemo(() => {
    if (optimisticAllocations.size === 0) return allocsByResource;

    const next = new Map<string, TimelineAssignmentEntry[]>();
    for (const [resourceId, allocs] of allocsByResource) {
      next.set(resourceId, applyVisualOverrides(allocs, optimisticAllocations));
    }
    return next;
  }, [allocsByResource, optimisticAllocations]);

  const resourceCapacityById = useMemo(
    () => buildResourceCapacitySeries(visualAllocsByResource, vacationsByResource, dates),
    [dates, vacationsByResource, visualAllocsByResource],
  );

  // ─── Memo 1: resourceRows — which rows to render ─────────────────────────
  // (virtualizer handles which subset is visible; this memo just pre-computes
  //  per-row data that the render loop needs)
  const resourceRows = useMemo(() => {
    return resources.map((resource) => {
      const allocs = visualAllocsByResource.get(resource.id) ?? [];
      const isContextResource = contextResourceIds.includes(resource.id);
      return { resource, allocs, isContextResource };
    });
  }, [resources, visualAllocsByResource, contextResourceIds]);

  // ─── Memo 2: vacationBlocks — vacation bar positions per resource ─────────
  const vacationBlocksByResource = useMemo(
    () =>
      buildVacationBlocksByResource(
        vacationsByResource,
        filters.showVacations,
        toLeft,
        toWidth,
        CELL_WIDTH,
        totalCanvasWidth,
      ),
    [vacationsByResource, toLeft, toWidth, CELL_WIDTH, totalCanvasWidth, filters.showVacations],
  );

  // ─── Memo 3: assignmentBlocks — pre-computed per resource for strip mode ──
  // (Bar mode computes differently per-day, so we only pre-compute for strip.)
  const assignmentBlocksByResource = useMemo(() => {
    if (displayMode === "bar")
      return new Map<string, { laneCount: number; blockData: AllocBlockData[] }>();

    const result = new Map<string, { laneCount: number; blockData: AllocBlockData[] }>();
    for (const { resource, allocs } of resourceRows) {
      if (allocs.length === 0) continue;

      const subLaneMap = computeSubLanes(
        allocs.map((a) => ({
          id: a.id,
          startDate: new Date(a.startDate),
          endDate: new Date(a.endDate),
        })),
      );
      const laneCount = subLaneMap.size > 0 ? Math.max(...subLaneMap.values()) + 1 : 1;
      const blockData: AllocBlockData[] = allocs.map((alloc) => ({
        alloc,
        lane: subLaneMap.get(alloc.id) ?? 0,
      }));
      result.set(resource.id, { laneCount, blockData });
    }
    return result;
  }, [displayMode, resourceRows]);

  // ─── Memo 4: utilization per resource for row background tint ───────────
  const utilizationByResource = useMemo(() => {
    const result = new Map<string, number>(); // resourceId -> avg utilization pct
    for (const { resource, allocs } of resourceRows) {
      if (allocs.length === 0) continue;
      const capacity = resourceCapacityById.get(resource.id);
      let totalPct = 0;
      let dayCount = 0;
      for (let dayIndex = 0; dayIndex < dates.length; dayIndex++) {
        const date = dates[dayIndex];
        if (!date) continue;
        const bookingFactor = capacity?.bookingFactorsByDay[dayIndex] ?? 1;
        const capacityHours = capacity?.capacityHoursByDay[dayIndex] ?? 8;
        let dayH = 0;
        for (const a of allocs) {
          if (isAllocationScheduledOnDate(a, date)) {
            dayH += a.hoursPerDay * bookingFactor;
          }
        }
        if (dayH > 0 && capacityHours > 0) {
          totalPct += (dayH / capacityHours) * 100;
          dayCount++;
        }
      }
      if (dayCount > 0) {
        result.set(resource.id, totalPct / dayCount);
      }
    }
    return result;
  }, [dates, resourceCapacityById, resourceRows]);

  // ─── Heatmap row hover handler ────────────────────────────────────────────
  const handleRowHeatmapMove = useCallback(
    (e: React.MouseEvent, allocs: TimelineAssignmentEntry[]) => {
      updateTooltipPosition(heatmapTooltipPosRef, heatmapTooltipRef, e.clientX, e.clientY, 16, -52);
      const rect = e.currentTarget.getBoundingClientRect();
      const dayIndex = Math.floor((e.clientX - rect.left) / CELL_WIDTH);
      if (dayIndex === lastHeatmapDayRef.current) return;

      pendingHeatmapRef.current = { clientX: e.clientX, rect, allocs };
      if (heatmapRafRef.current !== null) return;

      heatmapRafRef.current = requestAnimationFrame(() => {
        heatmapRafRef.current = null;
        const pending = pendingHeatmapRef.current;
        pendingHeatmapRef.current = null;
        if (!pending) return;

        const { clientX, rect: r, allocs: a } = pending;
        const dayIdx = Math.floor((clientX - r.left) / CELL_WIDTH);
        const date = dates[dayIdx];
        if (!date) {
          lastHeatmapDayRef.current = -1;
          startTransition(() => setHeatmapHover(null));
          return;
        }
        lastHeatmapDayRef.current = dayIdx;

        const resourceId = a[0]?.resourceId ?? null;
        const capacity = resourceId ? resourceCapacityById.get(resourceId) : undefined;
        const nextHeatmap = buildResourceHeatmapHover(date, a, {
          ...(capacity?.capacityHoursByDay[dayIdx] !== undefined
            ? { capacityHours: capacity.capacityHoursByDay[dayIdx] }
            : {}),
          ...(capacity?.bookingFactorsByDay[dayIdx] !== undefined
            ? { bookingFactor: capacity.bookingFactorsByDay[dayIdx] }
            : {}),
        });
        startTransition(() => {
          setHeatmapHover(nextHeatmap);
        });
      });
    },
    [CELL_WIDTH, dates, resourceCapacityById],
  );

  // ─── Vacation hover ───────────────────────────────────────────────────────
  const handleRowVacationHover = useCallback(
    (e: React.MouseEvent, resourceId: string) => {
      updateTooltipPosition(vacationTooltipPosRef, vacationTooltipRef, e.clientX, e.clientY, 14, -8);
      scheduleVacationHoverUpdate({
        frameRef: vacationHoverRafRef,
        hoveredKeyRef: hoveredVacationKeyRef,
        resourceId,
        clientX: e.clientX,
        rect: e.currentTarget.getBoundingClientRect(),
        xToDate,
        vacations: vacationsByResource.get(resourceId) ?? [],
        onHoverChange: (hit) => {
          startTransition(() => {
            setVacationHover(hit);
          });
        },
      });
    },
    [vacationsByResource, xToDate],
  );

  const clearHoverTooltips = useCallback(() => {
    cancelHoverFrame(heatmapRafRef);
    cancelHoverFrame(vacationHoverRafRef);

    const shouldClearHeatmap = lastHeatmapDayRef.current !== -1;
    const shouldClearVacation = hoveredVacationKeyRef.current !== null;

    lastHeatmapDayRef.current = -1;
    hoveredVacationKeyRef.current = null;

    if (shouldClearHeatmap || shouldClearVacation) {
      startTransition(() => {
        if (shouldClearHeatmap) setHeatmapHover(null);
        if (shouldClearVacation) setVacationHover(null);
      });
    }
  }, []);

  // ─── Cleanup rAF on unmount ───────────────────────────────────────────────
  useEffect(
    () => () => {
      cancelHoverFrame(heatmapRafRef);
      cancelHoverFrame(vacationHoverRafRef);
    },
    [],
  );

  useEffect(() => {
    if (!suppressHoverInteractions) return;
    clearHoverTooltips();
  }, [clearHoverTooltips, suppressHoverInteractions]);

  // ─── Render helpers ───────────────────────────────────────────────────────

  if (resources.length === 0) {
    return (
      <div className="text-center py-16 text-gray-400">
        No allocations in this time range{activeFilterCount > 0 && " (filtered)"}.
      </div>
    );
  }

  return (
    <div
      style={{
        height: totalRowHeight,
        position: "relative",
      }}
    >
      {virtualItems.map((virtualRow) => {
        const rowData = resourceRows[virtualRow.index];
        if (!rowData) return null;
        const { resource, allocs, isContextResource } = rowData;
        const inBarMode = displayMode === "bar";
        const precomputed = assignmentBlocksByResource.get(resource.id);
        const laneCount = inBarMode ? 1 : (precomputed?.laneCount ?? 1);
        const rowHeight = inBarMode
          ? ROW_HEIGHT
          : Math.max(ROW_HEIGHT, laneCount * SUB_LANE_HEIGHT + 16);

        // Utilization background tint
        const utilPct = utilizationByResource.get(resource.id) ?? 0;
        const utilBg = utilPct > 100
          ? "rgba(254,202,202,0.18)" // red tint for over-utilized
          : utilPct >= 50
            ? `rgba(59,130,246,${Math.min(0.06 + (utilPct - 50) * 0.0014, 0.12)})` // faint blue tint scaling 50-100%
            : undefined;

        return (
          <div
            key={resource.id}
            data-index={virtualRow.index}
            ref={rowVirtualizer.measureElement}
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              width: "100%",
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <div
              className={clsx(
                "flex border-b border-gray-100 dark:border-gray-800 hover:bg-blue-50/20 dark:hover:bg-gray-800/30 group transition-colors",
                dragState.isDragging && isContextResource && "border-l-4 border-l-brand-400",
              )}
              style={{ height: rowHeight, ...(utilBg ? { backgroundColor: utilBg } : {}) }}
            >
              {/* Label column */}
              <div
                className={clsx(
                  "flex-shrink-0 border-r border-gray-200 dark:border-gray-700 flex items-center px-4 gap-2.5 bg-white dark:bg-gray-900 sticky left-0 z-30 group-hover:bg-blue-50 dark:group-hover:bg-gray-800",
                  dragState.isDragging && isContextResource && "bg-brand-50 dark:bg-brand-950/40",
                )}
                style={{ width: LABEL_WIDTH }}
              >
                <div className="w-8 h-8 rounded-full bg-brand-100 dark:bg-brand-900/40 flex items-center justify-center text-xs font-bold text-brand-700 dark:text-brand-300 flex-shrink-0">
                  {resource.displayName.slice(0, 2).toUpperCase()}
                </div>
                <div className="min-w-0" data-resource-hover-id={resource.id}>
                  <div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate cursor-pointer hover:text-brand-600 dark:hover:text-brand-400 transition-colors">
                    {resource.displayName}
                  </div>
                  <div className="text-xs text-gray-400 dark:text-gray-500 truncate">
                    {resource.chapter ?? resource.eid}
                  </div>
                </div>
              </div>

              {/* Row canvas */}
              <div
                data-testid="timeline-resource-row-canvas"
                data-resource-id={resource.id}
                data-resource-eid={resource.eid}
                data-resource-name={resource.displayName}
                className="relative overflow-hidden touch-none"
                style={{ width: totalCanvasWidth, height: rowHeight, touchAction: "none" }}
                onMouseDown={(e) => {
                  const rect = e.currentTarget.getBoundingClientRect();
                  const date = xToDate(e.clientX, rect);
                  onRowMouseDown(e, { resourceId: resource.id, startDate: date });
                }}
                onTouchStart={(e) => {
                  const rect = e.currentTarget.getBoundingClientRect();
                  const date = xToDate(e.touches[0]?.clientX ?? 0, rect);
                  onRowTouchStart(e, { resourceId: resource.id, startDate: date });
                }}
                onMouseMove={(e) => {
                  if (suppressHoverInteractions) return;
                  handleRowHeatmapMove(e, allocs);
                  handleRowVacationHover(e, resource.id);
                }}
                onMouseLeave={clearHoverTooltips}
              >
                {gridLines}
                {inBarMode
                  ? renderDailyBars(
                      allocs,
                      rowHeight,
                      CELL_WIDTH,
                      dates,
                      allocDragState,
                      onAllocMouseDown,
                      onAllocTouchStart,
                      onAllocationContextMenu,
                      toLeft,
                      toWidth,
                      totalCanvasWidth,
                      resourceCapacityById.get(resource.id),
                      multiSelectState,
                      suppressHoverInteractions,
                    )
                  : renderAllocBlocksFromData(
                      precomputed?.blockData ?? [],
                      allocs,
                      dragState,
                      allocDragState,
                      toLeft,
                      toWidth,
                      CELL_WIDTH,
                      totalCanvasWidth,
                      onAllocMouseDown,
                      onAllocTouchStart,
                      onAllocationContextMenu,
                      multiSelectState,
                      suppressHoverInteractions,
                    )}
                {filters.showVacations &&
                  renderVacationBlocks(
                    vacationBlocksByResource.get(resource.id) ?? [],
                    rowHeight,
                  )}
                {displayMode === "strip" &&
                  renderLoadGraph(
                    allocs,
                    dates,
                    CELL_WIDTH,
                    resourceCapacityById.get(resource.id),
                  )}
                {displayMode === "heatmap" &&
                  renderHeatmapOverlay(
                    allocs,
                    dates,
                    CELL_WIDTH,
                    heatmapScheme,
                    resourceCapacityById.get(resource.id),
                  )}
                {blinkOverbookedDays &&
                  renderOverbookingBlink(
                    allocs,
                    dates,
                    CELL_WIDTH,
                    resourceCapacityById.get(resource.id)?.capacityHoursByDay,
                    resourceCapacityById.get(resource.id)?.bookingFactorsByDay,
                  )}
                {renderRangeOverlay(
                  rangeState,
                  resource.id,
                  rowHeight,
                  toLeft,
                  toWidth,
                  CELL_WIDTH,
                )}

                {dragState.isDragging &&
                  dragState.projectId &&
                  shiftPreview &&
                  !shiftPreview.valid &&
                  shiftPreview.conflictCount > 0 &&
                  allocs.some((a) => a.projectId === dragState.projectId) && (
                    <ConflictOverlay
                      left={toLeft(dragState.currentStartDate ?? viewStart) + 2}
                      width={
                        toWidth(
                          dragState.currentStartDate ?? viewStart,
                          dragState.currentEndDate ?? viewEnd,
                        ) - 4
                      }
                      height={rowHeight - 8}
                      type="availability"
                      message={`${shiftPreview.conflictCount} conflict(s)`}
                    />
                  )}
              </div>
            </div>
          </div>
        );
      })}

      {/* Tooltips rendered inside the panel so they live near their data source */}
      <TimelineTooltip
        heatmapTooltipRef={heatmapTooltipRef}
        heatmapTooltipPos={heatmapTooltipPosRef.current}
        vacationTooltipRef={vacationTooltipRef}
        vacationTooltipPos={vacationTooltipPosRef.current}
        heatmapHover={heatmapHover}
        vacationHover={vacationHover}
      />
    </div>
  );
}

// ResourcePanelTooltips removed — now uses shared TimelineTooltip component

// ─── Helper types ───────────────────────────────────────────────────────────

interface AllocBlockData {
  alloc: TimelineAssignmentEntry;
  lane: number;
}

// ─── Pure render functions (no hooks, extracted from TimelineView) ───────────

function renderAllocBlocksFromData(
  blockData: AllocBlockData[],
  _allocs: TimelineAssignmentEntry[],
  dragState: DragState,
  allocDragState: AllocDragState,
  toLeft: (d: Date) => number,
  toWidth: (s: Date, e: Date) => number,
  CELL_WIDTH: number,
  totalCanvasWidth: number,
  onAllocMouseDown: (e: React.MouseEvent, info: AllocMouseDownInfo) => void,
  onAllocTouchStart: (e: React.TouchEvent, info: AllocMouseDownInfo) => void,
  onAllocationContextMenu: (
    info: { allocationId: string; projectId: string; contextDate?: Date },
    anchorX: number,
    anchorY: number,
  ) => void,
  multiSelectState: MultiSelectState,
  suppressHoverInteractions: boolean,
) {
  const anyDragActive = dragState.isDragging || allocDragState.isActive;

  function toUtcDay(value: Date): Date {
    return new Date(Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()));
  }

  function addUtcDays(value: Date, days: number): Date {
    const next = new Date(value);
    next.setUTCDate(next.getUTCDate() + days);
    return next;
  }

  function resolveSegmentContextDate(
    clientX: number,
    rect: DOMRect,
    segmentStart: Date,
    segmentEnd: Date,
  ): Date {
    const start = toUtcDay(segmentStart);
    const end = toUtcDay(segmentEnd);
    const rawIndex = Math.floor((clientX - rect.left) / CELL_WIDTH);
    const maxIndex = Math.max(
      0,
      Math.round((end.getTime() - start.getTime()) / 86_400_000),
    );
    const dayIndex = Math.min(Math.max(rawIndex, 0), maxIndex);
    return addUtcDays(start, dayIndex);
  }

  function sameDate(a: Date | null, b: Date | null) {
    return Boolean(a && b) && a!.getTime() === b!.getTime();
  }

  return blockData.flatMap(({ alloc, lane }) => {
    const allocStart = toUtcDay(new Date(alloc.startDate));
    const allocEnd = toUtcDay(new Date(alloc.endDate));

    const isProjectShifted = dragState.isDragging && dragState.projectId === alloc.projectId;

    let dispStart = allocStart;
    let dispEnd = allocEnd;
    if (isProjectShifted && dragState.currentStartDate && dragState.currentEndDate) {
      dispStart = dragState.currentStartDate;
      dispEnd = dragState.currentEndDate;
    }

    // Multi-drag offset: shift selected allocations visually during multi-drag
    const isMultiDragTarget =
      multiSelectState.isMultiDragging &&
      multiSelectState.selectedAllocationIds.includes(alloc.id);
    const multiDragPx = isMultiDragTarget ? multiSelectState.multiDragDaysDelta * CELL_WIDTH : 0;
    const multiDragMode = multiSelectState.multiDragMode;

    const blockTop = 8 + lane * SUB_LANE_HEIGHT;
    const blockHeight = SUB_LANE_HEIGHT - 8;

    const customColor = (alloc.project as { color?: string | null }).color;
    const projectColor = getProjectColor(alloc.projectId);
    const blockBgColor = customColor ?? projectColor.hex + "B3";
    const hasRecurrence = !!(alloc.metadata as Record<string, unknown> | null)?.recurrence;

    const allocInfo: AllocMouseDownInfo = {
      mode: "move",
      allocationId: alloc.id,
      mutationAllocationId: getPlanningEntryMutationId(alloc),
      projectId: alloc.projectId,
      projectName: alloc.project.name,
      resourceId: alloc.resourceId,
      startDate: allocStart,
      endDate: allocEnd,
      allocationStartDate: allocStart,
      allocationEndDate: allocEnd,
      scope: "allocation",
    };

    const segments = buildAllocationWorkingDaySegments(
      { ...alloc, startDate: dispStart, endDate: dispEnd },
      dispStart,
      dispEnd,
    );

    if (segments.length === 0) {
      return [];
    }

    return segments.flatMap((segment, segmentIndex) => {
      const isFirstSegment = segmentIndex === 0;
      const segmentKey = `${alloc.id}-${segmentIndex}`;
      const draggedSegmentActive =
        allocDragState.isActive &&
        allocDragState.allocationId === alloc.id &&
        sameDate(allocDragState.originalStartDate, toUtcDay(segment.start)) &&
        sameDate(allocDragState.originalEndDate, toUtcDay(segment.end));
      const isBeingDragged = isProjectShifted || draggedSegmentActive;
      const isOtherDragged = anyDragActive && !isBeingDragged;

      let segmentLeft = toLeft(segment.start);
      let segmentWidth = Math.max(CELL_WIDTH, toWidth(segment.start, segment.end));
      let dragTransform: string | undefined;

      if (isProjectShifted) {
        const preview = applyPointerOffsetPreviewRect({
          left: segmentLeft,
          width: segmentWidth,
          mode: "move",
          pointerOffsetX: getDragPointerOffset(
            dragState.pointerDeltaX,
            dragState.daysDelta,
            CELL_WIDTH,
          ),
          minWidth: CELL_WIDTH,
        });
        segmentLeft = preview.left;
        segmentWidth = preview.width;
        dragTransform = preview.transform;
      } else if (draggedSegmentActive) {
        const preview = applyPointerOffsetPreviewRect({
          left: segmentLeft,
          width: segmentWidth,
          mode: allocDragState.mode,
          pointerOffsetX: getDragPointerOffset(
            allocDragState.pointerDeltaX,
            allocDragState.daysDelta,
            CELL_WIDTH,
          ),
          minWidth: CELL_WIDTH,
        });
        segmentLeft = preview.left;
        segmentWidth = preview.width;
        dragTransform = preview.transform;
      }

      if (isMultiDragTarget && multiDragMode === "resize-start") {
        segmentLeft += multiDragPx;
        segmentWidth = Math.max(CELL_WIDTH, segmentWidth - multiDragPx);
      } else if (isMultiDragTarget && multiDragMode === "resize-end") {
        segmentWidth = Math.max(CELL_WIDTH, segmentWidth + multiDragPx);
      }

      if (segmentWidth <= 0 || segmentLeft >= totalCanvasWidth) {
        return [];
      }

      const handleWidth = segmentWidth >= 48 ? 10 : 6;
      const dragInset = Math.min(handleWidth, Math.max(2, Math.floor(segmentWidth / 4)));
      const segmentInfo: AllocMouseDownInfo = {
        ...allocInfo,
        startDate: toUtcDay(segment.start),
        endDate: toUtcDay(segment.end),
        scope: "segment",
      };

      return (
        <div
          key={segmentKey}
          data-allocation-id={alloc.id}
          data-timeline-entry-type="allocation"
          data-timeline-drag-preview="project-shift allocation"
          data-timeline-project-id={alloc.projectId}
          data-allocation-segment-index={segmentIndex}
          data-allocation-segment-start={toLocalDateKey(segment.start)}
          data-allocation-segment-end={toLocalDateKey(segment.end)}
          className={clsx(
            "absolute text-white group/block",
            hasRecurrence && "opacity-80",
            isBeingDragged
              ? "opacity-90 z-20"
              : isOtherDragged
                ? "opacity-30 z-[10]"
                : "transition-[opacity] duration-75 z-[10]",
            multiSelectState.selectedAllocationIds.includes(alloc.id) && "z-20",
          )}
          style={{
            left: segmentLeft + 2,
            width: segmentWidth - 4,
            top: blockTop,
            height: blockHeight,
            ...((multiDragPx && multiDragMode === "move") || dragTransform
              ? {
                  transform: [
                    dragTransform,
                    multiDragPx && multiDragMode === "move"
                      ? `translateX(${multiDragPx}px)`
                      : null,
                  ]
                    .filter(Boolean)
                    .join(" "),
                }
              : {}),
          }}
          onMouseDown={(e) => {
            if (e.button === 2) e.stopPropagation();
          }}
          onContextMenu={(e) => {
            e.preventDefault();
            e.stopPropagation();
            if (suppressHoverInteractions) return;
            onAllocationContextMenu(
              {
                allocationId: alloc.id,
                projectId: alloc.projectId,
                contextDate: resolveSegmentContextDate(
                  e.clientX,
                  e.currentTarget.getBoundingClientRect(),
                  segment.start,
                  segment.end,
                ),
              },
              e.clientX,
              e.clientY,
            );
          }}
        >
          <div
            data-allocation-handle="start"
            className="absolute inset-y-0 left-0 z-30 flex items-center justify-center cursor-ew-resize rounded-l-md hover:bg-black/15 transition-colors"
            style={{ width: handleWidth }}
            onMouseDown={(e) => {
              e.stopPropagation();
              onAllocMouseDown(e, { ...segmentInfo, mode: "resize-start" });
            }}
            onTouchStart={(e) => {
              e.stopPropagation();
              onAllocTouchStart(e, { ...segmentInfo, mode: "resize-start" });
            }}
          >
            {handleWidth >= 10 && (
              <div className="flex flex-col gap-0.5 opacity-60 group-hover/block:opacity-100">
                <div className="w-px h-2.5 bg-white rounded" />
                <div className="w-px h-2.5 bg-white rounded" />
              </div>
            )}
          </div>

          <div
            aria-hidden="true"
            className={clsx(
              "pointer-events-none absolute inset-0 z-0 rounded-md overflow-hidden",
              hasRecurrence && "border-2 border-dashed border-white/60",
              isBeingDragged
                ? "shadow-2xl ring-2 ring-white ring-offset-1 scale-[1.01]"
                : "hover:ring-2 hover:ring-white hover:ring-offset-1",
              multiSelectState.selectedAllocationIds.includes(alloc.id) && "ring-2 ring-sky-500 ring-offset-1",
            )}
            style={{
              backgroundColor: blockBgColor,
            }}
          />

          <div
            data-allocation-interaction="body"
            className={clsx(
              "absolute inset-y-0 z-20 flex min-w-0 select-none items-center gap-1 px-1 pointer-events-auto",
              isBeingDragged ? "cursor-grabbing" : "cursor-grab",
            )}
            style={{ left: dragInset, right: dragInset }}
            onMouseDown={(e) => {
              e.stopPropagation();
              onAllocMouseDown(e, segmentInfo);
            }}
            onTouchStart={(e) => {
              e.stopPropagation();
              onAllocTouchStart(e, segmentInfo);
            }}
          >
            {hasRecurrence && segmentWidth > 28 && (
              <span className="text-[10px] opacity-80 flex-shrink-0">↻</span>
            )}
            {isFirstSegment && segmentWidth > 60 ? (
              <span className="text-xs font-semibold truncate">{alloc.project.name}</span>
            ) : isFirstSegment ? (
              <span className="text-[9px] font-bold truncate opacity-90">{alloc.project.shortCode}</span>
            ) : null}
            {isFirstSegment && segmentWidth > 130 && (
              <span className="text-[10px] opacity-75 truncate">{alloc.role}</span>
            )}
            {isFirstSegment && segmentWidth > 190 && (
              <span className="text-[10px] opacity-60 truncate">{alloc.hoursPerDay}h</span>
            )}
          </div>

          <div
            data-allocation-handle="end"
            className="absolute inset-y-0 right-0 z-30 flex items-center justify-center cursor-ew-resize rounded-r-md hover:bg-black/15 transition-colors"
            style={{ width: handleWidth }}
            onMouseDown={(e) => {
              e.stopPropagation();
              onAllocMouseDown(e, { ...segmentInfo, mode: "resize-end" });
            }}
            onTouchStart={(e) => {
              e.stopPropagation();
              onAllocTouchStart(e, { ...segmentInfo, mode: "resize-end" });
            }}
          >
            {handleWidth >= 10 && (
              <div className="flex flex-col gap-0.5 opacity-60 group-hover/block:opacity-100">
                <div className="w-px h-2.5 bg-white rounded" />
                <div className="w-px h-2.5 bg-white rounded" />
              </div>
            )}
          </div>
        </div>
      );
    });
  });
}

// ─── Strip-mode: daily load graph ────────────────────────────────────────────

function renderLoadGraph(
  allocs: TimelineAssignmentEntry[],
  dates: Date[],
  CELL_WIDTH: number,
  capacity: ResourceCapacitySeries | undefined,
) {
  const GRAPH_H = 12;

  function hoursOnDay(list: TimelineAssignmentEntry[], t: number) {
    const currentDate = new Date(t);
    return list.reduce((sum, a) => {
      return isAllocationScheduledOnDate(a, currentDate) ? sum + a.hoursPerDay : sum;
    }, 0);
  }

  return (
    <div className="absolute inset-x-0 bottom-1 pointer-events-none" style={{ height: GRAPH_H }}>
      {dates.map((date, i) => {
        const t = date.getTime();
        const bookingFactor = capacity?.bookingFactorsByDay[i] ?? 1;
        const capacityHours = capacity?.capacityHoursByDay[i] ?? 8;
        const totalH = hoursOnDay(allocs, t) * bookingFactor;
        if (totalH === 0) return null;

        const totalBarH =
          capacityHours > 0
            ? Math.min(GRAPH_H, Math.round((Math.min(totalH, capacityHours) / capacityHours) * GRAPH_H))
            : GRAPH_H;
        const utilizationPct = capacityHours > 0 ? (totalH / capacityHours) * 100 : 100;

        return (
          <div
            key={i}
            className={clsx(
              "absolute bottom-0 rounded-t-sm",
              utilizationPct > 100
                ? "bg-red-500 opacity-80"
                : utilizationPct > 75
                  ? "bg-amber-400 opacity-80"
                  : "bg-brand-500 opacity-80",
            )}
            style={{ left: i * CELL_WIDTH + 3, width: CELL_WIDTH - 6, height: totalBarH }}
          />
        );
      })}
    </div>
  );
}

// ─── Heatmap-mode: utilisation colour overlay ────────────────────────────────

function renderHeatmapOverlay(
  allocs: TimelineAssignmentEntry[],
  dates: Date[],
  CELL_WIDTH: number,
  heatmapScheme: HeatmapColorScheme,
  capacity: ResourceCapacitySeries | undefined,
) {
  return dates.map((date, i) => {
    const bookingFactor = capacity?.bookingFactorsByDay[i] ?? 1;
    const capacityHours = capacity?.capacityHoursByDay[i] ?? 8;
    const totalH = allocs.reduce((sum, a) => {
      return isAllocationScheduledOnDate(a, date) ? sum + a.hoursPerDay * bookingFactor : sum;
    }, 0);
    const pct = capacityHours > 0 ? (totalH / capacityHours) * 100 : totalH > 0 ? 100 : 0;
    const bg = heatmapBgColor(pct, heatmapScheme);
    if (!bg) return null;
    return (
      <div
        key={`hm-${i}`}
        className="absolute top-0 bottom-0 pointer-events-none z-10"
        style={{ left: i * CELL_WIDTH, width: CELL_WIDTH, backgroundColor: bg }}
      />
    );
  });
}

// ─── Bar-mode: stacked daily bars ────────────────────────────────────────────

function renderDailyBars(
  allocs: TimelineAssignmentEntry[],
  rowHeight: number,
  CELL_WIDTH: number,
  dates: Date[],
  allocDragState: AllocDragState,
  onAllocMouseDown: (e: React.MouseEvent, info: AllocMouseDownInfo) => void,
  onAllocTouchStart: (e: React.TouchEvent, info: AllocMouseDownInfo) => void,
  onAllocationContextMenu: (
    info: { allocationId: string; projectId: string; contextDate?: Date },
    anchorX: number,
    anchorY: number,
  ) => void,
  toLeft: (d: Date) => number,
  toWidth: (s: Date, e: Date) => number,
  totalCanvasWidth: number,
  capacity: ResourceCapacitySeries | undefined,
  multiSelectState: MultiSelectState,
  suppressHoverInteractions: boolean,
) {
  const BAR_AREA = rowHeight - 8;

  return dates.flatMap((date, i) => {
    const dayTimestamp = date.getTime();
    const covering = allocs.filter((a) => {
      const isDragged = allocDragState.isActive && allocDragState.allocationId === a.id;
      return isAllocationScheduledOnDate(
        {
          ...a,
          startDate:
            isDragged && allocDragState.currentStartDate
              ? allocDragState.currentStartDate
              : a.startDate,
          endDate:
            isDragged && allocDragState.currentEndDate
              ? allocDragState.currentEndDate
              : a.endDate,
        },
        date,
      );
    });

    if (covering.length === 0) return [];

    const bookingFactor = capacity?.bookingFactorsByDay[i] ?? 1;
    const capacityHours = capacity?.capacityHoursByDay[i] ?? 8;
    const totalH = covering.reduce((sum, a) => sum + a.hoursPerDay * bookingFactor, 0);
    const isOver = totalH > capacityHours;
    let stackedH = 0;

    const segs: React.ReactNode[] = covering.map((alloc) => {
      const customColor = (alloc.project as { color?: string | null }).color;
      const projectColor = getProjectColor(alloc.projectId);
      const segBgColor = customColor ?? projectColor.hex + "B3";
      const effectiveHours = alloc.hoursPerDay * bookingFactor;
      if (effectiveHours <= 0 || capacityHours <= 0) {
        return null;
      }
      const segH = Math.max(
        2,
        Math.min(
          BAR_AREA - stackedH,
          Math.round(
            ((capacityHours > 0 ? effectiveHours / capacityHours : effectiveHours > 0 ? 1 : 0) *
              BAR_AREA),
          ),
        ),
      );
      const bottom = 4 + stackedH;
      stackedH += segH;
      const isBeingDragged = allocDragState.isActive && allocDragState.allocationId === alloc.id;

      const dispStart = new Date(
        isBeingDragged && allocDragState.currentStartDate
          ? allocDragState.currentStartDate
          : alloc.startDate,
      );
      const dispEnd = new Date(
        isBeingDragged && allocDragState.currentEndDate
          ? allocDragState.currentEndDate
          : alloc.endDate,
      );
      dispStart.setHours(0, 0, 0, 0);
      dispEnd.setHours(0, 0, 0, 0);
      const isFirstDay = dayTimestamp === dispStart.getTime();
      const isLastDay = dayTimestamp === dispEnd.getTime();
      const EDGE_W = CELL_WIDTH >= 16 ? 4 : 0;
      const dragPointerOffset = isBeingDragged
        ? getDragPointerOffset(
            allocDragState.pointerDeltaX,
            allocDragState.daysDelta,
            CELL_WIDTH,
          )
        : 0;

      const allocInfo: AllocMouseDownInfo = {
        mode: "move",
        allocationId: alloc.id,
        mutationAllocationId: getPlanningEntryMutationId(alloc),
        projectId: alloc.projectId,
        projectName: alloc.project.name,
        resourceId: alloc.resourceId,
        startDate: new Date(alloc.startDate),
        endDate: new Date(alloc.endDate),
      };

      return (
        <div
          key={`bar-${i}-${alloc.id}`}
          data-allocation-id={alloc.id}
          data-timeline-entry-type="allocation"
          data-timeline-drag-preview="allocation"
          data-timeline-project-id={alloc.projectId}
          className={clsx(
            "absolute rounded-sm flex items-stretch overflow-hidden",
            isBeingDragged
              ? "opacity-90 ring-2 ring-white ring-offset-1 z-20"
              : "transition-opacity duration-75 hover:opacity-80 z-[10]",
            multiSelectState.selectedAllocationIds.includes(alloc.id) && "ring-2 ring-sky-500 ring-offset-1 z-20",
          )}
          style={{
            left: i * CELL_WIDTH + 2,
            width: CELL_WIDTH - 4,
            height: segH,
            bottom,
            backgroundColor: segBgColor,
            ...((multiSelectState.isMultiDragging &&
              multiSelectState.selectedAllocationIds.includes(alloc.id)) ||
            dragPointerOffset
              ? {
                  transform: [
                    dragPointerOffset ? `translateX(${dragPointerOffset}px)` : null,
                    multiSelectState.isMultiDragging &&
                    multiSelectState.selectedAllocationIds.includes(alloc.id)
                      ? `translateX(${multiSelectState.multiDragDaysDelta * CELL_WIDTH}px)`
                      : null,
                  ]
                    .filter(Boolean)
                    .join(" "),
                }
              : {}),
          }}
          onMouseDown={(e) => {
            if (e.button === 2) e.stopPropagation();
          }}
          onContextMenu={(e) => {
            e.preventDefault();
            e.stopPropagation();
            if (suppressHoverInteractions) return;
            onAllocationContextMenu(
              { allocationId: alloc.id, projectId: alloc.projectId, contextDate: new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())) },
              e.clientX,
              e.clientY,
            );
          }}
        >
          {isFirstDay && EDGE_W > 0 && (
            <div
              className="flex-shrink-0 cursor-ew-resize hover:bg-black/20"
              style={{ width: EDGE_W }}
              onMouseDown={(e) => {
                e.stopPropagation();
                onAllocMouseDown(e, { ...allocInfo, mode: "resize-start" });
              }}
              onTouchStart={(e) => {
                e.stopPropagation();
                onAllocTouchStart(e, { ...allocInfo, mode: "resize-start" });
              }}
            />
          )}
          <div
            className={clsx("flex-1 min-w-0", "cursor-grab")}
            onMouseDown={(e) => {
              e.stopPropagation();
              onAllocMouseDown(e, allocInfo);
            }}
            onTouchStart={(e) => {
              e.stopPropagation();
              onAllocTouchStart(e, allocInfo);
            }}
          />
          {isLastDay && EDGE_W > 0 && (
            <div
              className="flex-shrink-0 cursor-ew-resize hover:bg-black/20"
              style={{ width: EDGE_W }}
              onMouseDown={(e) => {
                e.stopPropagation();
                onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" });
              }}
              onTouchStart={(e) => {
                e.stopPropagation();
                onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" });
              }}
            />
          )}
        </div>
      );
    });

    if (isOver) {
      segs.push(
        <div
          key={`bar-${i}-over`}
          className="absolute bg-red-500/70 rounded-t-sm pointer-events-none z-30"
          style={{ left: i * CELL_WIDTH + 2, width: CELL_WIDTH - 4, top: 4, height: 3 }}
        />,
      );
    }

    return segs.filter(Boolean);
  });
}

export const TimelineResourcePanel = memo(TimelineResourcePanelInner);

// ─── Re-export tooltip types for the parent ─────────────────────────────────
export type { AllocBlockData };
export type { VacationBlockInfo } from "./renderHelpers.js";
