"use client";

import { clsx } from "clsx";
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import {
  useTimelineContext,
  type TimelineAssignmentEntry,
  type VacationEntry,
} from "./TimelineContext.js";
import { ConflictOverlay } from "./ConflictOverlay.js";
import { computeSubLanes } from "./utils.js";
import { heatmapBgColor, heatmapColor } from "./heatmapUtils.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import { formatDateLong } from "~/lib/format.js";
import {
  ROW_HEIGHT,
  SUB_LANE_HEIGHT,
  LABEL_WIDTH,
  ORDER_TYPE_COLORS,
} from "./timelineConstants.js";
import type {
  DragState,
  AllocDragState,
  RangeState,
  ShiftPreviewData,
} from "~/hooks/useTimelineDrag.js";
import type { HeatmapColorScheme } from "~/hooks/useAppPreferences.js";

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

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

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

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

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

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

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

  const [heatmapHover, setHeatmapHover] = useState<{
    date: Date;
    totalH: number;
    pct: number;
    breakdown: {
      projectId: string;
      shortCode: string;
      projectName: string;
      orderType: string;
      hoursPerDay: number;
      responsiblePerson?: string | null;
    }[];
  } | null>(null);

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

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

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

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

    const result = new Map<string, VacationBlockInfo[]>();
    for (const [resourceId, vacations] of vacationsByResource) {
      const blocks: VacationBlockInfo[] = [];
      for (const v of vacations) {
        const vStart = new Date(v.startDate);
        const vEnd = new Date(v.endDate);
        const left = toLeft(vStart);
        const width = Math.max(CELL_WIDTH, toWidth(vStart, vEnd));
        if (width <= 0 || left >= totalCanvasWidth) continue;
        blocks.push({ vacation: v, left, width });
      }
      if (blocks.length > 0) {
        result.set(resourceId, blocks);
      }
    }
    return result;
  }, [vacationsByResource, toLeft, toWidth, CELL_WIDTH, totalCanvasWidth, filters.showVacations]);

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

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

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

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

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

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

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

        const t = date.getTime();
        const REF_H = 8;
        const projectHours = new Map<
          string,
          {
            shortCode: string;
            projectName: string;
            orderType: string;
            hours: number;
            responsiblePerson?: string | null;
          }
        >();
        for (const alloc of a) {
          const s = new Date(alloc.startDate);
          s.setHours(0, 0, 0, 0);
          const ev = new Date(alloc.endDate);
          ev.setHours(0, 0, 0, 0);
          if (t < s.getTime() || t > ev.getTime()) continue;
          const existing = projectHours.get(alloc.projectId);
          if (existing) {
            existing.hours += alloc.hoursPerDay;
          } else {
            projectHours.set(alloc.projectId, {
              shortCode: alloc.project.shortCode,
              projectName: alloc.project.name,
              orderType: alloc.project.orderType,
              hours: alloc.hoursPerDay,
              responsiblePerson:
                (alloc.project as { responsiblePerson?: string | null }).responsiblePerson ?? null,
            });
          }
        }

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

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

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

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

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

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

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

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

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

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

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

  // ─── Cleanup rAF on unmount ───────────────────────────────────────────────
  useEffect(
    () => () => {
      if (heatmapRafRef.current !== null) cancelAnimationFrame(heatmapRafRef.current);
      if (vacationHoverRafRef.current !== null) cancelAnimationFrame(vacationHoverRafRef.current);
    },
    [],
  );

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

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

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

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

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

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

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

// ─── Tooltip sub-component (portal-free: positioned fixed) ──────────────────

function ResourcePanelTooltips({
  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}
    </>
  );
}

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

