import { MutableRefObject, useEffect, useRef } from 'react';
import { isNativePlatform } from '../../utils/capacitor';
import {
  ACTIVE_CLOSE_THRESHOLD_PX,
  DIRECTION_DEAD_ZONE_PX,
  PIN_COMMIT_THRESHOLD,
  PIN_TRAVEL_PX,
  REFRESH_COMMIT_PX,
} from './geometry';
import { CurtainSnap, isFormSnap, isRefreshSnap } from './useCurtainState';

type Args = {
  // Drag-handle element at the top of the curtain. ALL curtain
  // gestures bind here — the chat list's scroll viewport is left to
  // native vertical scroll so finger-down inside the list never races
  // a pin / refresh / form-close path. Mounted as the first flex child
  // of the curtain in StreamHeader.tsx.
  handleRef: MutableRefObject<HTMLDivElement | null>;
  // Current snap stop. Mirrored into a ref so the listener (bound
  // once per `disabled` flip) reads fresh values without re-attaching.
  // Every snap participates: closed → pin/refresh, refresh → close,
  // form-* → form-close, pinned-overlay → unpin.
  snap: CurtainSnap;
  // Per-pane pinned overlay. When true the handle's drag-down path
  // commits unpin; when false the snap drives which transition arms.
  pinned: boolean;
  // Setter for the pinned overlay; called on release once the user's
  // drag past the commit threshold qualifies the gesture.
  setPinned: (next: boolean) => void;
  // Setter for the live drag delta during touchmove. The hook reads
  // `liveDragPx` from the parent state too, so React drives the
  // curtain's `top` re-render — no direct DOM writes.
  setLiveDrag: (px: number, dragging: boolean) => void;
  // Snap commit. Called on release for refresh / close / form-close
  // (the pin / unpin paths flip `pinned` instead). Narrowed to the
  // two non-form destinations the hook ever reaches. Also resets
  // liveDragPx + isDragging atomically inside the parent state.
  commit: (next: 'refresh' | 'closed') => void;
  // Suppress gesture binding entirely. Used to gate motion when a
  // bottom sheet is open or when this pane is inactive inside the
  // swipe pager.
  disabled?: boolean;
  // Optional sink for handle-visual state — drives the grabber pill's
  // «idle / dragging / threshold reached» appearance via
  // `data-dragging` and `data-at-commit` on the handle div. Called
  // only when the state actually changes, so the consumer doesn't pay
  // a re-render on every touchmove.
  setHandleState?: (state: { dragging: boolean; atCommit: boolean }) => void;
};

// Curtain transitions either gesture surface can resolve. Each one
// has its own commit threshold and release destination (snap commit
// vs pin flip); per-surface dynamics (1:1 on the handle, rubber-band
// on the curtain body) decide how raw finger displacement translates
// into curtain motion — see `onTouchMove` here for the 1:1 branches
// and `useCurtainBodyGesture` for the rubber-banded equivalents.
//
// `closed-free` is the single free-range transition that spans the
// full pin↔closed↔refresh vertical range in one gesture. From the
// closed snap, neither direction is locked at the dead-zone: the user
// can drag up past the safe-top zone OR down into the refresh card in
// one motion, and the release decides pin / refresh / snap-back based
// on the final position. The earlier pair of one-shot `pin` and
// `refresh` transitions used a hard «gate» at the start point (each
// direction was clamped to one side of 0 once the dead-zone resolved
// the direction) and the user reported this as a regression — drag
// up, then back down, ran into an invisible wall at the closed
// position before the down reveal could engage.
//
// `pinned-free` is the free-range DOWN transition for the pinned
// overlay: from pinned + drag DOWN the curtain follows the finger and
// commits unpin (back to closed) past the threshold. UP is no-op (no
// destination above pinned). Only the handle resolves to `pinned-free`
// — the body gesture bails at touchstart while pinned so unpin remains
// a deliberate handle pull.
export type CurtainTransition = 'closed-free' | 'pinned-free' | 'close-refresh' | 'form-close';

// Exhaustive-check helper. Used in the `default` branch of every
// switch over `CurtainTransition | null` so that adding a fifth
// variant to the union fails typecheck at every dispatch site
// rather than silently no-op'ing through default. The argument is
// prefixed with `_` so eslint's `argsIgnorePattern: '^_'` keeps the
// rule happy without us tagging it `// eslint-disable`.
export const assertNeverCurtainTransition = (_value: never): void => {};

