<script lang="ts">
	import { goto } from '$app/navigation';
	import { getToken } from '$lib/auth';
	import { api } from '$lib/api';
	import { onMount, onDestroy } from 'svelte';
	import { connectSse, disconnectSse, onSseEvent } from '$lib/sse';
	import { toast, toastError } from '$lib/toast-store';
	import { focusTrap } from '$lib/actions/focus-trap';
	import IconButton from '$lib/components/IconButton.svelte';
	import { commentsEnabled } from '$lib/event-config-store';

	interface JobStatus {
		status: 'locked' | 'pending' | 'running' | 'done' | 'failed';
		progress_pct: number;
	}

	interface ExportStatus {
		released: boolean;
		zip: JobStatus;
		html: JobStatus;
	}

	const HTML_GUIDE_KEY = 'eventsnap_html_guide_seen';

	let status = $state<ExportStatus | null>(null);
	let showHtmlGuide = $state(false);
	let loading = $state(true);
	// A failed status fetch used to leave `status` null, which rendered "Export noch nicht
	// verfügbar / Schau nach der Veranstaltung noch einmal vorbei" — telling a guest whose
	// request timed out to come back after an event that has already happened. Distinguish
	// "we don't know" from "not released yet".
	let statusError = $state(false);
	// Guards against a double-tap minting two single-use tickets (the second would be spent
	// against a rate limit for a download the guest never sees).
	let downloading = $state(false);

	let unsubscribers: (() => void)[] = [];

	onMount(async () => {
		if (!getToken()) {
			goto('/join');
			return;
		}

		await loadStatus();
		connectSse();

		unsubscribers.push(
			onSseEvent('export-progress', async () => {
				await loadStatus();
			}),
			onSseEvent('export-available', async () => {
				await loadStatus();
			}),
			// A reopen invalidates the release (ready flags cleared server-side) — refresh so this
			// page stops offering a download that would now 404.
			onSseEvent('event-opened', async () => {
				await loadStatus();
			})
		);
	});

	onDestroy(() => {
		disconnectSse();
		for (const unsub of unsubscribers) unsub();
	});

	async function loadStatus() {
		try {
			status = await api.get<ExportStatus>('/export/status');
			statusError = false;
		} catch {
			// Background polls triggered by SSE stay silent — but only while we still have a
			// status to show. With nothing on screen the failure has to be visible, or the
			// page lies about the state of the one thing the guest came here for.
			if (!status) statusError = true;
		} finally {
			loading = false;
		}
	}

	async function retryStatus() {
		statusError = false;
		loading = true;
		await loadStatus();
	}

	function statusText(job: JobStatus): string {
		switch (job.status) {
			case 'locked':
				return 'Noch nicht freigegeben';
			case 'pending':
				return 'Wird vorbereitet…';
			case 'running':
				return `Wird erstellt (${job.progress_pct} %)`;
			case 'done':
				return 'Bereit zum Download';
			case 'failed':
				return 'Fehlgeschlagen';
		}
	}

	// Stream the (potentially multi-GB) archive straight to disk instead of fetch()+blob() —
	// buffering the whole ZIP in memory crashes mobile Safari/Chrome. The request can't carry the
	// Bearer header, so we exchange the token for a single-use ticket and pass it as a query param.
	//
	// The navigation targets a HIDDEN IFRAME, not the top-level page. On success the server sends
	// `Content-Disposition: attachment` and the browser downloads it either way — but on an ERROR
	// (404 because a takedown/ban just retired this generation and it's rebuilding, or 429 from the
	// per-IP export rate limit, which everyone on the venue WiFi shares) there is no attachment
	// header, so a top-level navigation would UNLOAD THE APP and dump the raw `{"error":…}` JSON on
	// the guest. Inside an iframe the error is swallowed harmlessly and the PWA stays put.
	//
	// We deliberately don't probe with HEAD first: the ticket is single-use and axum serves HEAD from
	// the GET route, so a probe would consume the ticket and burn a rate-limit slot.
	let downloadFrame: HTMLIFrameElement;

	async function downloadFile(endpoint: string, kind: 'zip' | 'html') {
		if (downloading) return;
		downloading = true;
		try {
			// `kind` makes the mint check that THIS archive exists before charging a slot.
			// `export_status` reports readiness from `export_job`, while the download resolves
			// through `export_current` + the filesystem — different sources of truth that can
			// legitimately disagree (a retired epoch, a file swept from under a done row). When
			// they did, the guest got a green success toast, a consumed single-use ticket, one
			// of three daily slots gone, and nothing in their Downloads. Now the mint fails
			// honestly and lands in the `catch` below.
			const { ticket } = await api.post<{ ticket: string }>(`/export/ticket?kind=${kind}`);
			downloadFrame.src = `${endpoint}?ticket=${encodeURIComponent(ticket)}`;
			// An iframe download produces NO visible change: no spinner, no navigation, and
			// on mobile often no browser chrome either. Without a word here the guest cannot
			// tell a download that started from a button that did nothing — and the archive
			// can take minutes, so they tap again, and again.
			toast('Download gestartet – schau in deine Downloads.', 'success');
			// If the archive was retired between the page render and this click, the download
			// silently does nothing — so re-sync the status to flip the UI to "generating".
			setTimeout(() => void loadStatus(), 1500);
		} catch (e) {
			// The export rate limit is enforced at ticket-mint, so THIS is the only place a
			// guest can be told they've hit the daily download cap. Swallowing it (as the old
			// code did by never inspecting anything) meant every tap after the third did
			// literally nothing, forever, at the emotional payoff of the whole product, with
			// no operator anywhere to explain why.
			toastError(e);
		} finally {
			downloading = false;
		}
	}

	function downloadZip() {
		downloadFile('/api/v1/export/zip', 'zip');
	}

	function downloadHtml() {
		if (localStorage.getItem(HTML_GUIDE_KEY)) {
			downloadFile('/api/v1/export/html', 'html');
		} else {
			showHtmlGuide = true;
		}
	}

	function confirmHtmlDownload() {
		localStorage.setItem(HTML_GUIDE_KEY, '1');
		showHtmlGuide = false;
		downloadFile('/api/v1/export/html', 'html');
	}
