"use client"

/**
 * MessageScroller — chat transcript scroll behaviors (shadcn MessageScroller).
 *
 * Covers: turn anchoring + previous-item peek, autoScroll only at the live
 * edge (released by wheel/touch/keyboard/selection), default open position,
 * jump to message / start / end (align + scrollMargin), visibility tracking
 * (currentAnchorId / visibleMessageIds), prepend preserve, and jump controls.
 * Styles use Exxat semantic tokens only.
 */

import * as React from "react"

import { cn } from "../../lib/utils"
import { Button } from "./button"

const DEFAULT_SCROLL_EDGE_THRESHOLD = 8
const DEFAULT_SCROLL_PREVIOUS_ITEM_PEEK = 64
const SCROLL_EPSILON = 1
const ANCHOR_EDGE_SLOP = 0.5
const AUTOSCROLLING_MS = 180
const USER_SCROLL_KEYS = new Set([
  "ArrowDown",
  "ArrowUp",
  "End",
  "Home",
  "PageDown",
  "PageUp",
  " ",
])

export type MessageScrollerDefaultScrollPosition =
  | "start"
  | "end"
  | "last-anchor"

export type MessageScrollerScrollAlign =
  | "start"
  | "center"
  | "end"
  | "nearest"

export type MessageScrollerScrollOptions = {
  align?: MessageScrollerScrollAlign
  behavior?: ScrollBehavior
  scrollMargin?: number
}

export type MessageScrollerScrollable = {
  /** Content is hidden above — can scroll toward the start. */
  start: boolean
  /** Content is hidden below — can scroll toward the end. */
  end: boolean
}

export type MessageScrollerVisibilityState = {
  currentAnchorId: string | null
  visibleMessageIds: string[]
}

type Mode =
  | "following-bottom"
  | "free-scrolling"
  | "anchored-to-message"
  | "settling-jump"

type Store<T> = {
  getSnapshot: () => T
  hasListeners: () => boolean
  setSnapshot: (next: T) => void
  subscribe: (
    listener: () => void,
    onFirst?: () => void,
    onLast?: () => void,
  ) => () => void
}

type MessageScrollerContextValue = {
  autoScroll: boolean
  scrollPreviousItemPeek: number
  scrollEdgeThreshold: number
  scrollMargin: number
  scrollableStore: Store<MessageScrollerScrollable>
  visibilityStore: Store<MessageScrollerVisibilityState>
  /** @deprecated Prefer `!scrollable.end` — kept for existing Leo call sites. */
  isAtBottom: boolean
  setRootEl: (el: HTMLDivElement | null) => void
  setViewportEl: (el: HTMLDivElement | null) => void
  setContentEl: (el: HTMLDivElement | null) => void
  setSpacerEl: (el: HTMLDivElement | null) => void
  scrollToEnd: (options?: MessageScrollerScrollOptions) => boolean
  scrollToStart: (options?: MessageScrollerScrollOptions) => boolean
  scrollToMessage: (
    messageId: string,
    options?: MessageScrollerScrollOptions,
  ) => boolean
  releaseAutoScroll: () => void
  notifyContentChange: () => void
  registerScrollAnchor: (el: HTMLElement | null) => void
  registerMessage: (messageId: string, el: HTMLElement | null) => void
  onViewportScroll: () => void
  userScrollIntent: () => void
  setAutoscrolling: (next: boolean) => void
  observeVisibility: () => void
  unobserveVisibility: () => void
  preserveScrollOnPrependRef: React.MutableRefObject<boolean>
}

const EMPTY_VISIBLE: string[] = []
const EMPTY_SCROLLABLE: MessageScrollerScrollable = { start: false, end: false }
const EMPTY_VISIBILITY: MessageScrollerVisibilityState = {
  currentAnchorId: null,
  visibleMessageIds: EMPTY_VISIBLE,
}

const MessageScrollerContext =
  React.createContext<MessageScrollerContextValue | null>(null)

function useMessageScrollerContext() {
  const ctx = React.useContext(MessageScrollerContext)
  if (!ctx) {
    throw new Error(
      "MessageScroller components must be used within MessageScrollerProvider",
    )
  }
  return ctx
}

function createStore<T>(
  initial: T,
  isEqual: (a: T, b: T) => boolean,
): Store<T> {
  let snapshot = initial
  const listeners = new Set<() => void>()
  return {
    getSnapshot: () => snapshot,
    hasListeners: () => listeners.size > 0,
    setSnapshot: (next) => {
      if (isEqual(snapshot, next)) return
      snapshot = next
      listeners.forEach((listener) => listener())
    },
    subscribe: (listener, onFirst, onLast) => {
      const first = listeners.size === 0
      listeners.add(listener)
      if (first) onFirst?.()
      return () => {
        listeners.delete(listener)
        if (listeners.size === 0) onLast?.()
      }
    },
  }
}