// Decide which transition the gesture arms based on the snap state
// at direction-resolution time and the finger direction. `null` means
// the (snap, pinned, direction) triple has no valid motion and the
// gesture must bail so native scroll / pager swipe / nothing-at-all
// owns the touch.
//
// Direction guards encoded here:
//   * pinned + UP    → no-op (would push the curtain past safe-top
//                       on commit — no destination above pinned).
//   * pinned + DOWN  → pinned-free (HANDLE-only contract — the body
//                       hook bails entirely while pinned so unpin
//                       stays a deliberate handle pull. See
//                       `useCurtainBodyGesture::onTouchStart`).
//   * closed (any)   → closed-free (single transition spanning the
//                       whole pin↔closed↔refresh range; direction at
//                       the dead-zone matters only for the
//                       horizontal-bail check).
//   * refresh + UP   → close-refresh (retreat to closed).
//   * refresh + DOWN → no-op (nothing lower to reveal).
//   * form-* + UP    → form-close.
//   * form-* + DOWN  → no-op (form is already the lowest snap).
export function resolveCurtainTransition(
  snap: CurtainSnap,
  pinned: boolean,
  direction: 'up' | 'down'
): CurtainTransition | null {
  if (pinned) return direction === 'down' ? 'pinned-free' : null;
  if (snap === 'closed') return 'closed-free';
  if (isRefreshSnap(snap)) return direction === 'up' ? 'close-refresh' : null;
  if (isFormSnap(snap)) return direction === 'up' ? 'form-close' : null;
  return null;
}

