<script lang="ts">
	import { logger } from '$lib/core/utils/logger';

	import { auth } from '$lib/core/stores/auth';
	import { addNotification } from '$lib/core/stores/noti';
	import Button from '$lib/components/ui/button/button.svelte';
	import * as Select from '$lib/components/ui/select/index.js';
	import Badge from '$lib/components/ui/badge/badge.svelte';
	import Spinner from '$lib/components/ui/spinner/spinner.svelte';
	import {
		Upload,
		X,
		CheckCircle,
		AlertCircle,
		RefreshCw,
		Play,
		Video,
		MonitorPlay,
		Eye
	} from '@lucide/svelte/icons';
	import * as adb from '$lib/core/adb/adb';
	import { AdbInstance } from '../../../state.svelte';
	import ScrcpyDialog from '$lib/components/scrcpy-dialog.svelte';

	const UPLOAD_PROXY_ENDPOINT = '/api/adv-upload';
	const LIST_PROXY_ENDPOINT = '/api/adv-list';
	const FILE_PROXY_ENDPOINT = '/api/adv-file';
	const THUMB_PROXY_ENDPOINT = '/api/adv-thumb';
	const ALLOWED_EXTENSIONS = ['.mp4'];

	// ─────────────────────────────────────────────────────────────────────────
	// Slot-based upload. There are 21 fixed slots (00–20). Each slot has a fixed
	// TARGET filename — the source file's own name is ignored; it is renamed to
	// the slot's name (taobin_adv_menu_NN.mp4) right before upload. Every upload
	// asks the backend to regenerate sync_1.file from the FTP listing.
	// ─────────────────────────────────────────────────────────────────────────
	const SLOT_COUNT = 21; // 00 .. 20
	const MENU_SPEC = { width: 1080, height: 380 }; // taobin_adv_menu_* banner size

	function slotFilename(i: number): string {
		return `taobin_adv_menu_${String(i).padStart(2, '0')}.mp4`;
	}

	// Only Thailand is enabled for now (other countries need their SFTP config on
	// the backend before being added).
	const COUNTRIES = [{ value: 'tha', label: 'Thailand (tha)' }];
	let selectedCountry = $state('tha');

	// adv folder on the machine. Domestic Thailand uses the flat folder; every
	// international country uses inter/<country>/adv (matches the on-machine layout).
	const MACHINE_PROJECT_DIR = '/sdcard/coffeevending/taobin_project';
	function machineAdvDir(country: string): string {
		return country === 'tha'
			? `${MACHINE_PROJECT_DIR}/adv`
			: `${MACHINE_PROJECT_DIR}/inter/${country}/adv`;
	}

	interface Slot {
		index: number;
		name: string; // fixed target filename
		file: File | null;
		preview: string | null; // object URL of the chosen file
		status: 'empty' | 'ready' | 'uploading' | 'success' | 'error';
		error?: string;
		width?: number;
		height?: number;
		dimChecked: boolean;
		dimOk: boolean;
	}

	function emptySlot(i: number): Slot {
		return {
			index: i,
			name: slotFilename(i),
			file: null,
			preview: null,
			status: 'empty',
			dimChecked: false,
			dimOk: false
		};
	}

	let slots = $state<Slot[]>(Array.from({ length: SLOT_COUNT }, (_, i) => emptySlot(i)));
	let uploadingAll = $state(false);

	const filledCount = $derived(slots.filter((s) => s.file).length);
	const pendingCount = $derived(slots.filter((s) => s.file && s.status !== 'success').length);
	// Push-to-machine (ADB) — preview the videos on a connected machine.
	let isAdbConnected = $derived(Boolean(AdbInstance.instance));
	let pushingToMachine = $state(false);
	let pushProgress = $state({ current: 0, total: 0, name: '', percent: 0 });
	let connecting = $state(false);

	let showScreenMirror = $state(false);

	const anyBusy = $derived(
		uploadingAll || pushingToMachine || slots.some((s) => s.status === 'uploading')
	);
	// How many of the 21 slot names currently have a video on the server.
	const onServerCount = $derived(
		slots.filter((s) => serverFilesByName[s.name] !== undefined).length
	);

	// Left-accent colour that encodes a slot's state at a glance.
	function slotAccent(s: Slot): string {
		if (s.status === 'success') return 'border-l-green-500';
		if (s.status === 'error') return 'border-l-red-500';
		if (s.file) return 'border-l-primary';
		// No local file yet, but a video already lives on the server for this slot.
		if (serverFilesByName[s.name] !== undefined) return 'border-l-green-500';
		return 'border-l-border';
	}

	async function authHeaders(): Promise<Record<string, string>> {
		const token = await $auth?.getIdToken?.();
		return token ? { Authorization: `Bearer ${token}` } : {};
	}

	function readVideoDimensions(file: File): Promise<{ width: number; height: number }> {
		return new Promise((resolve, reject) => {
			const video = document.createElement('video');
			video.preload = 'metadata';
			const url = URL.createObjectURL(file);
			video.onloadedmetadata = () => {
				URL.revokeObjectURL(url);
				resolve({ width: video.videoWidth, height: video.videoHeight });
			};
			video.onerror = () => {
				URL.revokeObjectURL(url);
				reject(new Error('Cannot read video metadata'));
			};
			video.src = url;
		});
	}

	// Assign a chosen file to a slot. The source name is irrelevant — only .mp4 is
	// required. Dimensions are checked against the banner spec as a soft warning.
	function pickSlotFile(index: number, fileList: FileList | null) {
		const file = fileList?.[0];
		if (!file) return;

		const ext = '.' + (file.name.split('.').pop()?.toLowerCase() ?? '');
		if (!ALLOWED_EXTENSIONS.includes(ext)) {
			addNotification(`WARN:${file.name} - Only .mp4 allowed`);
			return;
		}

		const prev = slots[index];
		if (prev.preview) URL.revokeObjectURL(prev.preview);

		slots[index] = {
			...emptySlot(index),
			file,
			preview: URL.createObjectURL(file),
			status: 'ready'
		};

		readVideoDimensions(file)
			.then(({ width, height }) => {
				if (slots[index].file !== file) return; // slot changed meanwhile
				slots[index].width = width;
				slots[index].height = height;
				slots[index].dimChecked = true;
				slots[index].dimOk = width === MENU_SPEC.width && height === MENU_SPEC.height;
			})
			.catch(() => {
				if (slots[index].file !== file) return;
				slots[index].dimChecked = true;
				slots[index].dimOk = false;
			});
	}

	function clearSlot(index: number) {
		const slot = slots[index];
		if (slot.preview) URL.revokeObjectURL(slot.preview);
		slots[index] = emptySlot(index);
	}

	// Upload a single slot: rename the file to the slot's fixed name, then POST.
	async function uploadSlot(index: number): Promise<boolean> {
		const currentUser = $auth;
		if (!currentUser) {
			addNotification('ERR:Not logged in');
			return false;
		}
		const slot = slots[index];
		if (!slot.file) return false;

		slots[index].status = 'uploading';
		slots[index].error = undefined;
		try {
			// Ignore the source filename — send it under the slot's target name.
			const renamed = new File([slot.file], slot.name, { type: 'video/mp4' });

			const formData = new FormData();
			formData.append('country', selectedCountry);
			formData.append('uid', currentUser.uid);
			formData.append('displayName', currentUser.displayName || 'unknown');
			formData.append('email', currentUser.email || 'unknown@email.com');
			formData.append('file', renamed);
			// regenerate=true → backend rebuilds sync_1.file from the FTP listing.
			formData.append('regenerate', 'true');

			const res = await fetch(UPLOAD_PROXY_ENDPOINT, {
				method: 'POST',
				headers: await authHeaders(),
				body: formData
			});
			if (!res.ok) {
				const e = await res.json().catch(() => ({ detail: res.statusText }));
				throw new Error(e.detail || e.message || 'Upload failed');
			}

			slots[index].status = 'success';
			addNotification(`INFO:Uploaded ${slot.name}`);
			// The server now has (a new) file for this slot — drop the stale preview
			// blob and refresh the indicator so "View current" shows the fresh one.
			if (videoBlobUrls[slot.name]) {
				URL.revokeObjectURL(videoBlobUrls[slot.name]);
				const { [slot.name]: _drop, ...rest } = videoBlobUrls;
				videoBlobUrls = rest;
			}
			if (thumbUrls[slot.name]) {
				URL.revokeObjectURL(thumbUrls[slot.name]);
				const { [slot.name]: _t, ...restThumb } = thumbUrls;
				thumbUrls = restThumb;
			}
			refreshServerIndex();
			return true;
		} catch (error) {
			slots[index].status = 'error';
			slots[index].error = error instanceof Error ? error.message : 'Unknown error';
			logger.error(`[Adv] slot ${index} upload error:`, error);
			return false;
		}
	}

	// Upload every filled slot that hasn't succeeded yet, one at a time.
	async function uploadAllFilled() {
		if (!$auth) {
			addNotification('ERR:Not logged in');
			return;
		}
		const targets = slots.filter((s) => s.file && s.status !== 'success');
		if (targets.length === 0) {
			addNotification('WARN:No slots to upload');
			return;
		}
		uploadingAll = true;
		try {
			let ok = 0;
			let fail = 0;
			for (const slot of targets) {
				const success = await uploadSlot(slot.index);
				success ? ok++ : fail++;
			}
			if (fail === 0) addNotification(`INFO:Uploaded ${ok} slot(s) successfully`);
			else addNotification(`WARN:Uploaded ${ok}, failed ${fail}`);
		} finally {
			uploadingAll = false;
		}
	}

	async function connectMachine() {
		if (AdbInstance.instance) {
			addNotification('INFO:Machine already connected');
			return;
		}
		connecting = true;
		try {
			await adb.connnectViaWebUSB(false);
			addNotification(AdbInstance.instance ? 'INFO:Machine connected' : 'WARN:No machine selected');
		} catch (error) {
			logger.error('[Adv] connect error:', error);
			addNotification(`ERR:Connect failed: ${error instanceof Error ? error.message : 'unknown'}`);
		} finally {
			connecting = false;
		}
	}

	// Push every filled slot's video to the connected machine's adv folder (under
	// the slot's fixed name) so it can be previewed on the machine before upload.
	async function pushToMachine() {
		if (!AdbInstance.instance) {
			addNotification('ERR:Machine not connected (ADB)');
			return;
		}
		const targets = slots.filter((s) => s.file);
		if (targets.length === 0) {
			addNotification('WARN:No slots to push');
			return;
		}
		const targetDir = machineAdvDir(selectedCountry);
		pushingToMachine = true;
		pushProgress = { current: 0, total: targets.length, name: '', percent: 0 };
		let success = 0;
		try {
			for (let i = 0; i < targets.length; i++) {
				const slot = targets[i];
				pushProgress = { current: i, total: targets.length, name: slot.name, percent: 0 };
				const bytes = new Uint8Array(await slot.file!.arrayBuffer());
				const ok = await adb.pushBinary(`${targetDir}/${slot.name}`, bytes, (sent, total) => {
					pushProgress = {
						current: i,
						total: targets.length,
						name: slot.name,
						percent: total > 0 ? Math.round((sent / total) * 100) : 0
					};
				});
				if (ok) {
					success++;
					pushProgress = { current: i + 1, total: targets.length, name: slot.name, percent: 100 };
				} else {
					addNotification(`ERR:Push failed: ${slot.name}`);
				}
			}
			if (success > 0) {
				addNotification(`INFO:Pushed ${success} video(s) to machine (${targetDir})`);
			}
		} catch (error) {
			logger.error('[Adv] push to machine error:', error);
			addNotification(`ERR:Push error: ${error instanceof Error ? error.message : 'unknown'}`);
		} finally {
			pushingToMachine = false;
		}
	}

	// ─────────────────────────────────────────────────────────────────────────
	// Preview adv videos already on the server (FTP).
	// Adv videos live only on the FTP server — no nginx/git copy — so we fetch
	// each one through the auth-guarded proxy as a Blob and play it via an object
	// URL. Lets you verify an upload actually landed correctly.
	// ─────────────────────────────────────────────────────────────────────────
	interface ServerVideo {
		name: string;
		size: number;
		mtime: number;
	}

	let serverVideos = $state<ServerVideo[]>([]);
	let loadingServerVideos = $state(false);
	let serverVideosLoaded = $state(false);
	// name -> object URL (created automatically after the list loads)
	let videoBlobUrls = $state<Record<string, string>>({});
	let loadingVideoName = $state<string | null>(null);
	let videoLoadProgress = $state({ current: 0, total: 0 });

	// Lightweight name→size map of what is on the server RIGHT NOW, used to show a
	// per-slot "on server" indicator (so you can preview the current video before
	// overwriting a slot). Refreshed on mount, on country change, and after upload.
	let serverFilesByName = $state<Record<string, number>>({});
	// The server video currently open in the preview lightbox (or null).
	let viewingName = $state<string | null>(null);
	// name -> object URL of the small JPEG poster (thumbnail) for a slot's video.
	let thumbUrls = $state<Record<string, string>>({});

	function formatBytes(bytes: number): string {
		if (!bytes) return '0 MB';
		return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
	}

	// List-only fetch (no blobs) — cheap, just to know what each slot currently has.
	async function refreshServerIndex() {
		try {
			const res = await fetch(LIST_PROXY_ENDPOINT, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json', ...(await authHeaders()) },
				body: JSON.stringify({ country: selectedCountry })
			});
			if (!res.ok) return;
			const data = await res.json();
			const map: Record<string, number> = {};
			for (const f of (data.files ?? []) as ServerVideo[]) map[f.name] = f.size;
			serverFilesByName = map;
			// Fetch a tiny JPEG poster for each slot video so the grid shows a
			// thumbnail (backend grabs one frame — no full-video download).
			autoLoadSlotThumbs();
		} catch (error) {
			logger.error('[Adv] refresh server index error:', error);
		}
	}

	// Fetch the small JPEG poster for one slot video → object URL for an <img>.
	async function loadSlotThumb(name: string) {
		if (thumbUrls[name]) return;
		try {
			const url = `${THUMB_PROXY_ENDPOINT}?country=${encodeURIComponent(selectedCountry)}&filename=${encodeURIComponent(name)}`;
			const res = await fetch(url, { headers: await authHeaders() });
			if (!res.ok) return;
			const blob = await res.blob();
			thumbUrls = { ...thumbUrls, [name]: URL.createObjectURL(blob) };
		} catch (error) {
			logger.error('[Adv] load thumb error:', error);
		}
	}

	// Load posters for the slot videos on the server (posters are tiny, so a
	// larger concurrency pool is fine).
	async function autoLoadSlotThumbs() {
		const slotNames = new Set(slots.map((s) => s.name));
		const names = Object.keys(serverFilesByName).filter(
			(n) => slotNames.has(n) && !thumbUrls[n]
		);
		if (names.length === 0) return;

		const CONCURRENCY = 5;
		let next = 0;
		async function worker() {
			while (next < names.length) {
				await loadSlotThumb(names[next++]);
			}
		}
		await Promise.all(Array.from({ length: Math.min(CONCURRENCY, names.length) }, worker));
	}

	// Open the current server video for a slot in the lightbox.
	async function viewCurrent(name: string) {
		await playServerVideo(name); // loads the blob if not already
		if (videoBlobUrls[name]) viewingName = name;
	}

	async function loadServerVideos() {
		loadingServerVideos = true;
		try {
			const res = await fetch(LIST_PROXY_ENDPOINT, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json', ...(await authHeaders()) },
				body: JSON.stringify({ country: selectedCountry })
			});
			if (!res.ok) {
				const err = await res.json().catch(() => ({ detail: res.statusText }));
				throw new Error(err.detail || 'Failed to list videos');
			}
			const data = await res.json();
			// Reset any previously loaded blobs (folder/country may have changed).
			for (const url of Object.values(videoBlobUrls)) URL.revokeObjectURL(url);
			videoBlobUrls = {};
			serverVideos = (data.files ?? []) as ServerVideo[];
			serverVideosLoaded = true;
			// Auto-fetch every video so each shows a ready-to-play player without a
			// manual click. Limited concurrency keeps it from hammering the SFTP.
			await autoLoadAllVideos();
		} catch (error) {
			logger.error('[Adv] list server videos error:', error);
			addNotification(
				`ERR:Load videos failed: ${error instanceof Error ? error.message : 'unknown'}`
			);
		} finally {
			loadingServerVideos = false;
		}
	}

	// Fetch every server video's blob with a small concurrency pool (each request
	// opens its own SFTP session on the backend, so we don't want 25 at once).
	async function autoLoadAllVideos() {
		const names = serverVideos.map((v) => v.name).filter((n) => !videoBlobUrls[n]);
		videoLoadProgress = { current: 0, total: names.length };
		if (names.length === 0) return;

		const CONCURRENCY = 3;
		let next = 0;
		async function worker() {
			while (next < names.length) {
				const i = next++;
				await playServerVideo(names[i]);
				videoLoadProgress = { ...videoLoadProgress, current: videoLoadProgress.current + 1 };
			}
		}
		await Promise.all(Array.from({ length: Math.min(CONCURRENCY, names.length) }, worker));
	}

	async function playServerVideo(name: string) {
		if (videoBlobUrls[name]) return; // already loaded
		loadingVideoName = name;
		try {
			const url = `${FILE_PROXY_ENDPOINT}?country=${encodeURIComponent(selectedCountry)}&filename=${encodeURIComponent(name)}`;
			const res = await fetch(url, { headers: await authHeaders() });
			if (!res.ok) {
				const err = await res.json().catch(() => ({ detail: res.statusText }));
				throw new Error(err.detail || 'Failed to fetch video');
			}
			const blob = await res.blob();
			videoBlobUrls = { ...videoBlobUrls, [name]: URL.createObjectURL(blob) };
		} catch (error) {
			logger.error('[Adv] fetch server video error:', error);
			addNotification(
				`ERR:Load "${name}" failed: ${error instanceof Error ? error.message : 'unknown'}`
			);
		} finally {
			loadingVideoName = null;
		}
	}

	// Refresh the per-slot "on server" indicators on mount and whenever the
	// selected country changes.
	$effect(() => {
		void selectedCountry;
		refreshServerIndex();
	});

	$effect(() => {
		return () => {
			for (const s of slots) if (s.preview) URL.revokeObjectURL(s.preview);
			for (const url of Object.values(videoBlobUrls)) URL.revokeObjectURL(url);
			for (const url of Object.values(thumbUrls)) URL.revokeObjectURL(url);
		};
	});
