import { useEffect, useRef, useState, type FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { CheckCircle, Eye, EyeSlash, Warning } from '@phosphor-icons/react';
import { useConvex, useQuery } from 'convex/react';
import { usePlatform } from '../../platform';
import { useSearch } from '../../contexts/SearchContext';
import { api } from '../../../../../convex/_generated/api';
import { AuthLayout } from './AuthLayout';
import styles from './LoginPage.module.css';

const MIN_PASSWORD_LENGTH = 8;
const MIN_USERNAME_LENGTH = 2;
const USERNAME_PATTERN = /^[a-zA-Z0-9._-]+$/;

type InviteState =
	| { status: 'idle' }
	| { status: 'checking' }
	| {
			status: 'valid';
			code: string;
			// Decoded channel-key map, if the pasted link carried a secret.
			// When the user only pastes a bare code, the map stays null and
			// the new account just won't have any existing channel keys.
			keys: Record<string, string> | null;
	  }
	| { status: 'invalid'; message: string };

/**
 * Parse the invite-code field. Accepts either a bare code or a full
 * invite URL of the form `${origin}/invite/CODE#key=SECRET`.
 */
function parseInviteInput(raw: string): { code: string; secret: string | null } | null {
	const trimmed = raw.trim();
	if (!trimmed) return null;
	// Full URL (possibly with a fragment)
	try {
		const url = new URL(trimmed);
		const match = url.pathname.match(/\/invite\/([^/]+)/);
		if (match) {
			const code = match[1];
			const hash = url.hash; // "#key=abcd"
			const keyMatch = hash.match(/[#&]key=([^&]+)/);
			return { code, secret: keyMatch ? keyMatch[1] : null };
		}
	} catch {
		/* not a URL — fall through to bare-code path */
	}
	// Bare code (treat whitespace-delimited first token as the code)
	const code = trimmed.split(/\s+/)[0];
	return { code, secret: null };
}

export function RegisterPage() {
	const navigate = useNavigate();
	const convex = useConvex();
	const { crypto, session } = usePlatform();
	const searchCtx = useSearch();

	// Used to detect the first-user bootstrap case — before any user
	// exists, the backend allows an empty invite code. This query is
	// cheap and public, so it's fine to run during register.
	const existingUsers = useQuery(api.auth.getPublicKeys) ?? [];
	const isFirstUser = existingUsers.length === 0;

	const [username, setUsername] = useState('');
	const [password, setPassword] = useState('');
	const [confirmPassword, setConfirmPassword] = useState('');
	const [inviteInput, setInviteInput] = useState('');
	const [invite, setInvite] = useState<InviteState>({ status: 'idle' });
	const [showPassword, setShowPassword] = useState(false);
	const [showConfirmPassword, setShowConfirmPassword] = useState(false);
	const [error, setError] = useState<string | null>(null);
	const [isRegistering, setIsRegistering] = useState(false);

	const debounceRef = useRef<number | null>(null);

	// Pick up an invite already accepted by /invite/:code — if
	// InviteAcceptPage stashed keys in sessionStorage, we skip the
	// validation UI and mark the invite as already valid.
	useEffect(() => {
		try {
			const pendingCode = sessionStorage.getItem('pendingInviteCode');
			const pendingRaw = sessionStorage.getItem('pendingInviteKeys');
			if (pendingCode) {
				setInviteInput(pendingCode);
				if (pendingRaw) {
					try {
						const parsed = JSON.parse(pendingRaw) as Record<string, string>;
						setInvite({ status: 'valid', code: pendingCode, keys: parsed });
						return;
					} catch {
						/* fall through to re-validation */
					}
				}
			}
		} catch {
			/* ignore */
		}
	}, []);

	// Live validation of whatever the user is typing. A 500 ms debounce
	// keeps us from hammering Convex on every keystroke.
	useEffect(() => {
		if (debounceRef.current !== null) {
			window.clearTimeout(debounceRef.current);
			debounceRef.current = null;
		}
		const trimmed = inviteInput.trim();
		if (!trimmed) {
			setInvite({ status: 'idle' });
			return;
		}
		// If this is the code we already validated on mount via
		// sessionStorage, don't re-fetch.
		if (invite.status === 'valid' && invite.code === trimmed) return;

		setInvite({ status: 'checking' });
		debounceRef.current = window.setTimeout(async () => {
			const parsed = parseInviteInput(trimmed);
			if (!parsed) {
				setInvite({ status: 'invalid', message: 'Empty invite.' });
				return;
			}
			try {
				const result = await convex.query(api.invites.use, { code: parsed.code });
				if ('error' in result) {
					setInvite({ status: 'invalid', message: result.error });
					return;
				}

				// If a secret was supplied, try to decrypt the payload
				// right now so we can (a) verify the secret actually
				// works, and (b) stash the decoded key map for the
				// submit handler to upload after account creation.
				let keys: Record<string, string> | null = null;
				if (parsed.secret && result.encryptedPayload) {
					try {
						const blob = JSON.parse(result.encryptedPayload) as {
							c: string;
							i: string;
							t: string;
						};
						const plaintext = await crypto.decryptData(
							blob.c,
							parsed.secret,
							blob.i,
							blob.t,
						);
						keys = JSON.parse(plaintext) as Record<string, string>;
					} catch {
						setInvite({
							status: 'invalid',
							message:
								'Invite secret does not match. Make sure you pasted the whole link.',
						});
						return;
					}
				}
				setInvite({ status: 'valid', code: parsed.code, keys });
			} catch (err: any) {
				setInvite({
					status: 'invalid',
					message: err?.message ?? 'Unable to verify invite.',
				});
			}
		}, 500);

		return () => {
			if (debounceRef.current !== null) {
				window.clearTimeout(debounceRef.current);
			}
		};
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [inviteInput, convex, crypto]);

	const canSubmit =
		!isRegistering &&
		(isFirstUser || invite.status === 'valid') &&
		username.trim().length >= MIN_USERNAME_LENGTH &&
		password.length >= MIN_PASSWORD_LENGTH &&
		password === confirmPassword;

	const handleSubmit = async (e: FormEvent) => {
		e.preventDefault();
		setError(null);

		const trimmedUsername = username.trim();
		if (trimmedUsername.length < MIN_USERNAME_LENGTH) {
			setError(`Username must be at least ${MIN_USERNAME_LENGTH} characters`);
			return;
		}
		if (!USERNAME_PATTERN.test(trimmedUsername)) {
			setError(
				'Username may only contain letters, numbers, dots, underscores, or hyphens',
			);
			return;
		}
		if (password.length < MIN_PASSWORD_LENGTH) {
			setError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
			return;
		}
		if (password !== confirmPassword) {
			setError('Passwords do not match');
			return;
		}
		if (!isFirstUser && invite.status !== 'valid') {
			setError('You need a valid invite to register.');
			return;
		}

		setIsRegistering(true);

		try {
			const keys = await crypto.generateKeys();
			const salt = await crypto.randomBytes(16);
			const masterKeyHex = await crypto.randomBytes(32);
			const { dek, dak } = await crypto.deriveAuthKeys(password, salt);
			const hak = await crypto.sha256(dak);
			const encryptedMK = JSON.stringify(await crypto.encryptData(masterKeyHex, dek));
			const encryptedEd = JSON.stringify(
				await crypto.encryptData(keys.edPriv, masterKeyHex),
			);
			const encryptedRsa = JSON.stringify(
				await crypto.encryptData(keys.rsaPriv, masterKeyHex),
			);
			const encryptedPrivateKeys = JSON.stringify({
				ed: JSON.parse(encryptedEd),
				rsa: JSON.parse(encryptedRsa),
			});

			const result = await convex.mutation(api.auth.createUserWithProfile, {
				username: trimmedUsername,
				salt,
				encryptedMK,
				hak,
				publicKey: keys.rsaPub,
				signingKey: keys.edPub,
				encryptedPrivateKeys,
				inviteCode: invite.status === 'valid' ? invite.code : undefined,
			});

			if ('error' in result) {
				throw new Error(result.error);
			}

			const searchKeys = await crypto.deriveAuthKeys(
				password,
				'searchdb-' + trimmedUsername,
			);
			sessionStorage.setItem('searchDbKey', searchKeys.dak);
			sessionStorage.setItem('masterKey', masterKeyHex);
			sessionStorage.setItem('signingKey', keys.edPriv);
			sessionStorage.setItem('privateKey', keys.rsaPriv);
			localStorage.setItem('userId', result.userId);
			localStorage.setItem('username', trimmedUsername);
			localStorage.setItem('publicKey', keys.rsaPub);

			if (session) {
				try {
					await session.save({
						userId: result.userId,
						username: trimmedUsername,
						publicKey: keys.rsaPub,
						signingKey: keys.edPriv,
						privateKey: keys.rsaPriv,
						masterKey: masterKeyHex,
						searchDbKey: searchKeys.dak,
						savedAt: Date.now(),
					});
				} catch (err) {
					console.warn('Session persistence unavailable:', err);
				}
			}

			// Upload the decoded invite channel keys. The keys map comes
			// from the validated invite state (either from direct input
			// or from InviteAcceptPage via sessionStorage).
			const inviteKeys =
				invite.status === 'valid' ? invite.keys : null;
			try {
				if (inviteKeys && Object.keys(inviteKeys).length > 0) {
					const batch: Array<{
						channelId: string;
						userId: string;
						encryptedKeyBundle: string;
						keyVersion: number;
					}> = [];
					for (const [channelId, keyHex] of Object.entries(inviteKeys)) {
						if (!keyHex) continue;
						try {
							const payload = JSON.stringify({ [channelId]: keyHex });
							const encryptedKeyBundle = await crypto.publicEncrypt(
								keys.rsaPub,
								payload,
							);
							batch.push({
								channelId,
								userId: result.userId,
								encryptedKeyBundle,
								keyVersion: 1,
							});
						} catch (err) {
							console.error(
								'Failed to encrypt invite key for channel',
								channelId,
								err,
							);
						}
					}
					if (batch.length > 0) {
						await convex.mutation(api.channelKeys.uploadKeys, {
							keys: batch as any,
						});
					}
				}
			} catch (err) {
				console.error('Failed to import invite keys:', err);
			} finally {
				sessionStorage.removeItem('pendingInviteKeys');
				sessionStorage.removeItem('pendingInviteCode');
			}

			searchCtx?.initialize();
			navigate('/channels/@me');
		} catch (err: any) {
			setError(err?.message || 'Registration failed');
		} finally {
			setIsRegistering(false);
		}
	};

	return (
		<AuthLayout>
			<h1 className={styles.title}>Create an account</h1>

			<form onSubmit={handleSubmit} className={styles.form}>
				<div className={styles.field}>
					<label className={styles.label}>Invite Link or Code</label>
					<input
						type="text"
						className={styles.input}
						value={inviteInput}
						onChange={(e) => setInviteInput(e.target.value)}
						placeholder="Paste your invite link or code"
						required={!isFirstUser}
						disabled={isRegistering}
					/>
					<div
						style={{
							marginTop: 6,
							fontSize: 12,
							minHeight: 18,
							display: 'flex',
							alignItems: 'center',
							gap: 6,
						}}
					>
						{isFirstUser && invite.status === 'idle' && (
							<span style={{ color: 'var(--text-tertiary)' }}>
								No users yet — you'll be the first and become admin.
							</span>
						)}
						{invite.status === 'checking' && (
							<span style={{ color: 'var(--text-tertiary)' }}>
								Checking invite…
							</span>
						)}
						{invite.status === 'valid' && (
							<>
								<CheckCircle
									size={14}
									weight="fill"
									style={{ color: '#3ba55d' }}
								/>
								<span style={{ color: '#3ba55d' }}>
									Invite valid
									{invite.keys && Object.keys(invite.keys).length > 0
										? ` — ${Object.keys(invite.keys).length} channel key(s) will be imported`
										: ''}
								</span>
							</>
						)}
						{invite.status === 'invalid' && (
							<>
								<Warning
									size={14}
									weight="fill"
									style={{ color: 'var(--status-danger, #ed4245)' }}
								/>
								<span style={{ color: 'var(--status-danger, #ed4245)' }}>
									{invite.message}
								</span>
							</>
						)}
					</div>
				</div>

				<div className={styles.field}>
					<label className={styles.label}>Username</label>
					<input
						type="text"
						className={styles.input}
						value={username}
						onChange={(e) => setUsername(e.target.value)}
						placeholder="Choose a username"
						required
						disabled={isRegistering}
					/>
				</div>

				<div className={styles.field}>
					<label className={styles.label}>Password</label>
					<div className={styles.inputWrap}>
						<input
							type={showPassword ? 'text' : 'password'}
							className={`${styles.input} ${styles.inputWithTrailing}`}
							value={password}
							onChange={(e) => setPassword(e.target.value)}
							placeholder={`At least ${MIN_PASSWORD_LENGTH} characters`}
							required
							disabled={isRegistering}
						/>
						<button
							type="button"
							className={styles.trailingIconButton}
							onClick={() => setShowPassword((v) => !v)}
							aria-label={showPassword ? 'Hide password' : 'Show password'}
							tabIndex={-1}
						>
							{showPassword ? (
								<EyeSlash size={18} weight="regular" />
							) : (
								<Eye size={18} weight="regular" />
							)}
						</button>
					</div>
				</div>

				<div className={styles.field}>
					<label className={styles.label}>Confirm Password</label>
					<div className={styles.inputWrap}>
						<input
							type={showConfirmPassword ? 'text' : 'password'}
							className={`${styles.input} ${styles.inputWithTrailing}`}
							value={confirmPassword}
							onChange={(e) => setConfirmPassword(e.target.value)}
							placeholder="Confirm your password"
							required
							disabled={isRegistering}
						/>
						<button
							type="button"
							className={styles.trailingIconButton}
							onClick={() => setShowConfirmPassword((v) => !v)}
							aria-label={
								showConfirmPassword ? 'Hide password' : 'Show password'
							}
							tabIndex={-1}
						>
							{showConfirmPassword ? (
								<EyeSlash size={18} weight="regular" />
							) : (
								<Eye size={18} weight="regular" />
							)}
						</button>
					</div>
				</div>

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

				<button
					type="submit"
					className={styles.submitButton}
					disabled={!canSubmit}
				>
					{isRegistering ? 'Registering…' : 'Register'}
				</button>

				<div className={styles.divider}>
					<hr className={styles.dividerLine} />
					<span className={styles.dividerText}>OR</span>
					<hr className={styles.dividerLine} />
				</div>

				<div className={styles.footer}>
					<span className={styles.footerText}>Already have an account? </span>
					<button
						type="button"
						className={styles.footerLink}
						onClick={() => navigate('/login')}
					>
						Log in
					</button>
				</div>
			</form>
		</AuthLayout>
	);
}

export default RegisterPage;