function scrollableEqual(
  a: MessageScrollerScrollable,
  b: MessageScrollerScrollable,
) {
  return a.start === b.start && a.end === b.end
}

function visibilityEqual(
  a: MessageScrollerVisibilityState,
  b: MessageScrollerVisibilityState,
) {
  if (a.currentAnchorId !== b.currentAnchorId) return false
  return (
    a.visibleMessageIds.length === b.visibleMessageIds.length &&
    a.visibleMessageIds.every((id, i) => id === b.visibleMessageIds[i])
  )
}

function contentChildren(
  content: HTMLElement,
  spacer: HTMLElement | null,
): HTMLElement[] {
  return Array.from(content.children).filter(
    (node): node is HTMLElement =>
      node instanceof HTMLElement && node !== spacer,
  )
}

function elementScrollTop(element: HTMLElement, viewport: HTMLElement) {
  const elementRect = element.getBoundingClientRect()
  const viewportRect = viewport.getBoundingClientRect()
  return elementRect.top - viewportRect.top + viewport.scrollTop
}

function resolveAlignScrollTop({
  align,
  element,
  scrollMargin,
  viewport,
}: {
  align: MessageScrollerScrollAlign
  element: HTMLElement
  scrollMargin: number
  viewport: HTMLElement
}) {
  const top = elementScrollTop(element, viewport)
  const height = element.getBoundingClientRect().height
  if (align === "center") {
    return top - (viewport.clientHeight - height) / 2 - scrollMargin
  }
  if (align === "end") {
    return top - viewport.clientHeight + height + scrollMargin
  }
  if (align === "nearest") {
    const bottom = top + height
    const viewStart = viewport.scrollTop
    const viewEnd = viewport.scrollTop + viewport.clientHeight
    if (top >= viewStart && bottom <= viewEnd) return viewport.scrollTop
    if (top < viewStart) return top - scrollMargin
    return bottom - viewport.clientHeight + scrollMargin
  }
  return top - scrollMargin
}

function readScrollable(
  el: HTMLDivElement,
  threshold: number,
): MessageScrollerScrollable {
  const maxScroll = Math.max(0, el.scrollHeight - el.clientHeight)
  return {
    start: el.scrollTop > threshold,
    end: maxScroll - el.scrollTop > threshold,
  }
}

function readVisibility({
  content,
  scrollMargin,
  scrollPreviousItemPeek,
  spacer,
  viewport,
  visibleMessageIds,
}: {
  content: HTMLElement | null
  scrollMargin: number
  scrollPreviousItemPeek: number
  spacer: HTMLElement | null
  viewport: HTMLDivElement | null
  visibleMessageIds: Set<string>
}): MessageScrollerVisibilityState {
  if (!content || !viewport) return EMPTY_VISIBILITY
  const viewportRect = viewport.getBoundingClientRect()
  const edge = viewportRect.top + scrollMargin + scrollPreviousItemPeek
  const noObserver = typeof IntersectionObserver === "undefined"
  const ids: string[] = []
  let currentAnchorId: string | null = null

  for (const item of contentChildren(content, spacer)) {
    const messageId = item.dataset.messageId
    if (!messageId) continue
    const isAnchor = item.dataset.scrollAnchor === "true"
    const rect =
      isAnchor || noObserver ? item.getBoundingClientRect() : null
    const visible = noObserver
      ? !!rect && rect.bottom > edge && rect.top < viewportRect.bottom
      : visibleMessageIds.has(messageId)
    if (visible) ids.push(messageId)
    if (isAnchor && rect && rect.top <= edge + ANCHOR_EDGE_SLOP) {
      currentAnchorId = messageId
    }
  }

  if (ids.length === 0 && currentAnchorId === null) return EMPTY_VISIBILITY
  return { currentAnchorId, visibleMessageIds: ids }
}

function useMessageScroller() {
  const ctx = useMessageScrollerContext()
  const scrollable = useMessageScrollerScrollable()
  return {
    scrollToEnd: ctx.scrollToEnd,
    scrollToStart: ctx.scrollToStart,
    scrollToMessage: ctx.scrollToMessage,
    releaseAutoScroll: ctx.releaseAutoScroll,
    scrollable,
    isAtBottom: ctx.isAtBottom,
  }
}

function useMessageScrollerScrollable() {
  const { scrollableStore } = useMessageScrollerContext()
  return React.useSyncExternalStore(
    scrollableStore.subscribe,
    scrollableStore.getSnapshot,
    scrollableStore.getSnapshot,
  )
}

