"use client";

import { clsx } from "clsx";
import { 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 { heatmapColor } from "./heatmapUtils.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import { formatDateLong } from "~/lib/format.js";
import {
  ROW_HEIGHT,
  SUB_LANE_HEIGHT,
  LABEL_WIDTH,
  PROJECT_HEADER_HEIGHT,
  ORDER_TYPE_COLORS,
} from "./timelineConstants.js";
import type { DragState, AllocDragState, RangeState } from "~/hooks/useTimelineDrag.js";
import type { AllocMouseDownInfo, RowMouseDownInfo } from "./TimelineResourcePanel.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: OpenDemandAssignment) => void;
  onAllocationContextMenu: (
    info: { allocationId: string; projectId: string },
    anchorX: number,
    anchorY: number,
  ) => void;
  // 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 HeatmapBreakdownEntry = {
  projectId: string;
  shortCode: string;
  projectName: string;
  orderType: string;
  hoursPerDay: number;
  responsiblePerson?: string | null;
};

type HeatmapHoverState = {
  date: Date;
  totalH: number;
  pct: number;
  breakdown: HeatmapBreakdownEntry[];
};

type ProjectDayMetric = {
  projH: number;
  totalH: number;
};

type HeatmapBreakdownAccumulator = {
  shortCode: string;
  projectName: string;
  orderType: string;
  responsiblePerson: string | null;
  hours: number;
};

type ProjectFlatRow =
  | {
      type: "header";
      key: string;
      project: NonNullable<ReturnType<typeof useTimelineContext>["projectGroups"]>[number];
    }
  | {
      type: "open-demand";
      key: string;
      projectId: string;
      openDemands: TimelineDemandEntry[];
    }
  | {
      type: "resource";
      key: string;
      project: NonNullable<ReturnType<typeof useTimelineContext>["projectGroups"]>[number];
      resource: NonNullable<
        ReturnType<typeof useTimelineContext>["projectGroups"]
      >[number]["resourceRows"][number]["resource"];
      allocs: TimelineAssignmentEntry[];
      metricsKey: string;
    };

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

