"use client";

import {
  Children,
  isValidElement,
  type ReactNode,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";
import { useI18n } from "@/translations/provider";
import { useQuestionProgress } from "./question-progress-tracker";
import { useQuestionInputFocusSync } from "./use-sheet-scroll-lock";

const WHEEL_GESTURE_IDLE_MS = 320;
const TOUCH_MIN_DISTANCE = 8;
const DRAG_ENGAGE_DISTANCE = 10;
const DRAG_COMMIT_RATIO = 0.3;
const DRAG_FLICK_VELOCITY = 0.55;
const SNAP_ANIMATION_MS = 340;
const SNAP_EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
const RUBBER_BAND_RESISTANCE = 0.4;
const AUTO_FOCUS_SELECTOR = [
  "textarea:not([disabled]):not([data-no-auto-focus])",
  'input[type="text"]:not([disabled]):not([data-no-auto-focus])',
  'input[type="number"]:not([disabled]):not([data-no-auto-focus])',
].join(", ");
const DRAG_IGNORE_SELECTOR = [
  "input",
  "textarea",
  "select",
  '[contenteditable="true"]',
  "[data-snap-drag-ignore]",
].join(", ");

type SnapDragState = {
  pointerDown: boolean;
  ignored: boolean;
  engaged: boolean;
  animating: boolean;
  baseOffset: number;
  startY: number;
  lastY: 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) {
  const { dictionary: t } = useI18n();
  const { isCompleted } = useQuestionProgress();
  useQuestionInputFocusSync();
  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 [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,
    ignored: false,
    engaged: false,
    animating: false,
    baseOffset: 0,
    startY: 0,
    lastY: 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;
      }

      onQuestionExit?.(activeIndex, nextIndex);

      onQuestionTransition?.(activeIndex, nextIndex);

      setActiveIndex(nextIndex);
    },
    [activeIndex, onQuestionExit, onQuestionTransition, 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;

    onQuestionTransition?.(previousActiveIndex ?? activeIndex, activeIndex);
    previousActiveIndexRef.current = activeIndex;
  }, [activeIndex, onQuestionTransition]);

  useEffect(() => {
    if (activeIndex === 0) {
      return;
    }

    const activeQuestion = questionRefs.current[activeIndex];

    if (!activeQuestion) {
      return;
    }

    const focusFrame = window.requestAnimationFrame(() => {
      if (document.querySelector('[role="dialog"]')) {
        return;
      }

      const firstFocusableInput = activeQuestion.querySelector<
        HTMLInputElement | HTMLTextAreaElement
      >(AUTO_FOCUS_SELECTOR);

      if (!firstFocusableInput) {
        return;
      }

      const isMobile =
        /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
          navigator.userAgent,
        );

      firstFocusableInput.focus({ preventScroll: !isMobile });

      if (isMobile) {
        setTimeout(() => {
          firstFocusableInput.scrollIntoView({
            behavior: "smooth",
            block: "center",
          });
        }, 150);
      }

      const valueLength = firstFocusableInput.value.length;

      if (
        valueLength > 0 &&
        typeof firstFocusableInput.setSelectionRange === "function"
      ) {
        firstFocusableInput.setSelectionRange(valueLength, valueLength);
      }
    });

    return () => {
      window.cancelAnimationFrame(focusFrame);
    };
  }, [activeIndex]);

  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;
      }

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

      const target = event.target as HTMLElement | null;
      drag.ignored = Boolean(target?.closest?.(DRAG_IGNORE_SELECTOR));

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

      if (drag.animating) {
        // Grabbed mid-snap (or mid wheel transition): freeze the panels
        // exactly where they currently are and continue the drag from there.
        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;
      drag.startY = event.touches[0]?.clientY ?? 0;
      drag.lastY = drag.startY;
      drag.lastMoveTime = event.timeStamp || performance.now();
      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) {
        event.preventDefault();
        return;
      }

      if (drag.ignored) {
        return;
      }

      const y = event.touches[0]?.clientY ?? drag.lastY;

      if (!drag.engaged) {
        if (Math.abs(drag.startY - y) < DRAG_ENGAGE_DISTANCE) {
          event.preventDefault();
          return;
        }
        drag.engaged = true;
      }

      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 - y) / deltaTime;
        drag.velocity = drag.velocity * 0.72 + instantVelocity * 0.28;
      }
      drag.lastY = y;
      drag.lastMoveTime = now;

      const index = activeIndexRef.current;
      const canNext = index < questionsCountRef.current - 1;
      const canPrev = index > 0;
      let offset = drag.baseOffset + (drag.startY - y);
      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) {
        return;
      }
      drag.pointerDown = false;
      touchStartYRef.current = null;

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

      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;
      snapPanelsTo(direction * drag.height);
      onQuestionExit?.(index, nextIndex);
      onQuestionTransition?.(index, nextIndex);
      setActiveIndex(nextIndex);
    },
    [onQuestionExit, onQuestionTransition, snapPanelsTo],
  );

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

      const drag = dragRef.current;

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

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

      // Fallback for very fast flicks whose touchmove never engaged the
      // drag layer: fall back to the distance-based step.
      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) < TOUCH_MIN_DISTANCE) {
        return;
      }

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

  const handleTouchCancel = useCallback(() => {
    finishDrag(true);
  }, [finishDrag]);

  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(" ")}
      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-all duration-300 ease-out px-[17px]",
              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;