function useMessageScrollerVisibility() {
  const { visibilityStore, observeVisibility, unobserveVisibility } =
    useMessageScrollerContext()
  const subscribe = React.useCallback(
    (listener: () => void) =>
      visibilityStore.subscribe(listener, observeVisibility, unobserveVisibility),
    [observeVisibility, unobserveVisibility, visibilityStore],
  )
  return React.useSyncExternalStore(
    subscribe,
    visibilityStore.getSnapshot,
    visibilityStore.getSnapshot,
  )
}

function MessageScrollerProvider({
  autoScroll = false,
  defaultScrollPosition = "end",
  scrollEdgeThreshold = DEFAULT_SCROLL_EDGE_THRESHOLD,
  scrollPreviousItemPeek = DEFAULT_SCROLL_PREVIOUS_ITEM_PEEK,
  scrollMargin = 0,
  children,
}: {
  autoScroll?: boolean
  defaultScrollPosition?: MessageScrollerDefaultScrollPosition
  scrollEdgeThreshold?: number
  scrollPreviousItemPeek?: number
  scrollMargin?: number
  children: React.ReactNode
}) {
  const rootRef = React.useRef<HTMLDivElement | null>(null)
  const viewportRef = React.useRef<HTMLDivElement | null>(null)
  const contentRef = React.useRef<HTMLDivElement | null>(null)
  const spacerRef = React.useRef<HTMLDivElement | null>(null)
  const modeRef = React.useRef<Mode>(
    autoScroll ? "following-bottom" : "free-scrolling",
  )
  const autoScrollRef = React.useRef(autoScroll)
  const lastScrollTopRef = React.useRef(0)
  const pendingAnchorRef = React.useRef<HTMLElement | null>(null)
  // `useRef` takes a value, not a factory, so `useRef(new Map())` would build a
  // Map on every render and keep only the first. These three start null and are
  // filled on first use inside the callbacks below, which also keeps them out of
  // dependency arrays.
  const messageMapRef = React.useRef<Map<string, HTMLElement> | null>(null)
  const pendingScrollToMessageRef = React.useRef<{
    messageId: string
    options?: MessageScrollerScrollOptions
  } | null>(null)
  const defaultAppliedRef = React.useRef(false)
  const autoscrollingTimeoutRef = React.useRef<ReturnType<
    typeof setTimeout
  > | null>(null)
  const itemCountRef = React.useRef(0)
  const firstItemRef = React.useRef<Element | null>(null)
  const prependHeightRef = React.useRef(0)
  const preserveScrollOnPrependRef = React.useRef(true)
  const visibleMessageIdsRef = React.useRef<Set<string> | null>(null)
  const visibilityObserverRef = React.useRef<IntersectionObserver | null>(null)
  const visibilityFrameRef = React.useRef<number | null>(null)
  const handledScrollAnchorsRef = React.useRef<WeakSet<HTMLElement> | null>(null)

  const scrollableStoreRef = React.useRef<Store<MessageScrollerScrollable> | null>(
    null,
  )
  if (scrollableStoreRef.current === null) {
    scrollableStoreRef.current = createStore(EMPTY_SCROLLABLE, scrollableEqual)
  }
  const visibilityStoreRef =
    React.useRef<Store<MessageScrollerVisibilityState> | null>(null)
  if (visibilityStoreRef.current === null) {
    visibilityStoreRef.current = createStore(EMPTY_VISIBILITY, visibilityEqual)
  }
  const scrollableStore = scrollableStoreRef.current
  const visibilityStore = visibilityStoreRef.current

  const [autoscrolling, setAutoscrollingState] = React.useState(false)
  const [isAtBottom, setIsAtBottom] = React.useState(true)

  // Every reader of this ref runs from a callback, a rAF or a layout effect, so
  // syncing it after commit is soon enough. Writing it during render is not:
  // React may replay or throw away a render, and the stale copy would still be
  // sitting in the ref when a scroll handler next asked what autoScroll was.
  React.useLayoutEffect(() => {
    autoScrollRef.current = autoScroll
  }, [autoScroll])

  const writeAttrs = React.useCallback(
    (next: MessageScrollerScrollable, isAuto: boolean) => {
      const scrollableAttr = [next.start && "start", next.end && "end"]
        .filter(Boolean)
        .join(" ")
      for (const el of [rootRef.current, viewportRef.current]) {
        if (!el) continue
        if (scrollableAttr) el.setAttribute("data-scrollable", scrollableAttr)
        else el.removeAttribute("data-scrollable")
        el.toggleAttribute("data-autoscrolling", isAuto)
      }
    },
    [],
  )

  const publishVisibility = React.useCallback(() => {
    if (!visibilityStore.hasListeners()) return
    const next = readVisibility({
      content: contentRef.current,
      scrollMargin,
      scrollPreviousItemPeek,
      spacer: spacerRef.current,
      viewport: viewportRef.current,
      visibleMessageIds: (visibleMessageIdsRef.current ??= new Set()),
    })
    visibilityStore.setSnapshot(next)
  }, [scrollMargin, scrollPreviousItemPeek, visibilityStore])

  const commitScrollable = React.useCallback(() => {
    const el = viewportRef.current
    if (!el) return
    const next = readScrollable(el, scrollEdgeThreshold)
    const published =
      modeRef.current === "following-bottom" ? { ...next, end: false } : next
    scrollableStore.setSnapshot(published)
    setIsAtBottom(!published.end)
    writeAttrs(published, autoscrolling)
    publishVisibility()
  }, [
    autoscrolling,
    publishVisibility,
    scrollEdgeThreshold,
    scrollableStore,
    writeAttrs,
  ])

  const setAutoscrolling = React.useCallback(
    (next: boolean) => {
      setAutoscrollingState(next)
      writeAttrs(scrollableStore.getSnapshot(), next)
      if (autoscrollingTimeoutRef.current) {
        clearTimeout(autoscrollingTimeoutRef.current)
        autoscrollingTimeoutRef.current = null
      }
      if (next) {
        autoscrollingTimeoutRef.current = setTimeout(() => {
          setAutoscrollingState(false)
          writeAttrs(scrollableStore.getSnapshot(), false)
        }, AUTOSCROLLING_MS)
      }
    },
    [scrollableStore, writeAttrs],
  )

  const releaseAutoScroll = React.useCallback(() => {
    if (
      modeRef.current === "following-bottom" ||
      modeRef.current === "anchored-to-message" ||
      modeRef.current === "settling-jump"
    ) {
      modeRef.current = "free-scrolling"
      pendingAnchorRef.current = null
    }
  }, [])

  const userScrollIntent = React.useCallback(() => {
    releaseAutoScroll()
  }, [releaseAutoScroll])

  const scrollElementIntoView = React.useCallback(
    (
      target: HTMLElement,
      opts?: {
        align?: MessageScrollerScrollAlign
        keepPreviousPeek?: boolean
        behavior?: ScrollBehavior
        scrollMargin?: number
      },
    ) => {
      const el = viewportRef.current
      if (!el) return false
      const align = opts?.align ?? "start"
      const peek = opts?.keepPreviousPeek ? scrollPreviousItemPeek : 0
      const margin = (opts?.scrollMargin ?? scrollMargin) + peek
      const top = resolveAlignScrollTop({
        align,
        element: target,
        scrollMargin: margin,
        viewport: el,
      })
      el.scrollTo({
        top: Math.max(0, top),
        behavior: opts?.behavior ?? "smooth",
      })
      requestAnimationFrame(() => {
        lastScrollTopRef.current = el.scrollTop
        commitScrollable()
      })
      return true
    },
    [commitScrollable, scrollMargin, scrollPreviousItemPeek],
  )

  const scrollToEnd = React.useCallback(
    (options?: MessageScrollerScrollOptions) => {
      const el = viewportRef.current
      if (!el) return false
      const behavior = options?.behavior ?? "smooth"
      if (autoScrollRef.current) modeRef.current = "following-bottom"
      setAutoscrolling(true)
      el.scrollTo({ top: el.scrollHeight, behavior })
      requestAnimationFrame(() => {
        lastScrollTopRef.current = el.scrollTop
        commitScrollable()
      })
      return true
    },
    [commitScrollable, setAutoscrolling],
  )

  const scrollToStart = React.useCallback(
    (options?: MessageScrollerScrollOptions) => {
      const el = viewportRef.current
      if (!el) return false
      releaseAutoScroll()
      modeRef.current = "settling-jump"
      setAutoscrolling(true)
      el.scrollTo({ top: 0, behavior: options?.behavior ?? "smooth" })
      requestAnimationFrame(() => {
        lastScrollTopRef.current = el.scrollTop
        modeRef.current = "free-scrolling"
        commitScrollable()
      })
      return true
    },
    [commitScrollable, releaseAutoScroll, setAutoscrolling],
  )

  const scrollToMessage = React.useCallback(
    (messageId: string, options?: MessageScrollerScrollOptions) => {
      const target = (messageMapRef.current ??= new Map()).get(messageId)
      if (!target) {
        if (itemCountRef.current === 0) {
          pendingScrollToMessageRef.current = { messageId, options }
          return true
        }
        return false
      }
      pendingScrollToMessageRef.current = null
      releaseAutoScroll()
      modeRef.current = "settling-jump"
      setAutoscrolling(true)
      const align = options?.align ?? "start"
      scrollElementIntoView(target, {
        align,
        keepPreviousPeek: align === "start",
        behavior: options?.behavior ?? "smooth",
        scrollMargin: options?.scrollMargin,
      })
      requestAnimationFrame(() => {
        modeRef.current = "free-scrolling"
        commitScrollable()
      })
      return true
    },
    [
      commitScrollable,
      releaseAutoScroll,
      scrollElementIntoView,
      setAutoscrolling,
    ],
  )

  const applyDefaultScrollPosition = React.useCallback(() => {
    if (defaultAppliedRef.current) return
    const el = viewportRef.current
    const content = contentRef.current
    if (!el || !content) return
    const items = contentChildren(content, spacerRef.current)
    if (items.length === 0) return

    if (defaultScrollPosition === "start") {
      el.scrollTop = 0
    } else if (defaultScrollPosition === "last-anchor") {
      const anchors = items.filter(
        (node) => node.dataset.scrollAnchor === "true",
      )
      const last = anchors[anchors.length - 1]
      if (!last) {
        el.scrollTop = el.scrollHeight
      } else {
        const viewportRect = el.getBoundingClientRect()
        const lastRect = last.getBoundingClientRect()
        const contentBottom = content.scrollHeight
        const anchorTop = el.scrollTop + (lastRect.top - viewportRect.top)
        const lastTurnFits =
          contentBottom - anchorTop <= el.clientHeight + SCROLL_EPSILON
        if (lastTurnFits) {
          el.scrollTop = el.scrollHeight
        } else {
          scrollElementIntoView(last, {
            keepPreviousPeek: true,
            behavior: "auto",
          })
        }
      }
    } else {
      el.scrollTop = el.scrollHeight
      if (autoScrollRef.current) modeRef.current = "following-bottom"
    }

    defaultAppliedRef.current = true
    lastScrollTopRef.current = el.scrollTop
    commitScrollable()
  }, [commitScrollable, defaultScrollPosition, scrollElementIntoView])

  const notifyContentChange = React.useCallback(() => {
    const el = viewportRef.current
    const content = contentRef.current
    if (!el || !content) return

    const items = contentChildren(content, spacerRef.current)
    const previousCount = itemCountRef.current
    const previousFirst = firstItemRef.current
    const nextFirst = items[0] ?? null

    if (
      preserveScrollOnPrependRef.current &&
      previousFirst &&
      nextFirst &&
      previousFirst !== nextFirst &&
      items.includes(previousFirst as HTMLElement) &&
      prependHeightRef.current > 0
    ) {
      const delta = el.scrollHeight - prependHeightRef.current
      if (delta > 0) el.scrollTop += delta
    }
    prependHeightRef.current = el.scrollHeight
    itemCountRef.current = items.length
    firstItemRef.current = nextFirst

    const pendingJump = pendingScrollToMessageRef.current
    if (pendingJump) {
      const mounted = (messageMapRef.current ??= new Map()).get(pendingJump.messageId)
      if (mounted) {
        pendingScrollToMessageRef.current = null
        scrollToMessage(pendingJump.messageId, pendingJump.options)
        return
      }
    }

    requestAnimationFrame(() => {
      if (previousCount === 0) {
        applyDefaultScrollPosition()
        return
      }

      if (pendingAnchorRef.current?.isConnected) {
        const anchor = pendingAnchorRef.current
        pendingAnchorRef.current = null
        if (
          autoScrollRef.current &&
          modeRef.current === "following-bottom" &&
          items.length > previousCount + 1
        ) {
          scrollToEnd({ behavior: "auto" })
          return
        }
        modeRef.current = "anchored-to-message"
        scrollElementIntoView(anchor, {
          keepPreviousPeek: true,
          behavior: "smooth",
        })
        return
      }

      if (modeRef.current === "following-bottom" && autoScrollRef.current) {
        scrollToEnd({ behavior: "auto" })
        return
      }

      commitScrollable()
    })
  }, [
    applyDefaultScrollPosition,
    commitScrollable,
    scrollElementIntoView,
    scrollToEnd,
    scrollToMessage,
  ])

  const registerScrollAnchor = React.useCallback((el: HTMLElement | null) => {
    if (!el) return
    const handled = (handledScrollAnchorsRef.current ??= new WeakSet())
    if (handled.has(el)) return
    handled.add(el)
    pendingAnchorRef.current = el
  }, [])

  const registerMessage = React.useCallback(
    (messageId: string, el: HTMLElement | null) => {
      const messageMap = (messageMapRef.current ??= new Map())
      if (el) {
        messageMap.set(messageId, el)
        visibilityObserverRef.current?.observe(el)
      } else {
        const existing = messageMap.get(messageId)
        if (existing) visibilityObserverRef.current?.unobserve(existing)
        messageMap.delete(messageId)
        visibleMessageIdsRef.current?.delete(messageId)
      }
    },
    [],
  )

  const onViewportScroll = React.useCallback(() => {
    const el = viewportRef.current
    if (!el) return
    const scrolledUp =
      el.scrollTop < lastScrollTopRef.current - SCROLL_EPSILON
    lastScrollTopRef.current = el.scrollTop

    const next = readScrollable(el, scrollEdgeThreshold)
    if (
      modeRef.current === "following-bottom" &&
      next.end &&
      scrolledUp &&
      !autoscrolling
    ) {
      modeRef.current = "free-scrolling"
    } else if (
      autoScrollRef.current &&
      !next.end &&
      modeRef.current !== "settling-jump" &&
      modeRef.current !== "anchored-to-message"
    ) {
      modeRef.current = "following-bottom"
    }

    commitScrollable()
  }, [autoscrolling, commitScrollable, scrollEdgeThreshold])

  const observeVisibility = React.useCallback(() => {
    const viewport = viewportRef.current
    if (!viewport || !visibilityStore.hasListeners()) return
    if (typeof IntersectionObserver === "undefined") {
      publishVisibility()
      return
    }
    if (!visibilityObserverRef.current) {
      visibilityObserverRef.current = new IntersectionObserver(
        (entries) => {
          for (const entry of entries) {
            const id = (entry.target as HTMLElement).dataset.messageId
            if (!id) continue
            const visible = (visibleMessageIdsRef.current ??= new Set())
            if (entry.isIntersecting) visible.add(id)
            else visible.delete(id)
          }
          if (visibilityFrameRef.current != null) {
            window.cancelAnimationFrame(visibilityFrameRef.current)
          }
          visibilityFrameRef.current = window.requestAnimationFrame(() => {
            visibilityFrameRef.current = null
            publishVisibility()
          })
        },
        {
          root: viewport,
          rootMargin: `${-(scrollMargin + scrollPreviousItemPeek)}px 0px 0px 0px`,
          threshold: [0, 0.01, 0.5, 1],
        },
      )
    }
    messageMapRef.current?.forEach((node) => {
      visibilityObserverRef.current?.observe(node)
    })
    publishVisibility()
  }, [
    publishVisibility,
    scrollMargin,
    scrollPreviousItemPeek,
    visibilityStore,
  ])

  const unobserveVisibility = React.useCallback(() => {
    if (visibilityFrameRef.current != null) {
      window.cancelAnimationFrame(visibilityFrameRef.current)
      visibilityFrameRef.current = null
    }
    visibilityObserverRef.current?.disconnect()
    visibilityObserverRef.current = null
    visibleMessageIdsRef.current?.clear()
    visibilityStore.setSnapshot(EMPTY_VISIBILITY)
  }, [visibilityStore])

  const setRootEl = React.useCallback((el: HTMLDivElement | null) => {
    rootRef.current = el
  }, [])

  const setViewportEl = React.useCallback(
    (el: HTMLDivElement | null) => {
      viewportRef.current = el
      if (el) {
        lastScrollTopRef.current = el.scrollTop
        commitScrollable()
        if (visibilityStore.hasListeners()) observeVisibility()
      }
    },
    [commitScrollable, observeVisibility, visibilityStore],
  )

  const setContentEl = React.useCallback((el: HTMLDivElement | null) => {
    contentRef.current = el
  }, [])

  const setSpacerEl = React.useCallback((el: HTMLDivElement | null) => {
    spacerRef.current = el
  }, [])

  React.useEffect(() => {
    const content = contentRef.current
    if (!content || typeof ResizeObserver === "undefined") return
    const ro = new ResizeObserver(() => {
      if (modeRef.current === "following-bottom" && autoScrollRef.current) {
        const el = viewportRef.current
        if (!el) return
        el.scrollTop = el.scrollHeight
        lastScrollTopRef.current = el.scrollTop
        commitScrollable()
        return
      }
      commitScrollable()
    })
    ro.observe(content)
    return () => ro.disconnect()
  }, [commitScrollable])

  React.useLayoutEffect(() => {
    applyDefaultScrollPosition()
  }, [applyDefaultScrollPosition])

  React.useEffect(() => {
    return () => {
      if (autoscrollingTimeoutRef.current) {
        clearTimeout(autoscrollingTimeoutRef.current)
      }
      unobserveVisibility()
    }
  }, [unobserveVisibility])

  const value = React.useMemo<MessageScrollerContextValue>(
    () => ({
      autoScroll,
      scrollPreviousItemPeek,
      scrollEdgeThreshold,
      scrollMargin,
      scrollableStore,
      visibilityStore,
      isAtBottom,
      setRootEl,
      setViewportEl,
      setContentEl,
      setSpacerEl,
      scrollToEnd,
      scrollToStart,
      scrollToMessage,
      releaseAutoScroll,
      notifyContentChange,
      registerScrollAnchor,
      registerMessage,
      onViewportScroll,
      userScrollIntent,
      setAutoscrolling,
      observeVisibility,
      unobserveVisibility,
      preserveScrollOnPrependRef,
    }),
    [
      autoScroll,
      scrollPreviousItemPeek,
      scrollEdgeThreshold,
      scrollMargin,
      scrollableStore,
      visibilityStore,
      isAtBottom,
      setRootEl,
      setViewportEl,
      setContentEl,
      setSpacerEl,
      scrollToEnd,
      scrollToStart,
      scrollToMessage,
      releaseAutoScroll,
      notifyContentChange,
      registerScrollAnchor,
      registerMessage,
      onViewportScroll,
      userScrollIntent,
      setAutoscrolling,
      observeVisibility,
      unobserveVisibility,
    ],
  )

  return (
    <MessageScrollerContext.Provider value={value}>
      {children}
    </MessageScrollerContext.Provider>
  )
}

