import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type {
  MouseEvent as ReactMouseEvent,
  PointerEvent as ReactPointerEvent,
  RefObject,
} from 'react';

export type UseInertialHorizontalScrollOptions = {
  axis?: 'x' | 'y';
  scrollStepRatio?: number;
  dragThreshold?: number;
  friction?: number;
  minVelocity?: number;
};

export type UseInertialHorizontalScrollResult = {
  scrollRef: RefObject<HTMLDivElement>;
  isDragging: boolean;
  canScrollBack: boolean;
  canScrollForward: boolean;
  scrollBack: () => void;
  scrollForward: () => void;
  scrollProps: {
    onPointerDown: (evt: ReactPointerEvent<HTMLDivElement>) => void;
    onPointerMove: (evt: ReactPointerEvent<HTMLDivElement>) => void;
    onPointerUp: (evt: ReactPointerEvent<HTMLDivElement>) => void;
    onPointerCancel: (evt: ReactPointerEvent<HTMLDivElement>) => void;
    onLostPointerCapture: (evt: ReactPointerEvent<HTMLDivElement>) => void;
    onDragStartCapture: (evt: React.DragEvent<HTMLDivElement>) => void;
    onClickCapture: (evt: ReactMouseEvent<HTMLDivElement>) => void;
  };
};

const TEXT_SELECTION_TARGET_SELECTOR =
  '[data-allow-text-selection="true"], input, textarea, [contenteditable="true"]';

type ScrollState = {
  activePointerId: number | null;
  startPrimary: number;
  startScrollPrimary: number;
  lastPrimary: number;
  lastTime: number;
  velocity: number;
  dragging: boolean;
  moved: boolean;
  suppressClick: boolean;
  animationFrameId: number | null;
};

type PointerLikeEvent = {
  pointerId: number;
  clientX: number;
  clientY: number;
  timeStamp: number;
  preventDefault?: () => void;
};

const DEFAULT_OPTIONS: Required<UseInertialHorizontalScrollOptions> = {
  axis: 'x',
  scrollStepRatio: 1.3,
  dragThreshold: 3,
  friction: 0.95,
  minVelocity: 0.02,
};

/**
 * Shared hook for drag-scrolling horizontal overflow areas with inertial momentum.
 */
