/**
 * BannerCropModal — opened after the user picks a banner file in
 * the profile settings. Same drag/zoom/canvas-export pattern as
 * AvatarCropModal, but with a 5:2 rectangular crop frame instead
 * of a circle so every render surface (settings preview, member
 * popout, member modal, mobile sheet, user-area popout) lines up
 * on the same crop edges.
 *
 * Output is a 1200×480 PNG (matches Discord's profile banner
 * ratio). Display surfaces use `aspect-ratio: 5 / 2` so the
 * uploaded image lines up edge-to-edge regardless of width.
 */
import {
	useCallback,
	useEffect,
	useMemo,
	useRef,
	useState,
	type PointerEvent as ReactPointerEvent,
	type WheelEvent as ReactWheelEvent,
} from 'react';
import { Modal } from '@discord-clone/ui';
import {
	ArrowClockwise,
	FrameCorners,
} from '@phosphor-icons/react';
import styles from './BannerCropModal.module.css';

// 5:2 rectangle. Editor footprint fits the medium modal width;
// output is high-res so the same crop stays sharp on a 600-wide
// MemberProfileModal banner and on retina displays.
const EDITOR_WIDTH = 480;
const EDITOR_HEIGHT = 192;
const OUTPUT_WIDTH = 1200;
const OUTPUT_HEIGHT = 480;
const ZOOM_MIN = 1;
const ZOOM_MAX = 4;
const ZOOM_STEP = 0.05;

interface BannerCropModalProps {
	isOpen: boolean;
	onClose: () => void;
	file: File | null;
	onCropped: (blob: Blob) => void;
	onSkipCrop: () => void;
}

interface Offset {
	x: number;
	y: number;
}