function MessageScroller({
  className,
  ref,
  ...props
}: React.ComponentProps<"div">) {
  const { setRootEl } = useMessageScrollerContext()
  const handleRef = React.useCallback(
    (node: HTMLDivElement | null) => {
      setRootEl(node)
      if (typeof ref === "function") ref(node)
      else if (ref) ref.current = node
    },
    [ref, setRootEl],
  )

  return (
    <div
      ref={handleRef}
      data-slot="message-scroller"
      className={cn(
        "group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden",
        className,
      )}
      {...props}
    />
  )
}

function MessageScrollerViewport({
  className,
  ref,
  onScroll,
  onWheel,
  onTouchMove,
  onKeyDown,
  onPointerDown,
  preserveScrollOnPrepend = true,
  role = "region",
  "aria-label": ariaLabel = "Messages",
  tabIndex = 0,
  ...props
}: React.ComponentProps<"div"> & {
  preserveScrollOnPrepend?: boolean
}) {
  const {
    setViewportEl,
    onViewportScroll,
    userScrollIntent,
    preserveScrollOnPrependRef,
  } = useMessageScrollerContext()

  preserveScrollOnPrependRef.current = preserveScrollOnPrepend

  const handleRef = React.useCallback(
    (node: HTMLDivElement | null) => {
      setViewportEl(node)
      if (typeof ref === "function") ref(node)
      else if (ref) ref.current = node
    },
    [ref, setViewportEl],
  )

  return (
    <div
      ref={handleRef}
      data-slot="message-scroller-viewport"
      role={role}
      aria-label={ariaLabel}
      tabIndex={tabIndex}
      onScroll={(event) => {
        onViewportScroll()
        onScroll?.(event)
      }}
      onWheel={(event) => {
        userScrollIntent()
        onWheel?.(event)
      }}
      onTouchMove={(event) => {
        userScrollIntent()
        onTouchMove?.(event)
      }}
      onPointerDown={(event) => {
        // Selecting text / dragging is reader intent — release follow-output.
        userScrollIntent()
        onPointerDown?.(event)
      }}
      onKeyDown={(event) => {
        if (USER_SCROLL_KEYS.has(event.key)) {
          userScrollIntent()
        }
        onKeyDown?.(event)
      }}
      className={cn(
        "size-full min-h-0 min-w-0 overflow-y-auto overscroll-y-contain contain-content [scrollbar-gutter:stable] [scrollbar-width:thin] [-webkit-overflow-scrolling:touch]",
        "[mask-image:linear-gradient(to_bottom,black_0%,black_calc(100%-1.25rem),transparent_100%)] [-webkit-mask-image:linear-gradient(to_bottom,black_0%,black_calc(100%-1.25rem),transparent_100%)]",
        "data-[autoscrolling]:[scrollbar-width:none] data-[autoscrolling]:[-ms-overflow-style:none] data-[autoscrolling]:[&::-webkit-scrollbar]:hidden",
        className,
      )}
      {...props}
    />
  )
}

