<script lang="ts">
	import { onMount } from 'svelte';
	import { goto } from '$app/navigation';
	import { api, ApiError } from '$lib/api';
	import { setAuth } from '$lib/auth';
	import { markGuideSeen } from '$lib/onboarding';
	import { focusTrap } from '$lib/actions/focus-trap';

	// Show which event the guest is joining (USER_JOURNEYS §1). Public, pre-auth.
	let eventName = $state('');
	// The operator's own data notice, if they set one. Usually empty — see the notice block below.
	let privacyNote = $state('');
	let noticeOpen = $state(false);
	onMount(async () => {
		try {
			const ev = await api.get<{ name: string; slug: string; privacy_note?: string }>('/event');
			eventName = ev.name;
			privacyNote = ev.privacy_note?.trim() ?? '';
		} catch {
			// Non-fatal — fall back to the generic heading if the lookup fails.
		}
	});

	let displayName = $state('');
	let error = $state('');
	let loading = $state(false);
	let showPinModal = $state(false);
	let pin = $state('');
	let copied = $state(false);

	// Name-taken state — shown instead of the normal form
	let nameTaken = $state(false);
	let takenName = $state('');
	let recoveryPin = $state('');
	let recoveryError = $state('');
	let recoveryLoading = $state(false);
	// Forgot-PIN request state (asks a host to reset it).
	let pinRequestSent = $state(false);
	let pinRequestLoading = $state(false);

	/**
	 * Stable idempotency key for THIS join attempt, surviving a reload or a PWA relaunch.
	 *
	 * The failure it closes: `/join` commits the account and the PIN hash, but the plaintext PIN
	 * only ever exists in the response body. Lose that response — the 5G-to-nothing handoff every
	 * venue car park has — and the retry used to 409 on the guest's own name, leaving them staring
	 * at a PIN prompt for a PIN nobody had ever seen. With a key the server recognises the retry
	 * and answers with a working PIN (it rotates it; see migration 027).
	 *
	 * Kept in localStorage rather than component state because the guest's instinctive response to
	 * a hung request is to reload the page, which would otherwise mint a fresh key and re-create
	 * the exact bug. Cleared once the join has demonstrably landed.
	 */
	const JOIN_KEY_STORAGE = 'eventsnap:join-attempt-id';
	function joinAttemptId(): string {
		let id: string | null = null;
		try {
			id = localStorage.getItem(JOIN_KEY_STORAGE);
		} catch {
			// Private mode / storage disabled. A per-call UUID is still better than none: it makes
			// a retry within this page view idempotent, which is the common case.
		}
		if (!id) {
			id = crypto.randomUUID();
			try {
				localStorage.setItem(JOIN_KEY_STORAGE, id);
			} catch {
				/* best effort */
			}
		}
		return id;
	}

	async function handleJoin() {
		if (!displayName.trim()) return;
		loading = true;
		error = '';
		try {
			const res = await api.post<{
				jwt: string;
				pin: string;
				user_id: string;
				is_new: boolean;
			}>('/join', {
				display_name: displayName.trim(),
				client_join_id: joinAttemptId()
			});

			setAuth(res.jwt, res.pin, res.user_id, displayName.trim());
			// The join landed and we hold the PIN — retire the key so a later deliberate join (a
			// second guest on a shared device) is a new attempt rather than a retry of this one.
			try {
				localStorage.removeItem(JOIN_KEY_STORAGE);
			} catch {
				/* best effort */
			}
			pin = res.pin;
			showPinModal = true;
		} catch (e) {
			if (e instanceof ApiError && e.code === 'conflict') {
				takenName = displayName.trim();
				nameTaken = true;
			} else if (e instanceof ApiError) {
				error = e.message;
			} else {
				error = 'Ein Fehler ist aufgetreten.';
			}
		} finally {
			loading = false;
		}
	}

	async function handleInlineRecover() {
		if (recoveryPin.length < 4) return;
		recoveryLoading = true;
		recoveryError = '';
		try {
			const res = await api.post<{ jwt: string; user_id: string }>('/recover', {
				display_name: takenName,
				pin: recoveryPin.trim()
			});
			setAuth(res.jwt, recoveryPin.trim(), res.user_id, takenName);
			// Same as the dedicated /recover page: this is an EXISTING guest reclaiming their
			// account, not a first-timer, so don't replay the onboarding guide at them.
			markGuideSeen();
			goto('/feed');
		} catch (e) {
			if (e instanceof ApiError) {
				recoveryError = e.message;
			} else {
				recoveryError = 'Ein Fehler ist aufgetreten.';
			}
		} finally {
			recoveryLoading = false;
		}
	}

	function tryDifferentName() {
		nameTaken = false;
		recoveryPin = '';
		recoveryError = '';
		pinRequestSent = false;
		// Keep displayName so the user can edit it slightly
	}

	// Forgot the PIN entirely: ask a host to reset it. The endpoint always 204s (no name
	// enumeration), so we optimistically show a confirmation regardless.
	async function requestPinReset() {
		pinRequestLoading = true;
		try {
			await api.post('/recover/request', { display_name: takenName });
		} catch {
			// Non-fatal (rate limit etc.) — still show the confirmation so the user isn't stuck.
		} finally {
			pinRequestLoading = false;
			pinRequestSent = true;
		}
	}

	function copyPin() {
		navigator.clipboard.writeText(pin);
		copied = true;
		setTimeout(() => (copied = false), 2000);
	}

	function goToFeed() {
		goto('/feed');
	}

	function closePinModal() {
		// setAuth has already run on join success — the user is authenticated.
		// Closing the PIN reminder while leaving them on /join would render the
		// (already-completed) join form again. Honor the dismissal by routing
		// them where they actually want to be.
		showPinModal = false;
		goto('/feed');
	}

	// Strip non-digits synchronously in the input handler so paste of "1234X"
	// never flashes the longer string. Auto-submits on the 4th digit so the
	// user doesn't have to chase the (now cosmetic) Anmelden button.
	function onRecoveryPinInput(e: Event) {
		const el = e.currentTarget as HTMLInputElement;
		const cleaned = el.value.replace(/\D/g, '').slice(0, 4);
		if (cleaned !== el.value) el.value = cleaned;
		recoveryPin = cleaned;
		if (recoveryPin.length === 4 && !recoveryLoading) {
			handleInlineRecover();
		}
	}
