"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 { ConflictOverlay } from "./ConflictOverlay.js";
import { computeSubLanes } from "./utils.js";
import { heatmapBgColor } from "./heatmapUtils.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import { TimelineTooltip } 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 {
  renderVacationBlocks,
  renderRangeOverlay,
  renderOverbookingBlink,
  type VacationBlockInfo,
} from "./renderHelpers.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 },
    anchorX: number,
    anchorY: number,
  ) => void;
  multiSelectState: MultiSelectState;
  // 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;
}

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

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

function TimelineResourcePanelInner({
  scrollContainerRef,
  dragState,
  allocDragState,
  rangeState,
  shiftPreview,
  contextResourceIds,
  onAllocMouseDown,
  onAllocTouchStart,
  onRowMouseDown,
  onRowTouchStart,
  onAllocationContextMenu,
  multiSelectState,
  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<{
    date: Date;
    totalH: number;
    pct: number;
    breakdown: {
      projectId: string;
      shortCode: string;
      projectName: string;
      orderType: string;
      hoursPerDay: number;
      responsiblePerson?: string | null;
      role?: string | null;
      status?: string;
      startDate?: string;
      endDate?: string;
    }[];
  } | null>(null);

  const [vacationHover, setVacationHover] = useState<null | {
    type: string;
    startDate: Date | string;
    endDate: Date | string;
    note?: string | null;
    requestedBy?: { name?: string | null; email: string } | null;
    approvedBy?: { name?: string | null; email: string } | null;
    approvedAt?: Date | string | 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();

  // ─── 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 = allocsByResource.get(resource.id) ?? [];
      const isContextResource = contextResourceIds.includes(resource.id);
      return { resource, allocs, isContextResource };
    });
  }, [resources, allocsByResource, contextResourceIds]);

  // ─── Memo 2: vacationBlocks — vacation bar positions per resource ─────────
  const vacationBlocksByResource = useMemo(() => {
    if (!filters.showVacations) return new Map<string, VacationBlockInfo[]>();

    const result = new Map<string, VacationBlockInfo[]>();
    for (const [resourceId, vacations] of vacationsByResource) {
      const blocks: VacationBlockInfo[] = [];
      for (const v of vacations) {
        const vStart = new Date(v.startDate);
        const vEnd = new Date(v.endDate);
        const left = toLeft(vStart);
        const width = Math.max(CELL_WIDTH, toWidth(vStart, vEnd));
        if (width <= 0 || left >= totalCanvasWidth) continue;
        blocks.push({ vacation: v, left, width });
      }
      if (blocks.length > 0) {
        result.set(resourceId, blocks);
      }
    }
    return result;
  }, [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 REF_H = 8;
    const result = new Map<string, number>(); // resourceId -> avg utilization pct
    for (const { resource, allocs } of resourceRows) {
      if (allocs.length === 0) continue;
      let totalHours = 0;
      let dayCount = 0;
      for (const date of dates) {
        const t = date.getTime();
        let dayH = 0;
        for (const a of allocs) {
          const s = new Date(a.startDate);
          s.setHours(0, 0, 0, 0);
          const e = new Date(a.endDate);
          e.setHours(0, 0, 0, 0);
          if (t >= s.getTime() && t <= e.getTime()) dayH += a.hoursPerDay;
        }
        if (dayH > 0) {
          totalHours += dayH;
          dayCount++;
        }
      }
      if (dayCount > 0) {
        result.set(resource.id, (totalHours / dayCount / REF_H) * 100);
      }
    }
    return result;
  }, [resourceRows, dates]);

  // ─── Heatmap row hover handler ────────────────────────────────────────────
  const handleRowHeatmapMove = useCallback(
    (e: React.MouseEvent, allocs: TimelineAssignmentEntry[]) => {
      heatmapTooltipPosRef.current = { left: e.clientX + 16, top: e.clientY - 52 };
      if (heatmapTooltipRef.current) {
        heatmapTooltipRef.current.style.left = `${heatmapTooltipPosRef.current.left}px`;
        heatmapTooltipRef.current.style.top = `${heatmapTooltipPosRef.current.top}px`;
      }
      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 t = date.getTime();
        const REF_H = 8;
        const projectHours = new Map<
          string,
          {
            shortCode: string;
            projectName: string;
            orderType: string;
            hours: number;
            responsiblePerson?: string | null;
            role?: string | null;
            status?: string;
            startDate?: string;
            endDate?: string;
          }
        >();
        for (const alloc of a) {
          const s = new Date(alloc.startDate);
          s.setHours(0, 0, 0, 0);
          const ev = new Date(alloc.endDate);
          ev.setHours(0, 0, 0, 0);
          if (t < s.getTime() || t > ev.getTime()) continue;
          const existing = projectHours.get(alloc.projectId);
          if (existing) {
            existing.hours += alloc.hoursPerDay;
          } else {
            projectHours.set(alloc.projectId, {
              shortCode: alloc.project.shortCode,
              projectName: alloc.project.name,
              orderType: alloc.project.orderType,
              hours: alloc.hoursPerDay,
              responsiblePerson:
                (alloc.project as { responsiblePerson?: string | null }).responsiblePerson ?? null,
              role: alloc.role ?? alloc.roleEntity?.name ?? null,
              status: alloc.status,
              startDate: new Date(alloc.startDate).toISOString().slice(0, 10),
              endDate: new Date(alloc.endDate).toISOString().slice(0, 10),
            });
          }
        }

        const breakdown = [...projectHours.entries()]
          .map(([projectId, v]) => ({ projectId, ...v, hoursPerDay: v.hours }))
          .sort((a, b) => b.hoursPerDay - a.hoursPerDay);

        const totalH = breakdown.reduce((sum, b) => sum + b.hoursPerDay, 0);
        startTransition(() => {
          setHeatmapHover({ date, totalH, pct: (totalH / REF_H) * 100, breakdown });
        });
      });
    },
    [CELL_WIDTH, dates],
  );

  // ─── Vacation hover ───────────────────────────────────────────────────────
  const handleRowVacationHover = useCallback(
    (e: React.MouseEvent, resourceId: string) => {
      vacationTooltipPosRef.current = { left: e.clientX + 14, top: e.clientY - 8 };
      if (vacationTooltipRef.current) {
        vacationTooltipRef.current.style.left = `${vacationTooltipPosRef.current.left}px`;
        vacationTooltipRef.current.style.top = `${vacationTooltipPosRef.current.top}px`;
      }
      const rect = e.currentTarget.getBoundingClientRect();
      const clientX = e.clientX;

      if (vacationHoverRafRef.current !== null) return;

      vacationHoverRafRef.current = requestAnimationFrame(() => {
        vacationHoverRafRef.current = null;
        const date = xToDate(clientX, rect);
        date.setHours(0, 0, 0, 0);
        const t = date.getTime();
        const resourceVacations = vacationsByResource.get(resourceId) ?? [];
        const hit =
          resourceVacations.find((v) => {
            const s = new Date(v.startDate);
            s.setHours(0, 0, 0, 0);
            const end = new Date(v.endDate);
            end.setHours(0, 0, 0, 0);
            return t >= s.getTime() && t <= end.getTime();
          }) ?? null;

        const nextKey = hit ? `${resourceId}:${hit.id}` : null;
        if (nextKey === hoveredVacationKeyRef.current) return;

        hoveredVacationKeyRef.current = nextKey;
        startTransition(() => {
          setVacationHover(hit);
        });
      });
    },
    [vacationsByResource, xToDate],
  );

  const clearHoverTooltips = useCallback(() => {
    if (heatmapRafRef.current !== null) {
      cancelAnimationFrame(heatmapRafRef.current);
      heatmapRafRef.current = null;
    }
    if (vacationHoverRafRef.current !== null) {
      cancelAnimationFrame(vacationHoverRafRef.current);
      vacationHoverRafRef.current = null;
    }

    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(
    () => () => {
      if (heatmapRafRef.current !== null) cancelAnimationFrame(heatmapRafRef.current);
      if (vacationHoverRafRef.current !== null) cancelAnimationFrame(vacationHoverRafRef.current);
    },
    [],
  );

  // ─── 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) => {
                  handleRowHeatmapMove(e, allocs);
                  handleRowVacationHover(e, resource.id);
                }}
                onMouseLeave={clearHoverTooltips}
              >
                {gridLines}
                {inBarMode
                  ? renderDailyBars(
                      allocs,
                      rowHeight,
                      CELL_WIDTH,
                      dates,
                      allocDragState,
                      onAllocMouseDown,
                      onAllocTouchStart,
                      onAllocationContextMenu,
                      toLeft,
                      toWidth,
                      totalCanvasWidth,
                      multiSelectState,
                    )
                  : renderAllocBlocksFromData(
                      precomputed?.blockData ?? [],
                      allocs,
                      dragState,
                      allocDragState,
                      toLeft,
                      toWidth,
                      CELL_WIDTH,
                      totalCanvasWidth,
                      onAllocMouseDown,
                      onAllocTouchStart,
                      onAllocationContextMenu,
                      multiSelectState,
                    )}
                {filters.showVacations &&
                  renderVacationBlocks(
                    vacationBlocksByResource.get(resource.id) ?? [],
                    rowHeight,
                  )}
                {displayMode === "strip" && renderLoadGraph(allocs, dates, CELL_WIDTH)}
                {displayMode === "heatmap" &&
                  renderHeatmapOverlay(allocs, dates, CELL_WIDTH, heatmapScheme)}
                {blinkOverbookedDays &&
                  renderOverbookingBlink(allocs, dates, CELL_WIDTH)}
                {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 },
    anchorX: number,
    anchorY: number,
  ) => void,
  multiSelectState: MultiSelectState,
) {
  const anyDragActive = dragState.isDragging || allocDragState.isActive;

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

    const isProjectShifted = dragState.isDragging && dragState.projectId === alloc.projectId;
    const isAllocDragged = allocDragState.isActive && allocDragState.allocationId === alloc.id;
    const isBeingDragged = isProjectShifted || isAllocDragged;
    const isOtherDragged = anyDragActive && !isBeingDragged;

    let dispStart = allocStart;
    let dispEnd = allocEnd;
    if (isProjectShifted && dragState.currentStartDate && dragState.currentEndDate) {
      dispStart = dragState.currentStartDate;
      dispEnd = dragState.currentEndDate;
    } else if (isAllocDragged && allocDragState.currentStartDate && allocDragState.currentEndDate) {
      dispStart = allocDragState.currentStartDate;
      dispEnd = allocDragState.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;

    let left = toLeft(dispStart);
    let width = Math.max(CELL_WIDTH, toWidth(dispStart, dispEnd));

    // For multi-drag resize, adjust left/width instead of using translateX
    if (isMultiDragTarget && multiDragMode === "resize-start") {
      left += multiDragPx;
      width = Math.max(CELL_WIDTH, width - multiDragPx);
    } else if (isMultiDragTarget && multiDragMode === "resize-end") {
      width = Math.max(CELL_WIDTH, width + multiDragPx);
    }
    if (width <= 0 || left >= totalCanvasWidth) return null;

    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 HANDLE_W = width >= 48 ? 10 : 6;
    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,
    };

    return (
      <div
        key={alloc.id}
        className={clsx(
          "absolute rounded-md flex items-stretch overflow-hidden transition-all duration-75 group/block text-white",
          hasRecurrence && "opacity-80 border-2 border-dashed border-white/60",
          isBeingDragged
            ? "opacity-90 shadow-2xl ring-2 ring-white ring-offset-1 z-20 scale-[1.01]"
            : isOtherDragged
              ? "opacity-30 z-[10]"
              : "hover:ring-2 hover:ring-white hover:ring-offset-1 z-[10]",
          multiSelectState.selectedAllocationIds.includes(alloc.id) && "ring-2 ring-sky-500 ring-offset-1 z-20",
        )}
        style={{
          left: left + 2,
          width: width - 4,
          top: blockTop,
          height: blockHeight,
          backgroundColor: blockBgColor,
          ...(multiDragPx && multiDragMode === "move" ? { transform: `translateX(${multiDragPx}px)` } : {}),
        }}
        onMouseDown={(e) => {
          // Stop right-click mouseDown from bubbling to the canvas,
          // which would falsely start a multi-selection rectangle.
          if (e.button === 2) e.stopPropagation();
        }}
        onContextMenu={(e) => {
          e.preventDefault();
          e.stopPropagation();
          onAllocationContextMenu(
            { allocationId: alloc.id, projectId: alloc.projectId },
            e.clientX,
            e.clientY,
          );
        }}
      >
        {/* Left resize handle */}
        <div
          className="flex-shrink-0 flex items-center justify-center cursor-ew-resize hover:bg-black/20 transition-colors"
          style={{ width: HANDLE_W }}
          onMouseDown={(e) => onAllocMouseDown(e, { ...allocInfo, mode: "resize-start" })}
          onTouchStart={(e) => {
            e.stopPropagation();
            onAllocTouchStart(e, { ...allocInfo, mode: "resize-start" });
          }}
        >
          {HANDLE_W >= 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>

        {/* Center -- move */}
        <div
          className={clsx(
            "flex-1 flex items-center gap-1 px-1 min-w-0 select-none",
            isBeingDragged ? "cursor-grabbing" : "cursor-grab",
          )}
          onMouseDown={(e) => onAllocMouseDown(e, allocInfo)}
          onTouchStart={(e) => {
            e.stopPropagation();
            onAllocTouchStart(e, allocInfo);
          }}
        >
          {hasRecurrence && width > 28 && (
            <span className="text-[10px] opacity-80 flex-shrink-0">↻</span>
          )}
          {width > 60 ? (
            <span className="text-xs font-semibold truncate">{alloc.project.name}</span>
          ) : (
            <span className="text-[9px] font-bold truncate opacity-90">{alloc.project.shortCode}</span>
          )}
          {width > 130 && <span className="text-[10px] opacity-75 truncate">{alloc.role}</span>}
          {width > 190 && (
            <span className="text-[10px] opacity-60 truncate">{alloc.hoursPerDay}h</span>
          )}
        </div>

        {/* Right resize handle */}
        <div
          className="flex-shrink-0 flex items-center justify-center cursor-ew-resize hover:bg-black/20 transition-colors"
          style={{ width: HANDLE_W }}
          onMouseDown={(e) => onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" })}
          onTouchStart={(e) => {
            e.stopPropagation();
            onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" });
          }}
        >
          {HANDLE_W >= 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) {
  const GRAPH_H = 12;
  const REF_H = 8;

  function hoursOnDay(list: TimelineAssignmentEntry[], t: number) {
    return list.reduce((sum, a) => {
      const s = new Date(a.startDate);
      s.setHours(0, 0, 0, 0);
      const e = new Date(a.endDate);
      e.setHours(0, 0, 0, 0);
      return t >= s.getTime() && t <= e.getTime() ? 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 totalH = hoursOnDay(allocs, t);
        if (totalH === 0) return null;

        const totalBarH = Math.min(GRAPH_H, Math.round((totalH / REF_H) * GRAPH_H));

        return (
          <div
            key={i}
            className={clsx(
              "absolute bottom-0 rounded-t-sm",
              totalH > 12
                ? "bg-red-500 opacity-80"
                : totalH > 8
                  ? "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,
) {
  const REF_H = 8;
  return dates.map((date, i) => {
    const t = date.getTime();
    const totalH = allocs.reduce((sum, a) => {
      const s = new Date(a.startDate);
      s.setHours(0, 0, 0, 0);
      const e = new Date(a.endDate);
      e.setHours(0, 0, 0, 0);
      return t >= s.getTime() && t <= e.getTime() ? sum + a.hoursPerDay : sum;
    }, 0);
    const bg = heatmapBgColor((totalH / REF_H) * 100, 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 },
    anchorX: number,
    anchorY: number,
  ) => void,
  toLeft: (d: Date) => number,
  toWidth: (s: Date, e: Date) => number,
  totalCanvasWidth: number,
  multiSelectState: MultiSelectState,
) {
  const BAR_AREA = rowHeight - 8;
  const REF_H = 8;

  return dates.flatMap((date, i) => {
    const t = date.getTime();

    const covering = allocs.filter((a) => {
      const isDragged = allocDragState.isActive && allocDragState.allocationId === a.id;
      const s = new Date(
        isDragged && allocDragState.currentStartDate
          ? allocDragState.currentStartDate
          : a.startDate,
      );
      const e = new Date(
        isDragged && allocDragState.currentEndDate ? allocDragState.currentEndDate : a.endDate,
      );
      s.setHours(0, 0, 0, 0);
      e.setHours(0, 0, 0, 0);
      return t >= s.getTime() && t <= e.getTime();
    });

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

    const totalH = covering.reduce((sum, a) => sum + a.hoursPerDay, 0);
    const isOver = totalH > REF_H;
    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 segH = Math.max(
        2,
        Math.min(BAR_AREA - stackedH, Math.round((alloc.hoursPerDay / REF_H) * 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 = t === dispStart.getTime();
      const isLastDay = t === dispEnd.getTime();
      const EDGE_W = CELL_WIDTH >= 16 ? 4 : 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}`}
          className={clsx(
            "absolute rounded-sm transition-all duration-75 flex items-stretch overflow-hidden",
            isBeingDragged
              ? "opacity-90 ring-2 ring-white ring-offset-1 z-20"
              : "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)
                ? { transform: `translateX(${multiSelectState.multiDragDaysDelta * CELL_WIDTH}px)` }
                : {}),
          }}
          onMouseDown={(e) => {
            if (e.button === 2) e.stopPropagation();
          }}
          onContextMenu={(e) => {
            e.preventDefault();
            e.stopPropagation();
            onAllocationContextMenu(
              { allocationId: alloc.id, projectId: alloc.projectId },
              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;
  });
}

export const TimelineResourcePanel = memo(TimelineResourcePanelInner);

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