function MessageScrollerContent({
  className,
  children,
  ref,
  spacerClassName,
  ...props
}: React.ComponentProps<"div"> & {
  spacerClassName?: string
}) {
  const { notifyContentChange, setContentEl, setSpacerEl } =
    useMessageScrollerContext()

  const handleRef = React.useCallback(
    (node: HTMLDivElement | null) => {
      setContentEl(node)
      if (typeof ref === "function") ref(node)
      else if (ref) ref.current = node
    },
    [ref, setContentEl],
  )

  React.useEffect(() => {
    notifyContentChange()
  }, [children, notifyContentChange])

  return (
    <div
      ref={handleRef}
      data-slot="message-scroller-content"
      role="log"
      aria-live="polite"
      aria-relevant="additions"
      className={cn("flex h-max min-h-full flex-col gap-4", className)}
      {...props}
    >
      {children}
      <div
        ref={setSpacerEl}
        data-slot="message-scroller-spacer"
        aria-hidden="true"
        className={cn("min-h-0 shrink-0 grow", spacerClassName)}
      />
    </div>
  )
}

function MessageScrollerItem({
  className,
  scrollAnchor = false,
  messageId,
  children,
  ...props
}: React.ComponentProps<"div"> & {
  scrollAnchor?: boolean
  messageId?: string
}) {
  const { registerScrollAnchor, registerMessage } = useMessageScrollerContext()
  const itemRef = React.useRef<HTMLDivElement | null>(null)
  const wasAnchorRef = React.useRef(false)

  React.useLayoutEffect(() => {
    const node = itemRef.current
    if (messageId) registerMessage(messageId, node)
    return () => {
      if (messageId) registerMessage(messageId, null)
    }
  }, [messageId, registerMessage, children])

  React.useLayoutEffect(() => {
    if (!scrollAnchor || !itemRef.current) {
      wasAnchorRef.current = false
      return
    }
    if (!wasAnchorRef.current) {
      registerScrollAnchor(itemRef.current)
    }
    wasAnchorRef.current = true
  }, [registerScrollAnchor, scrollAnchor, children])

  return (
    <div
      ref={itemRef}
      data-slot="message-scroller-item"
      data-message-id={messageId}
      data-scroll-anchor={scrollAnchor ? "true" : undefined}
      className={cn(
        "min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]",
        className,
      )}
      {...props}
    >
      {children}
    </div>
  )
}