function buildProjectRowGridBackground(dates: Date[], CELL_WIDTH: number, today: Date) {
  const gradientLayers: string[] = [
    `repeating-linear-gradient(to right, transparent 0, transparent ${Math.max(
      CELL_WIDTH - 1,
      0,
    )}px, rgba(229, 231, 235, 1) ${Math.max(CELL_WIDTH - 1, 0)}px, rgba(229, 231, 235, 1) ${CELL_WIDTH}px)`,
  ];

  dates.forEach((date, index) => {
    const left = index * CELL_WIDTH;
    const right = left + CELL_WIDTH;
    const isToday = date.toDateString() === today.toDateString();
    const isSaturday = date.getDay() === 6;
    const isSunday = date.getDay() === 0;

    if (isSaturday) {
      gradientLayers.push(
        `linear-gradient(to right, transparent ${left}px, rgba(254, 243, 199, 0.4) ${left}px, rgba(254, 243, 199, 0.4) ${right}px, transparent ${right}px)`,
      );
    } else if (isSunday) {
      gradientLayers.push(
        `linear-gradient(to right, transparent ${left}px, rgba(243, 244, 246, 0.6) ${left}px, rgba(243, 244, 246, 0.6) ${right}px, transparent ${right}px)`,
      );
    }

    if (isToday) {
      gradientLayers.push(
        `linear-gradient(to right, transparent ${left}px, rgba(110, 231, 183, 0.95) ${left}px, rgba(110, 231, 183, 0.95) ${Math.min(
          left + 2,
          right,
        )}px, transparent ${Math.min(left + 2, right)}px)`,
      );
    }
  });

  return {
    backgroundImage: gradientLayers.join(", "),
    backgroundRepeat: "no-repeat",
  } as const;
}

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

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

  // ─── 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 heatmapTooltipPosRef = useRef({ left: 0, top: 0 });
  const vacationTooltipPosRef = useRef({ left: 0, top: 0 });

  const [heatmapHover, setHeatmapHover] = useState<{
    date: Date;
    totalH: number;
    pct: number;
    breakdown: HeatmapBreakdownEntry[];
  } | 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);

  const { resourceHeatmapById, resourceTotalHoursById } = useMemo(() => {
    const dateIndexByTime = new Map<number, number>();
    dates.forEach((date, index) => {
      const normalized = new Date(date);
      normalized.setHours(0, 0, 0, 0);
      dateIndexByTime.set(normalized.getTime(), index);
    });

    const nextHeatmapById = new Map<string, (HeatmapHoverState | null)[]>();
    const nextTotalHoursById = new Map<string, number[]>();

    for (const [resourceId, allocs] of allocsByResource) {
      if (allocs.length === 0) continue;

      const totalHours = new Array<number>(dates.length).fill(0);
      const breakdownMaps = Array.from({ length: dates.length }, () => new Map<string, HeatmapBreakdownAccumulator>());

      for (const alloc of allocs) {
        const current = new Date(alloc.startDate);
        current.setHours(0, 0, 0, 0);
        const end = new Date(alloc.endDate);
        end.setHours(0, 0, 0, 0);

        while (current.getTime() <= end.getTime()) {
          const dayIndex = dateIndexByTime.get(current.getTime());
          if (dayIndex !== undefined) {
            totalHours[dayIndex] = (totalHours[dayIndex] ?? 0) + alloc.hoursPerDay;

            const dayBreakdown = breakdownMaps[dayIndex];
            if (!dayBreakdown) {
              current.setDate(current.getDate() + 1);
              continue;
            }

            const existing = dayBreakdown.get(alloc.projectId);
            if (existing) {
              existing.hours += alloc.hoursPerDay;
            } else {
              dayBreakdown.set(alloc.projectId, {
                shortCode: alloc.project.shortCode,
                projectName: alloc.project.name,
                orderType: alloc.project.orderType,
                responsiblePerson:
                  (alloc.project as { responsiblePerson?: string | null }).responsiblePerson ??
                  null,
                hours: alloc.hoursPerDay,
              });
            }
          }
          current.setDate(current.getDate() + 1);
        }
      }

      nextTotalHoursById.set(resourceId, totalHours);
      nextHeatmapById.set(
        resourceId,
        totalHours.map((totalH, dayIndex) => {
          if (totalH === 0) return null;

          const dayBreakdown = breakdownMaps[dayIndex];
          if (!dayBreakdown) return null;

          const breakdown: HeatmapBreakdownEntry[] = [...dayBreakdown.entries()]
            .map(([projectId, value]) => ({
              projectId,
              shortCode: value.shortCode,
              projectName: value.projectName,
              orderType: value.orderType,
              responsiblePerson: value.responsiblePerson,
              hoursPerDay: value.hours,
            }))
            .sort((a, b) => b.hoursPerDay - a.hoursPerDay);

          return {
            date: dates[dayIndex] ?? new Date(),
            totalH,
            pct: (totalH / 8) * 100,
            breakdown,
          };
        }),
      );
    }

    return {
      resourceHeatmapById: nextHeatmapById,
      resourceTotalHoursById: nextTotalHoursById,
    };
  }, [allocsByResource, dates]);

  const projectRowMetrics = useMemo(() => {
    const dateIndexByTime = new Map<number, number>();
    dates.forEach((date, index) => {
      const normalized = new Date(date);
      normalized.setHours(0, 0, 0, 0);
      dateIndexByTime.set(normalized.getTime(), index);
    });

    const nextMetrics = new Map<string, ProjectDayMetric[]>();

    for (const project of projectGroups) {
      for (const { resource, allocs } of project.resourceRows) {
        const projectHours = new Array<number>(dates.length).fill(0);

        for (const alloc of allocs) {
          const current = new Date(alloc.startDate);
          current.setHours(0, 0, 0, 0);
          const end = new Date(alloc.endDate);
          end.setHours(0, 0, 0, 0);

        while (current.getTime() <= end.getTime()) {
          const dayIndex = dateIndexByTime.get(current.getTime());
          if (dayIndex !== undefined) {
            projectHours[dayIndex] = (projectHours[dayIndex] ?? 0) + alloc.hoursPerDay;
          }
          current.setDate(current.getDate() + 1);
        }
        }

        const totalHours = resourceTotalHoursById.get(resource.id);
        nextMetrics.set(
          `${project.id}:${resource.id}`,
          projectHours.map((projH, dayIndex) => ({
            projH,
            totalH: totalHours?.[dayIndex] ?? 0,
          })),
        );
      }
    }

    return nextMetrics;
  }, [dates, projectGroups, resourceTotalHoursById]);

  const flatRows = useMemo(() => {
    const rows: ProjectFlatRow[] = [];

    for (const project of projectGroups) {
      rows.push({ type: "header", key: `header-${project.id}`, project });

      const openDemands = openDemandsByProject.get(project.id) ?? [];
      if (openDemands.length > 0) {
        rows.push({
          type: "open-demand",
          key: `open-demand-${project.id}`,
          projectId: project.id,
          openDemands,
        });
      }

      for (const { resource, allocs } of project.resourceRows) {
        rows.push({
          type: "resource",
          key: `${project.id}-${resource.id}`,
          project,
          resource,
          allocs,
          metricsKey: `${project.id}:${resource.id}`,
        });
      }
    }

    return rows;
  }, [openDemandsByProject, projectGroups]);

  const rowVirtualizer = useVirtualizer({
    count: flatRows.length,
    getScrollElement: () => scrollContainerRef.current,
    estimateSize: (index) => {
      const row = flatRows[index];
      if (!row) return ROW_HEIGHT;
      if (row.type === "header") return PROJECT_HEADER_HEIGHT;
      if (row.type === "open-demand") {
        const laneCount = assignDemandLanes(row.openDemands).size > 0
          ? Math.max(...assignDemandLanes(row.openDemands).values()) + 1
          : 1;
        return Math.max(ROW_HEIGHT, laneCount * (DEMAND_LANE_HEIGHT + DEMAND_LANE_GAP) + 8);
      }
      return ROW_HEIGHT;
    },
    overscan: 8,
    getItemKey: (index) => flatRows[index]?.key ?? index,
  });
  const virtualItems = rowVirtualizer.getVirtualItems();
  const totalRowHeight = rowVirtualizer.getTotalSize();

  const resourceRowGridStyle = useMemo(
    () => buildProjectRowGridBackground(dates, CELL_WIDTH, today),
    [CELL_WIDTH, dates, today],
  );

  const resourcesWithVacations = useMemo(() => {
    const result = new Set<string>();
    for (const [resourceId, vacations] of vacationsByResource) {
      if (vacations.length > 0) {
        result.add(resourceId);
      }
    }
    return result;
  }, [vacationsByResource]);

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

      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);
        const time = date.getTime();
        const resourceVacations = vacationsByResource.get(resourceId) ?? [];
        const hit =
          resourceVacations.find((vacation) => {
            const start = new Date(vacation.startDate);
            start.setHours(0, 0, 0, 0);
            const end = new Date(vacation.endDate);
            end.setHours(0, 0, 0, 0);
            return time >= start.getTime() && time <= end.getTime();
          }) ?? null;

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

        hoveredVacationKeyRef.current = nextKey;
        startTransition(() => {
          setVacationHover(hit);
        });
      });
    },
    [resourcesWithVacations, 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;
    lastHeatmapResourceRef.current = null;
    hoveredVacationKeyRef.current = null;

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

  useEffect(
    () => () => {
      if (heatmapRafRef.current !== null) cancelAnimationFrame(heatmapRafRef.current);
      if (vacationHoverRafRef.current !== null) cancelAnimationFrame(vacationHoverRafRef.current);
    },
    [],
  );

  if (projectGroups.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 colors = ORDER_TYPE_COLORS[project.orderType] ?? {
                  bg: "bg-gray-400",
                  text: "text-white",
                  light: "bg-gray-50 border-gray-200",
                };
                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 group/proj", colors.light)}
                    style={{ height: PROJECT_HEADER_HEIGHT }}
                  >
                    <div
                      className={clsx(
                        "flex-shrink-0 border-r border-gray-300 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="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
                          className={clsx(
                            "absolute rounded flex items-center px-2 gap-1.5 transition-all duration-75",
                            isThisProjectShifting
                              ? "opacity-90 shadow-lg ring-2 ring-white ring-offset-1 cursor-grabbing z-20 scale-[1.01]"
                              : "cursor-grab hover:opacity-90 hover:ring-2 hover:ring-white hover:ring-offset-1",
                            !customColor && colors.bg,
                            customColor ? "text-white" : colors.text,
                          )}
                          style={{
                            left: projLeft + 2,
                            width: projWidth - 4,
                            top: 8,
                            height: 24,
                            ...(customColor ? { backgroundColor: customColor } : {}),
                          }}
                          onClick={() => {
                            if (!dragState.isDragging) onOpenPanel(project.id);
                          }}
                          onMouseDown={(e) =>
                            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()}
                        >
                          <span className="text-xs font-semibold truncate">{project.name}</span>
                        </div>
                      )}
                    </div>
                  </div>
                );
              })()
            ) : row.type === "open-demand" ? (
              renderOpenDemandRow(
                row.openDemands,
                CELL_WIDTH,
                totalCanvasWidth,
                toLeft,
                toWidth,
                resourceRowGridStyle,
                onOpenDemandClick,
              )
            ) : (
              <div
                data-project-resource-row="true"
                className="flex border-b border-gray-100 hover:bg-blue-50/20 group"
                style={{ height: ROW_HEIGHT }}
              >
                <div
                  className="flex-shrink-0 border-r border-gray-200 flex items-center pl-8 pr-4 gap-2 bg-white sticky left-0 z-30 group-hover:bg-blue-50"
                  style={{ width: LABEL_WIDTH }}
                >
                  <div className="w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center text-[10px] font-bold text-gray-600 flex-shrink-0">
                    {row.resource.displayName.slice(0, 2).toUpperCase()}
                  </div>
                  <div className="min-w-0">
                    <div className="text-xs font-medium text-gray-800 truncate">
                      {row.resource.displayName}
                    </div>
                    <div className="text-[10px] text-gray-400 truncate">{row.resource.eid}</div>
                  </div>
                </div>

                <div
                  className="relative overflow-hidden touch-none"
                  style={{
                    width: totalCanvasWidth,
                    height: ROW_HEIGHT,
                    touchAction: "none",
                    ...resourceRowGridStyle,
                  }}
                  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) => {
                    handleRowHeatmapMove(e, row.resource.id);
                    handleRowVacationHover(e, row.resource.id);
                  }}
                  onMouseLeave={clearHoverTooltips}
                >
                  {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,
                  )}
                  {renderVacationBlocksForProjectRow(
                    vacationsByResource.get(row.resource.id) ?? [],
                    ROW_HEIGHT,
                    toLeft,
                    toWidth,
                    CELL_WIDTH,
                    totalCanvasWidth,
                    filters.showVacations,
                  )}
                  {renderRangeOverlayProject(
                    rangeState,
                    row.resource.id,
                    ROW_HEIGHT,
                    toLeft,
                    toWidth,
                    CELL_WIDTH,
                  )}
                </div>
              </div>
            )}
          </div>
        );
      })}

      <ProjectPanelTooltips
        heatmapTooltipRef={heatmapTooltipRef}
        heatmapTooltipPos={heatmapTooltipPosRef.current}
        vacationTooltipRef={vacationTooltipRef}
        vacationTooltipPos={vacationTooltipPosRef.current}
        heatmapHover={heatmapHover}
        vacationHover={vacationHover}
      />
    </div>
  );
}

