"use client";

import * as React from "react";
import { flushSync } from "react-dom";
import {
  DragOverlay,
  closestCenter,
  pointerWithin,
  rectIntersection,
  type DragStartEvent,
  type DragEndEvent,
  type DragOverEvent,
  type CollisionDetection,
} from "@dnd-kit/core";
import {
  SortableContext,
  horizontalListSortingStrategy,
  arrayMove,
} from "@dnd-kit/sortable";
import { Button } from "@/components/ui/button";
import { Plus, X } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { NoDataFound } from "@/components/ui/no-data-found";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { canDragKanbanCards, resolvePermissionActor } from "@/lib/permissions";
import type { PermissionActorLike } from "@/api/permissions/types";
import { consumeKanbanDndRemountPending } from "./kanban-return-reset";
import { useGetProfileQuery } from "@/api/rtk/auth-api";
import { useGetTimelineIntentsQuery } from "@/api/rtk";
import { useSession } from "next-auth/react";
import { usePathname } from "next/navigation";
import { useAppSelector } from "@/store/hooks";
import { selectKanbanExpandedByParentId } from "@/store/slices/kanban-ui-slice";
import {
  isEnteringDiscoverMeetingBookedStage,
  isLostDestinationStageId,
} from "@/lib/deal-stage-labels";
import {
  mergeAppendedDealsIntoBoardIndex,
  syncColumnDealsInBoardIndex,
  orderedDealIdsForStage,
  isLostStage,
  dealBelongsToBoardColumn,
} from "../deal-utils";
import {
  DEFAULT_KANBAN_COPY,
  EMPTY_CONVERTED_DEAL_IDS,
  LEADS_BOARD_STAGE_FETCH_PRIORITY,
} from "./kanban-constants";
import { KanbanDragActiveContext } from "./kanban-drag-context";
import { KanbanDndRuntime, nextKanbanDndSessionKey } from "./kanban-dnd-runtime";
import { parseKanbanCardDropId } from "./kanban-card-drop-utils";
import {
  clearKanbanColumnDropHighlight,
  setKanbanColumnDropHighlight,
} from "./kanban-column-drop-highlight";
import { KanbanGroup } from "./kanban-group";
import { KanbanBoardProvider } from "./kanban-board-context";
import { SortableGroup } from "../sortable-group";
import { DealCard } from "../deal-card";
import { useKanbanBoardQueries } from "./use-kanban-board-queries";
import type { Deal, DealStage, StageGroup, KanbanBoardProps } from "../types";

export type { KanbanBoardCopy } from "../types";