export function BannerCropModal({
	isOpen,
	onClose,
	file,
	onCropped,
	onSkipCrop,
}: BannerCropModalProps) {
	const [imageUrl, setImageUrl] = useState<string | null>(null);
	const [imageEl, setImageEl] = useState<HTMLImageElement | null>(null);
	const [offset, setOffset] = useState<Offset>({ x: 0, y: 0 });
	const [zoom, setZoom] = useState(1);
	const [rotation, setRotation] = useState(0);
	const [isSaving, setIsSaving] = useState(false);
	const [error, setError] = useState<string | null>(null);

	const editorRef = useRef<HTMLDivElement>(null);
	const dragRef = useRef<{
		pointerId: number;
		startX: number;
		startY: number;
		startOffset: Offset;
	} | null>(null);

	useEffect(() => {
		if (!isOpen || !file) {
			setImageUrl(null);
			setImageEl(null);
			return;
		}
		const url = URL.createObjectURL(file);
		setImageUrl(url);
		const img = new Image();
		img.onload = () => setImageEl(img);
		img.onerror = () => setError('Could not read that image.');
		img.src = url;
		return () => {
			URL.revokeObjectURL(url);
		};
	}, [isOpen, file]);

	useEffect(() => {
		if (!imageEl) return;
		setOffset({ x: 0, y: 0 });
		setZoom(1);
		setRotation(0);
		setError(null);
	}, [imageEl]);

	// Cover scale for a non-square viewport: take the larger of the
	// two axis ratios so the source always fully covers the editor.
	const coverScale = useMemo(() => {
		if (!imageEl) return 1;
		return Math.max(
			EDITOR_WIDTH / imageEl.naturalWidth,
			EDITOR_HEIGHT / imageEl.naturalHeight,
		);
	}, [imageEl]);

	const effectiveScale = coverScale * zoom;

	const displayWidth = imageEl
		? imageEl.naturalWidth * effectiveScale
		: EDITOR_WIDTH;
	const displayHeight = imageEl
		? imageEl.naturalHeight * effectiveScale
		: EDITOR_HEIGHT;

	const clampOffset = useCallback(
		(raw: Offset): Offset => {
			const maxX = Math.max(0, (displayWidth - EDITOR_WIDTH) / 2);
			const maxY = Math.max(0, (displayHeight - EDITOR_HEIGHT) / 2);
			return {
				x: Math.max(-maxX, Math.min(maxX, raw.x)),
				y: Math.max(-maxY, Math.min(maxY, raw.y)),
			};
		},
		[displayWidth, displayHeight],
	);

	useEffect(() => {
		setOffset((prev) => clampOffset(prev));
	}, [clampOffset]);

	const handlePointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
		if (!imageEl) return;
		(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
		dragRef.current = {
			pointerId: e.pointerId,
			startX: e.clientX,
			startY: e.clientY,
			startOffset: offset,
		};
	};

	const handlePointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
		const drag = dragRef.current;
		if (!drag || drag.pointerId !== e.pointerId) return;
		const dx = e.clientX - drag.startX;
		const dy = e.clientY - drag.startY;
		setOffset(
			clampOffset({
				x: drag.startOffset.x + dx,
				y: drag.startOffset.y + dy,
			}),
		);
	};

	const handlePointerUp = (e: ReactPointerEvent<HTMLDivElement>) => {
		const drag = dragRef.current;
		if (!drag || drag.pointerId !== e.pointerId) return;
		try {
			(e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
		} catch {
			// Already released — non-issue.
		}
		dragRef.current = null;
	};

	const handleWheel = (e: ReactWheelEvent<HTMLDivElement>) => {
		if (!imageEl) return;
		const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
		setZoom((prev) => Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, prev + delta)));
	};

	const handleReset = () => {
		setOffset({ x: 0, y: 0 });
		setZoom(1);
		setRotation(0);
	};

	const handleRotate = () => {
		setRotation((prev) => (prev + 90) % 360);
	};

	const handleFitFrame = () => {
		setZoom(1);
		setOffset({ x: 0, y: 0 });
	};

	const handleSkip = () => {
		onSkipCrop();
		onClose();
	};

	const handleSave = async () => {
		if (!imageEl) return;
		setIsSaving(true);
		setError(null);
		try {
			const blob = await renderCrop({
				image: imageEl,
				editorWidth: EDITOR_WIDTH,
				editorHeight: EDITOR_HEIGHT,
				outputWidth: OUTPUT_WIDTH,
				outputHeight: OUTPUT_HEIGHT,
				offset,
				scale: effectiveScale,
				rotation,
			});
			onCropped(blob);
			onClose();
		} catch (err) {
			setError(err instanceof Error ? err.message : 'Failed to crop image.');
			setIsSaving(false);
		}
	};

	const imageTransform = imageEl
		? `translate(-50%, -50%) translate(${offset.x}px, ${offset.y}px) rotate(${rotation}deg) scale(${effectiveScale})`
		: undefined;

	return (
		<Modal.Root isOpen={isOpen} onClose={onClose} size="medium">
			<Modal.Header title="Crop Banner" onClose={onClose} />
			<Modal.Content>
				<div className={styles.body}>
					<p className={styles.description}>
						Drag to reposition your banner and use the scroll wheel or
						pinch to zoom. The recommended size is 1200×480 pixels (5:2).
					</p>

					<div
						ref={editorRef}
						className={styles.editor}
						style={{ width: EDITOR_WIDTH, height: EDITOR_HEIGHT }}
						onPointerDown={handlePointerDown}
						onPointerMove={handlePointerMove}
						onPointerUp={handlePointerUp}
						onPointerCancel={handlePointerUp}
						onWheel={handleWheel}
					>
						{imageUrl && (
							<img
								src={imageUrl}
								alt=""
								className={styles.editorImage}
								style={{
									transform: imageTransform,
									width: imageEl?.naturalWidth ?? 0,
									height: imageEl?.naturalHeight ?? 0,
								}}
								draggable={false}
							/>
						)}
						{/* Rounded-rectangle crop mask. Same outer-shadow
						    technique as the avatar crop, but the mask
						    matches the 5:2 editor frame so the crop
						    region is identical to the rendered output. */}
						<div className={styles.cropMask} aria-hidden />
					</div>

					<div className={styles.zoomRow}>
						<label className={styles.zoomLabel} htmlFor="banner-zoom">
							Zoom
						</label>
						<input
							id="banner-zoom"
							type="range"
							className={styles.zoomSlider}
							min={ZOOM_MIN}
							max={ZOOM_MAX}
							step={ZOOM_STEP}
							value={zoom}
							onChange={(e) => setZoom(Number(e.target.value))}
							disabled={!imageEl}
						/>
						<button
							type="button"
							className={styles.iconButton}
							onClick={handleFitFrame}
							disabled={!imageEl}
							aria-label="Fit to frame"
							title="Fit to frame"
						>
							<FrameCorners size={20} weight="regular" />
						</button>
						<button
							type="button"
							className={styles.iconButton}
							onClick={handleRotate}
							disabled={!imageEl}
							aria-label="Rotate 90°"
							title="Rotate 90°"
						>
							<ArrowClockwise size={20} weight="regular" />
						</button>
					</div>

					{error && <div className={styles.error}>{error}</div>}

					<div className={styles.actions}>
						<button
							type="button"
							className={styles.resetButton}
							onClick={handleReset}
							disabled={!imageEl}
						>
							Reset
						</button>
						<div className={styles.actionsSpacer} />
						<button
							type="button"
							className={styles.secondaryButton}
							onClick={onClose}
							disabled={isSaving}
						>
							Cancel
						</button>
						<button
							type="button"
							className={styles.tertiaryButton}
							onClick={handleSkip}
							disabled={isSaving}
							title="Upload the original file without cropping"
						>
							Skip Cropping
						</button>
						<button
							type="button"
							className={styles.primaryButton}
							onClick={handleSave}
							disabled={!imageEl || isSaving}
						>
							{isSaving ? 'Saving…' : 'Save Banner'}
						</button>
					</div>
				</div>
			</Modal.Content>
		</Modal.Root>
	);
}

