<script lang="ts">
	import { goto } from '$app/navigation';
	import { getToken } from '$lib/auth';
	import { isStaff } from '$lib/role-store';
	import { addToQueue, loadQueue } from '$lib/upload-queue';
	import { toast } from '$lib/toast-store';
	import { showBottomNav } from '$lib/ui-store';
	import { pendingFiles, pendingCaption, clearPending } from '$lib/pending-upload-store';
	import { get } from 'svelte/store';
	import { onMount, onDestroy } from 'svelte';
	import { quotaStore, refreshQuota } from '$lib/quota-store';
	import ConfirmSheet from '$lib/components/ConfirmSheet.svelte';
	import UploadQueue from '$lib/components/UploadQueue.svelte';
	import IconButton from '$lib/components/IconButton.svelte';
	import { vibrate } from '$lib/haptics';
	import type { PendingFile } from '$lib/pending-upload-store';

	// StagedFile is just PendingFile under a domain-specific name (previewUrl + file).
	type StagedFile = PendingFile;

	let stagedFiles = $state<StagedFile[]>([]);
	let caption = $state('');
	let submitting = $state(false);
	let captionEl: HTMLTextAreaElement;
	let discardConfirmOpen = $state(false);

	const MAX_CAPTION_LENGTH = 2000;

	// Mirrors MAX_UPLOAD_BYTES in backend/src/main.rs — the axum body limit, which is a
	// BOOT CONSTANT rather than an admin-tunable value, so checking it here cannot drift
	// out of sync with the dashboard the way max_image_size_mb / max_video_size_mb would.
	// Anything above this is refused by the server no matter how the event is configured.
	const HARD_MAX_UPLOAD_BYTES = 576 * 1024 * 1024;

	/**
	 * Reject files the server is certain to refuse, BEFORE any bytes leave the phone.
	 *
	 * Without this a guest pushes the whole file over the venue wifi — saturating the AP
	 * for everyone else — only to get a German error at the end. Two cases are worth
	 * catching, and only two: both are certain rejections, so this can never over-reject
	 * a file the server would have taken.
	 *
	 * HEIC/HEIF: deliberately excluded from the backend allowlist (neither the `image`
	 * crate nor the bundled ffmpeg can decode them). iOS transcodes HEIC→JPEG when a photo
	 * comes from the Photos picker, but NOT via the Files app or a third-party share sheet,
	 * so this path is reachable in normal guest use.
	 */
	function uploadRejectReason(file: File): string | null {
		const type = file.type.toLowerCase();
		// Trust the MIME type. The filename is only consulted when the browser supplied no
		// MIME at all: iOS transcodes HEIC→JPEG for the Photos picker but can hand back a
		// File still NAMED *.HEIC, so an extension-based rule would reject good JPEGs — the
		// one thing this function must never do.
		const heicByName = type === '' && /\.hei[cf]$/.test(file.name.toLowerCase());
		if (type === 'image/heic' || type === 'image/heif' || heicByName) {
			return `„${file.name}“ ist ein HEIC-Bild und kann nicht verarbeitet werden. Bitte teile es aus der Fotos-App (dann wandelt iOS es automatisch in JPEG um).`;
		}
		if (file.size > HARD_MAX_UPLOAD_BYTES) {
			const mb = Math.round(file.size / (1024 * 1024));
			return `„${file.name}“ ist mit ${mb} MB zu groß. Bitte nimm einen kürzeren Clip auf.`;
		}
		return null;
	}

	// The storage widget is staff-only (host/admin). Guests never see server-derived
	// storage figures — the backend also zeroes the raw-disk fields for non-staff, so
	// this is UI-consistency on top of an API guarantee, not the security boundary.

	// Quick-tag chips derived from caption as the user types
	let captionTags = $derived.by(() => {
		const matches = [...caption.matchAll(/#(\w+)/g)];
		return [...new Set(matches.map((m) => m[1].toLowerCase()))];
	});

	onMount(() => {
		showBottomNav.set(false);
		if (!getToken()) {
			goto('/join');
			return;
		}
		loadQueue();
		void refreshQuota();

		// Pull staged files from the pending store (written by UploadSheet)
		const pf = get(pendingFiles);
		const pc = get(pendingCaption);
		stagedFiles = pf;
		caption = pc;

		// Auto-focus caption textarea after a short delay (let layout settle)
		setTimeout(() => captionEl?.focus(), 80);

		// Warn before a hard browser navigation (close / reload / external link) drops
		// staged files or an unsent caption. In-app navigation is already guarded by
		// the discard ConfirmSheet in cancel().
		const handleBeforeUnload = (e: BeforeUnloadEvent) => {
			if (stagedFiles.length > 0 || caption.trim().length > 0) {
				e.preventDefault();
				e.returnValue = '';
			}
		};
		window.addEventListener('beforeunload', handleBeforeUnload);

		return () => {
			window.removeEventListener('beforeunload', handleBeforeUnload);
		};
	});

	onDestroy(() => {
		showBottomNav.set(true);
		// Revoke any staged preview blob URLs on leave so they don't leak. The queue
		// holds the underlying File objects, so this is safe after submit too.
		for (const sf of stagedFiles) URL.revokeObjectURL(sf.previewUrl);
	});

	function removeFile(idx: number) {
		const removed = stagedFiles[idx];
		URL.revokeObjectURL(removed.previewUrl);
		stagedFiles = stagedFiles.filter((_, i) => i !== idx);
	}

	function cancel() {
		if (stagedFiles.length > 0 || caption.trim().length > 0) {
			discardConfirmOpen = true;
			return;
		}
		clearPending();
		goto('/feed');
	}

	function confirmDiscard() {
		discardConfirmOpen = false;
		clearPending();
		goto('/feed');
	}

	async function handleSubmit() {
		if (stagedFiles.length === 0 || submitting) return;
		if (caption.length > MAX_CAPTION_LENGTH) return;
		submitting = true;
		vibrate(10);
		const hashtagsString = captionTags.join(',');
		let full = 0;
		// `addToQueue` touches IndexedDB, which can reject outright (iOS private mode, a denied
		// quota) — and an unhandled throw here left BOTH buttons stuck on "Wird hochgeladen…"
		// with no error and no way forward, because `submitting` was never reset. Whatever
		// happens, the composer has to come back to life and say something in German.
		try {
			for (const sf of stagedFiles) {
				// Refuse certain-rejects before a single byte is sent (see uploadRejectReason).
				const reason = uploadRejectReason(sf.file);
				if (reason) {
					toast(reason, 'error', 8000);
					continue;
				}
				const result = await addToQueue(sf.file, caption, hashtagsString);
				if (result === 'full') full++;
			}
			// Don't let a full queue silently swallow photos the user thinks were queued.
			if (full > 0) {
				toast(
					`Warteschlange voll – ${full} ${full === 1 ? 'Foto' : 'Fotos'} nicht hinzugefügt. Bitte warte, bis laufende Uploads fertig sind.`,
					'error',
					6000
				);
			}
			clearPending();
			goto('/feed');
		} catch {
			// Staged files are deliberately NOT cleared: the guest stays in the composer with
			// their selection intact so a second tap can succeed.
			toast(
				'Der Upload konnte nicht gestartet werden. Bitte versuch es noch einmal.',
				'error',
				6000
			);
		} finally {
			submitting = false;
		}
	}

	function isVideo(file: File): boolean {
		return file.type.startsWith('video/');
	}

	function formatBytes(bytes: number | null | undefined): string {
		if (bytes == null || bytes <= 0) return '0 MB';
		const mb = bytes / (1024 * 1024);
		if (mb < 1024) return `${mb.toFixed(mb < 10 ? 1 : 0)} MB`;
		return `${(mb / 1024).toFixed(1)} GB`;
	}

	const totalStagedBytes = $derived(stagedFiles.reduce((sum, sf) => sum + sf.file.size, 0));
	const quotaPercent = $derived(
		$quotaStore.limit_bytes && $quotaStore.limit_bytes > 0
			? Math.min(100, (($quotaStore.used_bytes + totalStagedBytes) / $quotaStore.limit_bytes) * 100)
			: 0
	);
</script>

<!-- Full-screen composer — bottom nav is suppressed -->
<div class="flex min-h-screen flex-col bg-white dark:bg-gray-950">
	<!-- Header -->
	<div
		class="flex items-center justify-between border-b border-gray-100 px-4 py-3 pt-[calc(env(safe-area-inset-top)+0.75rem)] dark:border-gray-800"
	>
		<IconButton label="Abbrechen" onclick={cancel}>
			<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="M6 18 18 6M6 6l12 12" />
			</svg>
		</IconButton>
		<h1 class="text-base font-semibold text-gray-900 dark:text-gray-100">Neuer Beitrag</h1>
		<!-- Submit button in header for desktop convenience -->
		<button
			onclick={handleSubmit}
			disabled={stagedFiles.length === 0 || submitting}
			data-testid="upload-submit-header"
			class="btn btn-primary btn-sm"
		>
			{submitting ? 'Wird hochgeladen…' : 'Hochladen'}
		</button>
	</div>

	<div class="flex flex-1 flex-col overflow-y-auto">
		<!-- Thumbnail strip -->
		{#if stagedFiles.length > 0}
			<div class="flex gap-2 overflow-x-auto px-4 py-3 scrollbar-none">
				{#each stagedFiles as sf, i (sf.previewUrl)}
					<div
						class="relative h-20 w-20 shrink-0 overflow-hidden rounded-xl bg-gray-100 dark:bg-gray-800"
					>
						{#if isVideo(sf.file)}
							<div
								class="flex h-full w-full items-center justify-center bg-gray-800 dark:bg-gray-700"
							>
								<svg class="h-7 w-7 text-white/70" fill="currentColor" viewBox="0 0 24 24">
									<path d="M8 5v14l11-7z" />
								</svg>
							</div>
						{:else}
							<img src={sf.previewUrl} alt="" class="h-full w-full object-cover" />
						{/if}
						<button
							onclick={() => removeFile(i)}
							class="absolute right-1 top-1 flex h-8 w-8 items-center justify-center rounded-full bg-black/60 text-white"
							aria-label="Entfernen"
						>
							<svg
								class="h-4 w-4"
								fill="none"
								viewBox="0 0 24 24"
								stroke="currentColor"
								stroke-width="3"
							>
								<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
							</svg>
						</button>
					</div>
				{/each}
			</div>
			<div class="border-b border-gray-100 dark:border-gray-800"></div>
		{:else}
			<!-- No files: prompt to go back and pick some -->
			<div class="flex flex-1 flex-col items-center justify-center gap-4 p-8 text-center">
				<div
					class="flex h-20 w-20 items-center justify-center rounded-2xl bg-primary-50 dark:bg-primary-900/20"
				>
					<svg
						class="h-9 w-9 text-primary-500 dark:text-primary-500"
						fill="none"
						viewBox="0 0 24 24"
						stroke="currentColor"
						stroke-width="1.5"
					>
						<path
							stroke-linecap="round"
							stroke-linejoin="round"
							d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M9 9.75h.008v.008H9V9.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
						/>
					</svg>
				</div>
				<div>
					<p class="font-medium text-gray-500 dark:text-gray-400">Keine Dateien ausgewählt</p>
					<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">
						Geh zurück und tippe auf den Kamera-Button.
					</p>
				</div>
				<button onclick={cancel} class="btn btn-secondary btn-sm"> Zurück </button>
			</div>
		{/if}

		<!-- Caption textarea -->
		<div class="px-4 pt-4">
			<textarea
				bind:this={captionEl}
				bind:value={caption}
				maxlength={MAX_CAPTION_LENGTH}
				data-testid="upload-caption"
				placeholder="Beschreibung hinzufügen… (#hashtags möglich)"
				rows="4"
				class="input resize-none text-sm"
			></textarea>
			<div class="mt-1 text-xs text-gray-500 text-right dark:text-gray-400">
				{caption.length} / {MAX_CAPTION_LENGTH}
			</div>
		</div>

		<!-- Quick-tag chips (derived from typed caption) -->
		{#if captionTags.length > 0}
			<div class="flex flex-wrap gap-1.5 px-4 pt-2">
				{#each captionTags as tag (tag)}
					<span
						class="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-blue-600 dark:bg-blue-950/40 dark:text-blue-300"
					>
						#{tag}
					</span>
				{/each}
			</div>
		{/if}

		<!-- Per-user quota — staff-only (never shown to guests), and also hidden when
			 admin disabled enforcement. -->
		{#if $isStaff && $quotaStore.enabled && $quotaStore.limit_bytes != null}
			<div class="px-4 pt-3 text-xs text-gray-500 dark:text-gray-400">
				<div class="flex items-center justify-between">
					<span
						>Speicher: {formatBytes($quotaStore.used_bytes + totalStagedBytes)} / {formatBytes(
							$quotaStore.limit_bytes
						)}</span
					>
					<span
						class:text-amber-600={quotaPercent >= 80}
						class:dark:text-amber-400={quotaPercent >= 80}
						class:text-red-600={quotaPercent >= 95}
						class:dark:text-red-400={quotaPercent >= 95}
					>
						{Math.round(quotaPercent)}%
					</span>
				</div>
				<div class="mt-1 h-1.5 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
					<div
						class="h-full transition-all"
						class:bg-blue-500={quotaPercent < 80}
						class:bg-amber-500={quotaPercent >= 80 && quotaPercent < 95}
						class:bg-red-500={quotaPercent >= 95}
						style="width: {quotaPercent}%"
					></div>
				</div>
				{#if quotaPercent >= 100}
					<p class="mt-1 font-medium text-red-600 dark:text-red-400">
						Limit erreicht — bitte alte Beiträge löschen.
					</p>
				{:else if quotaPercent >= 95}
					<p class="mt-1 font-medium text-amber-600 dark:text-amber-400">Fast voll.</p>
				{/if}
			</div>
		{/if}

		<div class="h-8"></div>
	</div>

	<!-- Discard confirmation — appears only when the composer has unsaved content. -->
	<ConfirmSheet
		open={discardConfirmOpen}
		title="Verwerfen?"
		message="Deine Auswahl und der Text gehen verloren."
		confirmLabel="Verwerfen"
		cancelLabel="Weiter bearbeiten"
		tone="danger"
		onConfirm={confirmDiscard}
		onCancel={() => (discardConfirmOpen = false)}
	/>

	<!-- Sticky submit button at bottom (mobile-primary). This page suppresses the bottom nav, so
	     this bar IS the bottom of the layout and has to carry the safe-area inset itself — with
	     viewport-fit=cover the lower ~22px of the button otherwise sits in the iPhone home
	     indicator's gesture zone, where the first tap is swallowed by the system. -->
	<div
		class="border-t border-gray-100 px-4 py-3 pb-[calc(env(safe-area-inset-bottom)+0.75rem)] dark:border-gray-800"
	>
		<button
			onclick={handleSubmit}
			disabled={stagedFiles.length === 0 || submitting}
			data-testid="upload-submit"
			class="btn btn-primary btn-block"
		>
			{#if submitting}
				<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
					<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
					></circle>
					<path
						class="opacity-75"
						fill="currentColor"
						d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
					></path>
				</svg>
				Wird hochgeladen…
			{:else}
				<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
					/>
				</svg>
				{stagedFiles.length > 0
					? `${stagedFiles.length} Datei${stagedFiles.length > 1 ? 'en' : ''} hochladen`
					: 'Hochladen'}
			{/if}
		</button>

		<!--
			The queue list. This component existed, complete with the per-item error text, the
			"Erneut" retry button and the rate-limit countdown — and was never imported anywhere,
			so none of it could be reached. A rejected upload wrote a clear reason into a store
			nothing rendered.
		-->
		<UploadQueue />
	</div>
</div>
