"use client";

import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";

/**
 * SwipeableRow — gesture-driven row with reveal-on-swipe action shelves.
 *
 * Drag the row left to pull the right-side shelf into view, or drag
 * right to pull the left-side shelf into view. Past REVEAL_THRESHOLD
 * the shelf locks open; below the threshold it springs back closed.
 * Tapping outside an open row also closes it.
 *
 *      ← drag left            drag right →
 *   ┌─────────────┐──┐      ┌──┌─────────────┐
 *   │  row body   │R │      │ L│  row body   │
 *   └─────────────┘──┘      └──└─────────────┘
 *           ↑                        ↑
 *      Right shelf            Left shelf
 *
 * The component is direction-agnostic — both shelves accept arbitrary
 * action buttons. Activity rows wire it as:
 *   leftActions  = Archive (right swipe → non-destructive lives here)
 *   rightActions = Delete  (left swipe → destructive lives here)
 *
 * Pointer events only — no third-party gesture lib — to keep the
 * bundle small and avoid SSR / hydration headaches.
 */

const REVEAL_THRESHOLD = 60; // px — how far you have to drag to lock shelf open
const SHELF_WIDTH = 88; // px — width of each shelf (one button each side)

interface SwipeableRowProps {
  /** Right-side shelf, revealed by swiping LEFT (offset goes negative). */
  rightActions?: React.ReactNode;
  /** Left-side shelf, revealed by swiping RIGHT (offset goes positive). */
  leftActions?: React.ReactNode;
  /** Row body — the visible content above the shelves. */
  children: React.ReactNode;
  /** className for the OUTER wrapper (positioning, margins). */
  className?: string;
  /** className for the inner sliding row (background, padding). */
  rowClassName?: string;
  /** Override the default 88px right-shelf width. Use a multiple of
   *  88 when stacking multiple action buttons in the shelf. */
  rightShelfWidth?: number;
  /** Override the default 88px left-shelf width. */
  leftShelfWidth?: number;
}

export function SwipeableRow({
  rightActions,
  leftActions,
  children,
  className,
  rowClassName,
  rightShelfWidth,
  leftShelfWidth,
}: SwipeableRowProps) {
  const rightWidth = rightShelfWidth ?? SHELF_WIDTH;
  const leftWidth = leftShelfWidth ?? SHELF_WIDTH;
  // `offset` is the row's current x-translation in px:
  //   0           → closed
  //   -SHELF_WIDTH → right shelf fully open
  //   +SHELF_WIDTH → left shelf fully open
  const [offset, setOffset] = useState(0);
  const [dragging, setDragging] = useState(false);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const dragStart = useRef<{ x: number; baseOffset: number } | null>(null);

  // Close the shelf when the user taps anywhere outside an open row.
  useEffect(() => {
    if (offset === 0) return;
    function onDocPointer(e: PointerEvent) {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
        setOffset(0);
      }
    }
    window.addEventListener("pointerdown", onDocPointer);
    return () => window.removeEventListener("pointerdown", onDocPointer);
  }, [offset]);

  function clamp(next: number): number {
    // Limit drags to the available shelf width on each side.
    const maxLeft = leftActions ? leftWidth : 0;
    const maxRight = rightActions ? rightWidth : 0;
    if (next > maxLeft) return maxLeft;
    if (next < -maxRight) return -maxRight;
    return next;
  }

  function handlePointerDown(e: React.PointerEvent<HTMLDivElement>) {
    if (e.button !== 0 && e.pointerType === "mouse") return;
    dragStart.current = { x: e.clientX, baseOffset: offset };
    setDragging(true);
  }

  function handlePointerMove(e: React.PointerEvent<HTMLDivElement>) {
    if (!dragging || !dragStart.current) return;
    const dx = e.clientX - dragStart.current.x;
    setOffset(clamp(dragStart.current.baseOffset + dx));
  }

  function handlePointerUp() {
    if (!dragging) return;
    setDragging(false);
    dragStart.current = null;
    setOffset((prev) =>
      snapPosition(prev, {
        leftActions: !!leftActions,
        rightActions: !!rightActions,
        leftWidth,
        rightWidth,
      }),
    );
  }

  return (
    <div
      ref={containerRef}
      className={cn("relative overflow-hidden rounded-xl", className)}
      data-state={offset === 0 ? "closed" : "open"}
      data-testid="swipeable-row"
    >
      {/* Left shelf — pinned to the left, revealed by swipe-right. */}
      {leftActions && (
        <div
          aria-hidden={offset <= 0}
          className="absolute inset-y-0 left-0 flex items-stretch"
          style={{ width: leftWidth }}
        >
          {leftActions}
        </div>
      )}

      {/* Right shelf — pinned to the right, revealed by swipe-left. */}
      {rightActions && (
        <div
          aria-hidden={offset >= 0}
          className="absolute inset-y-0 right-0 flex items-stretch"
          style={{ width: rightWidth }}
        >
          {rightActions}
        </div>
      )}

      {/* Sliding row body. */}
      <div
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        onPointerCancel={handlePointerUp}
        style={{
          transform: `translateX(${offset}px)`,
          transition: dragging ? "none" : "transform 200ms ease-out",
          touchAction: "pan-y",
        }}
        className={cn("relative bg-card", rowClassName)}
      >
        {children}
      </div>
    </div>
  );
}

/**
 * Pure helper: given the post-drag offset and which shelves exist,
 * decide what offset to snap to on release.
 */
export function snapPosition(
  offset: number,
  shelves: {
    leftActions: boolean;
    rightActions: boolean;
    leftWidth?: number;
    rightWidth?: number;
  },
): number {
  const lw = shelves.leftWidth ?? SHELF_WIDTH;
  const rw = shelves.rightWidth ?? SHELF_WIDTH;
  if (offset >= REVEAL_THRESHOLD && shelves.leftActions) return lw;
  if (offset <= -REVEAL_THRESHOLD && shelves.rightActions) return -rw;
  return 0;
}

/**
 * Pure helper: compute the next dragOffset (clamped) and the
 * post-release snap target for a drag with delta `dx`.
 */
export function computeSwipeNext(
  baseOffset: number,
  dx: number,
  shelves: { leftActions: boolean; rightActions: boolean } = {
    leftActions: true,
    rightActions: true,
  },
): { dragOffset: number; snapAfterRelease: number } {
  const maxLeft = shelves.leftActions ? SHELF_WIDTH : 0;
  const maxRight = shelves.rightActions ? SHELF_WIDTH : 0;
  const raw = baseOffset + dx;
  let clamped = raw > maxLeft ? maxLeft : raw < -maxRight ? -maxRight : raw;
  // Avoid `-0` slipping out when a side has no shelf (`-maxRight` is
  // `-0` when maxRight is 0). Normalize so call sites can `=== 0`.
  if (Object.is(clamped, -0)) clamped = 0;
  return { dragOffset: clamped, snapAfterRelease: snapPosition(clamped, shelves) };
}

export const SWIPE_REVEAL_THRESHOLD = REVEAL_THRESHOLD;
export const SWIPE_SHELF_WIDTH = SHELF_WIDTH;