</script>

<!-- Download sink. Archive downloads are navigations (they can be multi-GB, so fetch()+blob() would
     OOM mobile browsers), and a navigation to an error response would unload the whole PWA. Pointing
     them at this hidden iframe keeps a 404/429 harmless. -->
<iframe bind:this={downloadFrame} title="Download" class="hidden" aria-hidden="true"></iframe>

<!-- HTML guide modal -->
{#if showHtmlGuide}
	<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
		<div
			class="w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-900"
			role="dialog"
			aria-modal="true"
			aria-labelledby="html-guide-title"
			use:focusTrap={{ onclose: () => (showHtmlGuide = false) }}
		>
			<h2 id="html-guide-title" class="mb-3 text-lg font-bold text-gray-900 dark:text-gray-100">
				Hinweis zum HTML-Viewer
			</h2>
			<ol class="mb-4 space-y-2 text-sm text-gray-700 dark:text-gray-300">
				<li class="flex gap-2">
					<span class="font-bold text-blue-600 dark:text-blue-400">1.</span> ZIP-Datei entpacken (Windows:
					Rechtsklick → "Alle extrahieren"; Mac: Doppelklick).
				</li>
				<li class="flex gap-2">
					<span class="font-bold text-blue-600 dark:text-blue-400">2.</span>
					<strong>index.html</strong> im Browser öffnen.
				</li>
				<li class="flex gap-2">
					<span class="font-bold text-blue-600 dark:text-blue-400">3.</span> Kein Internet nötig — alles
					ist lokal gespeichert.
				</li>
			</ol>
			<p
				class="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:bg-amber-950/30 dark:text-amber-300"
			>
				Tipp: Am besten im WLAN herunterladen — die Datei kann mehrere GB groß sein.
			</p>
			<div class="flex gap-2">
				<button onclick={() => (showHtmlGuide = false)} class="btn btn-secondary flex-1">
					Abbrechen
				</button>
				<button onclick={confirmHtmlDownload} class="btn btn-primary flex-1">
					Herunterladen
				</button>
			</div>
		</div>
	</div>
{/if}

<div class="min-h-screen bg-gray-50 pb-24 dark:bg-gray-950">
	<div
		class="border-b border-gray-200 bg-white pt-[env(safe-area-inset-top)] dark:border-gray-800 dark:bg-gray-900"
	>
		<div class="mx-auto flex max-w-lg items-center gap-2 px-4 py-4">
			<IconButton
				label="Zurück"
				onclick={() => goto('/feed')}
				data-testid="export-back"
				class="-ml-2"
			>
				<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
					<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
				</svg>
			</IconButton>
			<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">Export</h1>
		</div>
	</div>

	<div class="mx-auto max-w-lg space-y-4 p-4">
		{#if loading}
			<div class="py-16 text-center text-gray-400 dark:text-gray-500">Laden…</div>
		{:else if statusError}
			<!-- Ahead of the "noch nicht verfügbar" branch: both leave `status` null, but only
			     one of them is true, and guessing wrong sends the guest away for good. -->
			<div class="card p-6 text-center" data-testid="export-status-error">
				<svg
					class="mx-auto mb-3 h-12 w-12 text-gray-300 dark:text-gray-600"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
					stroke-width="1.5"
					aria-hidden="true"
				>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
					/>
				</svg>
				<p class="font-medium text-gray-700 dark:text-gray-300">Status nicht abrufbar</p>
				<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
					Die Verbindung hat nicht geklappt. Versuch es gleich noch einmal.
				</p>
				<button onclick={() => void retryStatus()} class="btn btn-primary btn-sm mt-4">
					Erneut versuchen
				</button>
			</div>
		{:else if !status?.released}
			<div class="card p-6 text-center">
				<svg
					class="mx-auto mb-3 h-12 w-12 text-gray-300 dark:text-gray-600"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
				>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="1.5"
						d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
					/>
				</svg>
				<p class="font-medium text-gray-700 dark:text-gray-300">Export noch nicht verfügbar</p>
				<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
					Schau nach der Veranstaltung noch einmal vorbei.
				</p>
			</div>
		{:else if status}
			<p class="text-sm text-gray-500 dark:text-gray-400">Wähle dein bevorzugtes Format:</p>

			<!-- ZIP card -->
			<div class="card p-5">
				<div class="flex items-start justify-between gap-4">
					<div class="min-w-0">
						<h2 class="section-title">ZIP-Archiv</h2>
						<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
							Alle Original-Fotos und Videos in strukturierten Ordnern.
						</p>
						<p
							class="mt-1 text-xs {status.zip.status === 'done'
								? 'text-green-600 dark:text-green-400'
								: status.zip.status === 'failed'
									? 'text-red-500 dark:text-red-400'
									: 'text-gray-400 dark:text-gray-500'}"
						>
							{statusText(status.zip)}
						</p>
					</div>
					<button
						onclick={downloadZip}
						disabled={status.zip.status !== 'done'}
						class="btn btn-primary btn-sm shrink-0"
					>
						Download
					</button>
				</div>
				{#if status.zip.status === 'running'}
					<div class="mt-3">
						<div class="h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
							<div
								class="h-full rounded-full bg-blue-500 transition-all"
								style="width: {status.zip.progress_pct}%"
							></div>
						</div>
					</div>
				{/if}
			</div>

			<!-- HTML card -->
			<div class="card p-5">
				<div class="flex items-start justify-between gap-4">
					<div class="min-w-0">
						<h2 class="section-title">HTML-Viewer</h2>
						<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
							Schöne Offline-Galerie mit Filterung{$commentsEnabled ? ', Kommentaren' : ''} und Likes
							— kein Internet nötig.
						</p>
						<p
							class="mt-1 text-xs {status.html.status === 'done'
								? 'text-green-600 dark:text-green-400'
								: status.html.status === 'failed'
									? 'text-red-500 dark:text-red-400'
									: 'text-gray-400 dark:text-gray-500'}"
						>
							{statusText(status.html)}
						</p>
					</div>
					<button
						onclick={downloadHtml}
						disabled={status.html.status !== 'done'}
						class="btn btn-primary btn-sm shrink-0"
					>
						Download
					</button>
				</div>
				{#if status.html.status === 'running'}
					<div class="mt-3">
						<div class="h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
							<div
								class="h-full rounded-full bg-blue-500 transition-all"
								style="width: {status.html.progress_pct}%"
							></div>
						</div>
					</div>
				{/if}
			</div>
		{/if}
	</div>
</div>