</script>

<div class="flex min-h-screen flex-col bg-muted/20">
	<!-- Header -->
	<header class="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
		<div class="mx-auto flex max-w-6xl flex-col gap-4 px-8 py-4">
			<div class="flex flex-wrap items-center justify-between gap-4">
				<div class="flex items-center gap-3">
					<div
						class="flex h-11 w-11 items-center justify-center rounded-xl bg-primary/10 text-primary"
					>
						<Video class="h-6 w-6" />
					</div>
					<div>
						<h1 class="text-xl font-bold tracking-tight">Advertisement Upload</h1>
						<!-- <p class="text-sm text-muted-foreground">21 fixed slots · menu banner {MENU_SPEC.width}×{MENU_SPEC.height}</p> -->
					</div>
				</div>

				<div class="flex flex-wrap items-center gap-2">
					<Badge variant={isAdbConnected ? 'default' : 'secondary'} class="gap-1">
						<span
							class="h-1.5 w-1.5 rounded-full {isAdbConnected ? 'bg-white' : 'bg-muted-foreground'}"
						></span>
						{isAdbConnected ? 'Machine connected' : 'Machine offline'}
					</Badge>

					{#if !isAdbConnected}
						<Button variant="outline" onclick={connectMachine} disabled={connecting}>
							{#if connecting}
								<Spinner class="mr-2 h-4 w-4" />
								Connecting...
							{:else}
								<MonitorPlay class="mr-2 h-4 w-4" />
								Connect
							{/if}
						</Button>
					{/if}

					<Button
						variant="outline"
						onclick={pushToMachine}
						disabled={anyBusy || !isAdbConnected || filledCount === 0}
					>
						{#if pushingToMachine}
							<Spinner class="mr-2 h-4 w-4" />
							Pushing {pushProgress.current}/{pushProgress.total}...
						{:else}
							<MonitorPlay class="mr-2 h-4 w-4" />
							Push to Machine{filledCount > 0 ? ` (${filledCount})` : ''}
						{/if}
					</Button>

					<Button variant="outline" onclick={() => (showScreenMirror = true)}>
						Show Android Screen
					</Button>

					<Select.Root type="single" bind:value={selectedCountry}>
						<Select.Trigger class="h-9 w-40">
							{COUNTRIES.find((c) => c.value === selectedCountry)?.label || 'Country'}
						</Select.Trigger>
						<Select.Content>
							{#each COUNTRIES as country}
								<Select.Item value={country.value}>{country.label}</Select.Item>
							{/each}
						</Select.Content>
					</Select.Root>

					<Button onclick={uploadAllFilled} disabled={anyBusy || pendingCount === 0}>
						{#if uploadingAll}
							<Spinner class="mr-2 h-4 w-4" />
							Uploading...
						{:else}
							<Upload class="mr-2 h-4 w-4" />
							Upload all{pendingCount > 0 ? ` (${pendingCount})` : ''}
						{/if}
					</Button>
				</div>
			</div>

			<!-- Progress bar: only while pushing to a machine -->
			{#if pushingToMachine}
				<div class="flex items-center gap-3">
					<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
						<div
							class="h-full rounded-full bg-primary transition-all"
							style="width: {pushProgress.total > 0
								? (pushProgress.current / pushProgress.total) * 100
								: 0}%"
						></div>
					</div>
					<span class="shrink-0 text-xs font-medium text-muted-foreground">
						Pushing {pushProgress.name} · {pushProgress.percent}%
					</span>
				</div>
			{/if}
		</div>
	</header>

	<!-- Content -->
	<div class="flex-1 overflow-y-auto">
		<div class="mx-auto max-w-6xl space-y-8 px-8 py-8">
			<!-- Slots -->
			<section>
				<div class="mb-3 flex items-end justify-between gap-4">
					<div>
						<h2 class="text-lg font-semibold">Slots</h2>
						<p class="text-sm text-muted-foreground">
							Choose a video per slot — it's renamed to the slot's fixed name on upload.
						</p>
					</div>
					{#if onServerCount > 0}
						<Badge
							class="gap-1 border-green-500/40 bg-green-500/15 whitespace-nowrap text-green-600 hover:bg-green-500/15 dark:text-green-400"
						>
							<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span>
							{onServerCount} on server
						</Badge>
					{/if}
				</div>

				<div class="grid grid-cols-1 gap-3 xl:grid-cols-2">
					{#each slots as slot (slot.index)}
						<div
							class="group flex items-center gap-3 rounded-xl border border-l-4 bg-card p-3 shadow-sm transition-shadow hover:shadow-md {slotAccent(
								slot
							)}"
						>
							<!-- Slot number -->
							<div
								class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-sm font-bold tabular-nums {slot.file
									? 'bg-primary/15 text-primary'
									: 'bg-muted text-muted-foreground'}"
							>
								{String(slot.index).padStart(2, '0')}
							</div>

							<!-- Thumbnail / picker -->
							<div class="relative aspect-video h-16 shrink-0 overflow-hidden rounded-lg bg-black">
								{#if slot.preview}
									<!-- svelte-ignore a11y_media_has_caption -->
									<video src={slot.preview} class="h-full w-full object-cover" muted></video>
									{#if slot.status === 'uploading'}
										<div class="absolute inset-0 flex items-center justify-center bg-black/60">
											<Spinner class="h-5 w-5 text-white" />
										</div>
									{:else if slot.status === 'success'}
										<div class="absolute inset-0 flex items-center justify-center bg-green-500/25">
											<CheckCircle class="h-6 w-6 text-white drop-shadow" />
										</div>
									{/if}
								{:else if thumbUrls[slot.name]}
									<!-- JPEG poster of the video already on the server; still a picker
									     so clicking replaces it with a new file. -->
									<label class="group/thumb relative block h-full w-full cursor-pointer">
										<input
											type="file"
											accept=".mp4,video/mp4"
											class="hidden"
											onchange={(e) => {
												const el = e.currentTarget as HTMLInputElement;
												pickSlotFile(slot.index, el.files);
												el.value = '';
											}}
										/>
										<img
											src={thumbUrls[slot.name]}
											alt="Current {slot.name}"
											class="h-full w-full object-cover"
										/>
										<div
											class="absolute inset-0 flex flex-col items-center justify-center gap-1 bg-black/50 text-white opacity-0 transition-opacity group-hover/thumb:opacity-100"
										>
											<Upload class="h-4 w-4" />
											<span class="text-[10px] font-medium">Replace</span>
										</div>
									</label>
								{:else}
									<label
										class="flex h-full w-full cursor-pointer flex-col items-center justify-center gap-1 border border-dashed border-muted-foreground/30 text-muted-foreground transition-colors hover:border-primary/50 hover:text-primary"
									>
										<input
											type="file"
											accept=".mp4,video/mp4"
											class="hidden"
											onchange={(e) => {
												const el = e.currentTarget as HTMLInputElement;
												pickSlotFile(slot.index, el.files);
												el.value = '';
											}}
										/>
										{#if serverFilesByName[slot.name] !== undefined}
											<Spinner class="h-4 w-4" />
											<span class="text-[10px] font-medium">Loading…</span>
										{:else}
											<Upload class="h-4 w-4" />
											<span class="text-[10px] font-medium">Choose</span>
										{/if}
									</label>
								{/if}
							</div>

							<!-- Info -->
							<div class="min-w-0 flex-1">
								<p class="truncate font-mono text-xs font-semibold" title={slot.name}>
									{slot.name}
								</p>
								{#if slot.file}
									<div class="mt-1 flex flex-wrap items-center gap-1.5">
										{#if !slot.dimChecked}
											<Badge variant="secondary" class="text-[10px]">Reading…</Badge>
										{:else if slot.dimOk}
											<Badge variant="default" class="bg-green-500 text-[10px] hover:bg-green-500">
												{slot.width}×{slot.height}
											</Badge>
										{:else}
											<Badge variant="destructive" class="text-[10px]">
												{slot.width ?? '?'}×{slot.height ?? '?'} · exp {MENU_SPEC.width}×{MENU_SPEC.height}
											</Badge>
										{/if}
										<span class="text-[10px] text-muted-foreground"
											>{formatBytes(slot.file.size)}</span
										>
									</div>
									{#if slot.error}
										<p
											class="mt-1 flex items-center gap-1 text-[10px] text-red-500"
											title={slot.error}
										>
											<AlertCircle class="h-3 w-3 shrink-0" />
											<span class="truncate">{slot.error}</span>
										</p>
									{/if}
								{:else}
									<p class="mt-1 text-[11px] text-muted-foreground">Empty · no file selected</p>
								{/if}

								<!-- What's currently on the server for this slot -->
								{#if serverFilesByName[slot.name] !== undefined}
									<div class="mt-2 flex items-center gap-2">
										<Badge
											class="gap-1 border-green-500/40 bg-green-500/15 text-[11px] text-green-600 hover:bg-green-500/15 dark:text-green-400"
										>
											<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span>
											On server · {formatBytes(serverFilesByName[slot.name])}
										</Badge>
										<Button
											size="sm"
											variant="secondary"
											class="gap-1.5 border border-green-500/40 text-green-600 hover:bg-green-500/15 hover:text-green-600 dark:text-green-400 dark:hover:text-green-400"
											onclick={() => viewCurrent(slot.name)}
											disabled={loadingVideoName === slot.name}
										>
											{#if loadingVideoName === slot.name}
												<Spinner class="h-4 w-4" />
												Loading
											{:else}
												<Eye class="h-4 w-4" />
												View video
											{/if}
										</Button>
									</div>
								{/if}
							</div>

							<!-- Actions -->
							<div class="flex shrink-0 items-center gap-1">
								{#if slot.file}
									<Button
										size="sm"
										variant={slot.status === 'success' ? 'outline' : 'default'}
										onclick={() => uploadSlot(slot.index)}
										disabled={anyBusy || slot.status === 'uploading'}
									>
										{#if slot.status === 'uploading'}
											<Spinner class="h-4 w-4" />
										{:else if slot.status === 'success'}
											<RefreshCw class="mr-1.5 h-3.5 w-3.5" />
											Re-upload
										{:else}
											<Upload class="mr-1.5 h-3.5 w-3.5" />
											Upload
										{/if}
									</Button>
									{#if slot.status !== 'uploading'}
										<Button
											size="icon"
											variant="ghost"
											class="h-8 w-8 text-muted-foreground hover:text-red-500"
											onclick={() => clearSlot(slot.index)}
											aria-label="Remove"
										>
											<X class="h-4 w-4" />
										</Button>
									{/if}
								{/if}
							</div>
						</div>
					{/each}
				</div>
			</section>

			<!-- Videos on server -->
			<section>
				<div class="mb-3 flex items-end justify-between gap-4">
					<div>
						<h2 class="text-lg font-semibold">Videos on server</h2>
						<p class="text-sm text-muted-foreground">
							Currently on the FTP folder for <code class="font-mono">{selectedCountry}</code> — loaded
							automatically to verify.
						</p>
					</div>
					<Button variant="outline" onclick={loadServerVideos} disabled={loadingServerVideos}>
						{#if loadingServerVideos}
							<Spinner class="mr-2 h-4 w-4" />
							{videoLoadProgress.total > 0
								? `Loading ${videoLoadProgress.current}/${videoLoadProgress.total}...`
								: 'Loading...'}
						{:else}
							<RefreshCw class="mr-2 h-4 w-4" />
							{serverVideosLoaded ? 'Refresh' : 'Load videos'}
						{/if}
					</Button>
				</div>

				{#if !serverVideosLoaded}
					<div
						class="flex flex-col items-center justify-center gap-2 rounded-xl border border-dashed py-12 text-center text-muted-foreground"
					>
						<Video class="h-8 w-8" />
						<p class="text-sm">Click "Load videos" to preview what's on the server.</p>
					</div>
				{:else if serverVideos.length === 0}
					<p class="text-sm text-muted-foreground">No videos found on the server.</p>
				{:else}
					<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
						{#each serverVideos as video (video.name)}
							<div
								class="overflow-hidden rounded-xl border bg-card shadow-sm transition-shadow hover:shadow-md"
							>
								<div class="flex aspect-video items-center justify-center bg-black">
									{#if videoBlobUrls[video.name]}
										<!-- svelte-ignore a11y_media_has_caption -->
										<video src={videoBlobUrls[video.name]} controls class="h-full w-full"></video>
									{:else}
										<Button
											variant="secondary"
											size="sm"
											onclick={() => playServerVideo(video.name)}
											disabled={loadingVideoName === video.name}
										>
											{#if loadingVideoName === video.name}
												<Spinner class="mr-2 h-4 w-4" />
												Loading...
											{:else}
												<Play class="mr-2 h-4 w-4" />
												Play
											{/if}
										</Button>
									{/if}
								</div>
								<div class="flex items-center justify-between gap-2 p-3">
									<span class="truncate font-mono text-xs" title={video.name}>{video.name}</span>
									<Badge variant="secondary" class="shrink-0">{formatBytes(video.size)}</Badge>
								</div>
							</div>
						{/each}
					</div>
				{/if}
			</section>
		</div>
	</div>

	<!-- Lightbox: preview the current server video for a slot -->
	{#if viewingName}
		<!-- svelte-ignore a11y_click_events_have_key_events -->
		<!-- svelte-ignore a11y_no_static_element_interactions -->
		<div
			class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4"
			onclick={() => (viewingName = null)}
		>
			<!-- svelte-ignore a11y_click_events_have_key_events -->
			<!-- svelte-ignore a11y_no_static_element_interactions -->
			<div
				class="w-full max-w-2xl overflow-hidden rounded-xl bg-card shadow-xl"
				onclick={(e) => e.stopPropagation()}
			>
				<div class="flex items-center justify-between gap-2 border-b px-4 py-2">
					<span class="truncate font-mono text-sm">{viewingName}</span>
					<Button
						size="icon"
						variant="ghost"
						class="h-8 w-8"
						onclick={() => (viewingName = null)}
						aria-label="Close"
					>
						<X class="h-4 w-4" />
					</Button>
				</div>
				<div class="bg-black">
					{#if videoBlobUrls[viewingName]}
						<!-- svelte-ignore a11y_media_has_caption -->
						<video src={videoBlobUrls[viewingName]} controls autoplay class="max-h-[70vh] w-full"
						></video>
					{/if}
				</div>
			</div>
		</div>
	{/if}

	<ScrcpyDialog bind:open={showScreenMirror} />
</div>
