"use client";

import { clsx } from "clsx";
import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import type { CSSProperties } from "react";
import {
  useTimelineContext,
  type TimelineAssignmentEntry,
  type TimelineDemandEntry,
} from "./TimelineContext.js";
import {
  applyPointerOffsetPreviewRect,
  applyVisualOverrides,
  getDragPointerOffset,
  type TimelineVisualOverrides,
} from "./allocationVisualState.js";
import { heatmapColor } from "./heatmapUtils.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import { formatDateLong } from "~/lib/format.js";
import {
  TimelineTooltip,
  type DemandHoverData,
  type HeatmapHoverData,
  type VacationHoverData,
} from "./TimelineTooltip.js";
import {
  ROW_HEIGHT,
  SUB_LANE_HEIGHT,
  LABEL_WIDTH,
  PROJECT_HEADER_HEIGHT,
  ORDER_TYPE_COLORS,
} from "./timelineConstants.js";
import { getProjectColor } from "~/lib/project-colors.js";
import type { DragState, AllocDragState, RangeState, MultiSelectState } from "~/hooks/useTimelineDrag.js";
import type { AllocMouseDownInfo, RowMouseDownInfo } from "./TimelineResourcePanel.js";
import {
  buildVacationBlocksByResource,
  renderVacationBlocks,
  renderRangeOverlay,
  renderOverbookingBlink,
  type VacationBlockInfo,
} from "./renderHelpers.js";
import {
  buildDemandHoverData,
  cancelHoverFrame,
  collectResourcesWithVacations,
  scheduleVacationHoverUpdate,
  updateTooltipPosition,
} from "./timelineHover.js";
import { buildResourceHeatmapSeries } from "./timelineHeatmap.js";
import { buildResourceCapacitySeries } from "./timelineCapacity.js";
import {
  buildProjectRowMetrics,
  type ProjectDayMetric,
} from "./timelineProjectMetrics.js";
import {
  buildProjectFlatRows,
  estimateProjectRowHeight,
  type OpenDemandRowLayout,
  type ProjectFlatRow,
} from "./timelineProjectRows.js";

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

