import { useCallback, useEffect, useState } from 'react';
import { useMutation, useQuery } from 'convex/react';
import { ArrowClockwise, Copy } from '@phosphor-icons/react';
import { Modal, Button } from '@discord-clone/ui';
import { api } from '../../../../../convex/_generated/api';
import { usePlatform } from '../../platform';
import styles from './InviteModal.module.css';

interface InviteModalProps {
	isOpen: boolean;
	onClose: () => void;
}

/**
 * InviteModal — generates a real E2E-encrypted invite.
 *
 * Flow:
 *   1. Decrypt the creator's own `channelKeys` bundles with their
 *      RSA private key (held in sessionStorage after login), merging
 *      everything into a single `{channelId: keyHex}` map.
 *   2. Generate a random `inviteSecret` (32 bytes hex) that will act
 *      as the AES key for the invite payload. This secret lives in
 *      the invite URL fragment — never sent to the server.
 *   3. Encrypt the key map with `inviteSecret` and store the
 *      resulting blob on the server via `api.invites.create`.
 *   4. Build the invite URL as `${origin}/invite/${code}#key=${secret}`
 *      so the fragment carries the secret client-side only.
 *
 * The acceptor opens the URL, hits `/invite/:code`, which pulls the
 * blob from the server, decrypts with the fragment secret, stashes
 * the decoded keys in sessionStorage, then hands off to RegisterPage.
 */
