"use client";

import {
  Children,
  isValidElement,
  type ReactNode,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";
import {
  resetQuestionKeyboardState,
  useQuestionViewportCoordinator,
} from "./question-viewport-coordinator";

const WHEEL_GESTURE_IDLE_MS = 320;
const BACKGROUND_DRAG_SLOP = 10;
const OPTION_CARD_DRAG_SLOP = 16;
const FAST_FLICK_MIN_DISTANCE = 36;
const DRAG_COMMIT_RATIO = 0.3;
const DRAG_FLICK_VELOCITY = 0.5;
const SNAP_ANIMATION_MS = 340;
const SNAP_EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
const RUBBER_BAND_RESISTANCE = 0.4;

/**
 * Text inputs and direct embedded controls that MUST be completely isolated
 * from the snap/drag gesture recognizer so first-tap focus, virtual keyboard,
 * and caret manipulation work 100% reliably.
 */
const TEXT_INPUT_SELECTOR = [
  'input:not([type="radio"]):not([type="checkbox"])',
  "textarea",
  "select",
  '[contenteditable="true"]',
  'input[type="range"]',
  "[data-snap-drag-ignore]",
].join(", ");

/**
 * Option cards, radio/checkbox labels, and buttons on choice questions
 * where tap = select option, but vertical drag > slop = swipe question.
 */
const OPTION_CARD_SELECTOR = [
  "label",
  "button",
  "a",
  '[role="button"]',
  '[role="option"]',
  '[role="checkbox"]',
  '[role="radio"]',
  '[role="switch"]',
  '[role="combobox"]',
  '[role="listbox"]',
].join(", ");

type SnapDragState = {
  pointerDown: boolean;
  hardIgnored: boolean;
  isOptionCard: boolean;
  engaged: boolean;
  didDrag: boolean;
  animating: boolean;
  baseOffset: number;
  startX: number;
  startY: number;
  lastY: number;
  startTime: number;
  lastMoveTime: number;
  velocity: number;
  offset: number;
  height: number;
  trioIndices: number[];
  cleanupTimer: number | null;
};

type QuestionSnapListProps = {
  children: ReactNode;
  className?: string;
  firstQuestionHint?: ReactNode;
  onQuestionExit?: (currentIndex: number, nextIndex: number) => void;
  onQuestionTransition?: (currentIndex: number, nextIndex: number) => void;
  onActiveIndexChange?: (index: number) => void;
  alignTop?: boolean;
};

export function QuestionSnapList({
  children,
  className,
  firstQuestionHint,
  onQuestionExit,
  onQuestionTransition,
  onActiveIndexChange,
  alignTop,
}: QuestionSnapListProps) {
  useQuestionViewportCoordinator();
  const questions = Children.toArray(children);
  const wheelLockedRef = useRef(false);
  const wheelUnlockTimeoutRef = useRef<number | null>(null);
  const touchStartYRef = useRef<number | null>(null);
  const questionRefs = useRef<Array<HTMLDivElement | null>>([]);
  const containerRef = useRef<HTMLElement | null>(null);
  const previousActiveIndexRef = useRef<number | null>(null);
  const suppressNextClickRef = useRef(false);
  const [activeIndex, setActiveIndex] = useState(0);

  const activeIndexRef = useRef(activeIndex);
  activeIndexRef.current = activeIndex;
  const questionsCountRef = useRef(questions.length);
  questionsCountRef.current = questions.length;

  const dragRef = useRef<SnapDragState>({
    pointerDown: false,
    hardIgnored: false,
    isOptionCard: false,
    engaged: false,
    didDrag: false,
    animating: false,
    baseOffset: 0,
    startX: 0,
    startY: 0,
    lastY: 0,
    startTime: 0,
    lastMoveTime: 0,
    velocity: 0,
    offset: 0,
    height: 0,
    trioIndices: [],
    cleanupTimer: null,
  });

  const stepQuestion = useCallback(
    (direction: 1 | -1) => {
      const nextIndex = Math.max(
        0,
        Math.min(questions.length - 1, activeIndex + direction),
      );

      if (nextIndex === activeIndex) {
        return;
      }

      resetQuestionKeyboardState();
      onQuestionExit?.(activeIndex, nextIndex);
      setActiveIndex(nextIndex);
    },
    [activeIndex, onQuestionExit, questions.length],
  );

  const scheduleWheelUnlock = useCallback(() => {
    if (wheelUnlockTimeoutRef.current !== null) {
      window.clearTimeout(wheelUnlockTimeoutRef.current);
    }

    wheelUnlockTimeoutRef.current = window.setTimeout(() => {
      wheelLockedRef.current = false;
      wheelUnlockTimeoutRef.current = null;
    }, WHEEL_GESTURE_IDLE_MS);
  }, []);

  const hasInitializedActiveIndexRef = useRef(false);

  useEffect(() => {
    if (questions.length > 0 && activeIndex >= questions.length) {
      setActiveIndex(questions.length - 1);
    }
  }, [questions.length, activeIndex]);

  useEffect(() => {
    if (hasInitializedActiveIndexRef.current || questions.length === 0) {
      return;
    }

    const timerId = window.setTimeout(() => {
      if (hasInitializedActiveIndexRef.current) {
        return;
      }

      for (let index = 0; index < questions.length; index += 1) {
        const questionElement = questionRefs.current[index];

        if (!questionElement) {
          continue;
        }

        const isDisabled =
          questionElement.getAttribute("data-question-disabled") === "true" ||
          questionElement.querySelector("[data-question-disabled='true']") !==
            null;

        if (isDisabled) {
          continue;
        }

        const explicitAnsweredState =
          questionElement.getAttribute("data-question-answered") ??
          questionElement
            .querySelector("[data-question-answered]")
            ?.getAttribute("data-question-answered");

        const isAnswered =
          explicitAnsweredState === "true" ||
          (explicitAnsweredState !== "false" &&
            Array.from(
              questionElement.querySelectorAll<
                HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
              >("input, select, textarea"),
            ).some((input) => {
              if (input instanceof HTMLInputElement) {
                if (input.type === "checkbox" || input.type === "radio") {
                  return input.checked;
                }
                if (input.type === "file") {
                  return input.files !== null && input.files.length > 0;
                }
              }
              return input.value.trim().length > 0;
            }));

        if (!isAnswered) {
          hasInitializedActiveIndexRef.current = true;
          setActiveIndex(index);
          return;
        }
      }

      hasInitializedActiveIndexRef.current = true;
    }, 50);

    return () => {
      window.clearTimeout(timerId);
    };
  }, [questions.length]);

  useEffect(() => {
    onActiveIndexChange?.(activeIndex);
  }, [activeIndex, onActiveIndexChange]);

  useEffect(() => {
    const previousActiveIndex = previousActiveIndexRef.current;
    if (previousActiveIndex !== null && previousActiveIndex !== activeIndex) {
      onQuestionTransition?.(previousActiveIndex, activeIndex);
    }
    previousActiveIndexRef.current = activeIndex;
  }, [activeIndex, onQuestionTransition]);

  useEffect(() => {
    return () => {
      if (wheelUnlockTimeoutRef.current !== null) {
        window.clearTimeout(wheelUnlockTimeoutRef.current);
      }
      if (dragRef.current.cleanupTimer !== null) {
        window.clearTimeout(dragRef.current.cleanupTimer);
      }
    };
  }, []);

  const readTranslateY = useCallback((element: HTMLElement): number => {
    const transform = window.getComputedStyle(element).transform;
    if (!transform || transform === "none") {
      return 0;
    }

    try {
      return new DOMMatrix(transform).m42;
    } catch {
      return 0;
    }
  }, []);

  const styleDragPanel = useCallback(
    (element: HTMLDivElement, transform: string, animate: boolean) => {
      element.style.transition = animate
        ? `transform ${SNAP_ANIMATION_MS}ms ${SNAP_EASE}`
        : "none";
      element.style.transform = transform;
      element.style.opacity = "1";
      element.style.zIndex = "10";
    },
    [],
  );

  // Positive offset slides the active panel up (towards the next question),
  // exactly like following a finger in a reels-style pager.
  const applyDragOffset = useCallback(
    (offset: number, animate: boolean) => {
      const index = activeIndexRef.current;
      const height = dragRef.current.height;

      const panels: Array<[HTMLDivElement | null, number]> = [
        [questionRefs.current[index], -offset],
        [questionRefs.current[index + 1], height - offset],
        [questionRefs.current[index - 1], -height - offset],
      ];

      for (const [element, translateY] of panels) {
        if (!element) continue;
        styleDragPanel(element, `translateY(${translateY}px)`, animate);
      }
    },
    [styleDragPanel],
  );

  const clearDragStyles = useCallback(() => {
    const drag = dragRef.current;
    const index = activeIndexRef.current;
    const targets = new Set<number>([
      ...drag.trioIndices,
      index - 1,
      index,
      index + 1,
    ]);
    for (const position of targets) {
      const element = questionRefs.current[position];
      if (!element) continue;
      element.style.transition = "";
      element.style.transform = "";
      element.style.opacity = "";
      element.style.zIndex = "";
    }
    drag.trioIndices = [];
  }, []);

  const snapPanelsTo = useCallback(
    (offset: number) => {
      const drag = dragRef.current;
      const index = activeIndexRef.current;
      drag.animating = true;
      drag.trioIndices = [index - 1, index, index + 1];
      applyDragOffset(offset, true);
      if (drag.cleanupTimer !== null) {
        window.clearTimeout(drag.cleanupTimer);
      }
      drag.cleanupTimer = window.setTimeout(() => {
        drag.cleanupTimer = null;
        drag.animating = false;
        clearDragStyles();
      }, SNAP_ANIMATION_MS + 40);
    },
    [applyDragOffset, clearDragStyles],
  );

  const handleWheel = useCallback(
    (event: React.WheelEvent<HTMLElement>) => {
      if (document.body.classList.contains("dropdown-open")) {
        return;
      }

      const delta = event.deltaY || event.deltaX;

      if (delta === 0 || questions.length < 2) {
        return;
      }

      event.preventDefault();

      if (!wheelLockedRef.current) {
        wheelLockedRef.current = true;
        stepQuestion(delta > 0 ? 1 : -1);
      }

      scheduleWheelUnlock();
    },
    [questions.length, scheduleWheelUnlock, stepQuestion],
  );

  const handleTouchStart = useCallback(
    (event: React.TouchEvent<HTMLElement>) => {
      if (document.body.classList.contains("dropdown-open")) {
        return;
      }

      // Always reset click suppression on any new touch start
      suppressNextClickRef.current = false;

      const drag = dragRef.current;
      if (drag.cleanupTimer !== null) {
        window.clearTimeout(drag.cleanupTimer);
        drag.cleanupTimer = null;
      }

      const target = event.target as HTMLElement | null;

      // 1. Hard-exclude text inputs and standalone controls
      const isTextInput = Boolean(target?.closest?.(TEXT_INPUT_SELECTOR));
      if (isTextInput) {
        drag.pointerDown = false;
        drag.engaged = false;
        drag.hardIgnored = true;
        drag.isOptionCard = false;
        drag.didDrag = false;
        drag.baseOffset = 0;
        drag.offset = 0;
        touchStartYRef.current = null;
        return;
      }

      // 2. Identify option cards / buttons for soft ownership
      const isOptionCard = Boolean(target?.closest?.(OPTION_CARD_SELECTOR));
      drag.hardIgnored = false;
      drag.isOptionCard = isOptionCard;
      drag.didDrag = false;

      drag.height =
        containerRef.current?.getBoundingClientRect().height ??
        window.innerHeight;

      if (drag.animating && !isOptionCard) {
        // Grabbed mid-snap on non-option background: freeze panels and continue drag
        const activeElement = questionRefs.current[activeIndexRef.current];
        if (activeElement) {
          drag.baseOffset = -readTranslateY(activeElement);
          applyDragOffset(drag.baseOffset, false);
        } else {
          drag.baseOffset = 0;
        }
        drag.animating = false;
        drag.engaged = true;
      } else {
        drag.baseOffset = 0;
        drag.engaged = false;
      }

      drag.pointerDown = true;
      drag.velocity = 0;
      drag.offset = drag.baseOffset;
      const touch = event.touches[0];
      drag.startX = touch?.clientX ?? 0;
      drag.startY = touch?.clientY ?? 0;
      drag.lastY = drag.startY;
      drag.startTime = event.timeStamp || performance.now();
      drag.lastMoveTime = drag.startTime;
      touchStartYRef.current = drag.startY;
    },
    [applyDragOffset, readTranslateY],
  );

  const handleTouchMove = useCallback(
    (event: React.TouchEvent<HTMLElement>) => {
      if (document.body.classList.contains("dropdown-open")) {
        return;
      }

      const drag = dragRef.current;

      if (!drag.pointerDown || drag.hardIgnored) {
        return;
      }

      const touch = event.touches[0];
      const currentX = touch?.clientX ?? drag.startX;
      const currentY = touch?.clientY ?? drag.lastY;

      const deltaX = currentX - drag.startX;
      const deltaY = currentY - drag.startY;
      const absX = Math.abs(deltaX);
      const absY = Math.abs(deltaY);

      if (!drag.engaged) {
        const slop = drag.isOptionCard
          ? OPTION_CARD_DRAG_SLOP
          : BACKGROUND_DRAG_SLOP;

        // If movement is under slop threshold, do not engage: let native clicks/focus happen
        if (absY < slop) {
          return;
        }

        // Must be primarily vertical gesture (avoid hijacking horizontal swipe / scroll)
        if (absY < absX * 1.1) {
          return;
        }

        // Pager claims the gesture!
        drag.engaged = true;
        drag.didDrag = true;
        suppressNextClickRef.current = true;

        // Seed initial velocity from total drag trajectory so flick filter doesn't start sluggishly at 0
        const now = event.timeStamp || performance.now();
        const elapsed = now - drag.startTime;
        if (elapsed > 0) {
          drag.velocity = (drag.startY - currentY) / elapsed;
        }
      }

      event.preventDefault();

      if (event.touches.length > 1) {
        drag.engaged = false;
        drag.pointerDown = false;
        snapPanelsTo(0);
        return;
      }

      const now = event.timeStamp || performance.now();
      const deltaTime = now - drag.lastMoveTime;
      if (deltaTime > 0) {
        const instantVelocity = (drag.lastY - currentY) / deltaTime;
        drag.velocity = drag.velocity * 0.72 + instantVelocity * 0.28;
      }
      drag.lastY = currentY;
      drag.lastMoveTime = now;

      const index = activeIndexRef.current;
      const canNext = index < questionsCountRef.current - 1;
      const canPrev = index > 0;
      let offset = drag.baseOffset + (drag.startY - currentY);
      if (offset > 0 && !canNext) {
        offset *= RUBBER_BAND_RESISTANCE;
      }
      if (offset < 0 && !canPrev) {
        offset *= RUBBER_BAND_RESISTANCE;
      }
      drag.offset = offset;

      applyDragOffset(offset, false);
    },
    [applyDragOffset, snapPanelsTo],
  );

  const finishDrag = useCallback(
    (cancelled: boolean) => {
      const drag = dragRef.current;
      if (!drag.pointerDown || drag.hardIgnored) {
        drag.pointerDown = false;
        drag.engaged = false;
        touchStartYRef.current = null;
        return;
      }
      drag.pointerDown = false;
      touchStartYRef.current = null;

      if (!drag.engaged) {
        return;
      }
      drag.engaged = false;

      // Keep suppressNextClickRef active briefly so trailing click from drag is suppressed
      if (drag.didDrag) {
        suppressNextClickRef.current = true;
        window.setTimeout(() => {
          suppressNextClickRef.current = false;
        }, 60);
      }

      const index = activeIndexRef.current;
      const canNext = index < questionsCountRef.current - 1;
      const canPrev = index > 0;
      const flicked = Math.abs(drag.velocity) > DRAG_FLICK_VELOCITY;
      const draggedFar =
        Math.abs(drag.offset) > drag.height * DRAG_COMMIT_RATIO;

      let direction: 0 | 1 | -1 = 0;
      if (!cancelled && (draggedFar || flicked)) {
        if ((drag.offset > 0 || drag.velocity > 0) && canNext) {
          direction = 1;
        } else if ((drag.offset < 0 || drag.velocity < 0) && canPrev) {
          direction = -1;
        }
      }

      if (direction === 0) {
        snapPanelsTo(0);
        return;
      }

      const nextIndex = index + direction;
      resetQuestionKeyboardState();
      snapPanelsTo(direction * drag.height);
      onQuestionExit?.(index, nextIndex);
      setActiveIndex(nextIndex);
    },
    [onQuestionExit, snapPanelsTo],
  );

  const handleTouchEnd = useCallback(
    (event: React.TouchEvent<HTMLElement>) => {
      if (document.body.classList.contains("dropdown-open")) {
        return;
      }

      const drag = dragRef.current;

      if (drag.hardIgnored) {
        drag.pointerDown = false;
        drag.engaged = false;
        touchStartYRef.current = null;
        return;
      }

      if (drag.engaged) {
        finishDrag(false);
        return;
      }

      if (!drag.pointerDown) {
        return;
      }
      drag.pointerDown = false;

      // If it was a clean tap on an option card / button, leave it to native click!
      if (drag.isOptionCard) {
        touchStartYRef.current = null;
        return;
      }

      // Fast flick fallback on background area
      const startY = touchStartYRef.current;
      const endY = event.changedTouches[0]?.clientY;
      touchStartYRef.current = null;

      if (startY === null || endY === undefined || questions.length < 2) {
        return;
      }

      const distance = startY - endY;

      if (Math.abs(distance) < FAST_FLICK_MIN_DISTANCE) {
        return;
      }

      stepQuestion(distance > 0 ? 1 : -1);
    },
    [finishDrag, questions.length, stepQuestion],
  );

  const handleTouchCancel = useCallback(() => {
    const drag = dragRef.current;
    if (drag.hardIgnored) {
      drag.pointerDown = false;
      drag.engaged = false;
      touchStartYRef.current = null;
      return;
    }
    finishDrag(true);
  }, [finishDrag]);

  const handleClickCapture = useCallback(
    (event: React.MouseEvent<HTMLElement>) => {
      const target = event.target as HTMLElement | null;
      const isTextInput = Boolean(target?.closest?.(TEXT_INPUT_SELECTOR));
      if (isTextInput) {
        suppressNextClickRef.current = false;
        return;
      }

      if (suppressNextClickRef.current) {
        event.preventDefault();
        event.stopPropagation();
        suppressNextClickRef.current = false;
      }
    },
    [],
  );

  if (questions.length === 0) {
    return null;
  }

  return (
    <section
      ref={containerRef}
      aria-label="Questions"
      className={[
        "question-snap-list relative touch-none overflow-hidden focus-visible:outline-none",
        "flex-1 min-h-0 pt-4 pb-4",
        className,
      ]
        .filter(Boolean)
        .join(" ")}
      onClickCapture={handleClickCapture}
      onTouchCancel={handleTouchCancel}
      onTouchEnd={handleTouchEnd}
      onTouchMove={handleTouchMove}
      onTouchStart={handleTouchStart}
      onWheel={handleWheel}
    >
      {questions.map((question, index) => {
        const offset = index - activeIndex;
        const isActive = offset === 0;
        const isPrev = offset === -1;
        const isNext = offset === 1;
        const questionKey =
          isValidElement(question) && question.key !== null
            ? question.key
            : String(question);

        let containerStyles = "";
        let wrapperStyles = "w-full my-auto shrink-0";

        if (isActive) {
          containerStyles =
            "top-0 left-0 bottom-0 z-10 opacity-100 scale-100 pointer-events-auto !justify-start";
          wrapperStyles = alignTop
            ? "w-full mt-3 shrink-0"
            : "w-full my-auto shrink-0";
        } else if (isPrev) {
          containerStyles =
            "top-0 left-0 z-0 opacity-0 pointer-events-none -translate-y-full";
        } else if (isNext) {
          containerStyles =
            "top-0 left-0 z-0 opacity-0 pointer-events-none translate-y-full";
        } else {
          containerStyles =
            "top-1/2 left-0 -translate-y-1/2 z-0 opacity-0 pointer-events-none scale-[0.95]";
        }

        return (
          <div
            key={questionKey}
            ref={(element) => {
              questionRefs.current[index] = element;
            }}
            aria-current={isActive ? "step" : undefined}
            aria-hidden={isActive ? undefined : true}
            inert={isActive ? undefined : true}
            className={[
              "question-snap-item absolute flex w-full flex-col justify-center transition-transform duration-300 ease-out px-[17px] will-change-transform [backface-visibility:hidden]",
              containerStyles,
            ].join(" ")}
          >
            <div className={`question-snap-content ${wrapperStyles}`}>
              {question}
            </div>
            {isActive && (
              <div
                className="question-snap-spacer h-[20%] shrink min-h-0"
                aria-hidden="true"
              />
            )}
          </div>
        );
      })}
      {firstQuestionHint ? (
        <div
          aria-hidden="true"
          className={[
            "pointer-events-none absolute bottom-24 left-1/2 -translate-x-1/2",
            "transition-opacity duration-500  motion-safe:animate-bounce",
            activeIndex === 0 ? "opacity-100" : "opacity-0",
          ].join(" ")}
        >
          {firstQuestionHint}
        </div>
      ) : null}
    </section>
  );
}

export default QuestionSnapList;
