import { MutableRefObject, useEffect, useRef } from 'react';
import {
  COMMIT_FRACTION,
  DEAD_ZONE_PX,
  EDGE_GUARD_PX,
  MIN_COMMIT_PX,
  RUBBER_BAND_FACTOR,
} from './geometry';

type Args = {
  // Root element the touch listeners attach to. Touches outside this
  // element never reach the pager — that's how we keep the gesture
  // scoped to the listing surface and out of detail routes.
  rootRef: MutableRefObject<HTMLDivElement | null>;
  // Index of the currently active pane. Mirrored into a ref so the
  // single bound effect reads fresh values without re-attaching.
  activeIdx: number;
  // Total number of panes. Used to clamp commit + rubber-band edges.
  tabsCount: number;
  // While true the listeners stay bound but every touchstart bails
  // immediately. Used by the parent to suppress the gesture when an
  // overlay sheet (settings, workspace switcher) is open — a swipe
  // there shouldn't navigate sibling tabs.
  disabled: boolean;
  // Setter for the live drag delta. The pager component re-renders the
  // strip transform on every change.
  setDragPx: (px: number, dragging: boolean) => void;
  // Commit a tab change. The caller is expected to reset dragPx to 0
  // AND call navigate(replace) in the same React batch so the strip's
  // transform jumps from (oldIdx, dragPx) to (newIdx, 0) in one render
  // — CSS transition then animates the (small) remaining distance
  // smoothly without an intermediate "snap back" flash.
  commitTo: (idx: number) => void;
};

