import { AnimatePresence, motion } from 'framer-motion';
import { type ReactNode, cloneElement, isValidElement, useRef, useState, useCallback, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';
import styles from './Tooltip.module.css';

export interface TooltipProps {
	content: ReactNode;
	placement?: 'top' | 'bottom' | 'left' | 'right';
	children: ReactNode;
	delay?: number;
	/**
	 * Optional keyboard shortcut rendered as a row of keycaps under
	 * the main label. Accepts a lowercase `+`-joined combo string
	 * like `"ctrl+shift+m"` (the same format the KeybindStore uses).
	 * Empty / undefined hides the row entirely.
	 */
	shortcut?: string;
	/**
	 * When true the tooltip stays open while the cursor is over the
	 * floating panel (not just the trigger), and clicks on the panel
	 * bubble through to `onContentClick`. Used for reaction chips
	 * where the tooltip doubles as a "click to view details" target.
	 */
	interactive?: boolean;
	/**
	 * Fired when the user clicks the floating content. Only hooked
	 * up when `interactive` is true.
	 */
	onContentClick?: () => void;
}

/** Split a KeybindStore-format combo into display tokens and label
 *  each one so the Tooltip can render them as individual keycaps. */
function parseShortcut(combo: string): string[] {
	if (!combo) return [];
	return combo
		.split('+')
		.map((t) => t.trim())
		.filter(Boolean)
		.map(formatShortcutToken);
}

function formatShortcutToken(token: string): string {
	// KeybindContext emits combos like `Ctrl+Shift+M` or `Ctrl+,` — so
	// normalize the incoming token to lowercase for switch matching,
	// but keep the original around for the default branch so
	// punctuation and single letters round-trip as uppercase keycaps.
	const lower = token.toLowerCase();
	switch (lower) {
		case 'ctrl':
		case 'control':
			return 'CTRL';
		case 'shift':
			return '⇧';
		case 'alt':
		case 'option':
			return 'ALT';
		case 'meta':
		case 'cmd':
		case 'command':
			return '⌘';
		case 'arrowup':
			return '↑';
		case 'arrowdown':
			return '↓';
		case 'arrowleft':
			return '←';
		case 'arrowright':
			return '→';
		case 'escape':
			return 'ESC';
		case 'enter':
			return '↵';
		case 'tab':
			return 'TAB';
		case ' ':
		case 'space':
			return 'SPACE';
		default:
			return token.length === 1 ? token.toUpperCase() : token.toUpperCase();
	}
}

export function Tooltip({
	content,
	placement = 'top',
	children,
	delay = 300,
	shortcut,
	interactive = false,
	onContentClick,
}: TooltipProps) {
	const [isOpen, setIsOpen] = useState(false);
	const referenceRef = useRef<HTMLElement | null>(null);
	const floatingRef = useRef<HTMLDivElement | null>(null);
	const openTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
	// Close timer — separate from the open timer so we can cancel a
	// pending close when the cursor enters the floating panel.
	const closeTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
	const [pos, setPos] = useState<React.CSSProperties>({ position: 'fixed', visibility: 'hidden' });

	/**
	 * Callback ref used when cloning the trigger. Stores the DOM node
	 * AND forwards it to whatever ref the caller already had on the
	 * child — without this forwarding the Tooltip would overwrite
	 * refs like `pinsButtonRef` and break any `getBoundingClientRect`
	 * logic on the consumer side.
	 */
	const makeSetRef = useCallback(
		(childRef: any) => (node: HTMLElement | null) => {
			referenceRef.current = node;
			if (!childRef) return;
			if (typeof childRef === 'function') {
				childRef(node);
			} else if (typeof childRef === 'object' && 'current' in childRef) {
				(childRef as { current: HTMLElement | null }).current = node;
			}
		},
		[],
	);

	// Compute position after the tooltip mounts and the reference is visible
	useLayoutEffect(() => {
		if (!isOpen || !referenceRef.current) return;

		const computePos = () => {
			const el = referenceRef.current;
			const floating = floatingRef.current;
			if (!el) return;

			const rect = el.getBoundingClientRect();
			const floatingRect = floating?.getBoundingClientRect();
			const fw = floatingRect?.width || 0;
			const fh = floatingRect?.height || 0;

			const style: React.CSSProperties = { position: 'fixed', zIndex: 20000 };

			if (placement === 'top') {
				style.left = rect.left + rect.width / 2 - fw / 2;
				style.top = rect.top - fh - 8;
			} else if (placement === 'bottom') {
				style.left = rect.left + rect.width / 2 - fw / 2;
				style.top = rect.bottom + 8;
			} else if (placement === 'left') {
				style.left = rect.left - fw - 8;
				style.top = rect.top + rect.height / 2 - fh / 2;
			} else {
				style.left = rect.right + 8;
				style.top = rect.top + rect.height / 2 - fh / 2;
			}

			setPos(style);
		};

		// First render: measure floating element then position
		requestAnimationFrame(computePos);
	}, [isOpen, placement]);

	const handleMouseEnter = useCallback(() => {
		if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
		if (openTimeoutRef.current) clearTimeout(openTimeoutRef.current);
		openTimeoutRef.current = setTimeout(() => setIsOpen(true), delay);
	}, [delay]);

	const handleMouseLeave = useCallback(() => {
		if (openTimeoutRef.current) clearTimeout(openTimeoutRef.current);
		// Interactive mode: give the user a short grace window to
		// move from the trigger onto the floating panel. Non-
		// interactive tooltips still close instantly to match the
		// classic label behaviour.
		if (interactive) {
			if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
			closeTimeoutRef.current = setTimeout(() => setIsOpen(false), 150);
		} else {
			setIsOpen(false);
		}
	}, [interactive]);

	const handleFloatingEnter = useCallback(() => {
		if (!interactive) return;
		if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
	}, [interactive]);

	const handleFloatingLeave = useCallback(() => {
		if (!interactive) return;
		if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
		closeTimeoutRef.current = setTimeout(() => setIsOpen(false), 150);
	}, [interactive]);

	return (
		<>
			{isValidElement(children) &&
				cloneElement(children as React.ReactElement<any>, {
					ref: makeSetRef((children as any).ref),
					onMouseEnter: handleMouseEnter,
					onMouseLeave: handleMouseLeave,
					onFocus: handleMouseEnter,
					onBlur: handleMouseLeave,
				})}
			{createPortal(
				<AnimatePresence>
					{isOpen && (
						<motion.div
							ref={floatingRef}
							style={{
								...pos,
								// Interactive tooltips must accept pointer events so
								// the user can hover onto them and click them. The
								// default tooltip is non-interactive and leaves the
								// CSS default alone.
								pointerEvents: interactive ? 'auto' : undefined,
								cursor: interactive && onContentClick ? 'pointer' : undefined,
								// Interactive tooltips often carry richer content
								// (emoji glyph, multi-line body) — give them a wider
								// clamp than the default 220px label limit.
								maxWidth: interactive ? 280 : undefined,
							}}
							className={styles.tooltip}
							initial={{ opacity: 0, scale: 0.95 }}
							animate={{ opacity: 1, scale: 1 }}
							exit={{ opacity: 0, scale: 0.95 }}
							transition={{ duration: 0.1 }}
							role={interactive && onContentClick ? 'button' : 'tooltip'}
							onMouseEnter={handleFloatingEnter}
							onMouseLeave={handleFloatingLeave}
							onClick={
								interactive && onContentClick
									? () => {
											if (closeTimeoutRef.current)
												clearTimeout(closeTimeoutRef.current);
											setIsOpen(false);
											onContentClick();
										}
									: undefined
							}
						>
							<div className={styles.content}>{content}</div>
							{shortcut && (
								<div className={styles.shortcutRow}>
									{parseShortcut(shortcut).map((token, i) => (
										<span key={`${i}-${token}`} className={styles.keycap}>
											{token}
										</span>
									))}
								</div>
							)}
						</motion.div>
					)}
				</AnimatePresence>,
				document.body,
			)}
		</>
	);
}