</script>

<div
	class="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-b from-primary-50 via-gray-50 to-gray-50 px-4 py-10 dark:from-gray-900 dark:via-gray-950 dark:to-gray-950"
>
	<!-- Soft celebratory glow behind the card — a champagne-gold and a silver wash
	     for the classic warm/cool wedding tension, kept subtle. -->
	<div
		aria-hidden="true"
		class="pointer-events-none absolute -top-20 h-72 w-72 rounded-full bg-primary-200/30 blur-3xl dark:bg-primary-900/25"
	></div>
	<div
		aria-hidden="true"
		class="pointer-events-none absolute -bottom-24 right-0 h-72 w-72 rounded-full bg-gray-200/50 blur-3xl dark:bg-gray-800/40"
	></div>

	<div class="relative w-full max-w-sm">
		<!-- Brand -->
		<div class="mb-7 flex flex-col items-center text-center">
			<div
				class="mb-3 flex h-16 w-16 items-center justify-center rounded-2xl bg-blue-600 text-white shadow-lg shadow-primary-600/30"
			>
				<svg class="h-8 w-8" viewBox="0 0 24 24" fill="none" aria-hidden="true">
					<path
						d="M4 8.5A2.5 2.5 0 0 1 6.5 6h1.2c.5 0 .95-.28 1.17-.72l.42-.85A1.5 1.5 0 0 1 10.9 3.6h2.2c.57 0 1.09.32 1.34.83l.42.85c.22.44.67.72 1.17.72h1.2A2.5 2.5 0 0 1 20 8.5v7A2.5 2.5 0 0 1 17.5 18h-11A2.5 2.5 0 0 1 4 15.5v-7Z"
						stroke="currentColor"
						stroke-width="1.6"
					/>
					<circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.6" />
				</svg>
			</div>
			<span
				class="font-display text-2xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
				>EventSnap</span
			>
		</div>

		<!-- Invitation card -->
		<div class="card p-6 shadow-xl">
			{#if nameTaken}
				<!-- Name-taken state: sign in with PIN or choose a different name -->
				<div
					class="mb-5 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800/60 dark:bg-amber-950/30"
				>
					<p class="font-semibold text-amber-900 dark:text-amber-200">
						„{takenName}“ ist bereits vergeben.
					</p>
					<p class="mt-1 text-sm text-amber-800 dark:text-amber-300/90">
						Wähle einen anderen Namen, z.&nbsp;B. einen Spitznamen oder füge deinen Nachnamen hinzu
						(„{takenName}
						M.“ oder „{takenName} aus Berlin“).
					</p>
				</div>

				<p class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
					Falls du das bist, melde dich mit deinem PIN an:
				</p>

				<form
					onsubmit={(e) => {
						e.preventDefault();
						handleInlineRecover();
					}}
				>
					<input
						type="text"
						value={recoveryPin}
						oninput={onRecoveryPinInput}
						placeholder="4-stelliger PIN"
						maxlength={4}
						inputmode="numeric"
						pattern="[0-9]*"
						data-testid="recovery-pin-input"
						class="input mb-3 text-center font-mono text-2xl tracking-widest"
					/>

					{#if recoveryError}
						<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="recovery-error">
							{recoveryError}
						</p>
					{/if}

					<button
						type="submit"
						disabled={recoveryLoading || recoveryPin.length < 4}
						data-testid="recovery-submit"
						class="btn btn-primary btn-block mb-3"
					>
						{recoveryLoading ? 'Wird angemeldet...' : 'Anmelden'}
					</button>
				</form>

				<button
					onclick={tryDifferentName}
					data-testid="try-different-name"
					class="btn btn-secondary btn-block"
				>
					Anderen Namen wählen
				</button>

				<!-- Forgot the PIN entirely — ask a host to reset it in-app. -->
				{#if pinRequestSent}
					<p
						class="mt-3 rounded-lg bg-green-50 px-4 py-3 text-center text-sm text-green-700 dark:bg-green-950/30 dark:text-green-300"
					>
						Anfrage gesendet. Bitte einen Host, deine PIN zurückzusetzen — danach kannst du dich mit
						der neuen PIN anmelden.
					</p>
				{:else}
					<button
						onclick={requestPinReset}
						disabled={pinRequestLoading}
						data-testid="request-pin-reset"
						class="mt-3 w-full text-center text-sm text-blue-600 underline decoration-dotted underline-offset-2 hover:text-blue-700 disabled:opacity-50 dark:text-blue-400"
					>
						{pinRequestLoading ? 'Wird gesendet…' : 'PIN vergessen? Host um Zurücksetzen bitten'}
					</button>
				{/if}
			{:else}
				<!-- Normal join form -->
				<p class="mb-1 text-center text-sm font-medium text-gray-500 dark:text-gray-400">
					Willkommen bei
				</p>
				{#if eventName}
					<h1
						class="mb-3 text-center text-3xl font-semibold text-gray-900 dark:text-gray-100"
						data-testid="join-event-name"
					>
						{eventName}
					</h1>
				{:else}
					<h1 class="mb-3 text-center text-3xl font-semibold text-gray-900 dark:text-gray-100">
						dem Event
					</h1>
				{/if}
				<p class="mb-6 text-center text-gray-600 dark:text-gray-400">
					Gib deinen Namen ein, um Fotos zu teilen und die Galerie zu sehen.
				</p>

				<form
					onsubmit={(e) => {
						e.preventDefault();
						handleJoin();
					}}
				>
					<input
						type="text"
						bind:value={displayName}
						placeholder="Dein Name"
						maxlength={50}
						data-testid="join-name-input"
						class="input mb-3 text-lg"
					/>

					{#if error}
						<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="join-error">
							{error}
						</p>
					{/if}

					<button
						type="submit"
						disabled={loading || !displayName.trim()}
						data-testid="join-submit"
						class="btn btn-primary btn-lg btn-block"
					>
						{loading ? 'Wird geladen...' : 'Beitreten'}
					</button>
				</form>

				<!--
					Data notice AT THE POINT OF COLLECTION.

					There was none at all — not on this page, not anywhere pre-auth — while
					PROJECT.md:407 claimed there was. For ~100 EU guests uploading photos of
					identifiable people, including children, that was the most consequential gap in
					the whole audit, and the least work to close.

					The baseline text below is hardcoded rather than read from `privacy_note`,
					because `privacy_note` defaults to '' (migration 009) — a notice an operator can
					leave blank is not a notice. The operator's own text is shown IN ADDITION when
					they have set one.

					Summary visible without a tap (that is the part that has to be unmissable);
					detail behind a disclosure so it does not bury the one field on the page.
				-->
				<div class="mt-5 border-t border-gray-200 pt-4 dark:border-gray-700">
					<p class="text-xs leading-relaxed text-gray-600 dark:text-gray-400">
						Mit dem Beitreten legst du ein Konto mit deinem Namen an. Deine Fotos, Kommentare und
						dein Name sind für alle Gäste dieses Events sichtbar.
					</p>
					<button
						type="button"
						onclick={() => (noticeOpen = !noticeOpen)}
						data-testid="join-privacy-toggle"
						aria-expanded={noticeOpen}
						class="mt-1 text-xs font-medium text-blue-600 hover:underline dark:text-blue-400"
					>
						{noticeOpen ? 'Weniger anzeigen' : 'Was passiert mit meinen Daten?'}
					</button>
					{#if noticeOpen}
						<div
							class="mt-2 space-y-2 text-xs leading-relaxed text-gray-600 dark:text-gray-400"
							data-testid="join-privacy-note"
						>
							<p>
								<strong class="text-gray-800 dark:text-gray-200">Was gespeichert wird:</strong> dein angezeigter
								Name, deine hochgeladenen Fotos und Videos samt Aufnahmezeitpunkt, deine Bildtexte, Kommentare
								und Likes. Dazu ein verschlüsselter Prüfwert deines PINs — der PIN selbst wird nicht gespeichert.
							</p>
							<p>
								<strong class="text-gray-800 dark:text-gray-200">Wer es sehen kann:</strong> alle Gäste
								dieses Events. Die Gastgeber können außerdem Beiträge und Kommentare entfernen. Am Ende
								erhalten die Gastgeber ein Archiv mit allen Fotos.
							</p>
							<p>
								<strong class="text-gray-800 dark:text-gray-200">Wie lange:</strong> bis die Gastgeber
								das Event abschließen und die Installation abbauen. Du kannst eigene Fotos jederzeit selbst
								löschen, und über „Mein Konto“ dein Konto samt aller Inhalte entfernen lassen.
							</p>
							<p>
								Lade bitte keine Fotos von Personen hoch, die damit nicht einverstanden sind — bei
								Kindern brauchst du das Einverständnis der Eltern.
							</p>
							{#if privacyNote}
								<p class="border-t border-gray-200 pt-2 dark:border-gray-700">
									<strong class="text-gray-800 dark:text-gray-200">Hinweis der Gastgeber:</strong>
									{privacyNote}
								</p>
							{/if}
						</div>
					{/if}
				</div>

				<p class="mt-4 text-center text-sm">
					<a
						href="/recover"
						data-testid="link-to-recover"
						class="text-blue-600 hover:underline dark:text-blue-400"
						>Ich habe bereits einen Account</a
					>
				</p>
			{/if}
		</div>
	</div>
</div>

{#if showPinModal}
	<div
		class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
		data-testid="pin-modal"
	>
		<!-- Vertically centred, so anything taller than the viewport is clipped equally at both
		     ends — and this card is ~320 px, which a phone in landscape does not have. The
		     casualty would be "Weiter zur Galerie" at the very moment a first-time guest has to
		     get past it, with no visible scrollbar to suggest there is more. Cap the height and
		     scroll; `overscroll-contain` stops the join page behind from scrolling instead. -->
		<div
			class="card max-h-full w-full max-w-sm overflow-y-auto overscroll-contain p-6 shadow-xl"
			role="dialog"
			aria-modal="true"
			aria-labelledby="pin-modal-title"
			use:focusTrap={{ onclose: closePinModal }}
		>
			<h2 id="pin-modal-title" class="mb-2 text-xl font-bold text-gray-900 dark:text-gray-100">
				Dein Wiederherstellungs-PIN
			</h2>
			<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
				Merke dir diesen PIN! Du brauchst ihn, um dein Konto auf einem anderen Gerät
				wiederherzustellen.
			</p>

			<div class="surface-muted mb-4 flex items-center justify-center gap-3 p-4">
				<span
					class="font-mono text-4xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
					data-testid="pin-display">{pin}</span
				>
				<button onclick={copyPin} data-testid="pin-copy" class="btn btn-secondary btn-sm min-h-11">
					{copied ? 'Kopiert!' : 'Kopieren'}
				</button>
			</div>

			<button
				onclick={goToFeed}
				data-testid="continue-to-feed"
				class="btn btn-primary btn-block mb-2"
			>
				Weiter zur Galerie
			</button>
			<button
				type="button"
				onclick={closePinModal}
				class="btn btn-ghost btn-block text-sm text-gray-500 dark:text-gray-400"
			>
				Schließen
			</button>
		</div>
	</div>
{/if}