export function InviteModal({ isOpen, onClose }: InviteModalProps) {
	const { crypto } = usePlatform();
	const createInvite = useMutation(api.invites.create);
	const serverSettings = useQuery(api.serverSettings.get);
	const serverName = serverSettings?.serverName ?? 'this server';

	const userId =
		typeof localStorage !== 'undefined'
			? localStorage.getItem('userId')
			: null;
	const myKeyBundles = useQuery(
		api.channelKeys.getKeysForUser,
		userId ? { userId: userId as any } : 'skip',
	);
	// The invite should only share keys for server channels (text/voice).
	// DMs are 1:1 and leaking their keys would let the invitee decrypt
	// the inviter's private conversations with everyone else. `channels.list`
	// already filters by `type !== 'dm'`, so use it as the allowlist.
	const serverChannels = useQuery(api.channels.list);

	const [code, setCode] = useState<string | null>(null);
	const [isGenerating, setIsGenerating] = useState(false);
	const [copied, setCopied] = useState(false);
	const [inviteUrl, setInviteUrl] = useState<string>('');
	const [error, setError] = useState<string | null>(null);

	const origin =
		typeof window !== 'undefined' ? window.location.origin : '';

	const generate = useCallback(async () => {
		if (!userId) {
			setError('You must be signed in to create an invite.');
			return;
		}
		if (!myKeyBundles || !serverChannels) {
			// Key bundles / channel list haven't loaded yet — surface a
			// soft error; the auto-generate effect fires again once both
			// queries resolve.
			setError('Loading your channel keys…');
			return;
		}

		const privateKey =
			typeof sessionStorage !== 'undefined'
				? sessionStorage.getItem('privateKey')
				: null;
		if (!privateKey) {
			setError(
				'No decryption key available. Log out and back in to restore your session.',
			);
			return;
		}

		setIsGenerating(true);
		setError(null);
		setCopied(false);

		try {
			// Build an allowlist of channel ids that belong to the server
			// (non-DM). Everything else — especially DM bundles — gets
			// dropped on the floor so invitees never see private DM keys.
			const allowedChannelIds = new Set<string>(
				serverChannels.map((c: any) => c._id as string),
			);

			// Decrypt every bundle we have for allowed channels and merge
			// into a single {channelId: keyHex} map. Bundles we can't
			// decrypt (old key versions, corrupt rows) are silently
			// skipped — they just won't be shared with the invitee.
			const mergedKeys: Record<string, string> = {};
			for (const bundle of myKeyBundles) {
				const bundleChannelId = bundle.channel_id as unknown as string;
				if (!allowedChannelIds.has(bundleChannelId)) continue;
				try {
					const plaintext = await crypto.privateDecrypt(
						privateKey,
						bundle.encrypted_key_bundle,
					);
					const parsed = JSON.parse(plaintext) as Record<string, string>;
					for (const [chId, keyHex] of Object.entries(parsed)) {
						// Double-check: an old bundle could theoretically
						// contain more than its owning row's channelId.
						// Only carry forward the ones on the allowlist.
						if (allowedChannelIds.has(chId)) {
							mergedKeys[chId] = keyHex;
						}
					}
				} catch {
					/* skip unreadable bundle */
				}
			}

			const inviteCode = Array.from({ length: 8 }, () =>
				Math.floor(Math.random() * 36).toString(36),
			).join('');
			const inviteSecret = await crypto.randomBytes(32);

			const payload = JSON.stringify(mergedKeys);
			const encrypted = await crypto.encryptData(payload, inviteSecret);
			const blob = JSON.stringify({
				c: encrypted.content,
				i: encrypted.iv,
				t: encrypted.tag,
			});

			await createInvite({
				code: inviteCode,
				encryptedPayload: blob,
				createdBy: userId as any,
				keyVersion: 1,
				expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
			});

			// Fragment URL — the secret never hits the server. The
			// acceptor's client reads it from `window.location.hash`.
			setInviteUrl(`${origin}/invite/${inviteCode}#key=${inviteSecret}`);
			setCode(inviteCode);
		} catch (err: any) {
			setError(err?.message ?? 'Unknown error');
			setCode(null);
			setInviteUrl('');
		} finally {
			setIsGenerating(false);
		}
	}, [createInvite, crypto, myKeyBundles, origin, userId]);

	useEffect(() => {
		if (!isOpen) return;
		setCopied(false);
		if (
			!code &&
			!isGenerating &&
			myKeyBundles !== undefined &&
			serverChannels !== undefined
		) {
			void generate();
		}
	}, [isOpen, code, isGenerating, myKeyBundles, serverChannels, generate]);

	const handleCopy = async () => {
		if (!inviteUrl) return;
		try {
			await navigator.clipboard.writeText(inviteUrl);
			setCopied(true);
			setTimeout(() => setCopied(false), 1500);
		} catch {
			/* clipboard blocked */
		}
	};

	const handleRegenerate = () => {
		setCode(null);
		setInviteUrl('');
		void generate();
	};

	return (
		<Modal.Root isOpen={isOpen} onClose={onClose} size="small">
			<Modal.Header title={`Invite People to ${serverName}`} onClose={onClose} />
			<Modal.Content>
				<div className={styles.content}>
					<p style={{ color: 'var(--text-secondary)', fontSize: 14, margin: 0 }}>
						Send an invite link to your friends. The link carries encrypted
						channel keys so the invitee can read existing messages.
					</p>

					{error ? (
						<div className={styles.error}>
							Unable to generate invite: {error}
						</div>
					) : (
						<>
							<div className={styles.field}>
								<label className={styles.label}>SERVER INVITE LINK</label>
								<div style={{ display: 'flex', gap: 8 }}>
									<input
										type="text"
										className={styles.input}
										value={
											isGenerating || !inviteUrl ? 'Generating…' : inviteUrl
										}
										readOnly
										onClick={(e) => e.currentTarget.select()}
										style={{ flex: 1 }}
									/>
									<Button
										variant="primary"
										size="sm"
										icon={<Copy size={16} />}
										onClick={handleCopy}
										disabled={!inviteUrl || isGenerating}
									>
										{copied ? 'Copied!' : 'Copy'}
									</Button>
								</div>
							</div>

							<div
								style={{
									display: 'flex',
									alignItems: 'center',
									justifyContent: 'space-between',
									gap: 12,
								}}
							>
								<span
									style={{
										fontSize: 12,
										color: 'var(--text-tertiary)',
									}}
								>
									Your invite link expires in 7 days.
								</span>
								<Button
									variant="ghost"
									size="sm"
									icon={<ArrowClockwise size={14} />}
									onClick={handleRegenerate}
									disabled={isGenerating}
								>
									Generate new link
								</Button>
							</div>
						</>
					)}
				</div>
			</Modal.Content>
			<Modal.Footer>
				<Button variant="primary" size="sm" onClick={onClose}>
					Done
				</Button>
			</Modal.Footer>
		</Modal.Root>
	);
}