interface VacationBlockInfo {
  vacation: VacationEntry;
  left: number;
  width: number;
}

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

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

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 renderVacationBlocksForRow(blocks: VacationBlockInfo[], rowHeight: number) {
  if (blocks.length === 0) return null;

  return blocks.map(({ vacation: v, left, width }) => {
    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;
    const isPending = v.status === "PENDING";

    return (
      <div
        key={`vac-${v.id}`}
        className={clsx(
          "absolute z-[5] flex items-end px-1 pb-0.5 overflow-hidden pointer-events-none",
          colorClass,
          isPending ? "border-t-2 border-dashed opacity-60" : "border-t-2",
          isPending ? "" : 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">
            {isPending ? "\u23F3" : "\uD83C\uDFD6"} {label}
          </span>
        )}
      </div>
    );
  });
}

function renderRangeOverlay(
  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 }}
    />
  );
}

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

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

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

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

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

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

    const customColor = (alloc.project as { color?: string | null }).color;
    const colors = ORDER_TYPE_COLORS[alloc.project.orderType] ?? {
      bg: "bg-gray-400",
      text: "text-white",
      light: "",
    };
    const HANDLE_W = width >= 48 ? 10 : 6;
    const hasRecurrence = !!(alloc.metadata as Record<string, unknown> | null)?.recurrence;

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

    return (
      <div
        key={alloc.id}
        className={clsx(
          "absolute rounded-md flex items-stretch overflow-hidden transition-all duration-75 group/block",
          !customColor && colors.bg,
          customColor ? "text-white" : colors.text,
          hasRecurrence && "opacity-80 border-2 border-dashed border-white/60",
          isBeingDragged
            ? "opacity-90 shadow-2xl ring-2 ring-white ring-offset-1 z-20 scale-[1.01]"
            : isOtherDragged
              ? "opacity-30 z-[10]"
              : "hover:ring-2 hover:ring-white hover:ring-offset-1 z-[10]",
        )}
        style={{
          left: left + 2,
          width: width - 4,
          top: blockTop,
          height: blockHeight,
          ...(customColor ? { backgroundColor: customColor } : {}),
        }}
        onContextMenu={(e) => {
          e.preventDefault();
          e.stopPropagation();
          onAllocationContextMenu(
            { allocationId: alloc.id, projectId: alloc.projectId },
            e.clientX,
            e.clientY,
          );
        }}
      >
        {/* Left resize handle */}
        <div
          className="flex-shrink-0 flex items-center justify-center cursor-ew-resize hover:bg-black/20 transition-colors"
          style={{ width: HANDLE_W }}
          onMouseDown={(e) => onAllocMouseDown(e, { ...allocInfo, mode: "resize-start" })}
          onTouchStart={(e) => {
            e.stopPropagation();
            onAllocTouchStart(e, { ...allocInfo, mode: "resize-start" });
          }}
        >
          {HANDLE_W >= 10 && (
            <div className="flex flex-col gap-0.5 opacity-60 group-hover/block:opacity-100">
              <div className="w-px h-2.5 bg-white rounded" />
              <div className="w-px h-2.5 bg-white rounded" />
            </div>
          )}
        </div>

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

        {/* Right resize handle */}
        <div
          className="flex-shrink-0 flex items-center justify-center cursor-ew-resize hover:bg-black/20 transition-colors"
          style={{ width: HANDLE_W }}
          onMouseDown={(e) => onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" })}
          onTouchStart={(e) => {
            e.stopPropagation();
            onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" });
          }}
        >
          {HANDLE_W >= 10 && (
            <div className="flex flex-col gap-0.5 opacity-60 group-hover/block:opacity-100">
              <div className="w-px h-2.5 bg-white rounded" />
              <div className="w-px h-2.5 bg-white rounded" />
            </div>
          )}
        </div>
      </div>
    );
  });
}

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