// Horizontal swipe driver for the mobile listing tab pager. Mirrors
// the shape of `useCurtainHandleGesture`: single listener bound to
// the pager root, refs for the latest snap/index state, axis-resolve
// in the dead-zone, rubber-band at boundaries, threshold-commit on
// release.
//
// Conflict resolution with other gestures sharing the same surface
// (curtain, MobileSettingsHorseshoe, ChannelsWorkspaceHorseshoe) is
// cooperative: every gesture-owner resolves axis inside a dead-zone
// and bails when its own axis doesn't dominate. The pager and the
// horseshoes resolve at 12px; the CURTAIN hooks resolve at 10px
// (`stream-header/geometry.ts::DIRECTION_DEAD_ZONE_PX`) — i.e. in the
// 10–12px window the curtain decides first, by design (its vertical
// pull is the most frequent gesture and should feel the most eager).
// The pager wins horizontal; the others win vertical.
export function useMobileTabsPagerGesture({
  rootRef,
  activeIdx,
  tabsCount,
  disabled,
  setDragPx,
  commitTo,
}: Args): void {
  const activeRef = useRef(activeIdx);
  const countRef = useRef(tabsCount);
  const disabledRef = useRef(disabled);
  activeRef.current = activeIdx;
  countRef.current = tabsCount;
  disabledRef.current = disabled;

  useEffect(() => {
    const root = rootRef.current;
    if (!root) return undefined;

    let startX: number | null = null;
    let startY: number | null = null;
    let engaged = false;
    let bailed = false;
    let lastDragPx = 0;

    const reset = () => {
      startX = null;
      startY = null;
      engaged = false;
      bailed = false;
      lastDragPx = 0;
    };

    const onTouchStart = (e: TouchEvent) => {
      if (disabledRef.current) {
        reset();
        return;
      }
      if (e.touches.length !== 1) {
        reset();
        return;
      }
      const t = e.touches[0];
      const vw = window.innerWidth;
      // Android system back-gesture lives in the L/R edge strip in
      // edge-to-edge mode. Ignore touches there so we don't fight it.
      if (t.clientX < EDGE_GUARD_PX || t.clientX > vw - EDGE_GUARD_PX) {
        reset();
        return;
      }
      startX = t.clientX;
      startY = t.clientY;
      engaged = false;
      bailed = false;
      lastDragPx = 0;
    };

    const onTouchMove = (e: TouchEvent) => {
      if (e.touches.length !== 1) {
        // Second finger landed mid-gesture — abort without commit.
        if (engaged) setDragPx(0, false);
        reset();
        bailed = true;
        return;
      }
      // Defensive symmetry with onTouchStart's disabled check: a sheet
      // opening async between touchstart and touchmove (e.g. atom flip
      // from a delayed effect) shouldn't let an already-armed pager
      // gesture commit through.
      if (disabledRef.current) {
        if (engaged) setDragPx(0, false);
        reset();
        bailed = true;
        return;
      }
      if (startX === null || startY === null || bailed) return;
      const t = e.touches[0];
      const dx = t.clientX - startX;
      const dy = t.clientY - startY;

      if (!engaged) {
        // Wait for the finger to leave the dead-zone before deciding
        // who owns the gesture. The pager only engages when |dx|
        // strictly dominates |dy|; ties go to vertical (curtain +
        // horseshoe pull-down feels more natural than horizontal
        // commit for ambiguous gestures).
        if (Math.abs(dx) < DEAD_ZONE_PX && Math.abs(dy) < DEAD_ZONE_PX) return;
        if (Math.abs(dy) >= Math.abs(dx)) {
          bailed = true;
          return;
        }
        engaged = true;
      }

      if (e.cancelable) e.preventDefault();
      const vw = window.innerWidth;
      const idx = activeRef.current;
      const count = countRef.current;
      let drag = dx;
      // Rubber-band at the leftmost (dx > 0 = trying to go past idx 0)
      // and rightmost (dx < 0 = trying to go past last idx) boundary.
      // Soft attenuation — commit threshold can never be reached, so
      // the spring-back on release lands us back on the current tab.
      if (idx === 0 && dx > 0) drag = dx * RUBBER_BAND_FACTOR;
      else if (idx === count - 1 && dx < 0) drag = dx * RUBBER_BAND_FACTOR;
      // Clamp to ±one viewport so an overshooting swipe doesn't
      // translate the strip into nonsense territory.
      drag = Math.max(-vw, Math.min(vw, drag));
      lastDragPx = drag;
      setDragPx(drag, true);
    };

    const onTouchEnd = () => {
      if (!engaged) {
        reset();
        return;
      }
      // Defensive recheck symmetric with onTouchStart / onTouchMove:
      // an overlay sheet could have opened between the last touchmove
      // and this touchend (atom flip from a delayed effect, a system
      // dialog, etc.). Committing under those circumstances would
      // navigate sibling tabs from beneath the overlay — same hazard
      // the touchstart/move gates exist to prevent. Spring back
      // instead.
      if (disabledRef.current) {
        setDragPx(0, false);
        reset();
        return;
      }
      const vw = window.innerWidth;
      const idx = activeRef.current;
      const count = countRef.current;
      const threshold = Math.max(MIN_COMMIT_PX, vw * COMMIT_FRACTION);

      let nextIdx = idx;
      // Negative drag (finger moved left) → next tab to the right.
      // Positive drag (finger moved right) → previous tab to the left.
      if (lastDragPx <= -threshold && idx < count - 1) nextIdx = idx + 1;
      else if (lastDragPx >= threshold && idx > 0) nextIdx = idx - 1;

      if (nextIdx !== idx) {
        commitTo(nextIdx);
      } else {
        // No commit — re-enable transition and animate the strip back
        // to its resting position at the current tab.
        setDragPx(0, false);
      }

      reset();
    };

    const onTouchCancel = () => {
      // System cancel (incoming call, scroll-take-over, etc.) — never
      // commit; just spring back if a drag was in flight.
      if (engaged) setDragPx(0, false);
      reset();
    };

    root.addEventListener('touchstart', onTouchStart, { passive: true });
    root.addEventListener('touchmove', onTouchMove, { passive: false });
    root.addEventListener('touchend', onTouchEnd, { passive: true });
    root.addEventListener('touchcancel', onTouchCancel, { passive: true });
    return () => {
      root.removeEventListener('touchstart', onTouchStart);
      root.removeEventListener('touchmove', onTouchMove);
      root.removeEventListener('touchend', onTouchEnd);
      root.removeEventListener('touchcancel', onTouchCancel);
    };
    // setDragPx / commitTo are stable useCallbacks from the parent;
    // activeIdx / tabsCount are mirrored via the refs above so the
    // listener reads fresh values without re-binding on every nav.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [rootRef, setDragPx, commitTo]);
}
