import { useEffect, useRef, useState, Component, type ReactNode, startTransition } from 'react'

interface Props {
  /** Children to mount when scrolled near, unmount when scrolled far.
   *  React Query's cache keeps data warm between mounts, so re-entering
   *  the area doesn't re-fetch (within the query's staleTime). */
  children: ReactNode
  /** Optional placeholder rendered while children are unmounted.
   *  Default: a skeleton row sized to match a typical ContentRow so
   *  the page's scroll geometry stays stable when content swaps in
   *  and out. */
  placeholder?: ReactNode
  /** Mount when the wrapper enters this margin around the viewport.
   *  Should be tighter than `unmountMargin` so the user doesn't see
   *  empty content as they scroll towards the row. */
  mountMargin?: string
  /** Unmount when the wrapper leaves this (larger) margin. The gap
   *  between mount + unmount margins gives hysteresis - the row stays
   *  mounted while the user is still scrolling around the visible
   *  area, and only tears down once they've moved well past it. */
  unmountMargin?: string
  /** Optional override on the wrapper className. */
  className?: string
}

/**
 * Windowed mount/unmount for expensive children (data fetches, large
 * image grids, motion trees). Used by HomePage to keep memory bounded
 * during a long scroll - rows mount when you approach them, stay
 * mounted while you're nearby, and unmount once you've scrolled well
 * past them.
 *
 * Two IntersectionObservers with different root margins provide the
 * hysteresis: a tight mount margin (~600px) so content's ready by the
 * time it scrolls into view, and a wider unmount margin (~1500px) so
 * the row doesn't thrash when the user scrolls back and forth across
 * the boundary.
 *
 * Directional unmount: only rows BELOW the viewport are allowed to
 * unmount. Anything above the viewport stays mounted so the layout
 * above the user's scroll position can't shrink (which would jump the
 * scroll). In practice this means scrolling down keeps everything
 * you've passed alive, and scrolling back up unmounts the rows you've
 * left behind underneath.
 */
export default function LazyMount({
  children,
  placeholder,
  mountMargin = '600px',
  unmountMargin = '1500px',
  className,
}: Props) {
  const ref = useRef<HTMLDivElement>(null)
  const [mounted, setMounted] = useState(false)

  useEffect(() => {
    if (typeof IntersectionObserver === 'undefined') {
      // SSR / very old browsers - render immediately so the page
      // doesn't ship blank skeletons forever.
      setMounted(true)
      return
    }
    const el = ref.current
    if (!el) return

    // Mount observer: when the wrapper is within mountMargin of the
    // viewport, become mounted. Stays attached so we re-mount if the
    // user scrolls back into range.
    const mountObs = new IntersectionObserver(
      entries => {
        for (const e of entries) {
          if (e.isIntersecting) {
            startTransition(() => setMounted(true))
          }
        }
      },
      { rootMargin: mountMargin },
    )
    // Unmount observer: only fires when the row is far OUTSIDE the
    // wider unmountMargin AND the row is currently below the viewport
    // (boundingClientRect.top > 0 means its top edge is below the
    // viewport's top). Rows above the viewport never unmount, because
    // shrinking layout above the user's scroll position would yank the
    // scroll position upward.
    const unmountObs = new IntersectionObserver(
      entries => {
        for (const e of entries) {
          if (!e.isIntersecting && e.boundingClientRect.top > 0) {
            setMounted(false)
          }
        }
      },
      { rootMargin: unmountMargin },
    )
    mountObs.observe(el)
    unmountObs.observe(el)
    return () => {
      mountObs.disconnect()
      unmountObs.disconnect()
    }
  }, [mountMargin, unmountMargin])

  return (
    <div ref={ref} className={className}>
      {mounted ? <RowBoundary>{children}</RowBoundary> : (placeholder ?? <DefaultRowSkeleton />)}
    </div>
  )
}

/**
 * Per-row error boundary that isolates a single home-page row from the rest
 * of the page. If a row crashes (bad data, a missing field, a thrown render),
 * only that row goes blank - sibling rows keep rendering. Failures are sent
 * to the console for diagnosis but never surfaced as a user-visible error
 * panel inside the row, since a silent gap is far less disruptive than a
 * red banner in the middle of the home feed.
 */
export class RowBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
  state = { failed: false }
  static getDerivedStateFromError() {
    return { failed: true }
  }
  componentDidCatch(error: unknown) {
    console.warn('[home row] render failed', error)
  }
  render() {
    if (this.state.failed) return null
    return this.props.children
  }
}

function DefaultRowSkeleton() {
  return (
    <section className="mb-10">
      <div className="px-7 mb-3.5">
        <div className="skeleton h-5 w-48 rounded mb-1.5" />
        <div className="skeleton h-3 w-64 rounded" />
      </div>
      <div className="px-7 flex gap-3 overflow-hidden">
        {Array.from({ length: 6 }).map((_, j) => (
          <div key={j} className="shrink-0 w-[160px]">
            <div className="skeleton aspect-[2/3] rounded-lg" />
          </div>
        ))}
      </div>
    </section>
  )
}