function renderLoadGraph(allocs: TimelineAssignmentEntry[], dates: Date[], CELL_WIDTH: number) {
  const GRAPH_H = 12;
  const REF_H = 8;

  function hoursOnDay(list: TimelineAssignmentEntry[], t: number) {
    return list.reduce((sum, a) => {
      const s = new Date(a.startDate);
      s.setHours(0, 0, 0, 0);
      const e = new Date(a.endDate);
      e.setHours(0, 0, 0, 0);
      return t >= s.getTime() && t <= e.getTime() ? sum + a.hoursPerDay : sum;
    }, 0);
  }

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

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

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

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

function renderHeatmapOverlay(
  allocs: TimelineAssignmentEntry[],
  dates: Date[],
  CELL_WIDTH: number,
  heatmapScheme: HeatmapColorScheme,
) {
  const REF_H = 8;
  return dates.map((date, i) => {
    const t = date.getTime();
    const totalH = allocs.reduce((sum, a) => {
      const s = new Date(a.startDate);
      s.setHours(0, 0, 0, 0);
      const e = new Date(a.endDate);
      e.setHours(0, 0, 0, 0);
      return t >= s.getTime() && t <= e.getTime() ? sum + a.hoursPerDay : sum;
    }, 0);
    const bg = heatmapBgColor((totalH / REF_H) * 100, heatmapScheme);
    if (!bg) return null;
    return (
      <div
        key={`hm-${i}`}
        className="absolute top-0 bottom-0 pointer-events-none z-10"
        style={{ left: i * CELL_WIDTH, width: CELL_WIDTH, backgroundColor: bg }}
      />
    );
  });
}

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

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

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

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

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

    const totalH = covering.reduce((sum, a) => sum + a.hoursPerDay, 0);
    const isOver = totalH > REF_H;
    let stackedH = 0;

    const segs: React.ReactNode[] = covering.map((alloc) => {
      const colors = ORDER_TYPE_COLORS[alloc.project.orderType] ?? {
        bg: "bg-gray-400",
        text: "text-white",
        light: "",
      };
      const segH = Math.max(
        2,
        Math.min(BAR_AREA - stackedH, Math.round((alloc.hoursPerDay / REF_H) * BAR_AREA)),
      );
      const bottom = 4 + stackedH;
      stackedH += segH;
      const isBeingDragged = allocDragState.isActive && allocDragState.allocationId === alloc.id;

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

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

      return (
        <div
          key={`bar-${i}-${alloc.id}`}
          className={clsx(
            "absolute rounded-sm transition-all duration-75 flex items-stretch overflow-hidden",
            colors.bg,
            isBeingDragged
              ? "opacity-90 ring-2 ring-white ring-offset-1 z-20"
              : "hover:opacity-80 z-[10]",
          )}
          style={{ left: i * CELL_WIDTH + 2, width: CELL_WIDTH - 4, height: segH, bottom }}
          onContextMenu={(e) => {
            e.preventDefault();
            e.stopPropagation();
            onAllocationContextMenu(
              { allocationId: alloc.id, projectId: alloc.projectId },
              e.clientX,
              e.clientY,
            );
          }}
        >
          {isFirstDay && EDGE_W > 0 && (
            <div
              className="flex-shrink-0 cursor-ew-resize hover:bg-black/20"
              style={{ width: EDGE_W }}
              onMouseDown={(e) => {
                e.stopPropagation();
                onAllocMouseDown(e, { ...allocInfo, mode: "resize-start" });
              }}
              onTouchStart={(e) => {
                e.stopPropagation();
                onAllocTouchStart(e, { ...allocInfo, mode: "resize-start" });
              }}
            />
          )}
          <div
            className={clsx("flex-1 min-w-0", "cursor-grab")}
            onMouseDown={(e) => {
              e.stopPropagation();
              onAllocMouseDown(e, allocInfo);
            }}
            onTouchStart={(e) => {
              e.stopPropagation();
              onAllocTouchStart(e, allocInfo);
            }}
          />
          {isLastDay && EDGE_W > 0 && (
            <div
              className="flex-shrink-0 cursor-ew-resize hover:bg-black/20"
              style={{ width: EDGE_W }}
              onMouseDown={(e) => {
                e.stopPropagation();
                onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" });
              }}
              onTouchStart={(e) => {
                e.stopPropagation();
                onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" });
              }}
            />
          )}
        </div>
      );
    });

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

    return segs;
  });
}

// ─── Re-export tooltip types for the parent ─────────────────────────────────
export type { VacationBlockInfo, AllocBlockData };