function MessageScrollerButton({
  direction = "end",
  behavior = "smooth",
  className,
  children,
  variant = "secondary",
  size = "icon-sm",
  ...props
}: React.ComponentProps<"button"> & {
  direction?: "start" | "end"
  behavior?: ScrollBehavior
  variant?: React.ComponentProps<typeof Button>["variant"]
  size?: React.ComponentProps<typeof Button>["size"]
}) {
  const scrollable = useMessageScrollerScrollable()
  const { scrollToEnd, scrollToStart } = useMessageScrollerContext()
  const active = direction === "end" ? scrollable.end : scrollable.start

  return (
    <Button
      type="button"
      data-slot="message-scroller-button"
      data-direction={direction}
      data-active={active ? "true" : "false"}
      variant={variant}
      size={size}
      tabIndex={active ? 0 : -1}
      aria-label={
        direction === "end" ? "Scroll to latest message" : "Scroll to start"
      }
      // Inert when inactive — keep out of AT tree without aria-hiding a focusable control.
      {...(!active
        ? ({ inert: true } as React.HTMLAttributes<HTMLButtonElement>)
        : {})}
      onClick={() => {
        if (direction === "end") scrollToEnd({ behavior })
        else scrollToStart({ behavior })
      }}
      className={cn(
        "absolute start-1/2 z-10 -translate-x-1/2 rounded-full border-border bg-background text-foreground shadow-sm transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground",
        "data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400",
        "data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100",
        "data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full",
        "data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full",
        "data-[direction=start]:[&_i]:rotate-180",
        className,
      )}
      {...props}
    >
      {children ?? (
        <>
          <i className="fa-light fa-arrow-down text-xs" aria-hidden="true" />
          <span className="sr-only">
            {direction === "end"
              ? "Scroll to latest message"
              : "Scroll to start"}
          </span>
        </>
      )}
    </Button>
  )
}

export {
  MessageScrollerProvider,
  MessageScroller,
  MessageScrollerViewport,
  MessageScrollerContent,
  MessageScrollerItem,
  MessageScrollerButton,
  useMessageScroller,
  useMessageScrollerScrollable,
  useMessageScrollerVisibility,
}
