import { Index, Show, createEffect, createMemo, createSignal, onCleanup, type Accessor, type JSX } from "solid-js"
import VirtualItem, { type VirtualItemHeightChangeMeta } from "./virtual-item"

const DEFAULT_SCROLL_SENTINEL_MARGIN_PX = 48
const USER_SCROLL_INTENT_WINDOW_MS = 600
const SCROLL_INTENT_KEYS = new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ", "Spacebar"])

export interface VirtualFollowListApi {
  scrollToTop: (opts?: { immediate?: boolean }) => void
  scrollToBottom: (opts?: { immediate?: boolean; suppressAutoAnchor?: boolean }) => void
  scrollToKey: (
    key: string,
    opts?: { behavior?: ScrollBehavior; block?: ScrollLogicalPosition; setAutoScroll?: boolean },
  ) => void
  notifyContentRendered: () => void
  setAutoScroll: (enabled: boolean) => void
  getAutoScroll: () => boolean
  getScrollElement: () => HTMLDivElement | undefined
  getShellElement: () => HTMLDivElement | undefined
}

export interface VirtualFollowListState {
  autoScroll: Accessor<boolean>
  showScrollTopButton: Accessor<boolean>
  showScrollBottomButton: Accessor<boolean>
  scrollButtonsCount: Accessor<number>
  activeKey: Accessor<string | null>
}

export interface VirtualFollowListProps<T> {
  items: Accessor<T[]>
  getKey: (item: T, index: number) => string
  renderItem: (item: T, index: number) => JSX.Element

  /**
   * Optional stable DOM id for the item wrapper.
   * Defaults to the key itself.
   */
  getAnchorId?: (key: string) => string

  /**
   * Decode an item key from an observed wrapper element id.
   * Defaults to identity.
   */
  getKeyFromAnchorId?: (anchorId: string) => string

  overscanPx?: number
  scrollSentinelMarginPx?: number
  virtualizationEnabled?: Accessor<boolean>
  suspendMeasurements?: Accessor<boolean>
  loading?: Accessor<boolean>
  isActive?: Accessor<boolean>

  /**
   * When switching back to an inactive (cached) pane, the list historically
   * re-pinned to the bottom if autoScroll was enabled.
   *
   * Disable this to preserve the existing scroll position across pane switches.
   */
  scrollToBottomOnActivate?: Accessor<boolean>

  /**
   * Controls whether the list should scroll to bottom the first time items
   * appear (default behavior for chat streams).
   *
   * Set to false when an outer component restores scroll from a cache.
   */
  initialScrollToBottom?: Accessor<boolean>

  /**
   * Initial value for the internal autoScroll signal.
   * Useful when restoring scroll state (e.g. start in non-follow mode).
   */
  initialAutoScroll?: Accessor<boolean>

  /**
   * When this value changes, the list resets internal follow/anchor state.
   * Useful when reusing the same list instance across different datasets.
   */
  resetKey?: Accessor<string | number>

  /**
   * If this value changes and autoScroll is enabled, the list will
   * anchor-scroll to the bottom (unless suppressed).
   */
  followToken?: Accessor<string | number>

  /**
   * Optional hooks to render content inside the scroll container.
   * Useful for empty/loading states that should scroll with the list.
   */
  renderBeforeItems?: Accessor<JSX.Element>

  /**
   * Render content inside the shell, above timeline/sidebar layers.
   * (Quote popovers, etc.)
   */
  renderOverlay?: Accessor<JSX.Element>

  /**
   * Provide localized labels for built-in controls.
   */
  scrollToTopAriaLabel?: Accessor<string>
  scrollToBottomAriaLabel?: Accessor<string>

  /**
   * Receive element refs for external logic (selection, geometry, etc.)
   */
  onScrollElementChange?: (element: HTMLDivElement | undefined) => void
  onShellElementChange?: (element: HTMLDivElement | undefined) => void

  /**
   * Callbacks for consumers.
   */
  onScroll?: () => void
  onMouseUp?: (event: MouseEvent) => void
  onClick?: (event: MouseEvent) => void
  onActiveKeyChange?: (key: string | null) => void
  registerApi?: (api: VirtualFollowListApi) => void
  registerState?: (state: VirtualFollowListState) => void
  renderControls?: (state: VirtualFollowListState, api: VirtualFollowListApi) => JSX.Element
}

