'use client';

import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';

export interface UseChatScrollOptions {
  containerRef: RefObject<HTMLElement | null>;
  bottomRef: RefObject<HTMLElement | null>;
  isStreaming?: boolean;
  /** Distance from bottom (px) considered "at bottom". */
  bottomThresholdPx?: number;
  /** Bump key — increment when a new message arrives so the hook re-evaluates auto-scroll. */
  messagesCount?: number;
}

export interface UseChatScrollReturn {
  isAtBottom: boolean;
  unreadCount: number;
  scrollToBottom: (smooth?: boolean) => void;
  resetUnread: () => void;
}

export function useChatScroll(options: UseChatScrollOptions): UseChatScrollReturn {
  const {
    containerRef,
    bottomRef,
    isStreaming = false,
    bottomThresholdPx = 80,
    messagesCount = 0,
  } = options;

  const [isAtBottom, setIsAtBottom] = useState(true);
  const [unreadCount, setUnreadCount] = useState(0);
  const lastCountRef = useRef(messagesCount);
  const stickyRef = useRef(true);
  const wasStreamingRef = useRef(isStreaming);

  const scrollToBottom = useCallback(
    (smooth = false) => {
      const el = containerRef.current;
      if (!el) return;
      el.scrollTo({
        top: el.scrollHeight,
        behavior: smooth ? 'smooth' : 'auto',
      });
      stickyRef.current = true;
      setIsAtBottom(true);
      setUnreadCount(0);
    },
    [containerRef],
  );

  const resetUnread = useCallback(() => setUnreadCount(0), []);

  // Track scroll position relative to bottom.
  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const onScroll = () => {
      const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
      const atBottom = distance <= bottomThresholdPx;
      stickyRef.current = atBottom;
      setIsAtBottom(atBottom);
      if (atBottom) setUnreadCount(0);
    };
    onScroll();
    el.addEventListener('scroll', onScroll, { passive: true });
    return () => {
      el.removeEventListener('scroll', onScroll);
    };
  }, [containerRef, bottomThresholdPx]);

  // Stick to bottom while streaming, and one extra rAF after stream ends so
  // the final layout (markdown re-render, sources/tool-call panels) doesn't
  // push the latest content out of view.
  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    if (isStreaming) {
      wasStreamingRef.current = true;
      if (!stickyRef.current) return;
      let raf = 0;
      const tick = () => {
        if (!stickyRef.current) return;
        el.scrollTop = el.scrollHeight;
        raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
      return () => cancelAnimationFrame(raf);
    }

    // Stream just ended — flush one more scroll on the next two frames so
    // both markdown swap and any post-stream panel insertions are reflected.
    if (wasStreamingRef.current && stickyRef.current) {
      wasStreamingRef.current = false;
      let raf1 = 0;
      let raf2 = 0;
      raf1 = requestAnimationFrame(() => {
        el.scrollTop = el.scrollHeight;
        raf2 = requestAnimationFrame(() => {
          el.scrollTop = el.scrollHeight;
        });
      });
      return () => {
        cancelAnimationFrame(raf1);
        cancelAnimationFrame(raf2);
      };
    }
    wasStreamingRef.current = false;
    return;
  }, [containerRef, isStreaming]);

  // On message count increase, decide whether to scroll or bump unread.
  useEffect(() => {
    if (messagesCount > lastCountRef.current) {
      if (stickyRef.current) {
        const el = containerRef.current;
        if (el) el.scrollTop = el.scrollHeight;
      } else {
        setUnreadCount((n) => n + (messagesCount - lastCountRef.current));
      }
    }
    lastCountRef.current = messagesCount;
  }, [containerRef, messagesCount]);

  // Watch bottom sentinel just for symmetry/future hooks.
  useEffect(() => {
    void bottomRef;
  }, [bottomRef]);

  return { isAtBottom, unreadCount, scrollToBottom, resetUnread };
}
