"use client";

/**
 * MobilePullToRefresh — iOS-style pull-to-refresh for the headgate list.
 *
 * Why a custom implementation (vs reusing the admin one):
 *   - The admin PullToRefresh calls `router.refresh()` to re-render
 *     the server component. The mobile flow is fully client-side
 *     (the headgate list comes from a server action), so we need a
 *     callback-based variant that re-fetches via `onRefresh`.
 *   - The mobile version also needs to feel snappier on a phone
 *     (smaller resistance curve, indicator pinned to the top of the
 *     scrollable region).
 *
 * The indicator:
 *   - Sits 16px below the top edge of the scroll container.
 *   - Scales + fades in based on pull progress (0 → 1 across the
 *     trigger threshold), with the spinner kicking in only once
 *     refresh actually starts.
 *   - Respects `prefers-reduced-motion` (the rest of the mobile
 *     bundle does too — see `usePrefersReducedMotion` in
 *     `MobileWaterApp`).
 */
import {
  useCallback,
  useRef,
  useState,
  type ReactNode,
} from "react";
import { Spinner } from "./icons";

type Props = {
  onRefresh: () => Promise<void> | void;
  children: ReactNode;
  /** Pull distance (px) at which refresh triggers. Default 72. */
  threshold?: number;
  /** Maximum stretch distance (px) under heavy pull. Default 120. */
  maxPull?: number;
  /** Disable pull-to-refresh (e.g. while loading). */
  disabled?: boolean;
};

export function MobilePullToRefresh({
  onRefresh,
  children,
  threshold = 72,
  maxPull = 120,
  disabled = false,
}: Props) {
  const scrollRef = useRef<HTMLDivElement>(null);
  const startY = useRef<number | null>(null);
  const [offset, setOffset] = useState(0);
  const [refreshing, setRefreshing] = useState(false);
  const [isPulling, setIsPulling] = useState(false);

  // The pull-to-refresh should only fire when the list is already at
  // the very top — otherwise the user is mid-scroll and we should
  // hand the gesture back to the native scroll.
  const atTop = useCallback(() => {
    const el = scrollRef.current;
    if (!el) return true;
    return el.scrollTop <= 0;
  }, []);

  const onTouchStart = useCallback(
    (e: React.TouchEvent<HTMLDivElement>) => {
      if (disabled || refreshing) return;
      if (!atTop()) return;
      startY.current = e.touches[0].clientY;
      setIsPulling(true);
    },
    [disabled, refreshing, atTop],
  );

  const onTouchMove = useCallback(
    (e: React.TouchEvent<HTMLDivElement>) => {
      if (!isPulling || startY.current == null) return;
      const dy = e.touches[0].clientY - startY.current;
      if (dy <= 0) {
        setOffset(0);
        return;
      }
      // Resistance curve — feels heavier as you pull further so a
      // casual swipe doesn't accidentally trigger refresh.
      const resistance = 1 - Math.min(1, dy / 600);
      setOffset(Math.min(maxPull, dy * resistance));
    },
    [isPulling, maxPull],
  );

  const onTouchEnd = useCallback(async () => {
    if (!isPulling) return;
    setIsPulling(false);
    startY.current = null;
    if (offset >= threshold) {
      setRefreshing(true);
      setOffset(threshold); // hold the indicator in place while refreshing
      try {
        await Promise.resolve(onRefresh());
      } finally {
        // A small delay so the spinner is visible even on a fast
        // refresh — feels less jarring.
        await new Promise((r) => setTimeout(r, 350));
        setRefreshing(false);
        setOffset(0);
      }
    } else {
      setOffset(0);
    }
  }, [isPulling, offset, threshold, onRefresh]);

  const progress = Math.min(1, offset / threshold);

  return (
    <div
      ref={scrollRef}
      onTouchStart={onTouchStart}
      onTouchMove={onTouchMove}
      onTouchEnd={onTouchEnd}
      onTouchCancel={onTouchEnd}
      className="relative h-full overflow-y-auto overscroll-contain"
      style={{
        WebkitOverflowScrolling: "touch",
      }}
    >
      {/* Indicator — absolutely positioned just below the top edge.
        *  Translates with the pull so it follows the finger. */}
      <div
        aria-hidden
        className="pointer-events-none sticky top-0 z-20 flex justify-center"
        style={{
          height: 56,
          transform: `translateY(${Math.max(0, offset - 56)}px)`,
          transition: isPulling
            ? "none"
            : "transform 280ms cubic-bezier(0.32, 0.72, 0, 1)",
        }}
      >
        <div
          className="mt-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/85 backdrop-blur-md"
          style={{
            opacity: refreshing ? 1 : 0.4 + progress * 0.6,
            transform: `scale(${0.55 + progress * 0.45}) rotate(${progress * 180}deg)`,
            transition: isPulling
              ? "none"
              : "transform 280ms cubic-bezier(0.32, 0.72, 0, 1), opacity 200ms ease",
            boxShadow: "0 1px 2px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.06)",
          }}
        >
          {refreshing ? (
            <Spinner
              size={20}
              className="text-[#1a4d2e]"
              style={{ animation: "spin 0.8s linear infinite" }}
            />
          ) : (
            <span
              className="text-[15px] font-bold leading-none text-[#1a4d2e]"
              style={{
                transform: `rotate(-${progress * 180}deg)`,
              }}
            >
              ↓
            </span>
          )}
        </div>
      </div>
      {children}
    </div>
  );
}

export default MobilePullToRefresh;