"use client";

import * as React from "react";
import { Plus } from "lucide-react";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import { Spinner } from "@/components/ui/spinner";
import { NoDataFound } from "@/components/ui/no-data-found";
import { cn } from "@/lib/utils";
import { useAuthToken } from "@/hooks/use-auth-token";
import { leadsKeys } from "@/features/leads/api/query-keys";
import {
  fetchLeadsBoardByStageClient,
  LEADS_BOARD_BY_STAGE_FIRST_PAGE_LIMIT,
  LEADS_BOARD_BY_STAGE_PAGE_LIMIT,
} from "@/features/leads/api/fetch-leads-board-by-stage";
import {
  fetchBusinessDealsBoardByStageClient,
  BUSINESS_BOARD_BY_STAGE_FIRST_PAGE_LIMIT,
  BUSINESS_BOARD_BY_STAGE_PAGE_LIMIT,
} from "@/features/business-deals/fetch-business-deals-board-by-stage";
import { mapBusinessRecordToBoardDeal } from "@/features/business-deals/map-business-record-to-board-deal";
import { businessDealsBoardKeys } from "@/features/business-deals/query-keys";
import type { LeadsListParams } from "@/features/leads/types";
import { dealMatchesClientBoardSearch } from "../deal-client-board-search";
import {
  dealMatchesKanbanClientFilters,
  kanbanClientBoardFiltersActive,
} from "./deal-kanban-client-filters";
import {
  dedupeDealsByIdFirst,
  dealIsAlreadyCustomer,
  isClosedWonStage,
  stageColumnIsClosedWon,
  formatKanbanCompactUsd,
  isLostStage,
} from "../deal-utils";
import { LEADS_BOARD_STAGE_FETCH_PRIORITY } from "./kanban-constants";
import { KanbanDragActiveContext } from "./kanban-drag-context";
import { DroppableColumn } from "./kanban-droppable-column";
import { KanbanCardDropShell } from "./kanban-card-drop-shell";
import { DraggableCard } from "./kanban-draggable-card";
import { useKanbanBoardContext } from "./kanban-board-context";
import type { Deal, BoardColumnPage, ColumnProps } from "../types";