export function useInertialHorizontalScroll(
  options: UseInertialHorizontalScrollOptions = {}
): UseInertialHorizontalScrollResult {
  const resolvedOptions = useMemo(
    () => ({ ...DEFAULT_OPTIONS, ...options }),
    [options]
  );

  const scrollRef = useRef<HTMLDivElement>(null);
  const stateRef = useRef<ScrollState>({
    activePointerId: null,
    startPrimary: 0,
    startScrollPrimary: 0,
    lastPrimary: 0,
    lastTime: 0,
    velocity: 0,
    dragging: false,
    moved: false,
    suppressClick: false,
    animationFrameId: null,
  });
  const [isDragging, setIsDragging] = useState(false);
  const [canScrollBack, setCanScrollBack] = useState(false);
  const [canScrollForward, setCanScrollForward] = useState(false);

  const updateScrollState = useCallback(() => {
    const scroll = scrollRef.current;
    if (!scroll) return;

    const isHorizontal = resolvedOptions.axis === 'x';
    const scrollPrimary = isHorizontal ? scroll.scrollLeft : scroll.scrollTop;
    const scrollSize = isHorizontal ? scroll.scrollWidth : scroll.scrollHeight;
    const clientSize = isHorizontal ? scroll.clientWidth : scroll.clientHeight;
    const maxScrollPrimary = Math.max(scrollSize - clientSize, 0);
    setCanScrollBack(scrollPrimary > 1);
    setCanScrollForward(scrollPrimary < maxScrollPrimary - 1);
  }, [resolvedOptions.axis]);

  useEffect(() => {
    updateScrollState();

    const scroll = scrollRef.current;
    if (!scroll) return undefined;

    const handleScroll = () => {
      updateScrollState();
    };

    const handleResize = () => {
      updateScrollState();
    };

    scroll.addEventListener('scroll', handleScroll, { passive: true });
    window.addEventListener('resize', handleResize, { passive: true });

    return () => {
      scroll.removeEventListener('scroll', handleScroll);
      window.removeEventListener('resize', handleResize);
    };
  }, [updateScrollState]);

  useEffect(
    () => () => {
      const { animationFrameId } = stateRef.current;
      if (animationFrameId !== null) {
        cancelAnimationFrame(animationFrameId);
      }
    },
    []
  );

  const scrollByStep = useCallback((direction: 1 | -1) => {
    const scroll = scrollRef.current;
    if (!scroll) return;

    const isHorizontal = resolvedOptions.axis === 'x';
    const currentPrimary = isHorizontal ? scroll.scrollLeft : scroll.scrollTop;
    const offsetSize = isHorizontal ? scroll.offsetWidth : scroll.offsetHeight;
    scroll.scrollTo({
      ...(isHorizontal
        ? { left: currentPrimary + offsetSize / resolvedOptions.scrollStepRatio * direction }
        : { top: currentPrimary + offsetSize / resolvedOptions.scrollStepRatio * direction }),
      behavior: 'smooth',
    });
  }, [resolvedOptions.axis, resolvedOptions.scrollStepRatio]);

  const stopInertia = useCallback(() => {
    const state = stateRef.current;
    if (state.animationFrameId !== null) {
      cancelAnimationFrame(state.animationFrameId);
      state.animationFrameId = null;
    }
  }, []);

  const startInertia = useCallback(() => {
    const state = stateRef.current;
    const scroll = scrollRef.current;
    if (!scroll) return;
    const isHorizontal = resolvedOptions.axis === 'x';

    const step = (timestamp: number, previousTimestamp: number) => {
      const currentState = stateRef.current;
      const currentScroll = scrollRef.current;
      if (!currentScroll) return;

      const deltaMs = Math.max(timestamp - previousTimestamp, 1);
      const deltaScroll = currentState.velocity * deltaMs;
      if (Math.abs(currentState.velocity) < resolvedOptions.minVelocity) {
        currentState.animationFrameId = null;
        updateScrollState();
        return;
      }

      const before = isHorizontal ? currentScroll.scrollLeft : currentScroll.scrollTop;
      if (isHorizontal) {
        currentScroll.scrollLeft = before - deltaScroll;
      } else {
        currentScroll.scrollTop = before - deltaScroll;
      }
      updateScrollState();

      const after = isHorizontal ? currentScroll.scrollLeft : currentScroll.scrollTop;
      if (after === before) {
        currentState.animationFrameId = null;
        return;
      }

      currentState.velocity *= Math.pow(resolvedOptions.friction, deltaMs / 16.6667);
      currentState.animationFrameId = requestAnimationFrame((nextTimestamp) => step(nextTimestamp, timestamp));
    };

    stopInertia();
    state.animationFrameId = requestAnimationFrame((timestamp) => step(timestamp, timestamp));
  }, [resolvedOptions.axis, resolvedOptions.friction, resolvedOptions.minVelocity, stopInertia, updateScrollState]);

  const endDrag = useCallback((pointerId: number | null) => {
    const state = stateRef.current;
    if (!state.dragging) return;

    const scroll = scrollRef.current;
    if (scroll && pointerId !== null && scroll.hasPointerCapture(pointerId)) {
      scroll.releasePointerCapture(pointerId);
    }

    if (state.moved) {
      state.suppressClick = true;
      startInertia();
    }

    state.dragging = false;
    state.activePointerId = null;
    state.moved = false;
    state.velocity = 0;
    setIsDragging(false);
  }, [startInertia]);

  const processPointerMove = useCallback((evt: PointerLikeEvent) => {
    const state = stateRef.current;
    if (!state.dragging || state.activePointerId !== evt.pointerId) {
      return;
    }

    const scroll = scrollRef.current;
    if (!scroll) return;
    const isHorizontal = resolvedOptions.axis === 'x';

    const currentPrimary = isHorizontal ? evt.clientX : evt.clientY;
    const deltaPrimary = currentPrimary - state.startPrimary;
    const now = evt.timeStamp;
    const deltaTime = Math.max(now - state.lastTime, 1);
    const hasExceededThreshold = Math.abs(deltaPrimary) >= resolvedOptions.dragThreshold;

    if (hasExceededThreshold) {
      state.moved = true;
    }

    if (!state.moved) {
      state.lastPrimary = currentPrimary;
      state.lastTime = now;
      return;
    }

    evt.preventDefault?.();

    if (isHorizontal) {
      scroll.scrollLeft = state.startScrollPrimary - deltaPrimary;
    } else {
      scroll.scrollTop = state.startScrollPrimary - deltaPrimary;
    }

    const rawVelocity = (currentPrimary - state.lastPrimary) / deltaTime;
    state.velocity = Math.max(Math.min(rawVelocity, 2), -2);
    state.lastPrimary = currentPrimary;
    state.lastTime = now;
  }, [resolvedOptions.axis, resolvedOptions.dragThreshold]);

  const handlePointerDown = useCallback((evt: React.PointerEvent<HTMLDivElement>) => {
    if (evt.button !== 0) return;
    if (!evt.isPrimary) return;

    const state = stateRef.current;
    if (state.dragging && state.activePointerId === evt.pointerId) return;

    const target = evt.target;
    if (target instanceof Element && target.closest(TEXT_SELECTION_TARGET_SELECTOR)) {
      return;
    }

    const scroll = scrollRef.current;
    if (!scroll) return;
    const isHorizontal = resolvedOptions.axis === 'x';

    stopInertia();

    state.activePointerId = evt.pointerId;
    state.startPrimary = isHorizontal ? evt.clientX : evt.clientY;
    state.startScrollPrimary = isHorizontal ? scroll.scrollLeft : scroll.scrollTop;
    state.lastPrimary = isHorizontal ? evt.clientX : evt.clientY;
    state.lastTime = evt.timeStamp;
    state.velocity = 0;
    state.dragging = true;
    state.moved = false;
    state.suppressClick = false;

    try {
      scroll.setPointerCapture(evt.pointerId);
    } catch {
      // Ignore capture failures when browser has already ended pointer interaction.
    }

    setIsDragging(true);
  }, [resolvedOptions.axis, stopInertia]);

  const handlePointerMove = useCallback((evt: React.PointerEvent<HTMLDivElement>) => {
    processPointerMove(evt);
  }, [processPointerMove]);

  const handlePointerUp = useCallback((evt: React.PointerEvent<HTMLDivElement>) => {
    const state = stateRef.current;
    if (state.activePointerId !== evt.pointerId) return;
    endDrag(evt.pointerId);
  }, [endDrag]);

  const handlePointerCancel = useCallback((evt: React.PointerEvent<HTMLDivElement>) => {
    const state = stateRef.current;
    if (state.activePointerId !== evt.pointerId) return;
    endDrag(evt.pointerId);
  }, [endDrag]);

  const handleLostPointerCapture = useCallback((evt: React.PointerEvent<HTMLDivElement>) => {
    const state = stateRef.current;
    if (state.activePointerId !== evt.pointerId) return;
    endDrag(evt.pointerId);
  }, [endDrag]);

  useEffect(() => {
    const handleWindowPointerMove = (evt: PointerEvent) => {
      processPointerMove(evt);
    };

    const handleWindowPointerUpOrCancel = (evt: PointerEvent) => {
      const state = stateRef.current;
      if (state.activePointerId !== evt.pointerId) return;
      endDrag(evt.pointerId);
    };

    window.addEventListener('pointermove', handleWindowPointerMove, true);
    window.addEventListener('pointerup', handleWindowPointerUpOrCancel, true);
    window.addEventListener('pointercancel', handleWindowPointerUpOrCancel, true);

    return () => {
      window.removeEventListener('pointermove', handleWindowPointerMove, true);
      window.removeEventListener('pointerup', handleWindowPointerUpOrCancel, true);
      window.removeEventListener('pointercancel', handleWindowPointerUpOrCancel, true);
    };
  }, [endDrag, processPointerMove]);

  useEffect(() => {
    const handleGlobalDragTermination = () => {
      const state = stateRef.current;
      if (!state.dragging) return;
      endDrag(state.activePointerId);
    };

    const handleVisibilityChange = () => {
      if (document.visibilityState !== 'visible') {
        handleGlobalDragTermination();
      }
    };

    window.addEventListener('mouseup', handleGlobalDragTermination, true);
    window.addEventListener('pointerup', handleGlobalDragTermination, true);
    window.addEventListener('blur', handleGlobalDragTermination, true);
    document.addEventListener('visibilitychange', handleVisibilityChange, true);

    return () => {
      window.removeEventListener('mouseup', handleGlobalDragTermination, true);
      window.removeEventListener('pointerup', handleGlobalDragTermination, true);
      window.removeEventListener('blur', handleGlobalDragTermination, true);
      document.removeEventListener('visibilitychange', handleVisibilityChange, true);
    };
  }, [endDrag]);

  const handleClickCapture = useCallback((evt: React.MouseEvent<HTMLDivElement>) => {
    const state = stateRef.current;
    if (!state.suppressClick) return;

    state.suppressClick = false;
    evt.preventDefault();
    evt.stopPropagation();
  }, []);

  const handleDragStartCapture = useCallback((evt: React.DragEvent<HTMLDivElement>) => {
    evt.preventDefault();
  }, []);

  return {
    scrollRef,
    isDragging,
    canScrollBack,
    canScrollForward,
    scrollBack: () => scrollByStep(-1),
    scrollForward: () => scrollByStep(1),
    scrollProps: {
      onPointerDown: handlePointerDown,
      onPointerMove: handlePointerMove,
      onPointerUp: handlePointerUp,
      onPointerCancel: handlePointerCancel,
      onLostPointerCapture: handleLostPointerCapture,
      onDragStartCapture: handleDragStartCapture,
      onClickCapture: handleClickCapture,
    },
  };
}