/**
 * ChannelHeaderPinsPopover — desktop popover anchored to the
 * ChannelHeader pin button. Fetches pinned messages from Convex,
 * decrypts them using the channel key, and renders a scrollable
 * list. Dismisses on outside click or Escape.
 */
import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { PushPin } from '@phosphor-icons/react';
import { useQuery } from 'convex/react';
import { api } from '../../../../../convex/_generated/api';
import { usePlatform } from '../../platform';
import { PinnedMessageRow, ReachedEndNotice, type PinnedMessage } from './PinnedMessageRow';
import { PinConfirmationModal } from './PinConfirmationModal';
import type { AttachmentMetadata } from './EncryptedAttachment';
import styles from './ChannelHeaderPinsPopover.module.css';

interface ChannelHeaderPinsPopoverProps {
	isOpen: boolean;
	channelId: string;
	anchorRect: DOMRect | null;
	onClose: () => void;
}

// Ciphertext format matches Messages.tsx: content + 32-hex-char GCM tag.
const TAG_LENGTH = 32;

export function ChannelHeaderPinsPopover({
	isOpen,
	channelId,
	anchorRect,
	onClose,
}: ChannelHeaderPinsPopoverProps) {
	const { crypto } = usePlatform();
	const ref = useRef<HTMLDivElement>(null);

	// Outside click + Escape dismiss.
	useEffect(() => {
		if (!isOpen) return;
		const handleClick = (e: MouseEvent) => {
			if (ref.current && !ref.current.contains(e.target as Node)) {
				onClose();
			}
		};
		const handleEscape = (e: KeyboardEvent) => {
			if (e.key === 'Escape') onClose();
		};
		document.addEventListener('mousedown', handleClick);
		document.addEventListener('keydown', handleEscape);
		return () => {
			document.removeEventListener('mousedown', handleClick);
			document.removeEventListener('keydown', handleEscape);
		};
	}, [isOpen, onClose]);

	// Position the popover below and right-aligned to the pin button,
	// clamped to the viewport.
	const positionStyle = useMemo<React.CSSProperties>(() => {
		if (!anchorRect) return { top: 0, left: 0 };
		const POPOVER_WIDTH = 480;
		const POPOVER_MAX_HEIGHT = Math.min(window.innerHeight - 120, 720);
		const GAP = 8;
		const MARGIN = 12;

		const rawLeft = anchorRect.right - POPOVER_WIDTH;
		const left = Math.max(
			MARGIN,
			Math.min(rawLeft, window.innerWidth - POPOVER_WIDTH - MARGIN),
		);
		const rawTop = anchorRect.bottom + GAP;
		const top = Math.max(
			MARGIN,
			Math.min(rawTop, window.innerHeight - POPOVER_MAX_HEIGHT - MARGIN),
		);
		return { top, left };
	}, [anchorRect]);

	// Channel key decryption — mirrors Messages.tsx.
	const userId =
		typeof localStorage !== 'undefined' ? localStorage.getItem('userId') : null;
	const privateKeyPem =
		typeof sessionStorage !== 'undefined'
			? sessionStorage.getItem('privateKey')
			: null;

	const allKeys = useQuery(
		api.channelKeys.getKeysForUser,
		userId && isOpen ? ({ userId: userId as any } as any) : 'skip',
	);

	// Each encrypted_key_bundle decrypts to a JSON object mapping
	// channelId → keyHex. Decrypt once per bundle and merge.
	const [channelKey, setChannelKey] = useState<string | null>(null);
	useEffect(() => {
		let cancelled = false;
		if (!allKeys || !privateKeyPem) {
			setChannelKey(null);
			return;
		}
		(async () => {
			const merged: Record<string, string> = {};
			for (const item of allKeys) {
				try {
					const bundleJson = await crypto.privateDecrypt(
						privateKeyPem,
						(item as any).encrypted_key_bundle,
					);
					Object.assign(merged, JSON.parse(bundleJson));
				} catch (err) {
					console.error('Failed to decrypt key bundle:', err);
				}
			}
			if (cancelled) return;
			setChannelKey(merged[channelId] ?? null);
		})();
		return () => {
			cancelled = true;
		};
	}, [allKeys, privateKeyPem, channelId, crypto]);

	// Fetch pinned messages.
	const pinnedRaw = useQuery(
		api.messages.listPinned,
		isOpen
			? ({
					channelId: channelId as any,
					userId: (userId as any) ?? undefined,
				} as any)
			: 'skip',
	);

	// Decrypt pinned message content.
	const [decryptedMap, setDecryptedMap] = useState<Map<string, string>>(new Map());

	useEffect(() => {
		if (!channelKey || !pinnedRaw) return;
		let cancelled = false;
		(async () => {
			const next = new Map(decryptedMap);
			let changed = false;
			for (const msg of pinnedRaw as any[]) {
				const id = msg.id as string;
				if (next.has(id)) continue;
				if (!msg.ciphertext || msg.ciphertext.length < TAG_LENGTH) {
					next.set(id, '[Invalid Encrypted Message]');
					changed = true;
					continue;
				}
				const tag = msg.ciphertext.slice(-TAG_LENGTH);
				const contentHex = msg.ciphertext.slice(0, -TAG_LENGTH);
				try {
					const plaintext = await crypto.decryptData(
						contentHex,
						channelKey,
						msg.nonce,
						tag,
					);
					if (cancelled) return;
					next.set(id, plaintext);
					changed = true;
				} catch {
					next.set(id, '[Unable to decrypt]');
					changed = true;
				}
			}
			if (changed && !cancelled) setDecryptedMap(next);
		})();
		return () => {
			cancelled = true;
		};
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [pinnedRaw, channelKey]);

	const pinned: PinnedMessage[] = useMemo(() => {
		if (!pinnedRaw) return [];
		return (pinnedRaw as any[])
			.slice()
			.sort((a, b) => {
				const at = a.created_at ? new Date(a.created_at).getTime() : 0;
				const bt = b.created_at ? new Date(b.created_at).getTime() : 0;
				return bt - at;
			})
			.map((msg) => {
				const id = msg.id as string;
				const rawContent = decryptedMap.get(id) ?? '';
				let text = rawContent;
				const attachments: AttachmentMetadata[] = [];
				try {
					const parsed = JSON.parse(rawContent);
					if (parsed && typeof parsed === 'object') {
						if (Array.isArray(parsed)) {
							for (const item of parsed) {
								if (item?.type === 'attachment' && item.url && item.key && item.iv) {
									attachments.push(item as AttachmentMetadata);
								}
							}
							text = '';
						} else if (parsed.type === 'attachment' && parsed.url && parsed.key && parsed.iv) {
							attachments.push(parsed as AttachmentMetadata);
							text = '';
						} else if (parsed.text !== undefined) {
							text = String(parsed.text);
						}
					}
				} catch {
					// plain text — leave as-is
				}
				return {
					id,
					authorName: msg.displayName || msg.username || 'User',
					authorAvatarUrl: msg.avatarUrl ?? null,
					content: text,
					timestamp: msg.created_at
						? new Date(msg.created_at).getTime()
						: Date.now(),
					attachments,
				} as PinnedMessage;
			});
	}, [pinnedRaw, decryptedMap]);

	const handleJumpTo = (messageId: string) => {
		window.dispatchEvent(
			new CustomEvent('brycord:scroll-to-message', {
				detail: { channelId, messageId },
			}),
		);
		onClose();
	};

	const [unpinTarget, setUnpinTarget] = useState<PinnedMessage | null>(null);

	if (!isOpen || !anchorRect) return null;

	return createPortal(
		<>
			<div className={styles.overlay}>
				<div ref={ref} className={styles.popover} style={positionStyle}>
					<div className={styles.header}>
						<span className={styles.headerIcon}>
							<PushPin size={20} weight="fill" />
						</span>
						<h2 className={styles.title}>Pinned Messages</h2>
					</div>
					<div className={styles.body}>
						{pinned.map((msg) => (
							<PinnedMessageRow
								key={msg.id}
								message={msg}
								onJumpTo={() => handleJumpTo(msg.id)}
								onUnpin={() => setUnpinTarget(msg)}
								showHoverActions
								canUnpin
							/>
						))}
						{pinnedRaw === undefined && (
							<div className={styles.loading}>Loading pinned messages…</div>
						)}
						<ReachedEndNotice />
					</div>
				</div>
			</div>

			<PinConfirmationModal
				isOpen={!!unpinTarget}
				onClose={() => setUnpinTarget(null)}
				channelId={channelId}
				messageId={unpinTarget?.id ?? null}
				message={unpinTarget}
				variant="unpin"
			/>
		</>,
		document.body,
	);
}