export const KanbanColumn = React.memo(function KanbanColumn({
  stage,
  deals,
  fetchStageIds = [],
  unifiedSearchBucket = null,
  stagedLeadsColumnIndex = -1,
  refetchOnMount = false,
}: ColumnProps) {
  const {
    stages,
    convertedDealIds,
    boardCopy,
    boardPagingEnabled,
    boardPagingEntity,
    businessPipelineId,
    boardListParams,
    boardUiKey,
    inflightStageByDealId,
    stageCounts,
    onColumnDealsChange,
    leadsStagedPaging,
    restStagesUnlocked,
    onLeadsPriorityColumnReady,
    canDragCards,
    clientSearchQuery,
    kanbanClientBoardFilters,
    leadsUnifiedSearchActive,
    closedWonCustomerPaymentKpis,
    closedWonCustomerPaymentKpisLoading,
  } = useKanbanBoardContext();
  const { token } = useAuthToken();
  const queryClient = useQueryClient();
  const isDragActive = React.useContext(KanbanDragActiveContext);
  const isDragActiveRef = React.useRef(isDragActive);
  isDragActiveRef.current = isDragActive;
  const scrollRootRef = React.useRef<HTMLDivElement>(null);
  const loadSentinelRef = React.useRef<HTMLDivElement>(null);
  const columnRootRef = React.useRef<HTMLDivElement | null>(null);
  const [columnRootNode, setColumnRootNode] = React.useState<HTMLDivElement | null>(null);
  /** When false, skip card DOM (viewport / collapsed column). Starts true for SSR; layout syncs before paint. */
  const [isColumnVisible, setIsColumnVisible] = React.useState(true);
  const columnRootRefCallback = React.useCallback((node: HTMLDivElement | null) => {
    columnRootRef.current = node;
    setColumnRootNode(node);
  }, []);

  const stageIdsKey = React.useMemo(() => [...fetchStageIds].sort().join("|"), [fetchStageIds]);
  const listParamsSafe = boardListParams ?? ({} as LeadsListParams);
  const bizPid = businessPipelineId ?? "";

  const deferredByLeadsStagedFetch = Boolean(
    leadsStagedPaging && stagedLeadsColumnIndex >= LEADS_BOARD_STAGE_FETCH_PRIORITY && !restStagesUnlocked,
  );

  // Fetch gate — Leads paged board: first columns are eager; the rest unlock when the user
  // scrolls the column scroller so the load sentinel intersects (staged fetch).
  // Sales / business (Deal) board: no staged leads mode — always fetch immediately; otherwise
  // `fetchUnlocked` stayed false forever and the Deal kanban looked broken for everyone (incl. admin).
  const isPrioritySlot =
    leadsStagedPaging &&
    stagedLeadsColumnIndex >= 0 &&
    stagedLeadsColumnIndex < LEADS_BOARD_STAGE_FETCH_PRIORITY;

  const [fetchUnlocked, setFetchUnlocked] = React.useState(
    () =>
      isPrioritySlot ||
      (boardPagingEnabled && boardPagingEntity === "business"),
  );

  React.useEffect(() => {
    if (boardPagingEnabled && boardPagingEntity === "business") {
      setFetchUnlocked(true);
    }
  }, [boardPagingEnabled, boardPagingEntity, bizPid, stageIdsKey]);

  // Unlock non-priority columns when the sentinel reaches the bottom of this column's scroller (not viewport — avoids drag/layout false positives).
  React.useEffect(() => {
    if (fetchUnlocked) return;
    if (!isColumnVisible) return;
    const scrollRoot = scrollRootRef.current;
    const target = loadSentinelRef.current;
    if (!scrollRoot || !target) return;
    const obs = new IntersectionObserver(
      (entries) => {
        if (isDragActiveRef.current) return;
        if (entries[0]?.isIntersecting) setFetchUnlocked(true);
      },
      { root: scrollRoot, rootMargin: "32px", threshold: 0 },
    );
    obs.observe(target);
    return () => obs.disconnect();
  }, [fetchUnlocked, isColumnVisible]);

  const leadPagedQuery = useInfiniteQuery({
    queryKey: leadsKeys.boardColumn(listParamsSafe, stageIdsKey),
    initialPageParam: null as string | null,
    queryFn: ({ pageParam }) =>
      fetchLeadsBoardByStageClient(listParamsSafe, fetchStageIds, {
        limit:
          pageParam == null
            ? LEADS_BOARD_BY_STAGE_FIRST_PAGE_LIMIT
            : LEADS_BOARD_BY_STAGE_PAGE_LIMIT,
        cursor: pageParam,
      }),
    getNextPageParam: (last) => last.nextCursor ?? undefined,
    staleTime: Number.POSITIVE_INFINITY,
    gcTime: 1000 * 60 * 60 * 24,
    refetchOnMount: refetchOnMount ? "always" : false,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
    placeholderData: (p) => p,
    enabled: Boolean(boardPagingEnabled && boardPagingEntity === "leads" && token && boardListParams?.pipelineId && fetchStageIds.length > 0 && !deferredByLeadsStagedFetch && fetchUnlocked && !leadsUnifiedSearchActive),
  });

  const stagingPrioritySlot = leadsStagedPaging && stagedLeadsColumnIndex >= 0 && stagedLeadsColumnIndex < LEADS_BOARD_STAGE_FETCH_PRIORITY;

  React.useEffect(() => {
    if (!stagingPrioritySlot || !onLeadsPriorityColumnReady) return;
    if (fetchStageIds.length === 0) onLeadsPriorityColumnReady(stagedLeadsColumnIndex);
  }, [stagingPrioritySlot, stagedLeadsColumnIndex, fetchStageIds.length, onLeadsPriorityColumnReady]);

  React.useEffect(() => {
    if (!stagingPrioritySlot || !onLeadsPriorityColumnReady) return;
    if (!leadPagedQuery.isEnabled) return;
    if (leadPagedQuery.isSuccess && (leadPagedQuery.data?.pages?.length ?? 0) > 0 && !leadPagedQuery.isFetching) {
      onLeadsPriorityColumnReady(stagedLeadsColumnIndex);
    }
  }, [stagingPrioritySlot, stagedLeadsColumnIndex, onLeadsPriorityColumnReady, leadPagedQuery.isEnabled, leadPagedQuery.isSuccess, leadPagedQuery.isFetching, leadPagedQuery.data?.pages?.length]);

  const bizPagedQuery = useInfiniteQuery({
    queryKey: businessDealsBoardKeys.column(bizPid, stageIdsKey, kanbanClientBoardFilters),
    initialPageParam: null as string | null,
    queryFn: async ({ pageParam }) => {
      const raw = await fetchBusinessDealsBoardByStageClient({
        pipelineId: bizPid,
        stageIds: fetchStageIds,
        filters: kanbanClientBoardFilters,
        limit:
          pageParam == null
            ? BUSINESS_BOARD_BY_STAGE_FIRST_PAGE_LIMIT
            : BUSINESS_BOARD_BY_STAGE_PAGE_LIMIT,
        cursor: pageParam,
      });
      return { ...raw, items: raw.items.map((b) => mapBusinessRecordToBoardDeal(b, bizPid, stages)) } as BoardColumnPage;
    },
    getNextPageParam: (last) => last.nextCursor ?? undefined,
    staleTime: Number.POSITIVE_INFINITY,
    gcTime: 1000 * 60 * 5,
    refetchOnMount: refetchOnMount ? "always" : false,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
    placeholderData: (p) => p,
    enabled: Boolean(boardPagingEnabled && boardPagingEntity === "business" && token && bizPid && fetchStageIds.length > 0 && fetchUnlocked),
  });

  const pagedQuery = boardPagingEntity === "business" ? bizPagedQuery : leadPagedQuery;

  const pagedDeals = React.useMemo(() => {
    if (leadsUnifiedSearchActive) {
      return dedupeDealsByIdFirst(unifiedSearchBucket?.items ?? []);
    }
    const flat = pagedQuery.data?.pages.flatMap((p) => p.items) ?? [];
    return dedupeDealsByIdFirst(flat);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [leadsUnifiedSearchActive, unifiedSearchBucket?.items, pagedQuery.data?.pages]);

  const onColumnDealsChangeRef = React.useRef(onColumnDealsChange);
  React.useLayoutEffect(() => { onColumnDealsChangeRef.current = onColumnDealsChange; });
  const lastReportedOrderSigRef = React.useRef<string | null>(null);
  React.useLayoutEffect(() => {
    if (!boardPagingEnabled || !onColumnDealsChangeRef.current) return;

    const orderSig = pagedDeals.map((d) => `${d.stage}:${d.id}`).join("|");
    if (lastReportedOrderSigRef.current === orderSig) return;

    // Sync before paint so drag/drop has deal rows in the board index as soon as cards render.
    onColumnDealsChangeRef.current?.(stage.id, pagedDeals);
    lastReportedOrderSigRef.current = orderSig;
  }, [boardPagingEnabled, stage.id, pagedDeals]);

  // ── Stagger constants and helpers (must be declared before effects that use them) ──
  const CLIENT_PAGE_SIZE = 10;
  const staggerRevealRef = React.useRef<number | null>(null);
  const staggerTo = React.useCallback((target: number) => {
    if (staggerRevealRef.current) cancelAnimationFrame(staggerRevealRef.current);
    const step = () => {
      setClientVisibleCount((curr) => {
        if (curr >= target) return curr;
        staggerRevealRef.current = requestAnimationFrame(step);
        return curr + 1;
      });
    };
    staggerRevealRef.current = requestAnimationFrame(step);
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
  React.useEffect(() => () => { if (staggerRevealRef.current) cancelAnimationFrame(staggerRevealRef.current); }, []);
  const [clientVisibleCount, setClientVisibleCount] = React.useState(CLIENT_PAGE_SIZE);
  const prevPagedCountRef = React.useRef(0);
  React.useEffect(() => { setClientVisibleCount(CLIENT_PAGE_SIZE); }, [stage.id, clientSearchQuery, kanbanClientBoardFilters]);

  // Reset prevPagedCountRef when pagedDeals resets (e.g. pipeline change)
  React.useEffect(() => {
    if (pagedDeals.length === 0) prevPagedCountRef.current = 0;
  }, [pagedDeals.length]);

  // Non-paged columns: sentinel + viewport IO reveals more rows (client-side stagger).
  React.useEffect(() => {
    if (boardPagingEnabled) return;
    const target = loadSentinelRef.current;
    if (!target) return;

    const obs = new IntersectionObserver(
      (entries) => {
        const intersecting = Boolean(entries[0]?.isIntersecting);
        if (isDragActiveRef.current) return;
        if (intersecting) {
          staggerTo(clientVisibleCount + CLIENT_PAGE_SIZE);
        }
      },
      { rootMargin: "120px", threshold: 0 },
    );
    obs.observe(target);
    return () => obs.disconnect();
  }, [boardPagingEnabled, clientVisibleCount, staggerTo]);

  React.useLayoutEffect(() => {
    const node = columnRootNode;
    if (!node) return;

    const applyEntry = (entry?: IntersectionObserverEntry) => {
      if (entry) {
        setIsColumnVisible(entry.isIntersecting || entry.intersectionRatio > 0);
        return;
      }
      const r = node.getBoundingClientRect();
      const buf = 56;
      const vw = window.innerWidth;
      const vh = window.innerHeight;
      setIsColumnVisible(
        r.width > 2 &&
        r.height > 2 &&
        r.bottom > -buf &&
        r.top < vh + buf &&
        r.right > -buf &&
        r.left < vw + buf,
      );
    };

    applyEntry();
    const observer = new IntersectionObserver(([e]) => applyEntry(e), {
      root: null,
      rootMargin: "72px",
      threshold: [0, 0.01, 0.05],
    });
    observer.observe(node);
    return () => observer.disconnect();
  }, [columnRootNode]);

  // Manual refresh invalidation
  React.useEffect(() => {
    const key = `kanban-refresh-${stage.id}`;
    const handler = () => {
      const params = boardListParams ?? ({} as LeadsListParams);
      if (boardPagingEntity === "leads") {
        if (!params.pipelineId) return;
        if (leadsUnifiedSearchActive && params.search?.trim()) {
          void queryClient.invalidateQueries({
            predicate: (query) => {
              const k = query.queryKey;
              return (
                Array.isArray(k) &&
                k[0] === "leads" &&
                k[1] === "board" &&
                k[2] === "search"
              );
            },
          });
        }
        void queryClient.invalidateQueries({ queryKey: leadsKeys.boardColumn(params, stageIdsKey) });
        void queryClient.invalidateQueries({ queryKey: leadsKeys.boardCounts(params) });
        return;
      }
      if (!businessPipelineId) return;
      void queryClient.invalidateQueries({ queryKey: businessDealsBoardKeys.column(bizPid, stageIdsKey) });
      void queryClient.invalidateQueries({ queryKey: businessDealsBoardKeys.counts(bizPid) });
    };
    window.addEventListener(key, handler);
    return () => window.removeEventListener(key, handler);
  }, [
    boardPagingEnabled,
    fetchStageIds.length,
    boardPagingEntity,
    boardListParams,
    businessPipelineId,
    queryClient,
    stageIdsKey,
    bizPid,
    stage.id,
  ]);

  /** Paged board: only merge rows for this column's stages (not the full board array). */
  const dealsForColumnMerge = React.useMemo(() => {
    if (!boardPagingEnabled || fetchStageIds.length === 0) return deals;
    const allow = new Set(fetchStageIds);
    return deals.filter((d) => allow.has(d.stage));
  }, [boardPagingEnabled, deals, fetchStageIds]);

  // sourceDeals: server paged order is canonical; append parent-only rows (e.g. optimistic) not yet in pages.
  const sourceDeals = React.useMemo(() => {
    if (!boardPagingEnabled) return deals;
    const pagedIds = new Set(pagedDeals.map((d) => d.id));
    const out: Deal[] = [...pagedDeals];
    for (const d of dealsForColumnMerge) {
      if (pagedIds.has(d.id)) continue;
      out.push(d);
    }
    if (!inflightStageByDealId?.size && fetchStageIds.length === 0) {
      return out;
    }
    const allow = new Set(fetchStageIds);
    const seenIds = new Set<string>();
    return out.filter((d) => {
      if (seenIds.has(d.id)) return false;
      const inflightStage = inflightStageByDealId?.get(d.id);
      const effectiveStage = inflightStage ?? d.stage;
      if (!allow.has(effectiveStage)) return false;
      seenIds.add(d.id);
      return true;
    });
  }, [
    boardPagingEnabled,
    deals,
    dealsForColumnMerge,
    pagedDeals,
    inflightStageByDealId,
    fetchStageIds,
  ]);

  const sortedDeals = React.useMemo(() => {
    const list = [...sourceDeals];
    if (boardPagingEnabled) {
      return list;
    }
    if (boardPagingEntity === "business") {
      return (list as any[]).sort((a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime());
    }
    return list.sort((a, b) => {
      const ao = a.stageOrder ?? 0;
      const bo = b.stageOrder ?? 0;
      if (ao !== bo) return ao - bo;
      return new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime();
    });
  }, [sourceDeals, boardPagingEnabled, boardPagingEntity]);

  const clientSearchQueryTrimmed = React.useMemo(
    () => clientSearchQuery?.trim() ?? "",
    [clientSearchQuery],
  );
  const clientFilterActive = Boolean(clientSearchQueryTrimmed) || kanbanClientBoardFiltersActive(kanbanClientBoardFilters);

  const filterPredicate = React.useMemo(() => {
    const hasSearch = Boolean(clientSearchQueryTrimmed);
    const hasFilters = kanbanClientBoardFiltersActive(kanbanClientBoardFilters);
    if (!hasSearch && !hasFilters) return undefined;
    return (deal: Deal) => {
      if (hasFilters && !dealMatchesKanbanClientFilters(deal, kanbanClientBoardFilters!)) {
        return false;
      }
      if (hasSearch && !dealMatchesClientBoardSearch(deal, clientSearchQueryTrimmed)) {
        return false;
      }
      return true;
    };
  }, [clientSearchQueryTrimmed, kanbanClientBoardFilters]);

  const filteredDeals = React.useMemo(() => {
    if (!filterPredicate) return sortedDeals;
    return sortedDeals.filter(filterPredicate);
  }, [sortedDeals, filterPredicate]);

  const displayDeals = React.useMemo(() => {
    let list = sortedDeals;
    if (kanbanClientBoardFiltersActive(kanbanClientBoardFilters)) {
      list = list.filter((d) => dealMatchesKanbanClientFilters(d, kanbanClientBoardFilters!));
    }
    if (clientSearchQuery?.trim()) {
      list = list.filter((d) => dealMatchesClientBoardSearch(d, clientSearchQuery));
    }
    // For paged boards: show all fetched deals — virtualizer handles DOM rendering
    // For non-paged: slice to staggered visible count
    if (boardPagingEnabled) return list;
    return list.slice(0, clientVisibleCount);
  }, [sortedDeals, clientSearchQuery, kanbanClientBoardFilters, boardPagingEnabled, clientVisibleCount]);

  const hasClientMore = sortedDeals.length > clientVisibleCount;

  // For paged boards: server stageCounts is the accurate total (may be 5000+).
  // For non-paged: sortedDeals.length is exact.
  // On an active inflight move, adjust the count optimistically to avoid double-flash.
  const serverCount = boardPagingEnabled
    ? leadsUnifiedSearchActive
      ? (unifiedSearchBucket?.total ?? 0)
      : fetchStageIds.reduce((s, id) => s + (stageCounts[id] ?? 0), 0)
    : sortedDeals.length;

  const totalCountLabel = boardPagingEnabled
    ? serverCount
    : clientFilterActive
      ? filteredDeals.length
      : sortedDeals.length;

  const waitingForLeadsStagedUnlock = boardPagingEnabled && boardPagingEntity === "leads" && deferredByLeadsStagedFetch;
  const waitingForFetchUnlock =
    boardPagingEnabled &&
    boardPagingEntity === "leads" &&
    !fetchUnlocked &&
    !leadsUnifiedSearchActive;
  const shouldVirtualize =
    isColumnVisible && (boardPagingEnabled || displayDeals.length > 0);

  const rowVirtualizer = useVirtualizer({
    count: displayDeals.length,
    getScrollElement: () => scrollRootRef.current,
    estimateSize: () => 208, // ~196px card + 12px gap (pb-3)
    overscan: 2,
    enabled: isColumnVisible && displayDeals.length > 0,
    getItemKey: (i) => displayDeals[i]?.id ?? i,
  });

  const virtualRows = shouldVirtualize ? rowVirtualizer.getVirtualItems() : [];
  const columnLoading = boardPagingEnabled && (
    leadsUnifiedSearchActive
      ? false
      : waitingForFetchUnlock ||
        waitingForLeadsStagedUnlock ||
        (pagedQuery.isPending && sortedDeals.length === 0)
  );

  const refreshIconSpinning = pagedQuery.isFetching && !pagedQuery.isFetchingNextPage;

  /** TanStack Virtual + useInfiniteQuery: load next page when the user scrolls near the end of already-fetched rows (official infinite-scroll pattern). */
  const lastVisibleRowIndex =
    virtualRows.length > 0 ? virtualRows[virtualRows.length - 1].index : -1;

  const pagedQueryRef = React.useRef(pagedQuery);
  pagedQueryRef.current = pagedQuery;

  React.useEffect(() => {
    if (!boardPagingEnabled || !fetchUnlocked) return;
    if (leadsUnifiedSearchActive) return;
    if (waitingForLeadsStagedUnlock) return;
    if (displayDeals.length === 0) return;
    if (lastVisibleRowIndex < 0) return;

    // Header search: server + client both filter, but merged optimistic/parent rows can stay
    // in `sortedDeals`. The virtualizer only sees `displayDeals`, so "near end" stays true
    // while `hasNextPage` is still true for the stage — spamming /deals/board/by-stage (100/min).
    if (
      clientSearchQueryTrimmed.length > 0 &&
      displayDeals.length < sortedDeals.length
    ) {
      return;
    }

    const NEAR_END = 6;
    if (lastVisibleRowIndex < displayDeals.length - NEAR_END) return;

    const q = pagedQueryRef.current;
    if (!q.hasNextPage || q.isFetchingNextPage) return;

    void q.fetchNextPage();
  }, [
    boardPagingEnabled,
    fetchUnlocked,
    waitingForLeadsStagedUnlock,
    displayDeals.length,
    sortedDeals.length,
    clientSearchQueryTrimmed,
    lastVisibleRowIndex,
    pagedQuery.hasNextPage,
    pagedQuery.isFetchingNextPage,
  ]);

  const dropTargetStageIds = React.useMemo(
    () => (fetchStageIds.length > 0 ? fetchStageIds : [stage.id]),
    [fetchStageIds, stage.id],
  );

  return (
    <DroppableColumn
      ref={columnRootRefCallback}
      stageId={stage.id}
      dropTargetStageIds={dropTargetStageIds}
    >
      {/* Column header */}
      <div className="flex items-center gap-2 shrink-0">
        <div className="flex items-center gap-2.5 flex-1 min-w-0">
          <div className="size-3 rounded-full shrink-0 shadow-sm" style={{ backgroundColor: stage.color }} />
          <h3 className="text-[13px] font-extrabold text-text font-['Lexend'] uppercase tracking-[0.04em] truncate">
            {stage.name}
          </h3>
          <span className="text-[11px] font-bold text-white bg-accent/80 px-2 py-0.5 rounded-full ml-auto shrink-0">
            {totalCountLabel}
          </span>
        </div>
        {boardPagingEnabled && (
          <button
            type="button"
            className="shrink-0 text-muted-foreground hover:text-accent transition-colors"
            onClick={() => window.dispatchEvent(new Event(`kanban-refresh-${stage.id}`))}
            title="Refresh column"
          >
            <svg className={cn("size-3.5", refreshIconSpinning && "animate-spin")} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
              <path d="M23 4v6h-6M1 20v-6h6" /><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
            </svg>
          </button>
        )}
      </div>

      {/* Scrollable card list — separate from droppable ref so scroll works */}
      <div
        ref={scrollRootRef}
        className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden scrollbar-themed flex flex-col gap-3 overscroll-contain"
      >
        {columnLoading && (
          <div className="flex justify-center py-6"><Spinner className="size-6 text-accent" /></div>
        )}
        {!columnLoading && displayDeals.length === 0 && (
          <NoDataFound message={boardCopy.emptyColumnMessage} description={boardCopy.emptyColumnDescription} icon={<Plus className="size-5 text-muted-foreground" />} />
        )}
        {!columnLoading && shouldVirtualize ? (
          <div className="relative w-full" style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
            {virtualRows.map((virtualRow) => {
              const deal = displayDeals[virtualRow.index];
              if (!deal) return null;
              const isConverted = dealIsAlreadyCustomer(deal, convertedDealIds);
              const isLost = isLostStage(deal, stages);
              const isMoveable = !isLost && (!isConverted || !isClosedWonStage(deal, stages));
              return (
                <div
                  key={virtualRow.key}
                  data-index={virtualRow.index}
                  ref={isDragActive ? undefined : rowVirtualizer.measureElement}
                  className="absolute left-0 top-0 w-full"
                  style={{ transform: `translateY(${virtualRow.start}px)`, willChange: "transform" }}
                >
                  <KanbanCardDropShell stageId={deal.stage} dealId={deal.id}>
                    <DraggableCard
                      deal={deal}
                      isAlreadyCustomer={isConverted}
                      disabled={!isMoveable || !canDragCards}
                    />
                  </KanbanCardDropShell>
                </div>
              );
            })}
          </div>
        ) : null}
        {!columnLoading &&
          isColumnVisible &&
          !shouldVirtualize
          ? displayDeals.map((deal) => {
            const isConverted = dealIsAlreadyCustomer(deal, convertedDealIds);
            const isLost = isLostStage(deal, stages);
            const isMoveable = !isLost && (!isConverted || !isClosedWonStage(deal, stages));
            return (
              <KanbanCardDropShell key={deal.id} stageId={deal.stage} dealId={deal.id}>
                <DraggableCard
                  deal={deal}
                  isAlreadyCustomer={isConverted}
                  disabled={!isMoveable || !canDragCards}
                />
              </KanbanCardDropShell>
            );
          })
          : null}
        {isColumnVisible && boardPagingEnabled ? (
          <div ref={loadSentinelRef} className="flex min-h-6 shrink-0 items-center justify-center py-1">
            {pagedQuery.isFetchingNextPage ? <Spinner className="size-4 text-accent" /> : null}
          </div>
        ) : isColumnVisible && hasClientMore ? (
          <div ref={loadSentinelRef} className="flex min-h-6 shrink-0 items-center justify-center py-1" />
        ) : null}
      </div>{/* end scroll div */}

      {/* Footer — business Closed Won only: customer payment-kpis upfront (API). */}
      {boardPagingEntity === "business" && stageColumnIsClosedWon(stage) ? (
        <div className="flex flex-col gap-1 shrink-0">
          {closedWonCustomerPaymentKpisLoading &&
          !closedWonCustomerPaymentKpis ? (
            <div className="flex justify-between items-center min-h-[18px]">
              <span className="text-[12px] font-semibold text-[#101828]">
                Upfront
              </span>
              <Spinner className="size-3.5 text-accent shrink-0" />
            </div>
          ) : closedWonCustomerPaymentKpis ? (
            <div className="flex justify-between items-center gap-2">
              <span className="text-[12px] font-semibold text-[#101828] shrink-0">
                Upfront
              </span>
              <span className="text-[12px] font-semibold text-[#4f39f6] tabular-nums whitespace-nowrap">
                {formatKanbanCompactUsd(
                  closedWonCustomerPaymentKpis.totalUpfrontValue,
                )}
              </span>
            </div>
          ) : null}
        </div>
      ) : null}
    </DroppableColumn>
  );
});