import React, {
  MutableRefObject,
  ReactNode,
  useCallback,
  useEffect,
  useLayoutEffect,
  useRef,
} from 'react';
import { useAtomValue } from 'jotai';
import { mediaViewerAtom } from '../../state/mediaViewer';
import { useSwipeBackGesture } from './useSwipeBackGesture';
import { PAGER_TRANSITION_MS } from './geometry';
import * as css from './style.css';

type SwipeBackOverlayProps = {
  // The shared `tabsHost` element. We write `--swipe-x` and toggle
  // `data-swipe-dragging` on it; the card reads the inherited `--swipe-x` to
  // slide — one source of truth, zero React re-renders of the heavy chat
  // subtree during the drag.
  hostRef: MutableRefObject<HTMLDivElement | null>;
  // Run the animated card-pop. False when the back target is itself a detail
  // screen (e.g. a thread) — the chat still renders, but the gesture is not
  // bound and the user falls back to header / hardware back.
  swipeEnabled: boolean;
  // Changes whenever the routed chat changes (location.key). Re-runs the
  // CSS-var reset below so a fresh chat never inherits a prior drag's state —
  // the overlay subtree stays mounted across chat→chat navigation, so a
  // mount-only reset wouldn't fire.
  resetKey: string;
  // Pop to the parent (animate-out then navigate is handled here; this just
  // performs the actual `navigate(parent, { replace })`).
  onBack: () => void;
  children: ReactNode;
};

// Sliding chat layer painted above the persistent, STATIC listing pager. A
// rightward drag moves the card off to reveal the list behind it; past the
// threshold we animate the card fully off and THEN navigate — so the chat is
// never torn out mid-animation (a pop unmounts this overlay, unlike the tab
// pager which never unmounts).
export function SwipeBackOverlay({
  hostRef,
  swipeEnabled,
  resetKey,
  onBack,
  children,
}: SwipeBackOverlayProps) {
  const cardRef = useRef<HTMLDivElement>(null);
  const closingRef = useRef(false);
  // Cancels an in-flight commit animation (removes the transitionend listener
  // + clears the fallback timer) WITHOUT navigating. Stored so an external
  // navigation that interrupts the slide can abort it — otherwise the
  // leftover listener/timer would later fire `onBack` with a now-stale parent
  // and override the new route. Null when no commit is in flight.
  const cancelCommitRef = useRef<(() => void) | null>(null);
  // ONLY the media viewer is suppressed: it renders inside the chat and has
  // its OWN horizontal image paging, which a rightward swipe would fight. The
  // profile / members horseshoes are deliberately NOT suppressed — they're
  // vertical-only (no horizontal gesture to conflict with), so a rightward
  // swipe simply pops the whole chat (panel included), consistent with
  // "swipe right anywhere = back".
  const mediaOpen = !!useAtomValue(mediaViewerAtom);

  // The host's `--swipe-x` survives a previous commit (left at +vw) and the
  // host is NOT unmounted between chats, so reset to rest before paint on
  // every fresh chat (`resetKey` = location.key) — otherwise the card could
  // appear off-screen or frozen mid-drag until the first new gesture.
  useLayoutEffect(() => {
    const host = hostRef.current;
    if (!host) return;
    // An external navigation may have interrupted an in-flight commit from the
    // PRIOR chat (the overlay subtree persists across chat→chat nav). Abort it
    // so its stale `onBack` can't fire and override this route.
    cancelCommitRef.current?.();
    closingRef.current = false;
    host.style.setProperty('--swipe-x', '0px');
    host.dataset.swipeDragging = 'false';
  }, [hostRef, resetKey]);

  // Belt-and-suspenders: if the overlay unmounts mid-commit (e.g. the screen
  // crosses the mobile breakpoint), abort the pending commit so it can't fire
  // after unmount.
  useEffect(
    () => () => {
      cancelCommitRef.current?.();
    },
    []
  );

  const setDrag = useCallback(
    (px: number, dragging: boolean) => {
      const host = hostRef.current;
      if (!host || closingRef.current) return;
      host.style.setProperty('--swipe-x', `${px}px`);
      host.dataset.swipeDragging = dragging ? 'true' : 'false';
    },
    [hostRef]
  );

  const onCommit = useCallback(() => {
    const host = hostRef.current;
    const card = cardRef.current;
    if (closingRef.current) return;
    if (!host || !card) {
      onBack();
      return;
    }
    closingRef.current = true;
    // Enable the transition (data-swipe-dragging=false) and drive the card
    // fully off-screen; the static base already shows the destination list, so
    // when the overlay unmounts off-screen there is zero flash.
    host.dataset.swipeDragging = 'false';
    const vw = window.innerWidth || 1;
    host.style.setProperty('--swipe-x', `${vw}px`);

    let settled = false;
    let timer = 0;
    let onEnd: ((e: TransitionEvent) => void) | null = null;
    // `navigate` true on natural completion, false when an external nav aborts
    // us mid-slide (then we must NOT navigate — that would clobber the route).
    const settle = (navigate: boolean) => {
      if (settled) return;
      settled = true;
      if (onEnd) card.removeEventListener('transitionend', onEnd);
      if (timer) window.clearTimeout(timer);
      cancelCommitRef.current = null;
      if (navigate) onBack();
    };
    onEnd = (e: TransitionEvent) => {
      if (e.target === card && e.propertyName === 'transform') settle(true);
    };
    card.addEventListener('transitionend', onEnd);
    // Fallback if transitionend doesn't fire (transform interrupted/replaced,
    // or the card was already off-screen so there's no transition to end).
    timer = window.setTimeout(() => settle(true), PAGER_TRANSITION_MS + 80);
    cancelCommitRef.current = () => settle(false);
  }, [hostRef, onBack]);

  useSwipeBackGesture({
    rootRef: cardRef,
    enabled: swipeEnabled,
    disabled: mediaOpen,
    setDrag,
    onBack: onCommit,
  });

  return (
    <div className={css.overlayRoot}>
      <div ref={cardRef} className={css.card}>
        {children}
      </div>
    </div>
  );
}
