/**
 * ImageLightbox — full-screen image viewer opened by clicking an
 * image attachment in the chat. Shows a file info card (filename +
 * dimensions) on the left, an action card (zoom toggle, download,
 * open-external) on the right, and a standalone close card. Clicking
 * the image toggles between fit-to-viewport and native-size modes —
 * when zoomed, the backdrop scrolls so the user can pan through the
 * overflow.
 *
 * Layout mirrors the Brycord/Fluxer reference 1:1, with Matrix-
 * specific bits (useMxcUrl, SavedMediaStore, MobileImageActionsSheet)
 * stripped out. The `src` prop is a blob URL already decrypted by
 * EncryptedAttachment — no async resolution happens here.
 *
 * Keyboard: Escape closes, `+` / `-` toggle zoom, `0` resets.
 */
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { useMutation, useQuery } from 'convex/react';
import {
	Download,
	ArrowSquareOut,
	Star,
	X,
	MagnifyingGlassPlus,
	MagnifyingGlassMinus,
} from '@phosphor-icons/react';
import { api } from '../../../../../convex/_generated/api';
import type { Id } from '../../../../../convex/_generated/dataModel';
import type { AttachmentMetadata } from './EncryptedAttachment';
import styles from './ImageLightbox.module.css';

interface ImageLightboxProps {
	isOpen: boolean;
	onClose: () => void;
	/** Blob URL of the decrypted image bytes. */
	src: string;
	/** Filename to show in the info card and to use for the download
	 *  attribute. Falls back to `alt` then to a generic "image". */
	filename?: string;
	/** Legacy alias for `filename` kept so existing MessageGroup callers
	 *  that only pass `alt` still render a label. */
	alt?: string;
	/** Pixel dimensions used by the info card's `WxH` readout. */
	width?: number;
	height?: number;
	/** Byte size of the attachment — rendered alongside the dimensions
	 *  when present. */
	size?: number;
	mimeType?: string;
	/**
	 * Original encrypted attachment metadata. When present, the
	 * lightbox shows a star button so the user can save the image to
	 * their personal media library — the saved entry stores the same
	 * url + per-file key + iv so it can be re-posted later without
	 * re-uploading the bytes.
	 */
	attachment?: AttachmentMetadata;
}

function formatBytes(bytes: number): string {
	if (bytes < 1024) return `${bytes} B`;
	if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
	if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
	return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}