interface TimelineProjectPanelProps {
  scrollContainerRef: React.RefObject<HTMLDivElement | null>;
  dragState: DragState;
  allocDragState: AllocDragState;
  rangeState: RangeState;
  onProjectBarMouseDown: (e: React.MouseEvent, info: ProjectBarInfo) => void;
  onProjectBarTouchStart: (e: React.TouchEvent, info: ProjectBarInfo) => void;
  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;
  onOpenPanel: (projectId: string) => void;
  onOpenDemandClick: (demand: TimelineDemandEntry, anchorX: number, anchorY: number) => 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 ProjectBarInfo {
  projectId: string;
  projectName: string;
  startDate: Date;
  endDate: Date;
}

export interface OpenDemandAssignment {
  id: string;
  projectId: string;
  roleId: string | null;
  role: string | null;
  headcount: number;
  budgetCents?: number;
  startDate: Date;
  endDate: Date;
  hoursPerDay: number;
  roleEntity?: { id: string; name: string; color: string | null } | null;
  project?: { id: string; name: string; shortCode: string };
}

type HeatmapHoverState = HeatmapHoverData;

const EMPTY_DAY_METRICS: ProjectDayMetric[] = [];
const SVG_XMLNS = "http://www.w3.org/2000/svg";

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

function TimelineProjectPanelInner({
  scrollContainerRef,
  dragState,
  allocDragState,
  rangeState,
  onProjectBarMouseDown,
  onProjectBarTouchStart,
  onAllocMouseDown,
  onAllocTouchStart,
  onRowMouseDown,
  onRowTouchStart,
  onOpenPanel,
  onOpenDemandClick,
  onAllocationContextMenu,
  multiSelectState,
  optimisticAllocations,
  suppressHoverInteractions,
  CELL_WIDTH,
  dates,
  totalCanvasWidth,
  toLeft,
  toWidth,
  gridLines,
  xToDate,
}: TimelineProjectPanelProps) {
  const {
    projectGroups,
    openDemandsByProject,
    allocsByResource,
    vacationsByResource,
    filters,
    displayMode,
    heatmapScheme,
    blinkOverbookedDays,
    activeFilterCount,
    today,
  } = useTimelineContext();

  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 visualProjectGroups = useMemo(
    () => projectGroups.map((project) => ({
      ...project,
      resourceRows: project.resourceRows.map((row) => ({
        ...row,
        allocs: applyVisualOverrides(row.allocs, optimisticAllocations),
      })),
    })),
    [projectGroups, optimisticAllocations],
  );

  // ─── Heatmap hover (same mechanism as resource panel) ─────────────────────
  const heatmapRafRef = useRef<number | null>(null);
  const lastHeatmapDayRef = useRef<number>(-1);
  const lastHeatmapResourceRef = useRef<string | null>(null);
  const vacationHoverRafRef = useRef<number | null>(null);
  const hoveredVacationKeyRef = useRef<string | null>(null);
  const pendingHeatmapRef = useRef<{
    clientX: number;
    rect: DOMRect;
    resourceId: string;
  } | null>(null);
  const heatmapTooltipRef = useRef<HTMLDivElement | null>(null);
  const vacationTooltipRef = useRef<HTMLDivElement | null>(null);
  const demandTooltipRef = useRef<HTMLDivElement | null>(null);
  const heatmapTooltipPosRef = useRef({ left: 0, top: 0 });
  const vacationTooltipPosRef = useRef({ left: 0, top: 0 });
  const demandTooltipPosRef = useRef({ left: 0, top: 0 });

  const [heatmapHover, setHeatmapHover] = useState<HeatmapHoverState | null>(null);
  const [vacationHover, setVacationHover] = useState<VacationHoverData | null>(null);
  const [demandHover, setDemandHover] = useState<DemandHoverData | null>(null);

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

  const { resourceHeatmapById, resourceTotalHoursById } = useMemo(
    () => buildResourceHeatmapSeries(visualAllocsByResource, dates, resourceCapacityById),
    [dates, resourceCapacityById, visualAllocsByResource],
  );

  const vacationBlocksByResource = useMemo(
    () =>
      buildVacationBlocksByResource(
        vacationsByResource,
        filters.showVacations,
        toLeft,
        toWidth,
        CELL_WIDTH,
        totalCanvasWidth,
      ),
    [CELL_WIDTH, filters.showVacations, toLeft, toWidth, totalCanvasWidth, vacationsByResource],
  );

  const projectRowMetrics = useMemo(() => {
    return buildProjectRowMetrics(
      dates,
      visualProjectGroups,
      resourceTotalHoursById,
      resourceCapacityById,
    );
  }, [dates, resourceCapacityById, resourceTotalHoursById, visualProjectGroups]);

  const flatRows = useMemo(
    () => buildProjectFlatRows(visualProjectGroups, openDemandsByProject, optimisticAllocations),
    [openDemandsByProject, optimisticAllocations, visualProjectGroups],
  );

  const rowVirtualizer = useVirtualizer({
    count: flatRows.length,
    getScrollElement: () => scrollContainerRef.current,
    estimateSize: (index) => estimateProjectRowHeight(flatRows[index]),
    overscan: 8,
    getItemKey: (index) => flatRows[index]?.key ?? index,
  });
  const virtualItems = rowVirtualizer.getVirtualItems();
  const totalRowHeight = rowVirtualizer.getTotalSize();

  const resourcesWithVacations = useMemo(
    () => collectResourcesWithVacations(vacationsByResource),
    [vacationsByResource],
  );

  const handleRowHeatmapMove = useCallback(
    (e: React.MouseEvent, resourceId: string) => {
      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 &&
        resourceId === lastHeatmapResourceRef.current
      )
        return;

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

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

        const { clientX, rect: pendingRect, resourceId: pendingResourceId } = pending;
        const nextDayIndex = Math.floor((clientX - pendingRect.left) / CELL_WIDTH);
        const nextHeatmap = resourceHeatmapById.get(pendingResourceId)?.[nextDayIndex] ?? null;
        if (nextDayIndex < 0 || nextDayIndex >= dates.length) {
          lastHeatmapDayRef.current = -1;
          lastHeatmapResourceRef.current = null;
          startTransition(() => setHeatmapHover(null));
          return;
        }

        lastHeatmapDayRef.current = nextDayIndex;
        lastHeatmapResourceRef.current = pendingResourceId;

        startTransition(() => {
          setHeatmapHover(nextHeatmap);
        });
      });
    },
    [CELL_WIDTH, dates.length, resourceHeatmapById],
  );

  const handleRowVacationHover = useCallback(
    (e: React.MouseEvent, resourceId: string) => {
      if (!resourcesWithVacations.has(resourceId)) {
        if (hoveredVacationKeyRef.current !== null) {
          hoveredVacationKeyRef.current = null;
          startTransition(() => {
            setVacationHover(null);
          });
        }
        return;
      }

      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);
          });
        },
      });
    },
    [resourcesWithVacations, vacationsByResource, xToDate],
  );

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

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

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

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

  const handleDemandHoverMove = useCallback(
    (e: React.MouseEvent, demand: TimelineDemandEntry) => {
      updateTooltipPosition(demandTooltipPosRef, demandTooltipRef, e.clientX, e.clientY, 16, -36);

      startTransition(() => {
        setDemandHover(buildDemandHoverData(demand));
      });
    },
    [],
  );

  useEffect(
    () => () => {
      cancelHoverFrame(heatmapRafRef);
      cancelHoverFrame(vacationHoverRafRef);
    },
    [],
  );

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

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

  return (
    <div
      style={{
        height: totalRowHeight,
        position: "relative",
      }}
    >
      {virtualItems.map((virtualRow) => {
        const row = flatRows[virtualRow.index];
        if (!row) return null;

        return (
          <div
            key={row.key}
            data-index={virtualRow.index}
            ref={rowVirtualizer.measureElement}
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              width: "100%",
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            {row.type === "header" ? (
              (() => {
                const { project } = row;
                const customColor = project.color;
                const projectColor = getProjectColor(project.id);
                const colors = ORDER_TYPE_COLORS[project.orderType] ?? {
                  bg: "bg-gray-400",
                  text: "text-white",
                  light: "bg-gray-50 border-gray-200 dark:bg-gray-800 dark:border-gray-700",
                };
                const isThisProjectShifting =
                  dragState.isDragging && dragState.projectId === project.id;
                const projDispStart =
                  isThisProjectShifting && dragState.currentStartDate
                    ? dragState.currentStartDate
                    : project.startDate;
                const projDispEnd =
                  isThisProjectShifting && dragState.currentEndDate
                    ? dragState.currentEndDate
                    : project.endDate;
                const projLeft = toLeft(projDispStart);
                const projWidth = Math.max(CELL_WIDTH, toWidth(projDispStart, projDispEnd));

                return (
                  <div
                    data-project-group="true"
                    className={clsx("flex border-b border-gray-200 dark:border-gray-700 group/proj", colors.light)}
                    style={{ height: PROJECT_HEADER_HEIGHT, borderLeft: `4px solid ${customColor ?? projectColor.hex}` }}
                  >
                    <div
                      className={clsx(
                        "flex-shrink-0 border-r border-gray-300 dark:border-gray-600 flex items-center px-4 gap-2.5 sticky left-0 z-30 cursor-pointer",
                        colors.light,
                      )}
                      style={{ width: LABEL_WIDTH }}
                      onClick={() => onOpenPanel(project.id)}
                    >
                      <div
                        className="w-3 h-3 rounded-full flex-shrink-0"
                        style={{ backgroundColor: customColor ?? projectColor.hex }}
                      />
                      <div className="min-w-0">
                        <div className="text-sm font-semibold text-gray-800 dark:text-gray-100 truncate">
                          {project.name}
                        </div>
                        <div className="text-xs text-gray-400 dark:text-gray-500">
                          {project.status}
                        </div>
                      </div>
                    </div>

                    <div
                      className="relative overflow-hidden"
                      style={{ width: totalCanvasWidth, height: PROJECT_HEADER_HEIGHT }}
                    >
                      {gridLines}
                      {projWidth > 0 && projLeft < totalCanvasWidth && (
                        <div
                          data-timeline-entry-type="project-bar"
                          data-timeline-drag-preview="project-shift"
                          data-timeline-project-id={project.id}
                          className={clsx(
                            "absolute rounded flex items-center px-2 gap-1.5 text-white",
                            isThisProjectShifting
                              ? "opacity-90 shadow-lg ring-2 ring-white ring-offset-1 cursor-grabbing z-20 scale-[1.01]"
                              : "cursor-grab transition-[opacity,box-shadow] duration-75 hover:opacity-90 hover:ring-2 hover:ring-white hover:ring-offset-1",
                          )}
                          style={{
                            left: projLeft + 2,
                            width: projWidth - 4,
                            top: 8,
                            height: 24,
                            backgroundColor: customColor ?? projectColor.hex + "CC",
                            ...(isThisProjectShifting
                              ? {
                                  transform: `translateX(${getDragPointerOffset(
                                    dragState.pointerDeltaX,
                                    dragState.daysDelta,
                                    CELL_WIDTH,
                                  )}px)`,
                                }
                              : {}),
                          }}
                          onClick={() => {
                            if (!dragState.isDragging) onOpenPanel(project.id);
                          }}
                          onMouseDown={(e) => {
                            if (e.button === 2) {
                              e.preventDefault();
                              e.stopPropagation();
                              if (!dragState.isDragging) {
                                onOpenPanel(project.id);
                              }
                              return;
                            }
                            onProjectBarMouseDown(e, {
                              projectId: project.id,
                              projectName: project.name,
                              startDate: project.startDate,
                              endDate: project.endDate,
                            });
                          }}
                          onTouchStart={(e) =>
                            onProjectBarTouchStart(e, {
                              projectId: project.id,
                              projectName: project.name,
                              startDate: project.startDate,
                              endDate: project.endDate,
                            })
                          }
                          onContextMenu={(e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            if (!dragState.isDragging) {
                              onOpenPanel(project.id);
                            }
                          }}
                        >
                          <span className="text-xs font-semibold truncate">{project.name}</span>
                        </div>
                      )}
                    </div>
                  </div>
                );
              })()
            ) : row.type === "open-demand" ? (
              renderOpenDemandRow(
                row.openDemandCount,
                row.layout,
                row.projectId,
                CELL_WIDTH,
                totalCanvasWidth,
                toLeft,
                toWidth,
                gridLines,
                onOpenDemandClick,
                onAllocMouseDown,
                onAllocTouchStart,
                onAllocationContextMenu,
                handleDemandHoverMove,
                clearHoverTooltips,
                multiSelectState,
                allocDragState,
                suppressHoverInteractions,
              )
            ) : (
              <div
                data-project-resource-row="true"
                data-project-id={row.project.id}
                data-resource-id={row.resource.id}
                className="flex border-b border-gray-100 dark:border-gray-800 hover:bg-blue-50/20 dark:hover:bg-gray-800/30 group"
                style={{ height: ROW_HEIGHT }}
              >
                <div
                  className="flex-shrink-0 border-r border-gray-200 dark:border-gray-700 flex items-center pl-8 pr-4 gap-2 bg-white dark:bg-gray-900 sticky left-0 z-30 group-hover:bg-blue-50 dark:group-hover:bg-gray-800"
                  style={{ width: LABEL_WIDTH }}
                >
                  <div className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-[10px] font-bold text-gray-600 dark:text-gray-300 flex-shrink-0">
                    {row.resource.displayName.slice(0, 2).toUpperCase()}
                  </div>
                  <div className="min-w-0" data-resource-hover-id={row.resource.id}>
                    <div className="text-xs font-medium text-gray-800 dark:text-gray-200 truncate cursor-pointer hover:text-brand-600 dark:hover:text-brand-400 transition-colors">
                      {row.resource.displayName}
                    </div>
                    <div className="text-[10px] text-gray-400 dark:text-gray-500 truncate">{row.resource.eid}</div>
                  </div>
                </div>

                <div
                  data-testid="timeline-project-resource-row-canvas"
                  data-project-id={row.project.id}
                  data-resource-id={row.resource.id}
                  className="relative overflow-hidden touch-none"
                  style={{
                    width: totalCanvasWidth,
                    height: ROW_HEIGHT,
                    touchAction: "none",
                  }}
                  onMouseDown={(e) => {
                    const rect = e.currentTarget.getBoundingClientRect();
                    const date = xToDate(e.clientX, rect);
                    onRowMouseDown(e, {
                      resourceId: row.resource.id,
                      startDate: date,
                      suggestedProjectId: row.project.id,
                    });
                  }}
                  onTouchStart={(e) => {
                    const rect = e.currentTarget.getBoundingClientRect();
                    const date = xToDate(e.touches[0]?.clientX ?? 0, rect);
                    onRowTouchStart(e, {
                      resourceId: row.resource.id,
                      startDate: date,
                      suggestedProjectId: row.project.id,
                    });
                  }}
                  onMouseMove={(e) => {
                    if (suppressHoverInteractions) return;
                    handleRowHeatmapMove(e, row.resource.id);
                    handleRowVacationHover(e, row.resource.id);
                  }}
                  onMouseLeave={clearHoverTooltips}
                >
                  {gridLines}
                  {renderProjectUtilOverlay(
                    projectRowMetrics.get(row.metricsKey) ?? EMPTY_DAY_METRICS,
                    CELL_WIDTH,
                    displayMode,
                    heatmapScheme,
                    totalCanvasWidth,
                  )}
                  {renderProjectDragHandles(
                    row.allocs,
                    allocDragState,
                    toLeft,
                    toWidth,
                    CELL_WIDTH,
                    totalCanvasWidth,
                    onAllocMouseDown,
                    onAllocTouchStart,
                    onAllocationContextMenu,
                    multiSelectState,
                    suppressHoverInteractions,
                  )}
                  {filters.showVacations &&
                    renderVacationBlocks(
                      vacationBlocksByResource.get(row.resource.id) ?? [],
                      ROW_HEIGHT,
                    )}
                  {blinkOverbookedDays &&
                    renderOverbookingBlink(
                      visualAllocsByResource.get(row.resource.id) ?? [],
                      dates,
                      CELL_WIDTH,
                      resourceCapacityById.get(row.resource.id)?.capacityHoursByDay,
                      resourceCapacityById.get(row.resource.id)?.bookingFactorsByDay,
                    )}
                  {renderRangeOverlay(
                    rangeState,
                    row.resource.id,
                    ROW_HEIGHT,
                    toLeft,
                    toWidth,
                    CELL_WIDTH,
                  )}
                </div>
              </div>
            )}
          </div>
        );
      })}

      <TimelineTooltip
        heatmapTooltipRef={heatmapTooltipRef}
        heatmapTooltipPos={heatmapTooltipPosRef.current}
        vacationTooltipRef={vacationTooltipRef}
        vacationTooltipPos={vacationTooltipPosRef.current}
        demandTooltipRef={demandTooltipRef}
        demandTooltipPos={demandTooltipPosRef.current}
        heatmapHover={heatmapHover}
        vacationHover={vacationHover}
        demandHover={demandHover}
      />
    </div>
  );
}

