import type { HTMLAttributes, ReactNode } from 'react';
import { useEffect, useRef } from 'react';
import './infinite-scroll.css';

export type InfiniteScrollProps = {
  children: ReactNode;
  /** When false, the sentinel is not rendered and no loads are requested. */
  hasMore: boolean;
  /** Called when the sentinel intersects the root. May return a Promise. */
  onLoadMore: () => void | Promise<void>;
  /** While true, `onLoadMore` is not invoked (prevents duplicate fetches). */
  loading?: boolean;
  /** When true, the observer is not attached. */
  disabled?: boolean;
  /**
   * Intersection root. Defaults to the browser viewport (`null`).
   * For a scrollable panel, pass that element (e.g. from `useState` + `ref` callback).
   */
  root?: Element | Document | null;
  rootMargin?: string;
  threshold?: number | number[];
  /** Optional loading UI rendered above the sentinel while `loading` is true. */
  loader?: ReactNode;
} & Omit<HTMLAttributes<HTMLDivElement>, 'children'>;

/**
 * Appends a sentinel observed with {@link IntersectionObserver}. When the sentinel
 * enters the intersection root, `onLoadMore` runs unless `loading`, `disabled`, or
 * `hasMore` blocks it. The observer is recreated when `loading` clears so another
 * batch can load if the sentinel remains visible (short lists).
 */
export function InfiniteScroll({
  children,
  hasMore,
  onLoadMore,
  loading = false,
  disabled = false,
  root = null,
  rootMargin = '0px',
  threshold = 0,
  loader,
  className,
  ...rest
}: InfiniteScrollProps) {
  const sentinelRef = useRef<HTMLDivElement>(null);
  const loadingRef = useRef(loading);
  const hasMoreRef = useRef(hasMore);
  const disabledRef = useRef(disabled);
  const onLoadMoreRef = useRef(onLoadMore);

  useEffect(() => {
    loadingRef.current = loading;
  }, [loading]);
  useEffect(() => {
    hasMoreRef.current = hasMore;
  }, [hasMore]);
  useEffect(() => {
    disabledRef.current = disabled;
  }, [disabled]);
  useEffect(() => {
    onLoadMoreRef.current = onLoadMore;
  }, [onLoadMore]);

  useEffect(() => {
    if (typeof IntersectionObserver === 'undefined') return;
    if (!hasMore || disabled) return;

    const el = sentinelRef.current;
    if (!el) return;

    const observer = new IntersectionObserver(
      (entries) => {
        const hit = entries.some((entry) => entry.isIntersecting);
        if (!hit) return;
        if (disabledRef.current || !hasMoreRef.current || loadingRef.current) return;
        void Promise.resolve(onLoadMoreRef.current());
      },
      {
        root: root ?? undefined,
        rootMargin,
        threshold,
      }
    );

    observer.observe(el);
    return () => observer.disconnect();
  }, [disabled, hasMore, loading, root, rootMargin, threshold]);

  return (
    <div className={['ccl-infinite-scroll', className].filter(Boolean).join(' ')} {...rest}>
      {children}
      {loading && loader ? <div className="ccl-infinite-scroll__loader">{loader}</div> : null}
      {hasMore ? <div ref={sentinelRef} className="ccl-infinite-scroll__sentinel" aria-hidden /> : null}
    </div>
  );
}