export function ImageLightbox({
	isOpen,
	onClose,
	src,
	filename,
	alt,
	width,
	height,
	size,
	attachment,
}: ImageLightboxProps) {
	const [zoomed, setZoomed] = useState(false);

	const label = filename ?? alt ?? 'image';

	// Saved-media wiring — fetches the user's library once so we can
	// flip the star button between "save" and "unsave" states. The
	// list query is cheap (per-user, indexed) and stays cached.
	const myUserId =
		typeof localStorage !== 'undefined' ? localStorage.getItem('userId') : null;
	const savedList = useQuery(
		api.savedMedia.list,
		myUserId && isOpen ? { userId: myUserId as Id<'userProfiles'> } : 'skip',
	);
	const isSaved = !!(
		attachment &&
		savedList?.some((m) => m.url === attachment.url)
	);
	const saveMutation = useMutation(api.savedMedia.save);
	const removeMutation = useMutation(api.savedMedia.remove);

	const handleToggleSaved = async () => {
		if (!attachment || !myUserId) return;
		try {
			if (isSaved) {
				await removeMutation({
					userId: myUserId as Id<'userProfiles'>,
					url: attachment.url,
				});
			} else {
				const kind = attachment.mimeType.split('/')[0]; // image | video | audio
				await saveMutation({
					userId: myUserId as Id<'userProfiles'>,
					url: attachment.url,
					kind,
					filename: attachment.filename,
					mimeType: attachment.mimeType,
					width: attachment.width,
					height: attachment.height,
					size: attachment.size,
					encryptionKey: attachment.key,
					encryptionIv: attachment.iv,
				});
			}
		} catch (err) {
			console.warn('Failed to toggle saved media:', err);
		}
	};

	// Close on Escape, toggle zoom via keyboard shortcuts. Also lock
	// body scroll while the lightbox is open so the background chat
	// doesn't jitter when the backdrop consumes the viewport.
	useEffect(() => {
		if (!isOpen) return;
		const handleKey = (e: KeyboardEvent) => {
			if (e.key === 'Escape') onClose();
			else if (e.key === '+' || e.key === '=') setZoomed(true);
			else if (e.key === '-' || e.key === '_') setZoomed(false);
			else if (e.key === '0') setZoomed(false);
		};
		document.addEventListener('keydown', handleKey);
		const prevOverflow = document.body.style.overflow;
		document.body.style.overflow = 'hidden';
		return () => {
			document.removeEventListener('keydown', handleKey);
			document.body.style.overflow = prevOverflow;
		};
	}, [isOpen, onClose]);

	// Reset transient state whenever the modal closes so a previously
	// zoomed session doesn't bleed into the next attachment clicked.
	useEffect(() => {
		if (!isOpen) setZoomed(false);
	}, [isOpen]);

	const dimensionsLabel = width && height ? `${width}×${height}` : '';
	const sizeLabel = size ? formatBytes(size) : '';
	const metaLabel = [dimensionsLabel, sizeLabel].filter(Boolean).join(' · ');

	const handleDownload = () => {
		if (!src) return;
		// Trigger a download via a temporary <a download>. Blob URLs
		// honour the `download` attribute in all modern browsers.
		const a = document.createElement('a');
		a.href = src;
		a.download = label;
		document.body.appendChild(a);
		a.click();
		document.body.removeChild(a);
	};

	const handleOpenExternal = () => {
		if (!src) return;
		window.open(src, '_blank', 'noopener,noreferrer');
	};

	const handleToggleZoom = () => setZoomed((z) => !z);

	// Wrap every action button click so it doesn't bubble up to the
	// backdrop (which would close the lightbox).
	const stop =
		(fn: () => void) =>
		(e: React.MouseEvent) => {
			e.stopPropagation();
			fn();
		};

	return createPortal(
		<AnimatePresence>
			{isOpen && (
				<motion.div
					className={styles.backdrop}
					initial={{ opacity: 0 }}
					animate={{ opacity: 1 }}
					exit={{ opacity: 0 }}
					transition={{ duration: 0.15 }}
					onClick={onClose}
				>
					{/* Desktop header — file info card on the left, action
					    card + close card on the right. Each sits in its
					    own `--background-textarea` pill. */}
					<div className={styles.header} onClick={(e) => e.stopPropagation()}>
						<div className={styles.fileInfoCard}>
							<div className={styles.filename} title={label}>
								{label}
							</div>
							{metaLabel && <div className={styles.dimensions}>{metaLabel}</div>}
						</div>

						<div className={styles.headerRight}>
							<div className={styles.actionCard}>
								<button
									type="button"
									className={styles.actionButton}
									onClick={stop(handleToggleZoom)}
									aria-label={zoomed ? 'Zoom out' : 'Zoom in'}
									title={zoomed ? 'Zoom out' : 'Zoom in'}
								>
									{zoomed ? (
										<MagnifyingGlassMinus size={18} weight="regular" />
									) : (
										<MagnifyingGlassPlus size={18} weight="regular" />
									)}
								</button>
								{attachment && (
									<button
										type="button"
										className={styles.actionButton}
										onClick={stop(() => void handleToggleSaved())}
										aria-label={isSaved ? 'Unfavorite' : 'Favorite'}
										title={isSaved ? 'Unfavorite' : 'Favorite'}
										style={
											isSaved
												? { color: 'var(--brand-primary, #5865f2)' }
												: undefined
										}
									>
										<Star
											size={18}
											weight={isSaved ? 'fill' : 'regular'}
										/>
									</button>
								)}
								<button
									type="button"
									className={styles.actionButton}
									onClick={stop(handleDownload)}
									aria-label="Download"
									title="Download"
									disabled={!src}
								>
									<Download size={18} weight="regular" />
								</button>
								<button
									type="button"
									className={styles.actionButton}
									onClick={stop(handleOpenExternal)}
									aria-label="Open in new tab"
									title="Open in new tab"
									disabled={!src}
								>
									<ArrowSquareOut size={18} weight="regular" />
								</button>
							</div>

							<div className={styles.closeCard}>
								<button
									type="button"
									className={styles.actionButton}
									onClick={stop(onClose)}
									aria-label="Close"
									title="Close (Esc)"
								>
									<X size={20} weight="bold" />
								</button>
							</div>
						</div>
					</div>

					{/* Centered image. Clicking it toggles zoom — event is
					    stopped so the click doesn't propagate to the
					    backdrop's close handler. */}
					<motion.img
						key={src || 'loading'}
						src={src || undefined}
						alt={label}
						className={`${styles.image} ${zoomed ? styles.imageZoomed : ''}`}
						initial={{ opacity: 0, scale: 0.96 }}
						animate={{ opacity: 1, scale: 1 }}
						exit={{ opacity: 0, scale: 0.96 }}
						transition={{ duration: 0.18, ease: 'easeOut' }}
						onClick={stop(handleToggleZoom)}
						draggable={false}
					/>
				</motion.div>
			)}
		</AnimatePresence>,
		document.body,
	);
}