// ProjectPanelTooltips removed — now uses shared TimelineTooltip component

// ─── Pure render functions ──────────────────────────────────────────────────

function renderOpenDemandRow(
  openDemandCount: number,
  layout: OpenDemandRowLayout,
  projectId: string,
  CELL_WIDTH: number,
  totalCanvasWidth: number,
  toLeft: (d: Date) => number,
  toWidth: (s: Date, e: Date) => number,
  rowGridLines: React.ReactNode,
  onOpenDemandClick: (demand: TimelineDemandEntry, anchorX: number, anchorY: number) => void,
  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,
  onDemandHoverMove: (e: React.MouseEvent, demand: TimelineDemandEntry) => void,
  onClearHoverTooltips: () => void,
  multiSelectState: MultiSelectState,
  allocDragState: AllocDragState,
  suppressHoverInteractions: boolean,
) {
  const { visibleOpenDemands, laneMap, rowHeight } = layout;
  if (visibleOpenDemands.length === 0) return null;

  return (
    <div
      data-project-demand-row="true"
      data-project-id={projectId}
      className="group relative isolate flex border-b border-dashed border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-slate-950 hover:bg-amber-100/80 dark:hover:bg-slate-900"
      style={{ height: rowHeight }}
    >
      <div
        className="sticky left-0 z-30 flex flex-shrink-0 items-center gap-2 border-r border-amber-200 bg-amber-50 pl-8 pr-4 dark:border-amber-800 dark:bg-slate-950"
        style={{ width: LABEL_WIDTH, height: rowHeight }}
      >
        <div className="pointer-events-none absolute inset-0 bg-amber-50 dark:bg-slate-950" />
        <div className="relative z-10 flex items-center gap-2 min-w-0">
          <div className="w-6 h-6 rounded-full bg-amber-100 dark:bg-amber-900/50 flex items-center justify-center text-[10px] font-bold text-amber-600 dark:text-amber-400 flex-shrink-0 border border-dashed border-amber-400 dark:border-amber-600">
            ?
          </div>
          <div className="min-w-0">
            <div className="text-xs font-medium text-amber-700 dark:text-amber-400 truncate">Open demand</div>
            <div className="text-[10px] text-amber-500 dark:text-amber-600 truncate">
              {openDemandCount} open demand{openDemandCount > 1 ? "s" : ""}
            </div>
          </div>
        </div>
      </div>

      <div
        className="relative overflow-hidden bg-amber-50 touch-none dark:bg-slate-950"
        style={{ width: totalCanvasWidth, height: rowHeight }}
        onMouseLeave={onClearHoverTooltips}
      >
        {rowGridLines}
        <div className="pointer-events-none absolute inset-x-0 inset-y-0 border-y border-dashed border-amber-200/70 dark:border-amber-800/80" />
        <div className="pointer-events-none absolute inset-x-0 inset-y-1 rounded-md bg-amber-100/25 dark:bg-amber-950/35" />
        {visibleOpenDemands.map((alloc) => {
          const allocStart = new Date(alloc.startDate);
          const allocEnd = new Date(alloc.endDate);

          const isAllocDragged = allocDragState.isActive && allocDragState.allocationId === alloc.id;
          const dispStart =
            isAllocDragged && allocDragState.currentStartDate
              ? allocDragState.currentStartDate
              : allocStart;
          const dispEnd =
            isAllocDragged && allocDragState.currentEndDate ? allocDragState.currentEndDate : allocEnd;

          // Multi-drag visual offset
          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));
          let dragTransform: string | undefined;

          if (isAllocDragged) {
            const preview = applyPointerOffsetPreviewRect({
              left,
              width,
              mode: allocDragState.mode,
              pointerOffsetX: getDragPointerOffset(
                allocDragState.pointerDeltaX,
                allocDragState.daysDelta,
                CELL_WIDTH,
              ),
              minWidth: CELL_WIDTH,
            });
            left = preview.left;
            width = preview.width;
            dragTransform = preview.transform;
          }

          // Clamp negative left (bar starts before view) to avoid extending outside canvas.
          if (left < 0) {
            width += left;
            left = 0;
          }
          if (width <= 0 || left >= totalCanvasWidth) return null;

          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);
          }

          const roleEntity = (
            alloc as { roleEntity?: { id: string; name: string; color: string | null } | null }
          ).roleEntity;
          const roleName =
            roleEntity?.name ?? (alloc as { role?: string | null }).role ?? "Open demand";
          const roleColor = roleEntity?.color ?? "#f59e0b";
          const headcount = (alloc as { headcount?: number }).headcount ?? 1;
          const lane = laneMap.get(alloc.id) ?? 0;
          const top = 8 + lane * SUB_LANE_HEIGHT;
          const blockHeight = SUB_LANE_HEIGHT - 8;

          const HANDLE_W = width >= 48 ? 8 : 6;

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

          return (
            <div
              key={alloc.id}
              data-allocation-id={alloc.id}
              data-timeline-entry-type="demand"
              data-timeline-drag-preview="project-shift allocation"
              data-timeline-project-id={alloc.projectId}
              className={clsx(
                "absolute rounded-md flex items-stretch overflow-hidden z-[10] group/demand",
                isAllocDragged
                  ? "ring-2 ring-amber-500 z-20"
                  : "hover:ring-2 hover:ring-amber-400 hover:ring-offset-1",
                multiSelectState.selectedAllocationIds.includes(alloc.id) && "ring-2 ring-sky-500 ring-offset-1 z-20",
              )}
              style={{
                left: left + 2,
                width: width - 4,
                top,
                height: blockHeight,
                backgroundColor: `${roleColor}4D`,
                border: `2px dashed ${roleColor}B3`,
                ...((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 },
                  e.clientX,
                  e.clientY,
                );
              }}
              onMouseMove={(e) => {
                if (suppressHoverInteractions) return;
                onDemandHoverMove(e, alloc);
              }}
              onClick={(e) => {
                e.stopPropagation();
                if (suppressHoverInteractions) return;
                onOpenDemandClick(alloc, e.clientX, e.clientY);
              }}
              onKeyDown={(e) => {
                if (e.key !== "Enter" && e.key !== " ") {
                  return;
                }
                e.preventDefault();
                e.stopPropagation();
                if (suppressHoverInteractions) return;
                const rect = e.currentTarget.getBoundingClientRect();
                onOpenDemandClick(alloc, rect.left + rect.width / 2, rect.top + rect.height / 2);
              }}
              role="button"
              tabIndex={0}
              aria-label={`Open demand details for ${roleName} on ${alloc.project.name}`}
            >
              {/* Left resize handle */}
              <div
                className="flex-shrink-0 cursor-ew-resize hover:bg-black/10"
                style={{ width: HANDLE_W }}
                onMouseDown={(e) => onAllocMouseDown(e, { ...allocInfo, mode: "resize-start" })}
                onTouchStart={(e) => {
                  e.stopPropagation();
                  onAllocTouchStart(e, { ...allocInfo, mode: "resize-start" });
                }}
              />

              {/* Center — move + click */}
              <div
                className={clsx(
                  "flex-1 min-w-0 flex items-center px-1 gap-1",
                  isAllocDragged ? "cursor-grabbing" : "cursor-grab",
                )}
                onMouseDown={(e) => {
                  e.stopPropagation();
                  onAllocMouseDown(e, allocInfo);
                }}
                onTouchStart={(e) => {
                  e.stopPropagation();
                  onAllocTouchStart(e, allocInfo);
                }}
              >
                <span className="text-xs font-medium truncate" style={{ color: roleColor }}>
                  {roleName}
                  {headcount > 1 ? ` x${headcount}` : ""}
                </span>
              </div>

              {/* Right resize handle */}
              <div
                className="flex-shrink-0 cursor-ew-resize hover:bg-black/10"
                style={{ width: HANDLE_W }}
                onMouseDown={(e) => onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" })}
                onTouchStart={(e) => {
                  e.stopPropagation();
                  onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" });
                }}
              />
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ─── Project-view: per-resource utilisation band ────────────────────────────

function renderProjectUtilOverlay(
  dayMetrics: ProjectDayMetric[],
  CELL_WIDTH: number,
  displayMode: string,
  heatmapScheme: string,
  totalCanvasWidth: number,
) {
  if (dayMetrics.length === 0 || totalCanvasWidth <= 0) return null;

  const BAND_H = 7;
  const BAR_H = ROW_HEIGHT - BAND_H - 11;
  const useHeatmapColors = displayMode === "bar";
  const svgParts: string[] = [
    `<svg xmlns="${SVG_XMLNS}" width="${totalCanvasWidth}" height="${ROW_HEIGHT}" viewBox="0 0 ${totalCanvasWidth} ${ROW_HEIGHT}" preserveAspectRatio="none" shape-rendering="crispEdges">`,
  ];

  dayMetrics.forEach(({ projH, totalH, capacityH }, i) => {
    if ((totalH === 0 && projH === 0) || capacityH <= 0) return;

    const isOver = totalH > capacityH;
    const totalBarH = Math.max(
      projH > 0 ? 2 : 0,
      Math.round((Math.min(totalH, capacityH) / capacityH) * BAR_H),
    );
    const projBarH =
      projH > 0 ? Math.min(totalBarH, Math.max(2, Math.round((projH / capacityH) * BAR_H))) : 0;
    const otherBarH = totalBarH - projBarH;
    const projPct = (projH / capacityH) * 100;
    const totalPct = (totalH / capacityH) * 100;
    const projColor = useHeatmapColors
      ? heatmapColor(
          projPct,
          heatmapScheme as import("~/hooks/useAppPreferences.js").HeatmapColorScheme,
          "bar",
        ) ?? "rgba(59,130,246,0.8)"
      : "rgba(96,165,250,0.8)";
    const totalColor = useHeatmapColors
      ? heatmapColor(
          totalPct,
          heatmapScheme as import("~/hooks/useAppPreferences.js").HeatmapColorScheme,
          "bar",
        ) ?? "rgba(156,163,175,0.5)"
      : isOver
        ? "rgba(252,211,77,0.8)"
        : "rgba(209,213,219,0.8)";
    const xBand = i * CELL_WIDTH + 1;
    const xBar = i * CELL_WIDTH + 3;
    const bandWidth = Math.max(CELL_WIDTH - 2, 0);
    const barWidth = Math.max(CELL_WIDTH - 6, 0);

    if (projH > 0 && bandWidth > 0) {
      svgParts.push(
        `<rect x="${xBand}" y="6" width="${bandWidth}" height="${BAND_H}" fill="${projColor}" />`,
      );
    }

    if (otherBarH > 0 && barWidth > 0) {
      svgParts.push(
        `<rect x="${xBar}" y="${ROW_HEIGHT - 4 - projBarH - otherBarH}" width="${barWidth}" height="${otherBarH}" fill="${totalColor}" />`,
      );
    }

    if (projBarH > 0 && barWidth > 0) {
      svgParts.push(
        `<rect x="${xBar}" y="${ROW_HEIGHT - 4 - projBarH}" width="${barWidth}" height="${projBarH}" fill="${projColor}" />`,
      );
    }

    if (isOver && totalBarH > 0 && barWidth > 0) {
      svgParts.push(
        `<rect x="${xBar}" y="${ROW_HEIGHT - 4 - totalBarH - 3}" width="${barWidth}" height="3" fill="rgb(239,68,68)" />`,
      );
    }
  });

  svgParts.push("</svg>");
  const svgDataUri = `url("data:image/svg+xml;utf8,${encodeURIComponent(svgParts.join(""))}")`;

  return (
    <div
      data-project-util-bar="true"
      className="absolute inset-0 pointer-events-none"
      style={{
        backgroundImage: svgDataUri,
        backgroundPosition: "0 0",
        backgroundRepeat: "no-repeat",
      }}
    />
  );
}

// ─── Project-view: transparent drag handles ─────────────────────────────────

function renderProjectDragHandles(
  allocs: TimelineAssignmentEntry[],
  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,
) {
  return allocs.map((alloc) => {
    const allocStart = new Date(alloc.startDate);
    const allocEnd = new Date(alloc.endDate);

    const isAllocDragged = allocDragState.isActive && allocDragState.allocationId === alloc.id;
    const dispStart =
      isAllocDragged && allocDragState.currentStartDate
        ? allocDragState.currentStartDate
        : allocStart;
    const dispEnd =
      isAllocDragged && allocDragState.currentEndDate ? allocDragState.currentEndDate : allocEnd;

    let left = toLeft(dispStart);
    let width = Math.max(CELL_WIDTH, toWidth(dispStart, dispEnd));
    let dragTransform: string | undefined;

    if (isAllocDragged) {
      const preview = applyPointerOffsetPreviewRect({
        left,
        width,
        mode: allocDragState.mode,
        pointerOffsetX: getDragPointerOffset(
          allocDragState.pointerDeltaX,
          allocDragState.daysDelta,
          CELL_WIDTH,
        ),
        minWidth: CELL_WIDTH,
      });
      left = preview.left;
      width = preview.width;
      dragTransform = preview.transform;
    }
    if (width <= 0 || left >= totalCanvasWidth) return null;

    // Multi-drag visual offset
    const isMultiDragTarget =
      multiSelectState.isMultiDragging &&
      multiSelectState.selectedAllocationIds.includes(alloc.id);
    const multiDragPx = isMultiDragTarget ? multiSelectState.multiDragDaysDelta * CELL_WIDTH : 0;
    const multiDragMode = multiSelectState.multiDragMode;

    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);
    }

    // Always show resize handles — for narrow bars, use overlapping handles
    const HANDLE_W = width >= 48 ? 8 : 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={`dh-${alloc.id}`}
        data-allocation-id={alloc.id}
        data-timeline-entry-type="allocation"
        data-timeline-drag-preview="project-shift allocation"
        data-timeline-project-id={alloc.projectId}
        className={clsx(
          "absolute flex items-stretch rounded",
          hasRecurrence && "border-2 border-dashed border-brand-400/60",
          isAllocDragged
            ? "ring-2 ring-brand-400 z-20"
            : "hover:ring-1 hover:ring-brand-300/70 z-[15]",
          multiSelectState.selectedAllocationIds.includes(alloc.id) && "ring-2 ring-sky-500 ring-offset-1 z-20",
        )}
        style={{
          left: left + 2,
          width: width - 4,
          top: 2,
          bottom: 2,
          ...((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: getPlanningEntryMutationId(alloc),
              projectId: alloc.projectId,
            },
            e.clientX,
            e.clientY,
          );
        }}
      >
        <div
          className="flex-shrink-0 cursor-ew-resize"
          style={{ width: HANDLE_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 flex items-center",
            isAllocDragged ? "cursor-grabbing" : "cursor-grab",
          )}
          onMouseDown={(e) => {
            e.stopPropagation();
            onAllocMouseDown(e, allocInfo);
          }}
          onTouchStart={(e) => {
            e.stopPropagation();
            onAllocTouchStart(e, allocInfo);
          }}
        >
          {hasRecurrence && width > 28 && (
            <span className="text-[10px] text-brand-600 opacity-70 pointer-events-none pl-1">
              ↻
            </span>
          )}
        </div>
        <div
          className="flex-shrink-0 cursor-ew-resize"
          style={{ width: HANDLE_W }}
          onMouseDown={(e) => {
            e.stopPropagation();
            onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" });
          }}
          onTouchStart={(e) => {
            e.stopPropagation();
            onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" });
          }}
        />
      </div>
    );
  });
}

export const TimelineProjectPanel = memo(TimelineProjectPanelInner);