function ProjectPanelTooltips({
  heatmapTooltipRef,
  heatmapTooltipPos,
  vacationTooltipRef,
  vacationTooltipPos,
  heatmapHover,
  vacationHover,
}: {
  heatmapTooltipRef: React.RefObject<HTMLDivElement | null>;
  heatmapTooltipPos: { left: number; top: number };
  vacationTooltipRef: React.RefObject<HTMLDivElement | null>;
  vacationTooltipPos: { left: number; top: number };
  heatmapHover: {
    date: Date;
    totalH: number;
    pct: number;
    breakdown: {
      projectId: string;
      shortCode: string;
      projectName: string;
      orderType: string;
      hoursPerDay: number;
      responsiblePerson?: string | null;
    }[];
  } | null;
  vacationHover: {
    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;
}) {
  return (
    <>
      {heatmapHover ? (
        <div
          ref={heatmapTooltipRef}
          style={{
            left: heatmapTooltipPos.left,
            top: heatmapTooltipPos.top,
            backgroundColor: "rgba(3, 7, 18, 0.96)",
          }}
          className="fixed z-40 max-w-sm pointer-events-none rounded-xl border border-gray-800 bg-gray-950/96 px-3 py-2 text-xs text-white shadow-2xl"
        >
          <div className="flex items-center justify-between gap-3">
            <span className="font-semibold">{formatDateLong(heatmapHover.date)}</span>
            <span className="text-[11px] text-gray-300">
              {heatmapHover.totalH.toFixed(1)}h · {Math.round(heatmapHover.pct)}%
            </span>
          </div>
          <div className="mt-2 space-y-1.5">
            {heatmapHover.breakdown.length > 0 ? (
              heatmapHover.breakdown.slice(0, 6).map((entry) => (
                <div
                  key={`${entry.projectId}-${entry.shortCode}`}
                  className="flex items-start justify-between gap-3"
                >
                  <div className="min-w-0">
                    <div className="truncate font-medium text-white">
                      {entry.shortCode ? `${entry.shortCode} · ` : ""}
                      {entry.projectName}
                    </div>
                    <div className="truncate text-[11px] text-gray-400">
                      {entry.responsiblePerson
                        ? `Lead: ${entry.responsiblePerson}`
                        : entry.orderType}
                    </div>
                  </div>
                  <span className="whitespace-nowrap text-[11px] font-semibold text-gray-200">
                    {entry.hoursPerDay}h
                  </span>
                </div>
              ))
            ) : (
              <div className="text-[11px] text-gray-400">No bookings on this day.</div>
            )}
          </div>
        </div>
      ) : null}

      {vacationHover ? (
        <div
          ref={vacationTooltipRef}
          style={{
            left: vacationTooltipPos.left,
            top: vacationTooltipPos.top,
            backgroundColor: "rgba(120, 53, 15, 0.95)",
          }}
          className="fixed z-40 max-w-xs pointer-events-none rounded-xl border border-amber-700/50 bg-amber-950/95 px-3 py-2 text-xs text-amber-50 shadow-2xl"
        >
          <div className="font-semibold">{vacationHover.type.replaceAll("_", " ")}</div>
          <div className="mt-1 text-[11px] text-amber-100/90">
            {formatDateLong(vacationHover.startDate)} to {formatDateLong(vacationHover.endDate)}
          </div>
          {vacationHover.note ? (
            <div className="mt-2 text-[11px] text-amber-100/80">{vacationHover.note}</div>
          ) : null}
        </div>
      ) : null}
    </>
  );
}

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

/** Assign lane indices to demands so overlapping bars don't stack on top of each other. */
function assignDemandLanes(
  demands: TimelineDemandEntry[],
): Map<string, number> {
  const laneMap = new Map<string, number>();
  // Each lane tracks the latest end-date occupying it
  const laneEnds: Date[] = [];

  // Sort by start date for greedy lane assignment
  const sorted = [...demands].sort(
    (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime(),
  );

  for (const d of sorted) {
    const start = new Date(d.startDate);
    let assigned = -1;
    for (let i = 0; i < laneEnds.length; i++) {
      if (laneEnds[i]! < start) {
        assigned = i;
        laneEnds[i] = new Date(d.endDate);
        break;
      }
    }
    if (assigned === -1) {
      assigned = laneEnds.length;
      laneEnds.push(new Date(d.endDate));
    }
    laneMap.set(d.id, assigned);
  }

  return laneMap;
}

const DEMAND_LANE_HEIGHT = 30;
const DEMAND_LANE_GAP = 2;

function renderOpenDemandRow(
  openDemands: TimelineDemandEntry[],
  CELL_WIDTH: number,
  totalCanvasWidth: number,
  toLeft: (d: Date) => number,
  toWidth: (s: Date, e: Date) => number,
  rowGridStyle: CSSProperties,
  onOpenDemandClick: (demand: OpenDemandAssignment) => void,
) {
  if (openDemands.length === 0) return null;

  const laneMap = assignDemandLanes(openDemands);
  const laneCount = laneMap.size > 0 ? Math.max(...laneMap.values()) + 1 : 1;
  const rowHeight = Math.max(ROW_HEIGHT, laneCount * (DEMAND_LANE_HEIGHT + DEMAND_LANE_GAP) + 8);

  return (
    <div
      className="flex border-b border-dashed border-amber-200 bg-amber-50/30 hover:bg-amber-50/50 group"
      style={{ minHeight: rowHeight }}
    >
      <div
        className="flex-shrink-0 border-r border-amber-200 flex items-center pl-8 pr-4 gap-2 bg-amber-50 sticky left-0 z-30"
        style={{ width: LABEL_WIDTH, minHeight: rowHeight }}
      >
        <div className="w-6 h-6 rounded-full bg-amber-100 flex items-center justify-center text-[10px] font-bold text-amber-600 flex-shrink-0 border border-dashed border-amber-400">
          ?
        </div>
        <div className="min-w-0">
          <div className="text-xs font-medium text-amber-700 truncate">Open demand</div>
          <div className="text-[10px] text-amber-500 truncate">
            {openDemands.length} open demand{openDemands.length > 1 ? "s" : ""}
          </div>
        </div>
      </div>

      <div
        className="relative overflow-hidden"
        style={{ width: totalCanvasWidth, minHeight: rowHeight, ...rowGridStyle }}
      >
        {openDemands.map((alloc) => {
          const allocStart = new Date(alloc.startDate);
          const allocEnd = new Date(alloc.endDate);
          const left = toLeft(allocStart);
          const width = Math.max(CELL_WIDTH, toWidth(allocStart, allocEnd));
          if (width <= 0 || left >= totalCanvasWidth) return null;

          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 = 4 + lane * (DEMAND_LANE_HEIGHT + DEMAND_LANE_GAP);

          return (
            <div
              key={alloc.id}
              className="absolute rounded-md flex items-center px-2 gap-1 overflow-hidden cursor-pointer hover:ring-2 hover:ring-amber-400 hover:ring-offset-1 z-[10]"
              style={{
                left: left + 2,
                width: width - 4,
                top,
                height: DEMAND_LANE_HEIGHT,
                backgroundColor: `${roleColor}33`,
                border: `2px dashed ${roleColor}99`,
              }}
              onClick={() => {
                onOpenDemandClick({
                  id: getPlanningEntryMutationId(alloc),
                  projectId: alloc.projectId,
                  roleId: (alloc as { roleId?: string | null }).roleId ?? null,
                  role: (alloc as { role?: string | null }).role ?? null,
                  headcount,
                  startDate: allocStart,
                  endDate: allocEnd,
                  hoursPerDay: alloc.hoursPerDay,
                  roleEntity: roleEntity ?? null,
                  project: alloc.project as { id: string; name: string; shortCode: string },
                });
              }}
            >
              <span className="text-xs font-medium truncate" style={{ color: roleColor }}>
                {roleName}
                {headcount > 1 ? ` x${headcount}` : ""}
              </span>
            </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 REF_H = 8;
  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 }, i) => {
    if (totalH === 0 && projH === 0) return;

    const isOver = totalH > REF_H;
    const totalBarH = Math.max(
      projH > 0 ? 2 : 0,
      Math.round((Math.min(totalH, REF_H) / REF_H) * BAR_H),
    );
    const projBarH =
      projH > 0 ? Math.min(totalBarH, Math.max(2, Math.round((projH / REF_H) * BAR_H))) : 0;
    const otherBarH = totalBarH - projBarH;
    const projPct = (projH / REF_H) * 100;
    const totalPct = (totalH / REF_H) * 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 },
    anchorX: number,
    anchorY: number,
  ) => void,
) {
  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;

    const left = toLeft(dispStart);
    const width = Math.max(CELL_WIDTH, toWidth(dispStart, dispEnd));
    if (width <= 0 || left >= totalCanvasWidth) return null;

    // 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}`}
        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]",
        )}
        style={{ left: left + 2, width: width - 4, top: 2, bottom: 2 }}
        onContextMenu={(e) => {
          e.preventDefault();
          e.stopPropagation();
          onAllocationContextMenu(
            { allocationId: alloc.id, projectId: alloc.projectId },
            e.clientX,
            e.clientY,
          );
        }}
      >
        <div
          className="flex-shrink-0 cursor-ew-resize"
          style={{ width: HANDLE_W }}
          onMouseDown={(e) => 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) => 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) => onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" })}
          onTouchStart={(e) => {
            e.stopPropagation();
            onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" });
          }}
        />
      </div>
    );
  });
}