export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
  const getAnchorId = (key: string) => (props.getAnchorId ? props.getAnchorId(key) : key)
  const getKeyFromAnchorId = (anchorId: string) => (props.getKeyFromAnchorId ? props.getKeyFromAnchorId(anchorId) : anchorId)

  const [scrollElement, setScrollElement] = createSignal<HTMLDivElement | undefined>()
  const [shellElement, setShellElement] = createSignal<HTMLDivElement | undefined>()
  const [topSentinel, setTopSentinel] = createSignal<HTMLDivElement | null>(null)
  const [bottomSentinelSignal, setBottomSentinelSignal] = createSignal<HTMLDivElement | null>(null)
  const bottomSentinel = () => bottomSentinelSignal()

  const isActive = () => (props.isActive ? props.isActive() : true)
  const scrollToBottomOnActivate = () => (props.scrollToBottomOnActivate ? props.scrollToBottomOnActivate() : true)
  const initialScrollToBottom = () => (props.initialScrollToBottom ? props.initialScrollToBottom() : true)
  const initialAutoScroll = () => (props.initialAutoScroll ? props.initialAutoScroll() : true)
  const isLoading = () => Boolean(props.loading?.())
  const virtualizationEnabled = () => (props.virtualizationEnabled ? props.virtualizationEnabled() : true)
  const measurementsSuspended = () => Boolean(props.suspendMeasurements?.())

  const [autoScroll, setAutoScroll] = createSignal(Boolean(initialAutoScroll()))
  const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
  const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
  const [topSentinelVisible, setTopSentinelVisible] = createSignal(true)
  const [bottomSentinelVisible, setBottomSentinelVisible] = createSignal(true)
  const [activeKey, setActiveKey] = createSignal<string | null>(null)

  const [anchorLock, setAnchorLock] = createSignal<{ key: string; block: ScrollLogicalPosition } | null>(null)

  const scrollButtonsCount = createMemo(() => (showScrollTopButton() ? 1 : 0) + (showScrollBottomButton() ? 1 : 0))

  let containerRef: HTMLDivElement | undefined
  let shellRef: HTMLDivElement | undefined
  let pendingScrollFrame: number | null = null
  let pendingAnchorScroll: number | null = null
  let pendingAnchorCorrectionFrame: number | null = null
  let pendingScrollCompensationScheduled = false
  let pendingScrollCompensations = new Map<string, number>()
  let scrollCompensationGen = 0
  let pendingActiveScroll = false
  let suppressAutoScrollOnce = false
  let pendingInitialScroll = true
  let scrollToBottomFrame: number | null = null
  let scrollToBottomDelayedFrame: number | null = null

  let lastKnownScrollTop = 0
  let lastUserScrollIntentDirection: "up" | "down" | null = null

  let userScrollIntentUntil = 0
  let detachScrollIntentListeners: (() => void) | undefined

  let lastResetKey: string | number | undefined

  const state: VirtualFollowListState = {
    autoScroll,
    showScrollTopButton,
    showScrollBottomButton,
    scrollButtonsCount,
    activeKey,
  }

  function markUserScrollIntent(direction?: "up" | "down" | null) {
    const now = typeof performance !== "undefined" ? performance.now() : Date.now()
    userScrollIntentUntil = now + USER_SCROLL_INTENT_WINDOW_MS
    if (direction) {
      lastUserScrollIntentDirection = direction
    }
  }

  function hasUserScrollIntent() {
    const now = typeof performance !== "undefined" ? performance.now() : Date.now()
    return now <= userScrollIntentUntil
  }

  function attachScrollIntentListeners(element: HTMLDivElement | undefined) {
    if (detachScrollIntentListeners) {
      detachScrollIntentListeners()
      detachScrollIntentListeners = undefined
    }
    if (!element) return
    const handleWheelIntent = (event: WheelEvent) => {
      const dir: "up" | "down" | null = event.deltaY < 0 ? "up" : event.deltaY > 0 ? "down" : null
      markUserScrollIntent(dir)
    }
    const handlePointerIntent = () => markUserScrollIntent(null)
    const handleKeyIntent = (event: KeyboardEvent) => {
      if (!SCROLL_INTENT_KEYS.has(event.key)) return
      const key = event.key
      const dir: "up" | "down" | null =
        key === "ArrowUp" || key === "PageUp" || key === "Home"
          ? "up"
          : key === "ArrowDown" || key === "PageDown" || key === "End"
            ? "down"
            : key === " " || key === "Spacebar"
              ? event.shiftKey
                ? "up"
                : "down"
              : null
      markUserScrollIntent(dir)
    }
    element.addEventListener("wheel", handleWheelIntent, { passive: true })
    element.addEventListener("pointerdown", handlePointerIntent)
    element.addEventListener("touchstart", handlePointerIntent, { passive: true })
    element.addEventListener("keydown", handleKeyIntent)
    detachScrollIntentListeners = () => {
      element.removeEventListener("wheel", handleWheelIntent)
      element.removeEventListener("pointerdown", handlePointerIntent)
      element.removeEventListener("touchstart", handlePointerIntent)
      element.removeEventListener("keydown", handleKeyIntent)
    }
  }

  function updateScrollIndicatorsFromVisibility() {
    const hasItems = props.items().length > 0
    const bottomVisible = bottomSentinelVisible()
    const topVisible = topSentinelVisible()
    setShowScrollBottomButton(hasItems && !bottomVisible)
    setShowScrollTopButton(hasItems && !topVisible)
  }

  function clearScrollToBottomFrames() {
    if (scrollToBottomFrame !== null) {
      cancelAnimationFrame(scrollToBottomFrame)
      scrollToBottomFrame = null
    }
    if (scrollToBottomDelayedFrame !== null) {
      cancelAnimationFrame(scrollToBottomDelayedFrame)
      scrollToBottomDelayedFrame = null
    }
  }

  function scrollToBottom(immediate = false, options?: { suppressAutoAnchor?: boolean }) {
    if (!containerRef) return
    if (anchorLock()) {
      clearAnchorLock()
    }
    const sentinel = bottomSentinel()
    const behavior: ScrollBehavior = immediate ? "auto" : "smooth"
    const suppressAutoAnchor = options?.suppressAutoAnchor ?? !immediate
    if (suppressAutoAnchor) {
      suppressAutoScrollOnce = true
    }
    sentinel?.scrollIntoView({ block: "end", inline: "nearest", behavior })
    setAutoScroll(true)
  }

  function requestScrollToBottom(immediate = true) {
    if (!isActive()) {
      pendingActiveScroll = true
      return
    }
    if (!containerRef || !bottomSentinel()) {
      pendingActiveScroll = true
      return
    }
    pendingActiveScroll = false
    clearScrollToBottomFrames()
    scrollToBottomFrame = requestAnimationFrame(() => {
      scrollToBottomFrame = null
      scrollToBottomDelayedFrame = requestAnimationFrame(() => {
        scrollToBottomDelayedFrame = null
        scrollToBottom(immediate)
      })
    })
  }

  function resolvePendingActiveScroll() {
    if (!pendingActiveScroll) return
    if (!isActive()) return
    requestScrollToBottom(true)
  }

  function scrollToTop(immediate = false) {
    if (!containerRef) return
    const behavior: ScrollBehavior = immediate ? "auto" : "smooth"
    if (anchorLock()) {
      clearAnchorLock()
    }
    setAutoScroll(false)
    topSentinel()?.scrollIntoView({ block: "start", inline: "nearest", behavior })
  }

  function scheduleAnchorScroll(immediate = false) {
    if (!autoScroll()) return
    if (!isActive()) {
      pendingActiveScroll = true
      return
    }
    const sentinel = bottomSentinel()
    if (!sentinel) {
      pendingActiveScroll = true
      return
    }
    if (pendingAnchorScroll !== null) {
      cancelAnimationFrame(pendingAnchorScroll)
      pendingAnchorScroll = null
    }
    pendingAnchorScroll = requestAnimationFrame(() => {
      pendingAnchorScroll = null
      sentinel.scrollIntoView({ block: "end", inline: "nearest", behavior: immediate ? "auto" : "smooth" })
    })
  }

  function clearAnchorLock() {
    setAnchorLock(null)
    if (pendingAnchorCorrectionFrame !== null) {
      cancelAnimationFrame(pendingAnchorCorrectionFrame)
      pendingAnchorCorrectionFrame = null
    }
  }

  function computeDesiredOffset(block: ScrollLogicalPosition, container: HTMLElement, anchorRect: DOMRect) {
    if (block === "end") {
      return Math.max(0, container.clientHeight - anchorRect.height)
    }
    if (block === "center") {
      return Math.max(0, container.clientHeight / 2 - anchorRect.height / 2)
    }
    // Default to start.
    return 0
  }

  function applyAnchorCorrection() {
    const lock = anchorLock()
    if (!lock) return
    if (autoScroll()) return
    if (!containerRef) return
    if (typeof document === "undefined") return

    const anchorId = getAnchorId(lock.key)
    const anchor = document.getElementById(anchorId)
    if (!anchor) return

    const containerRect = containerRef.getBoundingClientRect()
    const anchorRect = anchor.getBoundingClientRect()
    const currentOffset = anchorRect.top - containerRect.top
    const desiredOffset = computeDesiredOffset(lock.block, containerRef, anchorRect)
    const delta = currentOffset - desiredOffset
    if (!Number.isFinite(delta) || Math.abs(delta) < 0.5) {
      return
    }
    const nextTop = containerRef.scrollTop + delta
    const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0)
    containerRef.scrollTop = Math.min(maxScrollTop, Math.max(0, nextTop))
  }

  function scheduleAnchorCorrection() {
    if (pendingAnchorCorrectionFrame !== null) return
    pendingAnchorCorrectionFrame = requestAnimationFrame(() => {
      pendingAnchorCorrectionFrame = null
      applyAnchorCorrection()
    })
  }

  function handleContentRendered() {
    if (autoScroll() && !anchorLock()) {
      scheduleAutoPinToBottom()
      return
    }
    if (anchorLock() && !autoScroll()) {
      scheduleAnchorCorrection()
      return
    }
  }

  function handleScroll() {
    if (!containerRef) return
    if (pendingScrollFrame !== null) {
      cancelAnimationFrame(pendingScrollFrame)
    }
    const isUserScroll = hasUserScrollIntent()
    pendingScrollFrame = requestAnimationFrame(() => {
      pendingScrollFrame = null
      if (!containerRef) return
      const previousScrollTop = lastKnownScrollTop
      const currentScrollTop = containerRef.scrollTop
      const deltaScrollTop = currentScrollTop - previousScrollTop
      if (currentScrollTop !== lastKnownScrollTop) {
        lastKnownScrollTop = currentScrollTop
      }
      const atBottom = bottomSentinelVisible()

      const beforeAutoScroll = autoScroll()

      const inferredDirection: "up" | "down" | null =
        lastUserScrollIntentDirection ?? (deltaScrollTop < 0 ? "up" : deltaScrollTop > 0 ? "down" : null)

      // If the user scrolls manually, exit key-anchored mode.
      if (isUserScroll && anchorLock()) {
        clearAnchorLock()
      }

      if (isUserScroll) {
        // If the user is actively scrolling upward, exit follow-to-bottom mode
        // immediately. The bottom sentinel can remain "visible" for a short
        // distance due to its observer margin, which otherwise keeps autoScroll
        // enabled and makes the list feel stuck.
        if (inferredDirection === "up" && deltaScrollTop < -0.5 && autoScroll()) {
          if (pendingAnchorScroll !== null) {
            cancelAnimationFrame(pendingAnchorScroll)
            pendingAnchorScroll = null
          }
          setAutoScroll(false)
        }

        // Do not re-enable follow mode while the user's current scroll intent
        // is upward. This prevents transient anchor/pin scrolls from pulling
        // the list back into autoScroll(true).
        if (inferredDirection !== "up") {
          if (atBottom) {
            if (!autoScroll()) setAutoScroll(true)
          } else if (autoScroll()) {
            setAutoScroll(false)
          }
        } else if (!atBottom && autoScroll()) {
          // If the user is scrolling up and we are no longer at the bottom,
          // ensure follow mode is disabled.
          setAutoScroll(false)
        }
      }

      props.onScroll?.()
    })
  }

  function setContainerRef(element: HTMLDivElement | null) {
    containerRef = element || undefined
    setScrollElement(containerRef)
    props.onScrollElementChange?.(containerRef)
    attachScrollIntentListeners(containerRef)
    lastKnownScrollTop = containerRef?.scrollTop ?? 0
    lastUserScrollIntentDirection = null
    if (!containerRef) {
      return
    }
    resolvePendingActiveScroll()
  }

  function scheduleScrollCompensation(key: string, delta: number) {
    if (!containerRef) return
    if (!delta || !Number.isFinite(delta)) return
    if (typeof document === "undefined") return

    // Only compensate while the user scrolls upward (testing default).
    if (!hasUserScrollIntent() || lastUserScrollIntentDirection !== "up") return
    if (autoScroll() || anchorLock()) return

    const anchorId = getAnchorId(key)
    const anchor = document.getElementById(anchorId)
    if (!anchor) return
    const containerRect = containerRef.getBoundingClientRect()
    const rect = anchor.getBoundingClientRect()
    // Determine whether the item was fully above the viewport *before* the
    // height delta applied. Items can expand downward into the viewport; in that
    // case we still need to compensate to keep existing visible content stable.
    const bottomAfter = rect.bottom
    const bottomBefore = bottomAfter - delta
    const wasAboveViewport = bottomBefore < containerRect.top
    if (!wasAboveViewport) return

    const next = (pendingScrollCompensations.get(key) ?? 0) + delta
    pendingScrollCompensations.set(key, next)

    if (pendingScrollCompensationScheduled) return
    pendingScrollCompensationScheduled = true
    const gen = scrollCompensationGen

    // Flush in a microtask so compensation lands before the next paint.
    queueMicrotask(() => {
      if (gen !== scrollCompensationGen) return
      pendingScrollCompensationScheduled = false
      if (!containerRef) return
      if (!hasUserScrollIntent() || lastUserScrollIntentDirection !== "up") {
        pendingScrollCompensations = new Map()
        return
      }
      if (autoScroll() || anchorLock()) {
        pendingScrollCompensations = new Map()
        return
      }

      let applied = 0
      let count = 0
      for (const pendingDelta of pendingScrollCompensations.values()) {
        if (!pendingDelta) continue
        applied += pendingDelta
        count += 1
      }
      pendingScrollCompensations = new Map()
      if (!applied) return

      const before = containerRef.scrollTop
      const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0)
      const nextTop = Math.min(maxScrollTop, Math.max(0, before + applied))
      if (nextTop !== before) {
        containerRef.scrollTop = nextTop
        lastKnownScrollTop = nextTop
      }
    })
  }

  let pendingAutoPin = false
  let pendingAutoPinFrame: number | null = null

  function clearPendingAutoPinFrame() {
    if (pendingAutoPinFrame !== null) {
      cancelAnimationFrame(pendingAutoPinFrame)
      pendingAutoPinFrame = null
    }
  }

  function applyAutoPinToBottom() {
    if (!containerRef) return false
    if (!autoScroll()) return false
    if (anchorLock()) return false

    const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0)
    if (containerRef.scrollTop !== maxScrollTop) {
      containerRef.scrollTop = maxScrollTop
      lastKnownScrollTop = maxScrollTop
    }
    return true
  }

  function scheduleAutoPinToBottom() {
    if (!containerRef) return
    if (pendingAutoPin) return
    pendingAutoPin = true
    clearPendingAutoPinFrame()
    const gen = scrollCompensationGen

    // Flush in a microtask so adjustments land before the next paint,
    // then re-apply on the next two frames to catch deferred layout.
    queueMicrotask(() => {
      if (gen !== scrollCompensationGen) return
      pendingAutoPin = false
      if (!applyAutoPinToBottom()) return
      pendingAutoPinFrame = requestAnimationFrame(() => {
        pendingAutoPinFrame = null
        if (gen !== scrollCompensationGen) return
        if (!applyAutoPinToBottom()) return
        pendingAutoPinFrame = requestAnimationFrame(() => {
          pendingAutoPinFrame = null
          if (gen !== scrollCompensationGen) return
          applyAutoPinToBottom()
        })
      })
    })
  }

  function setShellRef(element: HTMLDivElement | null) {
    shellRef = element || undefined
    setShellElement(shellRef)
    props.onShellElementChange?.(shellRef)
  }

  function setBottomSentinel(element: HTMLDivElement | null) {
    setBottomSentinelSignal(element)
    resolvePendingActiveScroll()
  }

  const api: VirtualFollowListApi = {
    scrollToTop: (opts) => scrollToTop(Boolean(opts?.immediate)),
    scrollToBottom: (opts) => scrollToBottom(Boolean(opts?.immediate), { suppressAutoAnchor: opts?.suppressAutoAnchor }),
    scrollToKey: (key, opts) => {
      if (typeof document === "undefined") return
      const anchorId = getAnchorId(key)
      const behavior = opts?.behavior ?? "smooth"
      const block = opts?.block ?? "start"
      const nextAutoScroll = opts?.setAutoScroll ?? false
      setAutoScroll(nextAutoScroll)
      if (!nextAutoScroll) {
        if (anchorLock()) {
          clearAnchorLock()
        }
        setAnchorLock({ key, block })
      } else {
        if (anchorLock()) {
          clearAnchorLock()
        }
      }
      const first = document.getElementById(anchorId)
      first?.scrollIntoView({ block, behavior })
      // When using virtualization, the placeholder height can be stale until the
      // item mounts/measures. Re-run scrollIntoView() on the next frame to
      // stabilize the final position.
      requestAnimationFrame(() => {
        const second = document.getElementById(anchorId)
        second?.scrollIntoView({ block, behavior })
      })
    },
    notifyContentRendered: () => handleContentRendered(),
    setAutoScroll: (enabled) => setAutoScroll(Boolean(enabled)),
    getAutoScroll: () => autoScroll(),
    getScrollElement: () => scrollElement(),
    getShellElement: () => shellElement(),
  }

  createEffect(() => {
    props.registerApi?.(api)
  })

  createEffect(() => {
    props.registerState?.(state)
  })

  createEffect(() => {
    const nextKey = props.resetKey?.()
    if (nextKey === undefined) return
    if (lastResetKey === undefined) {
      lastResetKey = nextKey
      return
    }
    if (nextKey === lastResetKey) return
    lastResetKey = nextKey

    // Reset internal state when consumers swap datasets (e.g. session switch).
    if (pendingScrollFrame !== null) {
      cancelAnimationFrame(pendingScrollFrame)
      pendingScrollFrame = null
    }
    if (pendingAnchorScroll !== null) {
      cancelAnimationFrame(pendingAnchorScroll)
      pendingAnchorScroll = null
    }
    if (pendingAnchorCorrectionFrame !== null) {
      cancelAnimationFrame(pendingAnchorCorrectionFrame)
      pendingAnchorCorrectionFrame = null
    }
    clearScrollToBottomFrames()

    scrollCompensationGen += 1
    pendingScrollCompensationScheduled = false
    pendingScrollCompensations = new Map()
    pendingAutoPin = false
    clearPendingAutoPinFrame()

    suppressAutoScrollOnce = false
    pendingActiveScroll = false
    pendingInitialScroll = true

    setAnchorLock(null)
    setActiveKey(null)
    setShowScrollTopButton(false)
    setShowScrollBottomButton(false)
    setTopSentinelVisible(true)
    setBottomSentinelVisible(true)
    setAutoScroll(Boolean(initialAutoScroll()))

    lastKnownScrollTop = containerRef?.scrollTop ?? 0
    lastUserScrollIntentDirection = null
  })

  let lastActiveState = false
  createEffect(() => {
    const active = isActive()
    if (active) {
      resolvePendingActiveScroll()
      if (!lastActiveState && autoScroll() && scrollToBottomOnActivate()) {
        requestScrollToBottom(true)

        // When switching back to a cached session pane, items can mount/measure
        // after the initial scroll jump. Re-pin once layout settles so the
        // viewport stays at the bottom.
        requestAnimationFrame(() => {
          requestAnimationFrame(() => {
            scheduleAutoPinToBottom()
          })
        })
      }
    } else if (autoScroll() && scrollToBottomOnActivate()) {
      pendingActiveScroll = true
    }
    lastActiveState = active
  })

  createEffect(() => {
    const loading = isLoading()
    if (loading) {
      // Keep the initial scroll pending while loading so we can
      // anchor to the bottom as soon as items appear.
      pendingInitialScroll = true
    }

    if (!pendingInitialScroll) return

    const container = scrollElement()
    const sentinel = bottomSentinel()
    if (!container || !sentinel || props.items().length === 0) return

    if (!initialScrollToBottom()) {
      // An outer component is responsible for restoring scroll.
      pendingInitialScroll = false
      return
    }

    // Ensure we're in follow-to-bottom mode for the initial position.
    if (anchorLock()) {
      clearAnchorLock()
    }
    setAutoScroll(true)

    pendingInitialScroll = false
    // Scroll synchronously so the first paint prefers bottom content.
    scrollToBottom(true)
  })

  let previousFollowToken: string | number | undefined
  createEffect(() => {
    const token = props.followToken?.()
    if (token === undefined) {
      previousFollowToken = token
      return
    }
    if (previousFollowToken === undefined) {
      previousFollowToken = token
      return
    }
    if (token === previousFollowToken) {
      return
    }
    previousFollowToken = token
    if (suppressAutoScrollOnce) {
      suppressAutoScrollOnce = false
      return
    }
    if (autoScroll()) {
      scheduleAutoPinToBottom()
      return
    }
    if (anchorLock() && !autoScroll()) {
      scheduleAnchorCorrection()
    }
  })

  // Drop anchor lock if the anchored key is removed.
  createEffect(() => {
    const lock = anchorLock()
    if (!lock) return
    const keys = props.items().map((item, idx) => props.getKey(item, idx))
    if (!keys.includes(lock.key)) {
      clearAnchorLock()
    }
  })

  createEffect(() => {
    if (props.items().length === 0) {
      setShowScrollTopButton(false)
      setShowScrollBottomButton(false)
      setAutoScroll(true)
      return
    }
    updateScrollIndicatorsFromVisibility()
  })

  createEffect(() => {
    const container = scrollElement()
    const topTarget = topSentinel()
    const bottomTarget = bottomSentinel()
    if (!container || !topTarget || !bottomTarget) return
    if (typeof IntersectionObserver === "undefined") return

    const margin = props.scrollSentinelMarginPx ?? DEFAULT_SCROLL_SENTINEL_MARGIN_PX

    const observer = new IntersectionObserver(
      (entries) => {
        let visibilityChanged = false
        for (const entry of entries) {
          if (entry.target === topTarget) {
            setTopSentinelVisible(entry.isIntersecting)
            visibilityChanged = true
          } else if (entry.target === bottomTarget) {
            setBottomSentinelVisible(entry.isIntersecting)
            visibilityChanged = true
          }
        }
        if (visibilityChanged) {
          updateScrollIndicatorsFromVisibility()
        }
      },
      { root: container, threshold: 0, rootMargin: `${margin}px 0px ${margin}px 0px` },
    )
    observer.observe(topTarget)
    observer.observe(bottomTarget)
    onCleanup(() => observer.disconnect())
  })

  createEffect(() => {
    const container = scrollElement()
    const items = props.items()
    if (!container || items.length === 0) return
    if (typeof document === "undefined") return
    if (typeof IntersectionObserver === "undefined") return

    const observer = new IntersectionObserver(
      (entries) => {
        let best: IntersectionObserverEntry | null = null
        for (const entry of entries) {
          if (!entry.isIntersecting) continue
          if (!best || entry.boundingClientRect.top < best.boundingClientRect.top) {
            best = entry
          }
        }
        if (best) {
          const anchorId = (best.target as HTMLElement).id
          const key = getKeyFromAnchorId(anchorId)
          setActiveKey((current) => (current === key ? current : key))
        }
      },
      { root: container, rootMargin: "-10% 0px -80% 0px", threshold: 0 },
    )

    const anchorIds = items.map((item, idx) => getAnchorId(props.getKey(item, idx)))
    anchorIds.forEach((anchorId) => {
      const anchor = document.getElementById(anchorId)
      if (anchor) observer.observe(anchor)
    })

    onCleanup(() => observer.disconnect())
  })

  createEffect(() => {
    const key = activeKey()
    props.onActiveKeyChange?.(key)
  })

  onCleanup(() => {
    if (pendingScrollFrame !== null) {
      cancelAnimationFrame(pendingScrollFrame)
    }
    if (pendingAnchorScroll !== null) {
      cancelAnimationFrame(pendingAnchorScroll)
    }
    if (pendingAnchorCorrectionFrame !== null) {
      cancelAnimationFrame(pendingAnchorCorrectionFrame)
    }
    scrollCompensationGen += 1
    pendingScrollCompensationScheduled = false
    pendingScrollCompensations = new Map()
    clearPendingAutoPinFrame()
    clearScrollToBottomFrames()
    if (detachScrollIntentListeners) {
      detachScrollIntentListeners()
    }
  })

  const controls = () => {
    if (props.renderControls) {
      return props.renderControls(state, api)
    }

    // Avoid hardcoded user-visible strings; require consumers to supply
    // localized aria labels when using the default controls.
    if (!props.scrollToTopAriaLabel || !props.scrollToBottomAriaLabel) {
      return null
    }

    const labelTop = props.scrollToTopAriaLabel()
    const labelBottom = props.scrollToBottomAriaLabel()
    return (
      <Show when={showScrollTopButton() || showScrollBottomButton()}>
        <div class="message-scroll-button-wrapper">
          <Show when={showScrollTopButton()}>
            <button type="button" class="message-scroll-button" onClick={() => scrollToTop()} aria-label={labelTop}>
              <span class="message-scroll-icon" aria-hidden="true">
                ↑
              </span>
            </button>
          </Show>
          <Show when={showScrollBottomButton()}>
            <button
              type="button"
              class="message-scroll-button"
              onClick={() => scrollToBottom(false, { suppressAutoAnchor: false })}
              aria-label={labelBottom}
            >
              <span class="message-scroll-icon" aria-hidden="true">
                ↓
              </span>
            </button>
          </Show>
        </div>
      </Show>
    )
  }

  return (
    <div class="message-stream-shell" ref={setShellRef}>
      <div
        class="message-stream"
        ref={setContainerRef}
        onScroll={handleScroll}
        onMouseUp={(event) => props.onMouseUp?.(event)}
        onClick={(event) => props.onClick?.(event)}
      >
        <div ref={setTopSentinel} aria-hidden="true" style={{ height: "1px" }} />
        {props.renderBeforeItems?.()}
        <Index each={props.items()}>
          {(item, index) => {
            const key = () => props.getKey(item(), index)
            const anchorId = () => getAnchorId(key())
            const overscanPx = props.overscanPx ?? 800
            const suspendMeasurements = () => measurementsSuspended() || !isActive()
            const itemVirtualizationEnabled = () => virtualizationEnabled() && !autoScroll()
            return (
              <VirtualItem
                id={anchorId()}
                cacheKey={key()}
                scrollContainer={scrollElement}
                threshold={overscanPx}
                placeholderClass="message-stream-placeholder"
                virtualizationEnabled={itemVirtualizationEnabled}
                suspendMeasurements={suspendMeasurements}
                onHeightChange={(nextHeight, previousHeight, meta: VirtualItemHeightChangeMeta) => {
                  const delta = nextHeight - previousHeight

                  // Follow mode: keep the viewport pinned to the bottom as
                  // items mount/measure and change height.
                  if (delta && autoScroll() && !anchorLock()) {
                    scheduleAutoPinToBottom()
                    return
                  }

                  // Key-anchored mode: keep the target key in view when
                  // items above it mount/measure and shift layout.
                  if (anchorLock() && !autoScroll()) {
                    scheduleAnchorCorrection()
                    return
                  }

                  // Free-scroll mode: if items above the viewport change height
                  // while scrolling upward, compensate scrollTop so visible
                  // content stays stable.
                  if (delta) {
                    if (meta.isStaleCacheCorrection) return
                    scheduleScrollCompensation(key(), delta)
                  }
                }}
              >{() => props.renderItem(item(), index)}</VirtualItem>
            )
          }}
        </Index>
        <div ref={setBottomSentinel} aria-hidden="true" style={{ height: "1px" }} />
      </div>

      {controls()}

      {props.renderOverlay?.()}
    </div>
  )
}