export function KanbanBoard({
  stages,
  deals,
  convertedDealIds: convertedDealIdsProp,
  onOpen,
  onEdit,
  onDelete,
  onMove,
  onReorderInStage,
  onAddStage,
  onReorderStages,
  onAssignTask,
  canManageStages = true,
  isAddingStage: isAddingStageProp = false,
  boardCopy: boardCopyProp,
  emptyBoardTitle,
  emptyBoardDescription,
  boardPagingEnabled = false,
  boardPagingEntity = "leads",
  businessPipelineId,
  boardListParams,
  onColumnDealsChange,
  clientSearchQuery,
  kanbanClientBoardFilters,
  boardUiKey,
  onInterceptLostMove,
  onInterceptDiscoveryMeetingMove,
  onPagedLeadMoveOptimistic,
  canSubject = "deal",
  closedWonCustomerPaymentKpis,
  closedWonCustomerPaymentKpisLoading = false,
  teams,
}: KanbanBoardProps) {
  const dealsForResyncRef = React.useRef(deals);
  dealsForResyncRef.current = deals;

  const convertedDealIds = convertedDealIdsProp ?? EMPTY_CONVERTED_DEAL_IDS;
  const { data: session } = useSession();
  const { data: profile } = useGetProfileQuery(undefined);
  const boardMoveResource = canSubject === "lead" ? "LEAD" : "DEAL";
  const backendUser = (session as { backendUser?: PermissionActorLike | null } | null)
    ?.backendUser;
  const kanbanPermissions = React.useMemo(
    () => ({
      source: resolvePermissionActor(
        backendUser,
        profile as PermissionActorLike | undefined,
      ),
    }),
    [backendUser, profile],
  );
  /** Drag = stage move. Allow UPDATE or read-level catalog (READ/VIEW); row-level edit is enforced on the API. */
  const canDragCardsOnBoard = React.useMemo(
    () =>
      canDragKanbanCards(
        backendUser,
        profile as PermissionActorLike | undefined,
        boardMoveResource,
      ),
    [backendUser, profile, boardMoveResource],
  );
  const boardCopy = React.useMemo(() => ({ ...DEFAULT_KANBAN_COPY, ...boardCopyProp }), [boardCopyProp]);
  const { data: timelineIntents = [] } = useGetTimelineIntentsQuery(undefined, {
    skip: boardPagingEntity === "business",
  });

  const { leadsUnifiedSearchActive, stageCounts, boardSearchData } =
    useKanbanBoardQueries({
      stages,
      boardPagingEnabled,
      boardPagingEntity,
      businessPipelineId,
      boardListParams,
    });

  // O(1) board state — Map<dealId, Deal> + Map<stageId, Set<dealId>>
  // Move = update 3 entries, no array scan or copy
  const dealsMapRef = React.useRef<Map<string, Deal>>(null!);
  const stageIndexRef = React.useRef<Map<string, Set<string>>>(null!);
  const orderIndexRef = React.useRef<Map<string, string[]>>(null!); // stageId → ordered dealIds
  const dealsStageByIdRef = React.useRef<Map<string, DealStage>>(null!);

  // Initialize / rebuild index from deals array (called on mount + server sync)
  const rebuildIndex = React.useCallback((list: Deal[]) => {
    const dm = new Map<string, Deal>();
    const si = new Map<string, Set<string>>();
    const oi = new Map<string, string[]>();
    for (const d of list) {
      dm.set(d.id, d);
      if (!si.has(d.stage)) { si.set(d.stage, new Set()); oi.set(d.stage, []); }
      si.get(d.stage)!.add(d.id);
      oi.get(d.stage)!.push(d.id);
    }
    dealsMapRef.current = dm;
    stageIndexRef.current = si;
    orderIndexRef.current = oi;
  }, []);

  // Sync initialization on first render to avoid redundant Effect -> bumpVersion cycle on mount
  if (!dealsMapRef.current) {
    const dm = new Map<string, Deal>();
    const si = new Map<string, Set<string>>();
    const oi = new Map<string, string[]>();
    const sbi = new Map<string, DealStage>();
    for (const d of deals) {
      dm.set(d.id, d);
      if (!si.has(d.stage)) { si.set(d.stage, new Set()); oi.set(d.stage, []); }
      si.get(d.stage)!.add(d.id);
      oi.get(d.stage)!.push(d.id);
      sbi.set(d.id, d.stage);
    }
    dealsMapRef.current = dm;
    stageIndexRef.current = si;
    orderIndexRef.current = oi;
    dealsStageByIdRef.current = sbi;
  }

  // Used ONLY for triggering re-renders — a counter bumped on O(1) mutations
  const [boardVersion, setBoardVersion] = React.useState(0);
  const bumpVersion = React.useCallback(() => setBoardVersion((v) => v + 1), []);

  /** Fresh @dnd-kit session: bump remounts {@link KanbanDndRuntime} (sensors + context together). */
  const [dndContextKey, setDndContextKey] = React.useState(nextKanbanDndSessionKey);
  /** Guards async drag follow-ups and startTransition syncs after unmount or route teardown. */
  const mountedRef = React.useRef(true);

  // Build a flat `localDeals` array from the Map (used by dragRef, dealsByGroupId, etc.)
  // Re-derived only when boardVersion changes — O(n) but rare
  const localDeals = React.useMemo(() => Array.from(dealsMapRef.current.values()), [boardVersion]); // eslint-disable-line react-hooks/exhaustive-deps

  const expandedByParentId = useAppSelector(selectKanbanExpandedByParentId(boardUiKey));

  // Inflight moves: prevent server response from overwriting optimistic position
  const inflightMoves = React.useRef<Map<string, string>>(new Map());

  const inflightStageByDealId = React.useMemo(
    () => new Map(inflightMoves.current),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [boardVersion],
  );

  /** Coalesce pathname + popstate remounts into one frame (avoids double sensor teardown races). */
  const bumpDndRafRef = React.useRef<number | null>(null);

  // Prefer kd:: card targets; when several rects intersect, rectIntersection order can favor the top row — use pointerWithin / closestCenter among cards.
  const dndAutoScroll = React.useMemo(
    () => ({ enabled: true, threshold: { x: 0.1, y: 0.15 } }),
    [],
  );

  const collisionDetection: CollisionDetection = React.useCallback((args) => {
    const activeId = String(args.active.id);
    // Group reorder must only hit other group-* sortables; otherwise closestCenter
    // picks stage columns / card droppables and onDragEnd never finds toIdx.
    if (activeId.startsWith("group-")) {
      const groupContainers = args.droppableContainers.filter((c) =>
        String(c.id).startsWith("group-"),
      );
      if (groupContainers.length === 0) return [];
      return closestCenter({ ...args, droppableContainers: groupContainers });
    }
    const cardContainers: typeof args.droppableContainers = [];
    const columnContainers: typeof args.droppableContainers = [];
    for (const c of args.droppableContainers) {
      const id = String(c.id);
      if (id.startsWith("kd::")) cardContainers.push(c);
      else if (!id.startsWith("group-")) columnContainers.push(c);
    }
    const scopedArgs = { ...args, droppableContainers: cardContainers };
    const pointerCardHits = pointerWithin(scopedArgs).filter((h) =>
      String(h.id).startsWith("kd::"),
    );
    if (pointerCardHits.length > 0) return pointerCardHits;
    const hits = rectIntersection({
      ...args,
      droppableContainers: [...cardContainers, ...columnContainers],
    });
    const cardHits = hits.filter((h) => String(h.id).startsWith("kd::"));
    if (cardHits.length === 1) return cardHits;
    if (cardHits.length > 1) {
      const allowed = new Set(cardHits.map((h) => String(h.id)));
      return closestCenter({
        ...args,
        droppableContainers: cardContainers.filter((c) => allowed.has(String(c.id))),
      });
    }
    return hits;
  }, []);

  // Active drag state
  const activeCardIdRef = React.useRef<string | null>(null);
  /** True for any in-flight DnD from this board (cards or stage groups) — used for unmount teardown. */
  const boardDndActiveRef = React.useRef(false);
  const [overlayDeal, setOverlayDeal] = React.useState<Deal | null>(null);

  const [isAddingStage, setIsAddingStage] = React.useState(false);
  const [newStageName, setNewStageName] = React.useState("");
  const [localStages, setLocalStages] = React.useState(stages);

  const firstStageId = React.useMemo(() => {
    return stages[0]?.id ?? null;
  }, [stages]);

  React.useEffect(() => { setLocalStages(stages); }, [stages]);

  const prevAddingStageRef = React.useRef(false);
  React.useEffect(() => {
    if (prevAddingStageRef.current && !isAddingStageProp) { setNewStageName(""); setIsAddingStage(false); }
    prevAddingStageRef.current = isAddingStageProp;
  }, [isAddingStageProp]);
  React.useEffect(() => {
    const nextStageMap = new Map<string, DealStage>();
    for (const deal of deals) {
      nextStageMap.set(deal.id, deal.stage);
    }

    if (inflightMoves.current.size > 0) {
      dealsStageByIdRef.current = nextStageMap;
      React.startTransition(() => {
        const syncedDeals = deals.map((d) => {
          const s = inflightMoves.current.get(d.id);
          return s ? { ...d, stage: s as DealStage } : d;
        });
        rebuildIndex(syncedDeals);
        if (mountedRef.current) bumpVersion();
      });
      return;
    }

    if (boardPagingEnabled) {
      const prevIds = dealsMapRef.current;
      const nextIds = new Set(deals.map((d) => d.id));
      const added: Deal[] = [];
      for (const d of deals) {
        if (!prevIds.has(d.id)) added.push(d);
      }
      let removed = 0;
      for (const id of prevIds.keys()) {
        if (!nextIds.has(id)) removed++;
      }
      let stageMoves = 0;
      for (const id of nextIds) {
        const prevS = dealsStageByIdRef.current.get(id);
        const nextS = nextStageMap.get(id);
        if (prevS !== undefined && nextS !== undefined && prevS !== nextS) {
          stageMoves++;
        }
      }
      if (removed === 0 && stageMoves === 0 && added.length > 0) {
        mergeAppendedDealsIntoBoardIndex(
          dealsMapRef,
          stageIndexRef,
          orderIndexRef,
          added,
        );
        dealsStageByIdRef.current = nextStageMap;
        if (mountedRef.current) bumpVersion();
        return;
      }
    }

    let shouldSync = false;
    for (const deal of deals) {
      if (!shouldSync) {
        const previousStage = dealsStageByIdRef.current.get(deal.id);
        if (previousStage !== deal.stage) {
          shouldSync = true;
        }
      }
    }

    if (
      !shouldSync &&
      dealsStageByIdRef.current.size === nextStageMap.size &&
      inflightMoves.current.size === 0
    ) {
      return;
    }

    dealsStageByIdRef.current = nextStageMap;

    React.startTransition(() => {
      rebuildIndex(deals);
      if (mountedRef.current) bumpVersion();
    });
  }, [deals, boardPagingEnabled, rebuildIndex, bumpVersion]);

  const groupedStages = React.useMemo(() => {
    const groups: StageGroup[] = [];
    const map = new Map<string, StageGroup>();
    for (const stage of localStages) {
      if (!stage.isSubStage) {
        const g = { parentId: stage.id, parentName: stage.name, columns: [stage] };
        groups.push(g);
        map.set(stage.id, g);
      } else {
        const pid = stage.parentStageId || stage.id;
        let g = map.get(pid);
        if (!g) { g = { parentId: pid, parentName: stage.parentStageName || pid, columns: [] }; groups.push(g); map.set(pid, g); }
        g.columns.push(stage);
      }
    }
    return groups;
  }, [localStages]);

  const { columnSerialByStageId, visibleColumnCount } = React.useMemo(() => {
    const m = new Map<string, number>();
    let idx = 0;
    for (const group of groupedStages) {
      const exp = expandedByParentId[group.parentId] ?? false;
      for (const stage of group.columns) {
        const show = !stage.isSubStage || exp;
        if (show) m.set(stage.id, idx++);
      }
    }
    return { columnSerialByStageId: m, visibleColumnCount: idx };
  }, [groupedStages, expandedByParentId]);

  const leadsStagedPaging = Boolean(boardPagingEnabled && boardPagingEntity === "leads");
  const stagedPagingResetKey = React.useMemo(
    () => `${boardListParams?.pipelineId ?? ""}|${JSON.stringify(boardListParams ?? {})}|${localStages.map((s) => s.id).join(",")}`,
    [boardListParams, localStages],
  );

  const [restStagesUnlocked, setRestStagesUnlocked] = React.useState(() => !(boardPagingEnabled && boardPagingEntity === "leads"));
  const priorityReadyRef = React.useRef(new Set<number>());
  const visibleColumnCountRef = React.useRef(visibleColumnCount);
  React.useLayoutEffect(() => { visibleColumnCountRef.current = visibleColumnCount; }, [visibleColumnCount]);
  React.useEffect(() => {
    if (!leadsStagedPaging) { setRestStagesUnlocked(true); return; }
    setRestStagesUnlocked(false);
    priorityReadyRef.current = new Set();
    const t = window.setTimeout(() => setRestStagesUnlocked(true), 800);
    return () => window.clearTimeout(t);
  }, [leadsStagedPaging, stagedPagingResetKey]);
  React.useEffect(() => {
    if (!leadsStagedPaging) return;
    if (Math.min(LEADS_BOARD_STAGE_FETCH_PRIORITY, visibleColumnCount) === 0) setRestStagesUnlocked(true);
  }, [leadsStagedPaging, visibleColumnCount, stagedPagingResetKey]);

  const onLeadsPriorityColumnReady = React.useCallback((index: number) => {
    if (!leadsStagedPaging) return;
    if (index < 0 || index >= LEADS_BOARD_STAGE_FETCH_PRIORITY) return;
    priorityReadyRef.current.add(index);
    const target = Math.min(LEADS_BOARD_STAGE_FETCH_PRIORITY, visibleColumnCountRef.current);
    if (priorityReadyRef.current.size >= target) setRestStagesUnlocked(true);
  }, [leadsStagedPaging]);

  /**
   * Paged leads kanban passes `deals=[]` at the page level; each column fetches its own pages.
   * Merge column payloads into the O(1) board index so drag-end can resolve source stage/deal.
   */
  const handleColumnDealsChangeInternal = React.useCallback(
    (columnKey: string, columnDeals: Deal[]) => {
      if (boardPagingEnabled && columnDeals.length > 0) {
        const sizeBefore = dealsMapRef.current.size;
        const orderBefore = JSON.stringify(
          columnDeals.map((d) => `${d.stage}:${d.id}`),
        );
        syncColumnDealsInBoardIndex(
          dealsMapRef,
          stageIndexRef,
          orderIndexRef,
          columnDeals,
        );
        let touched = dealsMapRef.current.size !== sizeBefore;
        if (!touched) {
          const orderAfter = JSON.stringify(
            columnDeals.map((d) => `${d.stage}:${d.id}`),
          );
          touched = orderBefore !== orderAfter;
        }
        for (const d of columnDeals) {
          if (inflightMoves.current.has(d.id)) continue;
          dealsStageByIdRef.current.set(d.id, d.stage);
        }
        if (touched) bumpVersion();
      }
      onColumnDealsChange?.(columnKey, columnDeals);
    },
    [boardPagingEnabled, onColumnDealsChange, bumpVersion],
  );

  // Column highlight during drag is imperative (kanban-column-drop-highlight) — no React state.
  const hoveredStageIdRef = React.useRef<string | null>(null);
  /** When hovering another lead, precise drop target for same-stage reorder. */
  const hoveredKanbanDropRef = React.useRef<{ stageId: string; dealId: string } | null>(null);

  const pathname = usePathname();

  /** Full @dnd-kit session reset: refs, overlay, remount {@link KanbanDndRuntime} (sensors + context). */
  const bumpDndSession = React.useCallback(() => {
    const hadDrag = boardDndActiveRef.current || activeCardIdRef.current != null;
    boardDndActiveRef.current = false;
    activeCardIdRef.current = null;
    hoveredStageIdRef.current = null;
    hoveredKanbanDropRef.current = null;
    inflightMoves.current.clear();
    clearKanbanColumnDropHighlight();
    setOverlayDeal(null);
    if (hadDrag && typeof document !== "undefined") {
      try {
        document.dispatchEvent(
          new PointerEvent("pointercancel", { bubbles: true, cancelable: true }),
        );
      } catch {
        /* ignore */
      }
    }
    setDndContextKey((k) => k + 1);
  }, []);

  const scheduleDndRemount = React.useCallback(() => {
    if (typeof window === "undefined") {
      bumpDndSession();
      return;
    }
    if (bumpDndRafRef.current != null) return;
    bumpDndRafRef.current = window.requestAnimationFrame(() => {
      bumpDndRafRef.current = null;
      bumpDndSession();
    });
  }, [bumpDndSession]);

  /** After {@link bumpDndSession}, re-derive Maps from props so refs never stay desynced from React. */
  const initialDndContextKeyRef = React.useRef<number | null>(null);
  React.useLayoutEffect(() => {
    if (initialDndContextKeyRef.current === null) {
      initialDndContextKeyRef.current = dndContextKey;
      return;
    }
    if (initialDndContextKeyRef.current === dndContextKey) return;
    initialDndContextKeyRef.current = dndContextKey;
    if (inflightMoves.current.size > 0) return;
    const list = dealsForResyncRef.current;
    const nextStageMap = new Map<string, DealStage>();
    for (const d of list) {
      nextStageMap.set(d.id, d.stage);
    }
    dealsStageByIdRef.current = nextStageMap;
    rebuildIndex(list);
    bumpVersion();
  }, [dndContextKey, rebuildIndex, bumpVersion]);

  const mountDndResetDoneRef = React.useRef(false);
  React.useLayoutEffect(() => {
    if (mountDndResetDoneRef.current) return;
    mountDndResetDoneRef.current = true;
    consumeKanbanDndRemountPending();
    scheduleDndRemount();
  }, [scheduleDndRemount]);

  React.useLayoutEffect(() => {
    mountedRef.current = true;
    return () => {
      mountedRef.current = false;
      if (bumpDndRafRef.current != null) {
        cancelAnimationFrame(bumpDndRafRef.current);
        bumpDndRafRef.current = null;
      }
      if (dragOverRafRef.current != null) {
        cancelAnimationFrame(dragOverRafRef.current);
        dragOverRafRef.current = null;
      }
      const hadActiveBoardDrag =
        boardDndActiveRef.current || activeCardIdRef.current != null;
      boardDndActiveRef.current = false;
      activeCardIdRef.current = null;
      hoveredStageIdRef.current = null;
      hoveredKanbanDropRef.current = null;
      inflightMoves.current.clear();
      // If we unmount mid-drag, @dnd-kit may not run sensor teardown; nudge pointercancel so
      // document listeners detach (see AbstractPointerSensor.detach).
      if (hadActiveBoardDrag && typeof document !== "undefined") {
        try {
          document.dispatchEvent(
            new PointerEvent("pointercancel", { bubbles: true, cancelable: true }),
          );
        } catch {
          /* ignore */
        }
      }
    };
  }, []);

  React.useEffect(() => {
    const onPageShow = (e: PageTransitionEvent) => {
      if (!e.persisted) return;
      bumpDndSession();
    };
    window.addEventListener("pageshow", onPageShow);
    return () => window.removeEventListener("pageshow", onPageShow);
  }, [bumpDndSession]);

  /** Next.js client navigation can keep the board mounted across pathname updates — remount DnD. */
  const prevPathnameRef = React.useRef<string | null>(null);
  React.useEffect(() => {
    if (prevPathnameRef.current === null) {
      prevPathnameRef.current = pathname;
      return;
    }
    if (prevPathnameRef.current !== pathname) {
      prevPathnameRef.current = pathname;
      scheduleDndRemount();
    }
  }, [pathname, scheduleDndRemount]);

  /** Browser back/forward: remount DnD when history moves (sensors often stale). */
  React.useEffect(() => {
    const onPopState = () => {
      scheduleDndRemount();
    };
    window.addEventListener("popstate", onPopState);
    return () => window.removeEventListener("popstate", onPopState);
  }, [scheduleDndRemount]);

  /** Tab / OS app switch: @dnd-kit can leave listeners inconsistent; remount full DnD session. */
  React.useEffect(() => {
    let wasHidden = false;
    const onVisibility = () => {
      if (document.visibilityState === "hidden") {
        wasHidden = true;
        return;
      }
      if (document.visibilityState === "visible" && wasHidden) {
        wasHidden = false;
        bumpDndSession();
      }
    };
    document.addEventListener("visibilitychange", onVisibility);
    return () => document.removeEventListener("visibilitychange", onVisibility);
  }, [bumpDndSession]);

  // Safety net: pointercancel / lostpointercapture — align with onDragCancel so boardDndActiveRef
  // never stays true while overlay is cleared (stuck "disabled" board until refresh).
  // Do NOT use pointerleave/contextmenu here — they can fire mid-drag and break sensors.
  React.useEffect(() => {
    const cleanup = () => {
      if (!activeCardIdRef.current && !boardDndActiveRef.current) return;
      boardDndActiveRef.current = false;
      activeCardIdRef.current = null;
      hoveredStageIdRef.current = null;
      hoveredKanbanDropRef.current = null;
      clearKanbanColumnDropHighlight();
      setOverlayDeal(null);
    };
    window.addEventListener("pointercancel", cleanup, { capture: true });
    window.addEventListener("lostpointercapture", cleanup, { capture: true });
    return () => {
      window.removeEventListener("pointercancel", cleanup, { capture: true } as EventListenerOptions);
      window.removeEventListener("lostpointercapture", cleanup, { capture: true } as EventListenerOptions);
    };
  }, []);

  // Ref snapshot for drag handlers (keeps callbacks stable)
  const dragRef = React.useRef({
    localDeals, localStages, groupedStages, expandedByParentId,
    canManageStages, boardPagingEnabled, boardPagingEntity, boardUiKey,
    canDragCardsOnBoard,
    onMove, onReorderInStage, onReorderStages, onInterceptLostMove,
    onInterceptDiscoveryMeetingMove, onPagedLeadMoveOptimistic,
  });
  dragRef.current = {
    localDeals, localStages, groupedStages, expandedByParentId,
    canManageStages, boardPagingEnabled, boardPagingEntity, boardUiKey,
    canDragCardsOnBoard,
    onMove, onReorderInStage, onReorderStages, onInterceptLostMove,
    onInterceptDiscoveryMeetingMove, onPagedLeadMoveOptimistic,
  };

  const handleDragStart = React.useCallback((event: DragStartEvent) => {
    const id = String(event.active.id);
    if (!id.startsWith("group-")) {
      if (!canDragCardsOnBoard) return;
      boardDndActiveRef.current = true;
      activeCardIdRef.current = id;
      const payload = event.active.data.current as { deal?: Deal } | undefined;
      const deal = dealsMapRef.current.get(id) ?? payload?.deal ?? null;
      setOverlayDeal(deal);
      hoveredStageIdRef.current = null;
      return;
    }
    boardDndActiveRef.current = true;
  }, [canDragCardsOnBoard]);

  const dragOverRafRef = React.useRef<number | null>(null);
  const pendingDragOverRef = React.useRef<DragOverEvent | null>(null);

  const applyDragOver = React.useCallback((event: DragOverEvent) => {
    const overId = event.over ? String(event.over.id) : null;
    if (overId?.startsWith("kd::")) {
      const parsed = parseKanbanCardDropId(overId);
      if (parsed) {
        hoveredKanbanDropRef.current = parsed;
        hoveredStageIdRef.current = parsed.stageId;
        setKanbanColumnDropHighlight(parsed.stageId);
        return;
      }
    }
    hoveredKanbanDropRef.current = null;
    if (overId && !overId.startsWith("group-")) {
      hoveredStageIdRef.current = overId;
      setKanbanColumnDropHighlight(overId);
    } else if (!event.over) {
      hoveredStageIdRef.current = null;
      clearKanbanColumnDropHighlight();
    }
  }, []);

  const handleDragOver = React.useCallback((event: DragOverEvent) => {
    pendingDragOverRef.current = event;
    if (dragOverRafRef.current != null) return;
    dragOverRafRef.current = window.requestAnimationFrame(() => {
      dragOverRafRef.current = null;
      const pending = pendingDragOverRef.current;
      if (pending) applyDragOver(pending);
    });
  }, [applyDragOver]);

  const handleDragEnd = React.useCallback((event: DragEndEvent) => {
    boardDndActiveRef.current = false;
    // Clear overlay immediately (urgent — user released the mouse)
    activeCardIdRef.current = null;
    const destStageFromOver = hoveredStageIdRef.current;
    const dropTargetSnap = hoveredKanbanDropRef.current;
    hoveredStageIdRef.current = null;
    hoveredKanbanDropRef.current = null;
    clearKanbanColumnDropHighlight();
    // Clear overlay synchronously so KanbanDragActiveContext flips off before any flushSync
    // layout work — deferred updates kept measureElement detached and worsened desync bugs.
    setOverlayDeal(null);

    const { active, over } = event;
    // For cards: use the tracked hovered stage; for groups use over.id
    const activeId = String(active.id);

    // ── Stage (group) reorder ──
    if (activeId.startsWith("group-")) {
      if (!over || active.id === over.id) return;
      const overId = String(over.id);
      const {
        groupedStages: groupsNow,
        canManageStages: canReorder,
        onReorderStages: reorderStages,
      } = dragRef.current;
      if (!canReorder) return;
      const fromIdx = groupsNow.findIndex((g) => `group-${g.parentId}` === activeId);
      const toIdx = groupsNow.findIndex((g) => `group-${g.parentId}` === overId);
      if (fromIdx === -1 || toIdx === -1) return;
      const newGroups = arrayMove(groupsNow, fromIdx, toIdx);
      setLocalStages(newGroups.flatMap((g) => g.columns));
      reorderStages?.(newGroups.flatMap((g) => g.columns).map((s) => s.id));
      return;
    }

    // ── Card move ── O(1)
    const dealId = activeId;
    const {
      localDeals: dealsNow,
      localStages: stagesNow,
      boardPagingEnabled: pagingOn,
      boardPagingEntity: pagingEntity,
      canDragCardsOnBoard: allowCardDrag,
      onMove: moveDeal,
      onReorderInStage: reorderInStage,
      onPagedLeadMoveOptimistic: moveOptimistic,
    } = dragRef.current;
    if (!allowCardDrag) return;

    const activePayload = active.data.current as { deal?: Deal; stageId?: string } | undefined;
    const deal =
      dealsMapRef.current.get(dealId) ??
      dealsNow.find((d) => d.id === dealId) ??
      activePayload?.deal;
    if (deal && isLostStage(deal, stagesNow)) return;

    const overId = over ? String(over.id) : null;
    const overKd = overId ? parseKanbanCardDropId(overId) : null;
    const overDealFromId =
      !overKd && overId && !overId.startsWith("group-")
        ? dealsMapRef.current.get(overId) ??
          dealsNow.find((d) => d.id === overId)
        : undefined;

    const destStageId =
      overKd?.stageId ??
      overDealFromId?.stage ??
      destStageFromOver ??
      overId;
    if (!destStageId) return;
    if (!stagesNow.some((s) => s.id === destStageId) && !overDealFromId) return;

    const sourceStageId = deal?.stage ?? activePayload?.stageId ?? null;
    if (!sourceStageId) return;

    const reorderKey = sourceStageId;
    const dropDealId =
      (dropTargetSnap?.stageId === reorderKey ? dropTargetSnap.dealId : null) ??
      (overKd?.stageId === reorderKey ? overKd.dealId : null) ??
      (overDealFromId?.stage === reorderKey ? overDealFromId.id : null);
    const dropTargetDeal = dropDealId
      ? dealsMapRef.current.get(dropDealId) ??
        dealsNow.find((d) => d.id === dropDealId)
      : undefined;

    const resolvedDestStageId = overDealFromId?.stage ?? destStageId;
    const moveDestStageId = stagesNow.some((s) => s.id === resolvedDestStageId)
      ? resolvedDestStageId
      : stagesNow.some((s) => s.id === destStageId)
        ? destStageId
        : null;

    const columnDropReorder =
      !dropDealId &&
      deal &&
      over &&
      !String(over.id).startsWith("kd::") &&
      (String(over.id) === resolvedDestStageId ||
        dealBelongsToBoardColumn(String(over.id), reorderKey, stagesNow)) &&
      dealBelongsToBoardColumn(resolvedDestStageId, reorderKey, stagesNow);

    const inBucketReorder =
      Boolean(deal && reorderInStage) &&
      ((dropDealId &&
        dropTargetDeal &&
        dropTargetDeal.stage === reorderKey) ||
        columnDropReorder);

    // ── Reorder within the same pipeline stage (drop on another lead or on column chrome → bottom)
    if (inBucketReorder) {
      const order = [...(orderIndexRef.current.get(reorderKey) ?? [])];
      const fromIdx = order.indexOf(dealId);
      if (fromIdx === -1) return;

      let newOrder: string[];
      if (dropDealId && dropDealId !== dealId && order.includes(dropDealId)) {
        const toIdx = order.indexOf(dropDealId);
        newOrder = arrayMove(order, fromIdx, toIdx);
      } else if (columnDropReorder) {
        newOrder = [...order];
        newOrder.splice(fromIdx, 1);
        newOrder.push(dealId);
      } else {
        return;
      }

      const orderSnap = [...order];
      const dealsSnap = new Map<string, Deal>();
      for (const id of order) {
        const d = dealsMapRef.current.get(id);
        if (d) dealsSnap.set(id, { ...d });
      }

      flushSync(() => {
        orderIndexRef.current.set(reorderKey, newOrder);
        stageIndexRef.current.set(reorderKey, new Set(newOrder));
        newOrder.forEach((id, i) => {
          const d = dealsMapRef.current.get(id);
          if (d) dealsMapRef.current.set(id, { ...d, stageOrder: i });
        });
        bumpVersion();
      });

      if (reorderInStage) {
        void Promise.resolve(reorderInStage(reorderKey, newOrder)).catch(() => {
          if (!mountedRef.current) return;
          orderIndexRef.current.set(reorderKey, orderSnap);
          stageIndexRef.current.set(reorderKey, new Set(orderSnap));
          for (const [id, d] of dealsSnap) {
            dealsMapRef.current.set(id, d);
          }
          bumpVersion();
        });
      }
      return;
    }

    if (deal && !inBucketReorder && sourceStageId === resolvedDestStageId) {
      return;
    }

    if (!moveDestStageId) return;

    const onInterceptLostMoveNow = dragRef.current.onInterceptLostMove;
    if (
      deal &&
      !inBucketReorder &&
      onInterceptLostMoveNow &&
      isLostDestinationStageId(moveDestStageId, stagesNow) &&
      onInterceptLostMoveNow({
        dealId,
        destStageId: moveDestStageId as DealStage,
        sourceStageId,
        deal,
      })
    ) {
      return;
    }

    const onInterceptDiscoveryMeetingMoveNow =
      dragRef.current.onInterceptDiscoveryMeetingMove;
    if (
      deal &&
      !inBucketReorder &&
      onInterceptDiscoveryMeetingMoveNow &&
      isEnteringDiscoverMeetingBookedStage(
        sourceStageId,
        moveDestStageId,
        stagesNow,
      ) &&
      onInterceptDiscoveryMeetingMoveNow({
        dealId,
        destStageId: moveDestStageId as DealStage,
        sourceStageId,
        deal,
      })
    ) {
      return;
    }

    if (!deal && !pagingOn) return;
    const resolvedDeal =
      deal ?? ({ id: dealId, stage: sourceStageId } as Deal);
    const previousStage = resolvedDeal.stage ?? sourceStageId;
    const movedDeal = {
      ...resolvedDeal,
      stage: moveDestStageId as DealStage,
      stageOrder: -1,
    };

    inflightMoves.current.set(dealId, moveDestStageId);

    // O(1) optimistic update — mutate the Map + Set index, bump version
    flushSync(() => {
      if (
        pagingOn &&
        pagingEntity === "leads" &&
        moveOptimistic &&
        previousStage
      ) {
        moveOptimistic({
          dealId,
          fromStage: previousStage,
          toStage: moveDestStageId,
          deal: resolvedDeal,
        });
      }
      dealsMapRef.current.set(dealId, movedDeal);
      // Remove from source stage index
      stageIndexRef.current.get(sourceStageId)?.delete(dealId);
      const srcOrder = orderIndexRef.current.get(sourceStageId);
      if (srcOrder) {
        const i = srcOrder.indexOf(dealId);
        if (i !== -1) srcOrder.splice(i, 1);
      }
      // Add to dest stage index (prepend — moved card goes to top)
      if (!stageIndexRef.current.has(moveDestStageId)) {
        stageIndexRef.current.set(moveDestStageId, new Set());
        orderIndexRef.current.set(moveDestStageId, []);
      }
      stageIndexRef.current.get(moveDestStageId)!.add(dealId);
      orderIndexRef.current.get(moveDestStageId)!.unshift(dealId);
      bumpVersion();
    });

    void Promise.resolve(moveDeal(dealId, moveDestStageId as DealStage))
      .then(() => {
        inflightMoves.current.delete(dealId);
      })
      .catch(() => {
        inflightMoves.current.delete(dealId);
        if (!previousStage || !mountedRef.current) return;
        // Revert — O(1)
        React.startTransition(() => {
          if (!mountedRef.current) return;
          const revertedDeal = { ...movedDeal, stage: previousStage };
          dealsMapRef.current.set(dealId, revertedDeal);
          stageIndexRef.current.get(moveDestStageId)?.delete(dealId);
          const dstOrder = orderIndexRef.current.get(moveDestStageId);
          if (dstOrder) { const i = dstOrder.indexOf(dealId); if (i !== -1) dstOrder.splice(i, 1); }
          if (!stageIndexRef.current.has(previousStage)) {
            stageIndexRef.current.set(previousStage, new Set());
            orderIndexRef.current.set(previousStage, []);
          }
          stageIndexRef.current.get(previousStage)!.add(dealId);
          orderIndexRef.current.get(previousStage)!.unshift(dealId);
          bumpVersion();
        });
      });
  }, []);


  const onCreateStage = () => { if (newStageName.trim()) onAddStage(newStageName.trim()); };

  const groupIds = React.useMemo(() => groupedStages.map((g) => `group-${g.parentId}`), [groupedStages]);

  // Per-group deal arrays — O(1) per group using the stage index
  // Produces stable references for unaffected groups so React.memo skips them
  const dealsByGroupIdRef = React.useRef(new Map<string, Deal[]>());
  const dealsByGroupId = React.useMemo(() => {
    const stageToGroup = new Map<string, string>();
    for (const g of groupedStages) {
      for (const s of g.columns) stageToGroup.set(s.id, g.parentId);
    }
    const dm = dealsMapRef.current;
    const si = stageIndexRef.current;
    const oi = orderIndexRef.current;
    const prev = dealsByGroupIdRef.current;
    const stable = new Map<string, Deal[]>();

    for (const g of groupedStages) {
      // Collect all deal ids belonging to this group's stages (canonical order from orderIndexRef)
      const ids: string[] = [];
      for (const s of g.columns) {
        for (const id of orderedDealIdsForStage(s.id, si, oi)) {
          ids.push(id);
        }
      }
      const arr = ids.map((id) => dm.get(id)!).filter(Boolean);
      const prevArr = prev.get(g.parentId);
      // Stable ref if same deals in same order
      if (prevArr && prevArr.length === arr.length && prevArr.every((d, i) => d === arr[i])) {
        stable.set(g.parentId, prevArr);
      } else {
        stable.set(g.parentId, arr);
      }
    }
    dealsByGroupIdRef.current = stable;
    return stable;
    // boardVersion change is the signal — we read the Maps directly (mutable refs)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [groupedStages, boardVersion]);

  const boardContextValue = React.useMemo(
    () => ({
      boardUiKey,
      stages: localStages,
      convertedDealIds,
      expandedByParentId,
      onOpen,
      onEdit,
      onDelete,
      onAssignTask,
      canManageStages,
      boardCopy,
      boardPagingEnabled,
      boardPagingEntity,
      businessPipelineId,
      boardListParams,
      inflightStageByDealId,
      stageCounts,
      onColumnDealsChange: handleColumnDealsChangeInternal,
      columnSerialByStageId,
      leadsStagedPaging,
      restStagesUnlocked,
      onLeadsPriorityColumnReady,
      kanbanPermissions,
      canDragCards: canDragCardsOnBoard,
      clientSearchQuery,
      kanbanClientBoardFilters,
      leadsUnifiedSearchActive,
      unifiedSearchBuckets: boardSearchData?.stages,
      canSubject,
      closedWonCustomerPaymentKpis,
      closedWonCustomerPaymentKpisLoading,
      refetchOnMountStageId: firstStageId,
      teams,
      timelineIntents,
    }),
    [
      boardUiKey,
      localStages,
      convertedDealIds,
      expandedByParentId,
      onOpen,
      onEdit,
      onDelete,
      onAssignTask,
      canManageStages,
      boardCopy,
      boardPagingEnabled,
      boardPagingEntity,
      businessPipelineId,
      boardListParams,
      inflightStageByDealId,
      stageCounts,
      handleColumnDealsChangeInternal,
      columnSerialByStageId,
      leadsStagedPaging,
      restStagesUnlocked,
      onLeadsPriorityColumnReady,
      kanbanPermissions,
      canDragCardsOnBoard,
      clientSearchQuery,
      kanbanClientBoardFilters,
      leadsUnifiedSearchActive,
      boardSearchData?.stages,
      canSubject,
      closedWonCustomerPaymentKpis,
      closedWonCustomerPaymentKpisLoading,
      firstStageId,
      teams,
      timelineIntents,
    ],
  );

  return (
    <KanbanDragActiveContext.Provider value={overlayDeal !== null}>
      <KanbanDndRuntime
        key={dndContextKey}
        autoScroll={dndAutoScroll}
        collisionDetection={collisionDetection}
        onDragStart={handleDragStart}
        onDragOver={handleDragOver}
        onDragEnd={handleDragEnd}
        onDragCancel={() => {
          boardDndActiveRef.current = false;
          activeCardIdRef.current = null;
          hoveredStageIdRef.current = null;
          hoveredKanbanDropRef.current = null;
          clearKanbanColumnDropHighlight();
          setOverlayDeal(null);
        }}
      >
        <KanbanBoardProvider value={boardContextValue}>
          <div
            className="flex items-start gap-4 overflow-x-auto overflow-y-visible pb-4 scrollbar-themed"
            style={overlayDeal ? { userSelect: "none", WebkitUserSelect: "none" } : undefined}
            data-dragging={overlayDeal ? "true" : undefined}
          >
            <SortableContext items={groupIds} strategy={horizontalListSortingStrategy}>
              <div className="flex items-start gap-4">
                {groupedStages.length === 0 && (emptyBoardTitle || emptyBoardDescription) && (
                  <div className="flex min-w-[min(100%,360px)] max-w-md shrink-0 items-center justify-center px-2 self-stretch">
                    <NoDataFound message={emptyBoardTitle ?? "No stages"} description={emptyBoardDescription ?? ""} icon={<Plus className="size-5 text-muted-foreground" />} />
                  </div>
                )}
                {groupedStages.map((group) => (
                  <SortableGroup key={group.parentId} groupId={group.parentId} disabled={!canManageStages}>
                    {(isDragging, dragHandleProps) => (
                      <KanbanGroup
                        group={group}
                        deals={dealsByGroupId.get(group.parentId) ?? []}
                        isDragging={isDragging}
                        dragHandleProps={dragHandleProps}
                      />
                    )}
                  </SortableGroup>
                ))}
              </div>
            </SortableContext>

          {canManageStages && (
            <div className="w-[300px] shrink-0 h-full">
              {isAddingStage ? (
                <Card className="border-2 border-accent p-5 shadow-[0_8px_32px_rgba(108,99,255,0.12)] animate-in fade-in zoom-in-95 duration-200 flex flex-col gap-4 rounded-[24px] bg-white ring-4 ring-accent/5">
                  <div className="space-y-1.5">
                    <label className="text-[10px] font-bold text-gray-400 uppercase tracking-widest px-1">Stage Name</label>
                    <Input
                      autoFocus
                      type="text"
                      placeholder="e.g. Qualified Lead"
                      disabled={isAddingStageProp}
                      className="w-full bg-[#f8f9ff] text-[#101828] border border-accent/10 rounded-[16px] px-4 py-3.5 text-[14px] font-semibold outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-accent/20 focus:bg-white transition-all disabled:opacity-60"
                      value={newStageName}
                      onChange={(e) => setNewStageName(e.target.value)}
                      onKeyDown={(e) => {
                        if (e.key === "Enter") onCreateStage();
                        if (e.key === "Escape" && !isAddingStageProp) setIsAddingStage(false);
                      }}
                    />
                  </div>
                  <div className="flex gap-2.5 items-center">
                    <Button variant="accentHeroSubmit" className="h-10 px-6 rounded-[14px] text-xs flex-1" onClick={onCreateStage} disabled={isAddingStageProp || !newStageName.trim()}>
                      {isAddingStageProp ? <span className="flex items-center gap-2 justify-center"><Spinner className="size-4" />Creating...</span> : "Create Stage"}
                    </Button>
                    <Button variant="ghostSoft" className="rounded-[14px] size-10" onClick={() => setIsAddingStage(false)} disabled={isAddingStageProp}>
                      <X size={18} />
                    </Button>
                  </div>
                </Card>
              ) : (
                <Button
                  variant="dashed"
                  onClick={() => setIsAddingStage(true)}
                  className="group min-h-[140px] border-2 border-dashed border-gray-200 bg-gray-50/30 hover:border-accent/40 hover:bg-accent/[0.02] transition-all duration-300 rounded-[24px] flex flex-col gap-4"
                >
                  <div className="size-11 rounded-2xl bg-white shadow-sm border border-gray-100 flex items-center justify-center group-hover:rotate-90 group-hover:scale-110 group-hover:shadow-md group-hover:border-accent/20 transition-all duration-300">
                    <Plus size={22} className="text-gray-400 group-hover:text-accent transition-colors" />
                  </div>
                  <div className="flex flex-col items-center gap-1">
                    <span className="text-[14px] font-black font-['Lexend'] text-gray-500 group-hover:text-accent transition-colors">Add New Stage</span>
                    <span className="text-[10px] font-medium text-gray-400 group-hover:text-accent/70 transition-colors">Expand your pipeline flow</span>
                  </div>
                </Button>
              )}
            </div>
          )}
        </div>
        </KanbanBoardProvider>

        {/* DragOverlay: renders floating card during drag — no ghost, instant */}
        <DragOverlay dropAnimation={null}>
          {overlayDeal ? (
            <div
              className="w-[305px] rotate-[1.5deg] shadow-2xl pointer-events-none"
              style={{ willChange: "transform" }}
            >
              <DealCard
                deal={overlayDeal}
                stages={localStages}
                isAlreadyCustomer={false}
                kanbanPermissions={kanbanPermissions}
                onOpen={onOpen}
                onEdit={onEdit}
                onDelete={onDelete}
                onAssignTask={onAssignTask}
                teams={teams}
                timelineIntents={timelineIntents}
              />
            </div>
          ) : null}
        </DragOverlay>
      </KanbanDndRuntime>
    </KanbanDragActiveContext.Provider>
  );
}
