import {
  Fragment,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from "react";
import { useVirtualizer } from "@tanstack/react-virtual";

// The virtualization engine, lifted out of VirtualFeed so any long client- or server-side list can
// reuse it (the feed's video grid, the channel manager's card/row views). It owns ONLY the layout
// mechanics — measure the container, derive the column count, chunk into rows, virtualize, and place
// each row absolutely — and knows nothing about what an item is. The caller supplies `renderItem`.
//
// ⚠️ Each row is placed with `position:absolute` + `transform` (below), which makes EVERY ROW ITS OWN
// STACKING CONTEXT. A z-indexed child therefore cannot escape its row: a menu or popover rendered
// inside an item draws UNDER the next row, whatever z-index it carries — the channel manager's tag
// picker hit exactly this. Anything floating that an item renders must portal out (see Overlay).
//
// Column sizing mirrors the CSS the feed used before virtualization
// (grid-cols-[repeat(auto-fill,minmax(260px,1fr))] with a gap-4 / 1rem gutter).
const GRID_GAP = 16;
const LIST_GAP = 4;
// Start fetching the next page this many rows before the end scrolls into view
// (keeps the old IntersectionObserver's generous prefetch feel).
const PREFETCH_ROWS = 4;

/** Columns that fit `width`, or 1 for the single-column layout (`minCol == null`). */
function columnsFor(minCol: number | null, width: number): number {
  if (minCol == null) return 1;
  return Math.max(1, Math.floor((width + GRID_GAP) / (minCol + GRID_GAP)));
}

function chunk<T>(arr: T[], size: number): T[][] {
  if (size <= 1) return arr.map((x) => [x]);
  const out: T[][] = [];
  for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
  return out;
}

// The list scrolls inside an <main overflow-y-auto> (the page scroller), not the window, so the
// virtualizer needs that element. Walk up to the nearest scrollable ancestor.
function getScrollParent(node: HTMLElement | null): HTMLElement {
  let el = node?.parentElement ?? null;
  while (el) {
    const oy = getComputedStyle(el).overflowY;
    if (oy === "auto" || oy === "scroll") return el;
    el = el.parentElement;
  }
  return document.documentElement;
}

export interface VirtualGridProps<T> {
  items: T[];
  getKey: (item: T) => string;
  /** Narrowest grid column in px, or null for ONE full-width column. The column count is derived
   * from this and the container width, so every multi-column layout reflows for free. */
  minCol: number | null;
  /** Single-column layout only: the reading-width cap in px (null = fill). */
  maxCol?: number | null;
  /** Estimated row height before measurement; real heights are measured per-row, so this only
   * affects the very first paint and the scrollbar's initial guess. Measure it, don't guess — a
   * bad estimate makes the scrollbar jump as you scroll in. */
  rowEst: number;
  /** Renders one item. `itemWidth` is the measured px width of one cell — for width-derived
   * layouts (e.g. a one-line row that drops columns as it narrows); ignore it otherwise, and to
   * protect a memo'd item from re-rendering on every resize frame, pass a stable value down. */
  renderItem: (item: T, itemWidth: number) => ReactNode;
  /** Infinite scroll — omit all three for a finite client-side list. */
  hasNextPage?: boolean;
  isFetchingNextPage?: boolean;
  fetchNextPage?: () => void;
}

export default function VirtualGrid<T>({
  items,
  getKey,
  minCol,
  maxCol = null,
  rowEst,
  renderItem,
  hasNextPage = false,
  isFetchingNextPage = false,
  fetchNextPage,
}: VirtualGridProps<T>) {
  const listRef = useRef<HTMLDivElement>(null);
  const [scrollEl, setScrollEl] = useState<HTMLElement | null>(null);
  const singleCol = minCol == null;
  const [colCount, setColCount] = useState(singleCol ? 1 : 4);
  // Distance from the scroll element's content top to where this list starts (a toolbar sits above
  // it inside the same scroll area).
  const [scrollMargin, setScrollMargin] = useState(0);
  // How wide ONE row/card actually renders. It has to be the real container width, not the
  // viewport: side rails/panels move it independently of the window, and a viewport breakpoint got
  // this wrong before. This is measured here anyway, for colCount.
  const [itemWidth, setItemWidth] = useState(0);

  useLayoutEffect(() => {
    if (listRef.current) setScrollEl(getScrollParent(listRef.current));
  }, []);

  // Re-measure the column count and start offset on container/scroll-area resize.
  useLayoutEffect(() => {
    const node = listRef.current;
    if (!node) return;
    const measure = () => {
      if (scrollEl) {
        const m =
          node.getBoundingClientRect().top -
          scrollEl.getBoundingClientRect().top +
          scrollEl.scrollTop;
        setScrollMargin(m);
      }
      const w = node.clientWidth;
      const cols = columnsFor(minCol, w);
      setColCount(cols);
      setItemWidth(
        minCol == null ? Math.min(w, maxCol ?? w) : Math.floor((w - GRID_GAP * (cols - 1)) / cols),
      );
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(node);
    if (scrollEl) ro.observe(scrollEl);
    return () => ro.disconnect();
  }, [scrollEl, minCol, maxCol]);

  const rows = useMemo(() => chunk(items, colCount), [items, colCount]);

  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => scrollEl,
    estimateSize: () => rowEst,
    overscan: 4,
    gap: singleCol ? LIST_GAP : GRID_GAP,
    scrollMargin,
  });

  const virtualRows = virtualizer.getVirtualItems();

  // Infinite-scroll trigger: once the last rendered row is within PREFETCH_ROWS of the end, pull
  // the next page. A no-op for finite lists (no fetchNextPage / hasNextPage false).
  useEffect(() => {
    if (!fetchNextPage) return;
    const last = virtualRows[virtualRows.length - 1];
    if (!last) return;
    if (last.index >= rows.length - 1 - PREFETCH_ROWS && hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  }, [virtualRows, rows.length, hasNextPage, isFetchingNextPage, fetchNextPage]);

  return (
    <div ref={listRef} style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
      {virtualRows.map((vr) => {
        const row = rows[vr.index];
        if (!row) return null;
        return (
          <div
            key={vr.key}
            data-index={vr.index}
            ref={virtualizer.measureElement}
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              width: "100%",
              transform: `translateY(${vr.start - virtualizer.options.scrollMargin}px)`,
            }}
          >
            {/* One branch for both shapes: the single-column layout chunks to colCount 1 and gets
                its reading-width cap instead of grid columns. */}
            <div
              className={singleCol ? "mx-auto pb-1" : "grid gap-4"}
              style={
                singleCol
                  ? { maxWidth: maxCol ?? undefined }
                  : { gridTemplateColumns: `repeat(${colCount}, minmax(0, 1fr))` }
              }
            >
              {/* A transparent Fragment carries React's key so the rendered item stays the DIRECT
                  grid/column child — some cards rely on that (e.g. `h-full` stretch, actions pinned
                  to the bottom edge), which a wrapper element would silently break. */}
              {row.map((item) => (
                <Fragment key={getKey(item)}>{renderItem(item, itemWidth)}</Fragment>
              ))}
            </div>
          </div>
        );
      })}
    </div>
  );
}