// Touch-gesture driver for the dedicated 32 px drag-handle at the top
// of the curtain. Native-only: listeners aren't attached on web /
// desktop.
//
// The handle is the «authoritative» gesture surface — it owns every
// transition (closed-free, pinned-free, close-refresh, form-close)
// with crisp 1:1 finger ↔ curtain tracking regardless of whether
// the chat list inside the curtain is scrollable. The curtain BODY
// has a parallel gesture (`useCurtainBodyGesture`) with rubber-
// banded dynamics that only engages when the body's chat list has
// no scrollable content — so the user can pull the curtain «from
// anywhere» on empty / short lists but a real list-scroll is never
// hijacked under their finger. The body is also fully inert while
// pinned, so unpin stays a deliberate handle pull.
//
// Design rationale: gestures used to bind to the chat list's scroll
// viewport directly, which produced repeating «drag-at-scrollTop=0
// hijacks for pin/reveal» bugs. Moving every transition onto a
// dedicated handle (plus an opt-in body surface that bails on
// scrollable lists) removes the scroll/gesture race entirely.
//
// Per-transition dynamics — all track the finger 1:1, but the clamp
// shapes differ to keep on-screen motion sensible while preserving
// the «drag up off-screen from anywhere» feel the user explicitly
// asked for:
//   * closed-free       — NO clamps either side. Finger goes off-
//                         screen up → curtain follows past safe-top;
//                         finger crosses back below the start point →
//                         curtain continues into the refresh card in
//                         the same gesture. Direction-aware commit
//                         on release: pin if pulled UP past
//                         PIN_COMMIT_THRESHOLD × PIN_TRAVEL_PX, refresh
//                         if pulled DOWN past REFRESH_COMMIT_PX, else
//                         snap back to closed.
//   * pinned-free       — DOWN-only free-range drag from pinned.
//                         Clamped at 0 below (no destination above
//                         pinned). Release commits unpin past
//                         PIN_COMMIT_THRESHOLD × PIN_TRAVEL_PX, else
//                         snaps back to pinned.
//   * close-refresh     — capped at 0 below (no transition lower than
//                         the refresh card), NO upper clamp (drag past
//                         closed into safe-top freely). Commit at
//                         ACTIVE_CLOSE_THRESHOLD_PX (absolute).
//   * form-close        — capped at 0 so a downward jitter can't
//                         push the curtain below its form-snap top,
//                         NO upper clamp. Commit at
//                         ACTIVE_CLOSE_THRESHOLD_PX (absolute).
//
// Handle visual: emitHandle(true, atCommit) fires on every transition
// during touchmove so the grabber pill animates Primary-blue +
// stretches as the user crosses the commit threshold, no matter which
// motion is in flight. The dedupe keeps consumer re-renders bounded
// to actual state flips. The body hook shares the same setHandleState
// sink — only one of the two surfaces is engaged at any moment, so
// they never fight over the visual.
export function useCurtainHandleGesture({
  handleRef,
  snap,
  pinned,
  setPinned,
  setLiveDrag,
  commit,
  disabled,
  setHandleState,
}: Args): void {
  const snapRef = useRef<CurtainSnap>(snap);
  snapRef.current = snap;
  const pinnedRef = useRef<boolean>(pinned);
  pinnedRef.current = pinned;
  const setPinnedRef = useRef(setPinned);
  setPinnedRef.current = setPinned;
  const commitRef = useRef(commit);
  commitRef.current = commit;
  const setHandleStateRef = useRef(setHandleState);
  setHandleStateRef.current = setHandleState;

  useEffect(() => {
    if (!isNativePlatform()) return undefined;
    if (disabled) return undefined;
    const handle = handleRef.current;
    if (!handle) return undefined;

    let startX: number | null = null;
    let startY: number | null = null;
    let direction: 'up' | 'down' | null = null;
    let transition: CurtainTransition | null = null;
    let engaged = false;
    let lastDelta = 0;
    // Last visual state emitted to the consumer. We dedupe here so the
    // setter (a React useState) only re-renders when something actually
    // changed, not on every 60fps touchmove.
    let emittedDragging = false;
    let emittedAtCommit = false;
    const emitHandle = (dragging: boolean, atCommit: boolean) => {
      if (dragging === emittedDragging && atCommit === emittedAtCommit) return;
      emittedDragging = dragging;
      emittedAtCommit = atCommit;
      setHandleStateRef.current?.({ dragging, atCommit });
    };

    const onTouchStart = (e: TouchEvent) => {
      if (e.touches.length !== 1) return;
      startX = e.touches[0].clientX;
      startY = e.touches[0].clientY;
      direction = null;
      transition = null;
      engaged = false;
      lastDelta = 0;
    };

    const onTouchMove = (e: TouchEvent) => {
      if (e.touches.length !== 1) {
        // Second finger landed mid-gesture — abort.
        startX = null;
        startY = null;
        direction = null;
        transition = null;
        if (engaged) setLiveDrag(0, false);
        engaged = false;
        lastDelta = 0;
        emitHandle(false, false);
        return;
      }
      if (startY === null) return;

      const delta = e.touches[0].clientY - startY;
      const deltaX = startX !== null ? e.touches[0].clientX - startX : 0;

      // Resolve a direction once the finger crosses the dead-zone.
      if (direction === null) {
        if (Math.abs(delta) < DIRECTION_DEAD_ZONE_PX) return;
        // Horizontal-bail: if |dx| strictly exceeds |dy|, the user is
        // swiping the mobile tab pager, not pulling the curtain. Drop
        // tracking so the pager owns the gesture.
        if (Math.abs(deltaX) > Math.abs(delta)) {
          startX = null;
          startY = null;
          direction = null;
          return;
        }
        direction = delta > 0 ? 'down' : 'up';
        transition = resolveCurtainTransition(snapRef.current, pinnedRef.current, direction);
        // (snap, pinned, direction) has no valid motion — pinned+up,
        // refresh+down, form+down. Bail so the gesture can be re-armed
        // on the next touch sequence; no preventDefault is fired so the
        // browser keeps any default behaviour (it would be a no-op
        // here anyway — the handle has touchAction:none in CSS).
        if (transition === null) {
          startX = null;
          startY = null;
          direction = null;
          return;
        }
      }

      engaged = true;
      e.preventDefault();

      // Clamp the raw finger delta into the live curtain displacement
      // (`lastDelta`). Stored separately because the commit math on
      // release needs the same value the curtain was visually showing.
      let atCommit = false;
      switch (transition) {
        case 'closed-free':
          // Single free-range drag spanning pin↔closed↔refresh. 1:1
          // with NO clamps either side: the curtain follows the finger
          // off-screen upward (past safe-top) and continuously into the
          // refresh card downward in the same gesture. The release
          // decides pin / refresh / snap-back from the final lastDelta.
          lastDelta = delta;
          // Direction-aware atCommit so the grabber pill stretches
          // whichever way the user is committing. Pin and refresh are
          // sign-exclusive (one branch can't fire simultaneously with
          // the other) so a simple ternary on `lastDelta` suffices.
          atCommit =
            lastDelta <= 0
              ? -lastDelta / PIN_TRAVEL_PX >= PIN_COMMIT_THRESHOLD
              : lastDelta >= REFRESH_COMMIT_PX;
          break;
        case 'pinned-free':
          // 1:1 down from pinned. Clamped at 0 below (a downward
          // jitter past the start mustn't push the curtain into
          // safe-top — there's no destination above pinned). Release
          // commits unpin past the threshold.
          lastDelta = Math.max(0, delta);
          atCommit = lastDelta / PIN_TRAVEL_PX >= PIN_COMMIT_THRESHOLD;
          break;
        case 'close-refresh':
          // 1:1 up; delta is negative. Lower-capped at 0 (a downward
          // jitter shouldn't push past the refresh snap), NO upper clamp
          // — the curtain follows the finger off-screen freely in the
          // safe-top direction, matching the «drag up off-screen from
          // anywhere» expectation.
          lastDelta = Math.min(0, delta);
          atCommit = -lastDelta >= ACTIVE_CLOSE_THRESHOLD_PX;
          break;
        case 'form-close':
          // Form close: finger moves UP (delta < 0). Track 1:1, capped
          // at 0 so an accidental downward jitter doesn't push the
          // curtain below its resting form-snap position.
          lastDelta = Math.min(0, delta);
          atCommit = -lastDelta >= ACTIVE_CLOSE_THRESHOLD_PX;
          break;
        case null:
          // Unreachable: `engaged` is set only after `transition` is
          // resolved non-null in the dead-zone block above; reaching
          // this case would imply the gesture engaged without a
          // transition, which the control flow above forbids.
          break;
        default: {
          // Exhaustive guard. The `never` cast turns a future addition
          // to `CurtainTransition` into a compile error here — adding
          // a fifth member without wiring its dispatch fails typecheck.
          assertNeverCurtainTransition(transition);
          break;
        }
      }
      setLiveDrag(lastDelta, true);
      emitHandle(true, atCommit);
    };

    const onTouchEnd = () => {
      if (!engaged) {
        startX = null;
        startY = null;
        direction = null;
        transition = null;
        return;
      }
      // Commit decision per transition. setPinned() and commit() each
      // reset liveDragPx + isDragging in the same batched update —
      // React renders the curtain at the new resting top with the snap
      // transition re-enabled. Non-commit paths drop the live drag back
      // to 0 with transition active so the curtain springs back.
      switch (transition) {
        case 'closed-free':
          // Direction-aware commit from the free-range drag. Pin
          // wins over refresh if both somehow qualified (sign-exclusive
          // in practice — lastDelta can't be simultaneously <0 and
          // >0). Below either threshold, spring back to closed.
          if (-lastDelta / PIN_TRAVEL_PX >= PIN_COMMIT_THRESHOLD) {
            setPinnedRef.current(true);
          } else if (lastDelta >= REFRESH_COMMIT_PX) {
            commitRef.current('refresh');
          } else {
            setLiveDrag(0, false);
          }
          break;
        case 'pinned-free':
          // Unpin if the finger crossed the unpin threshold; else snap
          // back to pinned.
          if (lastDelta / PIN_TRAVEL_PX >= PIN_COMMIT_THRESHOLD) {
            setPinnedRef.current(false);
          } else {
            setLiveDrag(0, false);
          }
          break;
        case 'close-refresh':
          if (-lastDelta >= ACTIVE_CLOSE_THRESHOLD_PX) {
            commitRef.current('closed');
          } else {
            setLiveDrag(0, false);
          }
          break;
        case 'form-close':
          if (-lastDelta >= ACTIVE_CLOSE_THRESHOLD_PX) {
            commitRef.current('closed');
          } else {
            setLiveDrag(0, false);
          }
          break;
        case null:
          // Unreachable: `engaged` is set only after `transition` is
          // resolved non-null. Mirrors the touchmove switch.
          setLiveDrag(0, false);
          break;
        default: {
          // Exhaustive guard — see the touchmove switch for the same
          // pattern. setLiveDrag fallback preserves spring-back if a
          // future transition lands here unhandled at runtime.
          assertNeverCurtainTransition(transition);
          setLiveDrag(0, false);
          break;
        }
      }
      startX = null;
      startY = null;
      direction = null;
      transition = null;
      engaged = false;
      lastDelta = 0;
      emitHandle(false, false);
    };

    const onTouchCancel = () => {
      // System cancel never commits — always snap back to current snap.
      if (engaged) setLiveDrag(0, false);
      startX = null;
      startY = null;
      direction = null;
      transition = null;
      engaged = false;
      lastDelta = 0;
      emitHandle(false, false);
    };

    handle.addEventListener('touchstart', onTouchStart, { passive: true });
    handle.addEventListener('touchmove', onTouchMove, { passive: false });
    handle.addEventListener('touchend', onTouchEnd, { passive: true });
    handle.addEventListener('touchcancel', onTouchCancel, { passive: true });
    return () => {
      handle.removeEventListener('touchstart', onTouchStart);
      handle.removeEventListener('touchmove', onTouchMove);
      handle.removeEventListener('touchend', onTouchEnd);
      handle.removeEventListener('touchcancel', onTouchCancel);
      // If `disabled` flips true while a drag is in flight, the touchend
      // we'd normally rely on for snap-back never reaches us (the listener
      // is gone). Without an explicit reset the curtain stays frozen at
      // the finger position with `transition: none` and the grabber pill
      // stuck Primary-blue until the user starts a new touch — visible as
      // a half-open curtain after, say, a sheet opens mid-drag.
      if (engaged) {
        setLiveDrag(0, false);
        emitHandle(false, false);
      }
    };
    // setLiveDrag is a stable useCallback; handleRef is stable. `snap`,
    // `pinned`, `setPinned` and `commit` are mirrored via the refs
    // above so the listener (bound once per `disabled` flip) reads
    // fresh values without re-attaching every render. `disabled` is
    // the only signal that needs to tear the listeners down — it goes
    // into the deps.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [handleRef, setLiveDrag, disabled]);
}
