import { useEffect, useState } from 'react';
import { usePlatform } from '../../platform';
import { AttachmentAudio } from './AttachmentAudio';
import { AttachmentVideo } from './AttachmentVideo';

export interface AttachmentMetadata {
	type: 'attachment';
	url: string;
	filename: string;
	mimeType: string;
	size: number;
	/** Hex-encoded AES-256 key. The file bytes are encrypted with this
	 *  key before upload, so we need it to decrypt back to a blob URL. */
	key: string;
	iv: string;
	width?: number;
	height?: number;
}

const TAG_HEX_LEN = 32;

// Shared blob-URL cache so re-mounts don't refetch + redecrypt the
// same file. Keyed by the remote storage URL.
const attachmentCache = new Map<string, string>();

function fromHexString(hex: string): Uint8Array {
	const matches = hex.match(/.{1,2}/g) ?? [];
	return new Uint8Array(matches.map((b) => parseInt(b, 16)));
}

function toHexString(bytes: Uint8Array): string {
	let s = '';
	for (const b of bytes) s += b.toString(16).padStart(2, '0');
	return s;
}

/**
 * Fetch an encrypted attachment from storage, decrypt it with the
 * per-file AES key from the message metadata, and return a blob: URL
 * that can be fed to <img>, <video>, <audio>, or <a download>.
 */
export function useDecryptedAttachmentUrl(metadata: AttachmentMetadata): {
	url: string | null;
	loading: boolean;
	error: string | null;
} {
	const { crypto } = usePlatform();
	const [state, setState] = useState<{
		url: string | null;
		loading: boolean;
		error: string | null;
	}>(() => {
		const cached = attachmentCache.get(metadata.url);
		return { url: cached ?? null, loading: !cached, error: null };
	});

	useEffect(() => {
		const cached = attachmentCache.get(metadata.url);
		if (cached) {
			setState({ url: cached, loading: false, error: null });
			return;
		}

		let cancelled = false;
		setState({ url: null, loading: true, error: null });

		(async () => {
			try {
				const res = await fetch(metadata.url);
				if (!res.ok) throw new Error(`HTTP ${res.status}`);
				const buf = new Uint8Array(await res.arrayBuffer());
				const hex = toHexString(buf);
				if (hex.length < TAG_HEX_LEN) throw new Error('Invalid file data');
				const contentHex = hex.slice(0, -TAG_HEX_LEN);
				const tagHex = hex.slice(-TAG_HEX_LEN);
				const plaintextBuf = await crypto.decryptData(
					contentHex,
					metadata.key,
					metadata.iv,
					tagHex,
					{ encoding: 'buffer' },
				);
				const bytes =
					plaintextBuf instanceof Uint8Array
						? plaintextBuf
						: new Uint8Array(plaintextBuf as ArrayBuffer);
				const blob = new Blob([bytes], { type: metadata.mimeType });
				const objectUrl = URL.createObjectURL(blob);
				attachmentCache.set(metadata.url, objectUrl);
				if (!cancelled) setState({ url: objectUrl, loading: false, error: null });
			} catch (err: any) {
				console.error('Attachment decrypt error:', err);
				if (!cancelled)
					setState({ url: null, loading: false, error: err?.message ?? 'decrypt failed' });
			}
		})();

		return () => {
			cancelled = true;
		};
	}, [metadata.url, metadata.key, metadata.iv, metadata.mimeType]);

	return state;
}

interface AttachmentProps {
	metadata: AttachmentMetadata;
	onImageClick?: (url: string, metadata: AttachmentMetadata) => void;
	className?: string;
}

export function EncryptedAttachment({ metadata, onImageClick, className }: AttachmentProps) {
	const { url, loading, error } = useDecryptedAttachmentUrl(metadata);
	const kind = metadata.mimeType.split('/')[0];

	if (error) {
		return <div style={{ color: 'var(--status-danger)', fontSize: 13 }}>{error}</div>;
	}

	if (kind === 'image') {
		// Reserve the exact final layout box up-front so the loaded
		// image lands in the same slot the placeholder occupied — no
		// post-load height shift, no scroll jump. When the metadata
		// carries both width + height we compute the box from them;
		// otherwise we fall back to a ratio-aware aspect-ratio so the
		// browser still reserves a sensible chunk of space.
		const hasDims = !!metadata.width && !!metadata.height;
		const maxW = metadata.width ? Math.min(metadata.width, 400) : 300;
		const renderedH =
			hasDims
				? Math.round(maxW * (metadata.height! / metadata.width!))
				: undefined;
		const sharedBoxStyle: React.CSSProperties = {
			width: maxW,
			...(renderedH !== undefined ? { height: renderedH } : {}),
			...(hasDims
				? { aspectRatio: `${metadata.width} / ${metadata.height}` }
				: { aspectRatio: '4 / 3' }),
			maxHeight: '50vh',
			borderRadius: 'var(--radius-lg)',
		};
		if (loading || !url) {
			return (
				<div
					style={{
						...sharedBoxStyle,
						backgroundColor: 'var(--background-tertiary)',
					}}
				/>
			);
		}
		return (
			<img
				src={url}
				alt={metadata.filename}
				className={className}
				// `loading="eager"` so the image decodes synchronously
				// with the placeholder it's replacing — keeps the
				// "scroll to bottom on initial load" anchor accurate
				// instead of jumping after a deferred lazy decode.
				loading="eager"
				decoding="async"
				style={{
					...sharedBoxStyle,
					objectFit: 'cover',
					cursor: 'pointer',
				}}
				onLoad={() => {
					// Tell the Messages scroller that an attachment
					// finished decoding so it can re-pin to bottom if
					// the user is still anchored there. Belt-and-
					// suspenders — the parent ResizeObserver should
					// also catch this, but a same-size box with
					// `objectFit: cover` may not trigger one.
					window.dispatchEvent(
						new CustomEvent('brycord:attachment-loaded'),
					);
				}}
				onClick={() => onImageClick?.(url, metadata)}
			/>
		);
	}

	if (kind === 'video') {
		if (loading || !url) {
			return (
				<div
					style={{
						width: 400,
						height: 225,
						backgroundColor: 'var(--background-tertiary)',
						borderRadius: 'var(--radius-lg)',
					}}
				/>
			);
		}
		return (
			<AttachmentVideo
				src={url}
				filename={metadata.filename}
				width={metadata.width}
				height={metadata.height}
				attachment={metadata}
			/>
		);
	}

	if (kind === 'audio') {
		if (loading || !url) {
			return (
				<div
					style={{
						width: 360,
						height: 96,
						backgroundColor: 'var(--background-tertiary)',
						borderRadius: 'var(--radius-lg)',
					}}
				/>
			);
		}
		return (
			<AttachmentAudio
				src={url}
				filename={metadata.filename}
				attachment={metadata}
			/>
		);
	}

	// Generic file
	if (loading || !url) {
		return (
			<div style={{ padding: 10, color: 'var(--text-tertiary)' }}>
				{metadata.filename} (decrypting…)
			</div>
		);
	}
	return (
		<a
			href={url}
			download={metadata.filename}
			className={className}
			target="_blank"
			rel="noopener noreferrer"
		>
			{metadata.filename}
		</a>
	);
}
