import { ReactNode, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useIsMobile } from "../hooks/useMediaQuery";
import { useScrollLock } from "../hooks/useScrollLock";
import { CloseIcon } from "./icons";

const EXIT_MS = 240;

/**
 * Responsive overlay primitive: a drag-dismissable bottom sheet on phones, a
 * centred dialog from `sm` up. Everything modal in the app renders through this
 * so the dismiss gesture, scroll lock and safe-area handling stay consistent.
 */
export function Sheet({
	open,
	onClose,
	onSubmit,
	title,
	children,
	footer,
	wide,
	padded = true,
}: {
	open: boolean;
	onClose: () => void;
	onSubmit?: () => void;
	title?: string;
	children: ReactNode;
	footer?: ReactNode;
	wide?: boolean;
	padded?: boolean;
}) {
	const isMobile = useIsMobile();
	const [mounted, setMounted] = useState(open);
	const [exiting, setExiting] = useState(false);
	const [dragY, setDragY] = useState(0);
	const [dragging, setDragging] = useState(false);
	const panelRef = useRef<HTMLDivElement>(null);
	const dragStart = useRef<{ y: number; t: number } | null>(null);

	// Keep the panel mounted through its exit transition.
	useEffect(() => {
		if (open) {
			setMounted(true);
			setExiting(false);
			setDragY(0);
			return;
		}
		if (!mounted) return;
		setExiting(true);
		const t = window.setTimeout(() => {
			setMounted(false);
			setExiting(false);
			setDragY(0);
		}, EXIT_MS);
		return () => window.clearTimeout(t);
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [open]);

	useScrollLock(mounted);

	useEffect(() => {
		if (!open) return;
		const onKey = (e: KeyboardEvent) => {
			if (e.key === "Escape") onClose();
			if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && onSubmit) onSubmit();
		};
		window.addEventListener("keydown", onKey);
		return () => window.removeEventListener("keydown", onKey);
	}, [open, onClose, onSubmit]);

	if (!mounted) return null;

	// ── drag-to-dismiss (mobile, from the grabber/header only, so it never
	//    fights the scroll container underneath) ────────────────────────────────
	const onPointerDown = (e: React.PointerEvent) => {
		if (!isMobile || e.pointerType === "mouse") return;
		dragStart.current = { y: e.clientY, t: e.timeStamp };
		setDragging(true);
		(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
	};

	const onPointerMove = (e: React.PointerEvent) => {
		if (!dragStart.current) return;
		// Resist upward drags so the sheet feels anchored.
		const dy = e.clientY - dragStart.current.y;
		setDragY(dy > 0 ? dy : dy / 4);
	};

	const endDrag = (e: React.PointerEvent) => {
		const start = dragStart.current;
		dragStart.current = null;
		setDragging(false);
		if (!start) return;
		const dy = e.clientY - start.y;
		const dt = Math.max(1, e.timeStamp - start.t);
		const velocity = dy / dt; // px/ms
		const height = panelRef.current?.offsetHeight ?? 400;
		if (dy > height * 0.28 || velocity > 0.5) onClose();
		else setDragY(0);
	};

	const dismissProgress = Math.min(1, Math.max(0, dragY) / 320);

	const panelTransform = exiting
		? isMobile
			? "translateY(100%)"
			: "scale(0.96)"
		: dragY
			? `translateY(${dragY}px)`
			: undefined;

	// Portalled to <body>: any ancestor with a transform (page fade-ins, the
	// blurred header) would otherwise become the containing block and knock the
	// fixed overlay out of place.
	return createPortal(
		<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center sm:p-4" role="dialog" aria-modal="true">
			<div
				className={`absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-200 ${
					exiting ? "opacity-0" : "animate-backdrop-in"
				}`}
				style={{ opacity: exiting ? 0 : 1 - dismissProgress * 0.7 }}
				onClick={onClose}
			/>

			<div
				ref={panelRef}
				className={`relative z-10 flex max-h-[92dvh] w-full flex-col border border-border-strong bg-surface shadow-2xl ${
					wide ? "sm:max-w-3xl" : "sm:max-w-lg"
				} rounded-t-2xl sm:max-h-[85vh] sm:rounded-2xl ${
					exiting
						? isMobile
							? "" // slides out; fading as well reads as a glitch on phones
							: "opacity-0"
						: isMobile
							? "animate-sheet-up"
							: "animate-scale-in"
				}`}
				style={{
					transform: panelTransform,
					transition: dragging ? "none" : `transform ${EXIT_MS}ms cubic-bezier(0.32,0.72,0,1), opacity 180ms ease`,
				}}
			>
				{/* Grab area — the whole header is draggable, like iOS sheets. */}
				<div
					className="shrink-0 touch-none"
					onPointerDown={onPointerDown}
					onPointerMove={onPointerMove}
					onPointerUp={endDrag}
					onPointerCancel={endDrag}
				>
					<div className="flex justify-center pt-2 sm:hidden">
						<span className="h-1 w-9 rounded-full bg-border-strong" />
					</div>
					{title && (
						<div className="flex items-center justify-between gap-3 border-b border-border px-5 py-3.5 sm:py-4">
							<h2 className="min-w-0 truncate text-base font-semibold text-text sm:text-lg">{title}</h2>
							<button
								onClick={onClose}
								className="tap-sm -mr-2 flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-muted hover:bg-surface-raised hover:text-text"
								aria-label="Close"
							>
								<CloseIcon className="h-5 w-5" />
							</button>
						</div>
					)}
				</div>

				{/* The home-indicator inset is folded into the padding with calc so it
				    adds to the base spacing instead of replacing it. */}
				<div
					className={`min-h-0 flex-1 overflow-y-auto overscroll-contain ${
						padded ? "px-4 py-4 sm:px-5" : ""
					} ${footer ? "" : padded ? "pb-[calc(1rem_+_var(--safe-bottom))]" : "pb-safe"}`}
				>
					{children}
				</div>

				{footer && (
					<div className="flex shrink-0 flex-wrap justify-end gap-2 border-t border-border px-4 pb-[calc(0.75rem_+_var(--safe-bottom))] pt-3 sm:px-5 sm:pb-[calc(1rem_+_var(--safe-bottom))] sm:pt-4">
						{footer}
					</div>
				)}
			</div>
		</div>,
		document.body,
	);
}
