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

/**
 * Auto-scrolls a freshly expanded collapsible group into view inside a scroll
 * container, so opening a group near the bottom reveals its content without
 * manual scrolling. The (sticky) group header stays pinned at the top.
 *
 * Wiring:
 *   const { setContainer, registerGroup, onToggle } = useRevealOnExpand();
 *   <div ref={setContainer} className="overflow-y-auto">
 *     {groups.map((g) => (
 *       <div key={g.name} ref={registerGroup(g.name)}>
 *         <Header
 *           onToggle={() => {
 *             const willExpand = !isExpanded(g.name);
 *             toggle(g.name);
 *             onToggle(g.name, willExpand);
 *           }}
 *         />
 *         {isExpanded(g.name) && <Body />}
 *       </div>
 *     ))}
 *   </div>
 *
 * When the container element is also driven by another ref (e.g. the callback
 * ref from useRetainedScrollTop), compose them by calling both inside a single
 * callback ref.
 */
export function useRevealOnExpand<T extends HTMLElement = HTMLDivElement>() {
  const containerRef = useRef<T | null>(null);
  const groupRefs = useRef<Map<string, T>>(new Map());
  const groupRefCallbacks = useRef<Map<string, (node: T | null) => void>>(new Map());
  // A fresh object each toggle so re-expanding the same group still re-triggers.
  const [groupToReveal, setGroupToReveal] = useState<{ name: string } | null>(null);

  const setContainer = useCallback((node: T | null) => {
    containerRef.current = node;
  }, []);

  // Returns a stable ref callback per group name so React doesn't churn the map
  // (cleanup + re-register) on every render.
  const registerGroup = useCallback((name: string) => {
    const existing = groupRefCallbacks.current.get(name);
    if (existing) return existing;
    const cb = (node: T | null) => {
      if (node) groupRefs.current.set(name, node);
      else groupRefs.current.delete(name);
    };
    groupRefCallbacks.current.set(name, cb);
    return cb;
  }, []);

  // Call from a group's toggle handler: reveals the group when it is being
  // expanded, and cancels any pending reveal when it is being collapsed.
  const onToggle = useCallback((name: string, willExpand: boolean) => {
    setGroupToReveal(willExpand ? { name } : null);
  }, []);

  useEffect(() => {
    if (!groupToReveal) return;
    const el = groupRefs.current.get(groupToReveal.name);
    const container = containerRef.current;
    if (!el || !container) return;

    // Scroll down just enough to bring the bottom of the opened group into view.
    // For groups taller than the pane this scrolls past the header, but it stays
    // visible via its sticky positioning. Never scroll up (group already in view).
    const alignBottom = () => {
      const delta = el.getBoundingClientRect().bottom - container.getBoundingClientRect().bottom;
      if (delta <= 1) return;
      container.scrollTo({ top: container.scrollTop + delta });
    };

    alignBottom();

    // The group's charts and async data (e.g. nearby stations) can grow its
    // height after this first pass, so keep the bottom pinned as it settles.
    // Stop once the user scrolls or after a short grace period so we never fight
    // a deliberate scroll.
    let timer = 0;
    const observer = new ResizeObserver(alignBottom);
    const stop = () => {
      observer.disconnect();
      container.removeEventListener('wheel', stop);
      container.removeEventListener('touchmove', stop);
      window.clearTimeout(timer);
    };
    observer.observe(el);
    container.addEventListener('wheel', stop, { passive: true });
    container.addEventListener('touchmove', stop, { passive: true });
    timer = window.setTimeout(stop, 1500);

    return stop;
  }, [groupToReveal]);

  return { setContainer, registerGroup, onToggle };
}
