import { useEffect, useCallback, useState, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { X } from '@phosphor-icons/react';
import clsx from 'clsx';
import styles from './BottomSheet.module.css';

/**
 * BottomSheet — a mobile-style slide-up drawer. Portal-based, backdrop
 * + Escape + click-outside close, drag handle, safe-area-inset aware.
 *
 * Usage:
 *   <BottomSheet isOpen={open} onClose={close} title="Search">
 *     ...body...
 *   </BottomSheet>
 *
 * By default the sheet snaps to 95svh and scrolls its body internally.
 * Callers that need a partial-height drawer can pass `initialHeightSvh`
 * (e.g. 50 for half-screen) and `expandable` to allow the user to
 * drag the handle UP to expand the sheet to full size.
 *
 * Callers that need a custom header layout can pass `disableDefaultHeader`
 * and render their own header as the first child.
 */
export interface BottomSheetProps {
	isOpen: boolean;
	onClose: () => void;
	/** Title shown in the default header. Ignored when disableDefaultHeader is true. */
	title?: string;
	/** Render the title-X header strip. Default true. */
	disableDefaultHeader?: boolean;
	/** Show the top drag handle strip. Default true. */
	showHandle?: boolean;
	/** Close when the backdrop or drag handle is clicked. Default true. */
	dismissible?: boolean;
	/**
	 * Whether the sheet can be dragged at all (to close or to expand).
	 * When false, the sheet is a pure open/closed component: no drag
	 * gestures are attached, no intermediate snap points exist, and
	 * the only ways to dismiss it are the backdrop click, the Escape
	 * key, or the owner calling `onClose` explicitly. Default true.
	 */
	draggable?: boolean;
	/** Optional extra class for the sheet container. */
	className?: string;
	/**
	 * Initial visible portion of the viewport in svh units (0–95).
	 * Default 95 (the full sheet shown). When set lower (e.g. 50), the
	 * sheet animates in to that partial height. The user can still
	 * drag down to dismiss; pair with `expandable` to also let them
	 * drag UP to grow it.
	 */
	initialHeightSvh?: number;
	/**
	 * When true and `initialHeightSvh` is less than 95, the user can
	 * drag the sheet UP from its initial position to the fully open
	 * (95svh) snap point. Default false.
	 */
	expandable?: boolean;
	/**
	 * Optional z-index override for the backdrop + sheet overlay.
	 * Needed when the sheet is opened on top of another portaled
	 * surface that already claims a high z-index (e.g. the
	 * ImageLightbox backdrop at 16000). Leave unset to inherit the
	 * global `--z-index-modal` token.
	 */
	zIndex?: number;
	children: ReactNode;
}

export function BottomSheet({
	isOpen,
	onClose,
	title,
	disableDefaultHeader = false,
	showHandle = true,
	dismissible = true,
	draggable = true,
	className,
	initialHeightSvh,
	expandable = false,
	zIndex,
	children,
}: BottomSheetProps) {
	const handleEscape = useCallback(
		(e: KeyboardEvent) => {
			if (e.key === 'Escape' && dismissible) onClose();
		},
		[onClose, dismissible],
	);

	useEffect(() => {
		if (!isOpen) return;
		document.addEventListener('keydown', handleEscape);
		return () => document.removeEventListener('keydown', handleEscape);
	}, [isOpen, handleEscape]);

	// Lock body scroll while the sheet is open — keeps the underlying page
	// from scrolling behind the drawer on mobile.
	useEffect(() => {
		if (!isOpen) return;
		const prev = document.body.style.overflow;
		document.body.style.overflow = 'hidden';
		return () => {
			document.body.style.overflow = prev;
		};
	}, [isOpen]);

	// ── Snap-point logic ────────────────────────────────────────────────
	// Track the live viewport height in pixels so we can convert the
	// svh-based snap props into px values for Framer Motion's drag.
	// We resize-listen so rotating the device or showing/hiding the
	// mobile browser chrome doesn't leave the sheet stuck at a stale
	// pixel snap.
	const [vh, setVh] = useState(() =>
		typeof window !== 'undefined' ? window.innerHeight : 0,
	);
	useEffect(() => {
		const onResize = () => setVh(window.innerHeight);
		window.addEventListener('resize', onResize);
		return () => window.removeEventListener('resize', onResize);
	}, []);

	// The sheet's CSS height is 95svh. translate-Y of 0 means it's
	// fully open (95svh visible). Translating DOWN by N pixels hides
	// N pixels worth of the sheet's top portion off the bottom of the
	// viewport.
	//
	// snapInitialPx — translation needed to show only `initialHeightSvh`
	//                of viewport height
	// snapFullPx    — 0 (sheet fully open)
	// snapClosedPx  — vh (sheet fully off-screen)
	const partial = initialHeightSvh !== undefined && initialHeightSvh < 95;
	const snapInitialPx = partial ? Math.max(0, vh * (1 - (initialHeightSvh as number) / 95)) : 0;
	const snapFullPx = 0;
	const snapClosedPx = vh;

	// Current snap state — only matters when the sheet is partial AND
	// expandable, since otherwise there's only one snap target.
	type SnapName = 'initial' | 'full';
	const [snap, setSnap] = useState<SnapName>('initial');

	// Reset to the initial snap whenever the sheet (re-)opens so a
	// previously-expanded sheet doesn't come back already expanded.
	useEffect(() => {
		if (isOpen) setSnap('initial');
	}, [isOpen]);

	const targetY = snap === 'full' ? snapFullPx : snapInitialPx;

	return createPortal(
		<AnimatePresence>
			{isOpen && (
				<div
					className={styles.overlay}
					style={zIndex !== undefined ? { zIndex } : undefined}
				>
					{/* Backdrop */}
					<motion.div
						className={styles.backdrop}
						initial={{ opacity: 0 }}
						animate={{ opacity: 1 }}
						exit={{ opacity: 0 }}
						transition={{ duration: 0.18 }}
						onClick={dismissible ? onClose : undefined}
					/>

					{/* Sheet surface */}
					<motion.div
						className={clsx(styles.sheet, className)}
						initial={{ y: '100%' }}
						animate={{ y: targetY }}
						exit={{ y: '100%' }}
						transition={{ type: 'spring', stiffness: 400, damping: 36 }}
						onClick={(e) => e.stopPropagation()}
						drag={dismissible && draggable ? 'y' : false}
						dragConstraints={{
							// Allow dragging UP to the fully-open snap when
							// the sheet is partial and expandable; otherwise
							// pin top at the initial position.
							top: partial && expandable ? snapFullPx - targetY : 0,
							bottom: snapClosedPx - targetY,
						}}
						dragElastic={{ top: partial && expandable ? 0.05 : 0, bottom: 0.2 }}
						onDragEnd={(_, info) => {
							const finalY = targetY + info.offset.y;

							// Velocity-driven flicks first — they win over
							// position-based heuristics so a fast swipe
							// always does the obvious thing.
							if (info.velocity.y > 600) {
								// Strong downward flick → close.
								onClose();
								return;
							}
							if (partial && expandable && info.velocity.y < -600) {
								// Strong upward flick → expand to full.
								setSnap('full');
								return;
							}

							// Position-based snap fallback.
							if (!partial) {
								// Original behavior — close if dragged
								// down past the threshold, otherwise
								// spring back to fully open.
								if (info.offset.y > 120) onClose();
								return;
							}

							// Partial sheet position-based snap:
							// - past the initial position by half its
							//   distance to closed → close
							// - between snaps → snap to nearest
							// - above the initial position by 1/3 the
							//   distance to full → expand
							const closeThreshold = snapInitialPx + (snapClosedPx - snapInitialPx) * 0.4;
							const expandThreshold = snapInitialPx - snapInitialPx * 0.33;

							if (finalY > closeThreshold) {
								onClose();
								return;
							}
							if (expandable && finalY < expandThreshold) {
								setSnap('full');
								return;
							}
							setSnap('initial');
						}}
					>
						{showHandle && (
							<div className={styles.handleRow}>
								<div className={styles.handle} />
							</div>
						)}

						{!disableDefaultHeader && title && (
							<div className={styles.header}>
								<h2 className={styles.title}>{title}</h2>
								<button
									type="button"
									className={styles.closeButton}
									onClick={onClose}
									aria-label="Close"
								>
									<X size={20} weight="bold" />
								</button>
							</div>
						)}

						<div className={styles.body}>{children}</div>
					</motion.div>
				</div>
			)}
		</AnimatePresence>,
		document.body,
	);
}