// ─── Vacation blocks for project view rows ──────────────────────────────────

const TYPE_COLORS: Record<string, string> = {
  ANNUAL: "bg-orange-400/40",
  SICK: "bg-red-500/40",
  PUBLIC_HOLIDAY: "bg-violet-400/40",
  OTHER: "bg-amber-400/40",
};
const TYPE_BORDER: Record<string, string> = {
  ANNUAL: "border-orange-500",
  SICK: "border-red-600",
  PUBLIC_HOLIDAY: "border-violet-500",
  OTHER: "border-amber-500",
};
const TYPE_LABELS_SHORT: Record<string, string> = {
  ANNUAL: "Annual",
  SICK: "Sick",
  PUBLIC_HOLIDAY: "Holiday",
  OTHER: "Other",
};

function renderVacationBlocksForProjectRow(
  vacations: { id: string; type: string; startDate: Date | string; endDate: Date | string }[],
  rowHeight: number,
  toLeft: (d: Date) => number,
  toWidth: (s: Date, e: Date) => number,
  CELL_WIDTH: number,
  totalCanvasWidth: number,
  showVacations: boolean,
) {
  if (!showVacations || vacations.length === 0) return null;

  return vacations.map((v) => {
    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) return null;

    const colorClass = TYPE_COLORS[v.type] ?? "bg-orange-400/40";
    const borderClass = TYPE_BORDER[v.type] ?? "border-orange-500";
    const label = TYPE_LABELS_SHORT[v.type] ?? v.type;

    return (
      <div
        key={`vac-${v.id}`}
        className={clsx(
          "absolute z-[5] flex items-end px-1 pb-0.5 overflow-hidden border-t-2 pointer-events-none",
          colorClass,
          borderClass,
        )}
        style={{ left: left + 1, width: width - 2, top: 0, height: rowHeight }}
      >
        {width > 40 && (
          <span className="text-[9px] font-bold truncate opacity-70 text-gray-700 dark:text-gray-200 pointer-events-none">
            🏖 {label}
          </span>
        )}
      </div>
    );
  });
}

// ─── Range overlay for project view ─────────────────────────────────────────

function renderRangeOverlayProject(
  rangeState: RangeState,
  resourceId: string,
  rowHeight: number,
  toLeft: (d: Date) => number,
  toWidth: (s: Date, e: Date) => number,
  CELL_WIDTH: number,
) {
  if (!rangeState.isSelecting || rangeState.resourceId !== resourceId || !rangeState.startDate) {
    return null;
  }
  const end = rangeState.currentDate ?? rangeState.startDate;
  const [selStart, selEnd] =
    rangeState.startDate <= end ? [rangeState.startDate, end] : [end, rangeState.startDate];

  const left = toLeft(selStart);
  const width = Math.max(CELL_WIDTH, toWidth(selStart, selEnd));

  return (
    <div
      className="absolute bg-brand-200/40 border-2 border-brand-400 rounded pointer-events-none z-10"
      style={{ left, width, top: 4, height: rowHeight - 8 }}
    />
  );
}
