"use client";

import { clsx } from "clsx";
import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import {
  useTimelineData,
  useTimelineView,
  useTimelineDisplay,
  type TimelineAssignmentEntry,
  type TimelineDemandEntry,
} from "./TimelineContext.js";
import {
  applyVisualOverrides,
  getDragPointerOffset,
  type TimelineVisualOverrides,
} from "./allocationVisualState.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import {
  renderOpenDemandRow,
  renderProjectUtilOverlay,
  renderProjectDragHandles,
} from "./timelineProjectRenderers.js";
import {
  TimelineTooltip,
  type DemandHoverData,
  type HeatmapHoverData,
  type VacationHoverData,
} from "./TimelineTooltip.js";
import {
  ROW_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,
} 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 } 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[] = [];

// ─── 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 } =
    useTimelineData();
  const { filters, activeFilterCount, today } = useTimelineView();
  const { displayMode, heatmapScheme, blinkOverbookedDays } = useTimelineDisplay();

  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,
        filters.showWeekends,
      ),
    [
      CELL_WIDTH,
      filters.showVacations,
      filters.showWeekends,
      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-gray-50/40 dark:hover:bg-[rgb(var(--surface-elevated)/0.3)] 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-[rgb(var(--surface-card))] sticky left-0 z-30 group-hover:bg-gray-50 dark:group-hover:bg-[rgb(var(--surface-elevated))]"
                  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>
  );
}

export const TimelineProjectPanel = memo(TimelineProjectPanelInner);