interface RenderCropArgs {
	image: HTMLImageElement;
	editorWidth: number;
	editorHeight: number;
	outputWidth: number;
	outputHeight: number;
	offset: Offset;
	scale: number;
	rotation: number;
}

/**
 * Bakes the editor transform into a PNG Blob sized
 * `outputWidth × outputHeight`. Editor and output must share the
 * same aspect ratio so the uniform `ratio` scalar can map every
 * transform from editor space into output space.
 */
function renderCrop(args: RenderCropArgs): Promise<Blob> {
	return new Promise((resolve, reject) => {
		const {
			image,
			editorWidth,
			outputWidth,
			outputHeight,
			offset,
			scale,
			rotation,
		} = args;
		const canvas = document.createElement('canvas');
		canvas.width = outputWidth;
		canvas.height = outputHeight;
		const ctx = canvas.getContext('2d');
		if (!ctx) {
			reject(new Error('Could not get 2D canvas context.'));
			return;
		}

		// Editor and output share an aspect ratio, so a single
		// scalar maps transforms between the two spaces.
		const ratio = outputWidth / editorWidth;

		ctx.save();
		ctx.translate(outputWidth / 2, outputHeight / 2);
		ctx.translate(offset.x * ratio, offset.y * ratio);
		ctx.rotate((rotation * Math.PI) / 180);
		const drawScale = scale * ratio;
		ctx.drawImage(
			image,
			(-image.naturalWidth * drawScale) / 2,
			(-image.naturalHeight * drawScale) / 2,
			image.naturalWidth * drawScale,
			image.naturalHeight * drawScale,
		);
		ctx.restore();

		canvas.toBlob(
			(blob) => {
				if (!blob) {
					reject(new Error('Canvas export failed.'));
					return;
				}
				resolve(blob);
			},
			'image/png',
			0.95,
		);
	});
}
