<script lang="ts">
	import { auth } from '$lib/core/stores/auth';
	import { addNotification } from '$lib/core/stores/noti';
	import Button from '$lib/components/ui/button/button.svelte';
	import Label from '$lib/components/ui/label/label.svelte';
	import * as Card from '$lib/components/ui/card/index.js';
	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 Progress from '$lib/components/ui/progress/progress.svelte';
	import { Upload, X, Video, CheckCircle, AlertCircle, MonitorPlay } from '@lucide/svelte/icons';
	import * as adb from '$lib/core/adb/adb';
	import { AdbInstance } from '../../../state.svelte';

	const UPLOAD_PROXY_ENDPOINT = '/api/adv-upload';
	const MANIFEST_PROXY_ENDPOINT = '/api/adv-manifest';
	const MANIFEST_FILENAME = 'sync_1.file';
	const ALLOWED_EXTENSIONS = ['.mp4'];
	const MACHINE_PROJECT_DIR = '/sdcard/coffeevending/taobin_project';

	// ─────────────────────────────────────────────────────────────────────────
	// CONFIG — choose how the sync_1.file manifest is built (change this value).
	// Either way the manifest lists ONLY the selected/active set, never the whole
	// FTP folder (production keeps many inactive variants in the folder).
	//   'ftp_listdir' = manifest built (in the browser) from the files you upload
	//                   this session. No ADB, doesn't touch the machine. Recommended.
	//   'machine'     = original flow. On Upload it: rm -rf the machine adv folder,
	//                   pushes the selected .mp4 (from the browser), then
	//                   `ls -l > sync_1.file` on the machine, pulls it, uploads it.
	//                   ⚠️ FULL REPLACE — requires ADB; select the COMPLETE adv set.
	const MANIFEST_MODE: 'ftp_listdir' | 'machine' = 'ftp_listdir';
	//const MANIFEST_MODE: 'ftp_listdir' | 'machine' = 'machine';
	// ─────────────────────────────────────────────────────────────────────────

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

	// Each country can target a different SFTP host (configured on the backend
	// via ADV_SFTP_COUNTRY_CONFIG). The selected country is sent with the upload.
	const COUNTRIES = [
		{ value: 'tha', label: 'Thailand (tha)' },
		{ value: 'mys', label: 'Malaysia (mys)' },
		{ value: 'aus', label: 'Australia (aus)' },
		{ value: 'sgp', label: 'Singapore (sgp)' },
		{ value: 'hkg', label: 'Hong Kong (hkg)' },
		{ value: 'gbr', label: 'United Kingdom (gbr)' },
		{ value: 'uae_dubai', label: 'UAE Dubai (uae_dubai)' },
		{ value: 'ltu', label: 'Lithuania (ltu)' },
		{ value: 'rou', label: 'Romania (rou)' },
		{ value: 'lva', label: 'Latvia (lva)' },
		{ value: 'est', label: 'Estonia (est)' }
	];

	let selectedCountry = $state('tha');

	// Expected dimensions are derived from the filename convention.
	const ADV_SPECS = {
		menu: { width: 1080, height: 380, label: 'Menu banner', pattern: /^taobin_adv_menu_[a-z0-9]+\.mp4$/i },
		fullscreen: { width: 1080, height: 608, label: 'Fullscreen / Idle', pattern: /^taobin_adv_[a-z0-9]+\.mp4$/i }
	} as const;

	type AdvCategory = 'menu' | 'fullscreen' | 'invalid';

	interface AdvFileItem {
		id: string;
		file: File;
		preview: string;
		status: 'pending' | 'uploading' | 'success' | 'error';
		error?: string;
		category: AdvCategory;
		expectedWidth?: number;
		expectedHeight?: number;
		width?: number;
		height?: number;
		dimChecked: boolean;
		dimOk: boolean;
	}

	let files = $state<AdvFileItem[]>([]);
	let uploading = $state(false);
	let uploadProgress = $state({ current: 0, total: 0 });
	let dragOver = $state(false);

	// Push-to-machine (ADB) state
	let isAdbConnected = $derived(Boolean(AdbInstance.instance));
	let pushingToMachine = $state(false);
	let pushProgress = $state({ current: 0, total: 0, name: '', percent: 0 });

	let generatingManifest = $state(false);

	function generateId() {
		return Math.random().toString(36).substring(2, 9);
	}

	// Classify by filename. Menu must be checked before fullscreen because a menu
	// name also satisfies the broader fullscreen pattern.
	function classify(name: string): { category: AdvCategory; width?: number; height?: number } {
		const lower = name.toLowerCase();
		if (!lower.endsWith('.mp4')) return { category: 'invalid' };
		if (ADV_SPECS.menu.pattern.test(lower)) {
			return { category: 'menu', width: ADV_SPECS.menu.width, height: ADV_SPECS.menu.height };
		}
		if (ADV_SPECS.fullscreen.pattern.test(lower)) {
			return {
				category: 'fullscreen',
				width: ADV_SPECS.fullscreen.width,
				height: ADV_SPECS.fullscreen.height
			};
		}
		return { category: 'invalid' };
	}

	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;
		});
	}

	function handleFileSelect(event: Event) {
		const input = event.target as HTMLInputElement;
		if (input.files) {
			addFiles(Array.from(input.files));
		}
		input.value = '';
	}

	function handleDrop(event: DragEvent) {
		event.preventDefault();
		dragOver = false;
		if (event.dataTransfer?.files) {
			addFiles(Array.from(event.dataTransfer.files));
		}
	}

	function handleDragOver(event: DragEvent) {
		event.preventDefault();
		dragOver = true;
	}

	function handleDragLeave() {
		dragOver = false;
	}

	function addFiles(newFiles: File[]) {
		const toAdd: AdvFileItem[] = [];

		for (const file of newFiles) {
			const ext = '.' + file.name.split('.').pop()?.toLowerCase();
			if (!ALLOWED_EXTENSIONS.includes(ext)) {
				addNotification(`WARN:${file.name} - Only .mp4 allowed`);
				continue;
			}
			if (files.some((f) => f.file.name === file.name)) {
				addNotification(`WARN:${file.name} - Already added`);
				continue;
			}

			const { category, width, height } = classify(file.name);
			if (category === 'invalid') {
				addNotification(
					`WARN:${file.name} - Name must be taobin_adv_*.mp4 or taobin_adv_menu_*.mp4`
				);
				continue;
			}

			toAdd.push({
				id: generateId(),
				file,
				preview: URL.createObjectURL(file),
				status: 'pending',
				category,
				expectedWidth: width,
				expectedHeight: height,
				dimChecked: false,
				dimOk: false
			});
		}

		if (toAdd.length === 0) return;
		files = [...files, ...toAdd];

		// Read each video's real dimensions and validate against the spec.
		for (const item of toAdd) {
			readVideoDimensions(item.file)
				.then(({ width, height }) => {
					const index = files.findIndex((f) => f.id === item.id);
					if (index === -1) return;
					const ok = width === item.expectedWidth && height === item.expectedHeight;
					files[index].width = width;
					files[index].height = height;
					files[index].dimChecked = true;
					files[index].dimOk = ok;
					if (!ok) {
						files[index].status = 'error';
						files[index].error = `Size ${width}x${height}, expected ${item.expectedWidth}x${item.expectedHeight}`;
					}
				})
				.catch(() => {
					const index = files.findIndex((f) => f.id === item.id);
					if (index === -1) return;
					files[index].dimChecked = true;
					files[index].dimOk = false;
					files[index].status = 'error';
					files[index].error = 'Cannot read video dimensions';
				});
		}
	}

	function removeFile(id: string) {
		const item = files.find((f) => f.id === id);
		if (item) URL.revokeObjectURL(item.preview);
		files = files.filter((f) => f.id !== id);
	}

	function clearAllFiles() {
		files.forEach((f) => URL.revokeObjectURL(f.preview));
		files = [];
	}

	// A video that passed name + dimension validation (eligible for push/upload).
	function isValidVideo(item: AdvFileItem) {
		return item.dimChecked && item.dimOk && item.category !== 'invalid';
	}

	function isUploadable(item: AdvFileItem) {
		return isValidVideo(item) && (item.status === 'pending' || item.status === 'error');
	}

	const uploadableCount = $derived(files.filter(isUploadable).length);
	const validVideoCount = $derived(files.filter(isValidVideo).length);

	// Step 1 (mirrors original sync.sh): push the valid videos to the connected
	// machine via ADB so you can preview them before sending to the server.
	async function pushToMachine() {
		if (!AdbInstance.instance) {
			addNotification('ERR:Machine not connected (ADB)');
			return;
		}
		const targets = files.filter(isValidVideo);
		if (targets.length === 0) {
			addNotification('WARN:No valid videos 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 item = targets[i];
				pushProgress = { current: i, total: targets.length, name: item.file.name, percent: 0 };

				const bytes = new Uint8Array(await item.file.arrayBuffer());
				const ok = await adb.pushBinary(
					`${targetDir}/${item.file.name}`,
					bytes,
					(sent, total) => {
						pushProgress = {
							current: i,
							total: targets.length,
							name: item.file.name,
							percent: total > 0 ? Math.round((sent / total) * 100) : 0
						};
					}
				);
				if (ok) {
					success++;
					pushProgress = { current: i + 1, total: targets.length, name: item.file.name, percent: 100 };
				} else {
					addNotification(`ERR:Push failed: ${item.file.name}`);
				}
			}

			if (success > 0) {
				addNotification(`INFO:Pushed ${success} video(s) to machine (${targetDir})`);
			}
		} catch (error) {
			console.error('[Adv] push to machine error:', error);
			addNotification(`ERR:Push error: ${error instanceof Error ? error.message : 'unknown'}`);
		} finally {
			pushingToMachine = false;
		}
	}

	async function uploadFiles() {
		const currentUser = $auth;
		if (!currentUser) {
			addNotification('ERR:Not logged in');
			return;
		}

		const pendingFiles = files.filter(isUploadable);
		if (pendingFiles.length === 0) {
			addNotification('WARN:No valid files to upload');
			return;
		}

		const uid = currentUser.uid;
		const displayName = currentUser.displayName || 'unknown';
		const email = currentUser.email || 'unknown@email.com';

		// Method 2 mirrors the original flow: make the machine's adv folder hold
		// EXACTLY the selected set (rm -rf + push) BEFORE its ls -l manifest is
		// generated — so machine = FTP = manifest. The .mp4 still come from the
		// browser (the dragged files), only the manifest comes from the machine.
		if (MANIFEST_MODE === 'machine') {
			const synced = await syncMachineFolder(pendingFiles);
			if (!synced) return;
		}

		uploading = true;
		uploadProgress = { current: 0, total: pendingFiles.length };

		for (let i = 0; i < pendingFiles.length; i++) {
			const item = pendingFiles[i];
			const index = files.findIndex((f) => f.id === item.id);
			if (index === -1) continue;

			files[index].status = 'uploading';
			files[index].error = undefined;

			try {
				const formData = new FormData();
				formData.append('country', selectedCountry);
				formData.append('uid', uid);
				formData.append('displayName', displayName);
				formData.append('email', email);
				formData.append('file', item.file);
					// Manifest is built from the selected set (method 1) or the machine
					// (method 2) — never from the whole FTP folder. So tell the backend
					// not to rebuild it from the FTP listing.
					formData.append('regenerate', 'false');

				const response = await fetch(UPLOAD_PROXY_ENDPOINT, {
					method: 'POST',
					body: formData
				});

				if (!response.ok) {
					const errorData = await response.json().catch(() => ({ detail: response.statusText }));
					throw new Error(errorData.detail || errorData.message || 'Upload failed');
				}

				files[index].status = 'success';
				uploadProgress = { current: i + 1, total: pendingFiles.length };
			} catch (error) {
				files[index].status = 'error';
				files[index].error = error instanceof Error ? error.message : 'Unknown error';
				console.error(`Upload error for ${item.file.name}:`, error);
			}
		}

		uploading = false;

		const successCount = files.filter((f) => f.status === 'success').length;
		const errorCount = files.filter((f) => f.status === 'error').length;

		if (errorCount === 0) {
			addNotification(`INFO:Uploaded ${successCount} adv video(s) successfully`);
		} else {
			addNotification(`WARN:Uploaded ${successCount}, failed ${errorCount}`);
		}

		// Build the manifest (sync_1.file) — only from the selected/active set,
		// never the whole FTP folder (production keeps many inactive variants there).
		if (successCount > 0) {
			if (MANIFEST_MODE === 'machine') {
				// Method 2: ls -l on the (rm -rf + pushed) machine, then upload.
				await generateMachineManifest(uid, displayName, email);
			} else {
				// Method 1: manifest = the successfully-uploaded files in the UI list.
				await uploadSelectedManifest(uid, displayName, email);
			}
		}
	}

	// Method 1 — build sync_1.file from the active set the user assembled in the
	// UI (the successfully-uploaded files), NOT the whole FTP folder. The FTP keeps
	// any other/variant files; the manifest lists only what you chose.
	async function uploadSelectedManifest(uid: string, displayName: string, email: string) {
		const active = files.filter((f) => f.status === 'success');
		if (active.length === 0) return;
		generatingManifest = true;
		try {
			const text = buildManifestText(active);
			await uploadManifestText(text, uid, displayName, email);
			addNotification(`INFO:Manifest uploaded (${active.length} active file(s))`);
		} catch (error) {
			console.error('[Adv] selected manifest error:', error);
			addNotification(`ERR:Manifest failed: ${error instanceof Error ? error.message : 'unknown'}`);
		} finally {
			generatingManifest = false;
		}
	}

	// ls -l style line the machine's FileSyncServer parses (size at field 4, name
	// at field 7). sync_1.file lists itself as size 0 so the machine skips it.
	function buildManifestText(items: AdvFileItem[]): string {
		const pad = (n: number) => String(n).padStart(2, '0');
		const fmt = (ms: number) => {
			const d = new Date(ms);
			return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
		};
		const now = Date.now();
		const rows = [
			{ name: MANIFEST_FILENAME, size: 0, mtime: now },
			...items.map((it) => ({
				name: it.file.name,
				size: it.file.size,
				mtime: it.file.lastModified || now
			}))
		].sort((a, b) => a.name.localeCompare(b.name));

		const lines = ['total 0'];
		for (const r of rows) {
			lines.push(`-rw-rw---- 1 root sdcard_rw ${r.size} ${fmt(r.mtime)} ${r.name}`);
		}
		return lines.join('\n') + '\n';
	}

	async function uploadManifestText(
		text: string,
		uid: string,
		displayName: string,
		email: string
	) {
		const fd = new FormData();
		fd.append('country', selectedCountry);
		fd.append('uid', uid);
		fd.append('displayName', displayName);
		fd.append('email', email);
		fd.append('file', new File([text], MANIFEST_FILENAME, { type: 'text/plain' }));

		const res = await fetch(MANIFEST_PROXY_ENDPOINT, { method: 'POST', body: fd });
		if (!res.ok) {
			const e = await res.json().catch(() => ({ detail: res.statusText }));
			throw new Error(e.detail || 'Manifest upload failed');
		}
	}

	// Method 2 step 1 — make the machine's adv folder hold EXACTLY the selected
	// files (rm -rf + push), like the original `rm -Rf adv` + push of the whole
	// folder. After this, the machine's `ls -l` matches what we upload to the FTP.
	// ⚠️ This WIPES the machine's adv folder — method 2 is a full replace, so the
	// dragged set must be the complete intended adv set.
	async function syncMachineFolder(targets: AdvFileItem[]): Promise<boolean> {
		if (!AdbInstance.instance) {
			addNotification('ERR:Method 2 (machine manifest) needs the machine connected via ADB');
			return false;
		}
		const advDir = machineAdvDir(selectedCountry);
		pushingToMachine = true;
		try {
			// Wipe + recreate so only the selected set remains on the machine.
			await adb.executeCmd(`rm -rf "${advDir}" && mkdir -p "${advDir}"`);

			for (let i = 0; i < targets.length; i++) {
				const item = targets[i];
				pushProgress = { current: i, total: targets.length, name: item.file.name, percent: 0 };
				const bytes = new Uint8Array(await item.file.arrayBuffer());
				const ok = await adb.pushBinary(
					`${advDir}/${item.file.name}`,
					bytes,
					(sent, total) => {
						pushProgress = {
							current: i,
							total: targets.length,
							name: item.file.name,
							percent: total > 0 ? Math.round((sent / total) * 100) : 0
						};
					}
				);
				if (!ok) {
					addNotification(`ERR:Push to machine failed: ${item.file.name}`);
					return false;
				}
				pushProgress = { current: i + 1, total: targets.length, name: item.file.name, percent: 100 };
			}
			addNotification(`INFO:Machine adv folder synced (${targets.length} file(s))`);
			return true;
		} catch (error) {
			console.error('[Adv] machine sync error:', error);
			addNotification(`ERR:Machine sync failed: ${error instanceof Error ? error.message : 'unknown'}`);
			return false;
		} finally {
			pushingToMachine = false;
		}
	}

	// Method 2 step 2 — generate the manifest from the (now synced) machine adv
	// folder (`ls -l > sync_1.file`), pull it, and upload it to the FTP.
	async function generateMachineManifest(uid: string, displayName: string, email: string) {
		if (!AdbInstance.instance) {
			addNotification('ERR:Machine not connected — cannot generate manifest from machine');
			return;
		}
		const advDir = machineAdvDir(selectedCountry);
		generatingManifest = true;
		try {
			// ls -l > sync_1.file on the machine (shell truncates the manifest first,
			// so it lists itself as size 0 — exactly what the original flow produces).
			await adb.executeCmd(`cd ${advDir} && ls -l > ${MANIFEST_FILENAME}`);

			const manifestText = await adb.pull(`${advDir}/${MANIFEST_FILENAME}`);
			if (!manifestText || manifestText.trim().length === 0) {
				addNotification('ERR:Failed to read sync_1.file from machine');
				return;
			}

			const fd = new FormData();
			fd.append('country', selectedCountry);
			fd.append('uid', uid);
			fd.append('displayName', displayName);
			fd.append('email', email);
			fd.append(
				'file',
				new File([manifestText], MANIFEST_FILENAME, { type: 'text/plain' })
			);

			const res = await fetch(MANIFEST_PROXY_ENDPOINT, { method: 'POST', body: fd });
			if (!res.ok) {
				const e = await res.json().catch(() => ({ detail: res.statusText }));
				throw new Error(e.detail || 'Manifest upload failed');
			}
			addNotification('INFO:Manifest (from machine) uploaded to server');
		} catch (error) {
			console.error('[Adv] machine manifest error:', error);
			addNotification(`ERR:Machine manifest failed: ${error instanceof Error ? error.message : 'unknown'}`);
		} finally {
			generatingManifest = false;
		}
	}

	$effect(() => {
		return () => {
			files.forEach((f) => URL.revokeObjectURL(f.preview));
		};
	});
</script>

<div class="flex min-h-screen flex-col">
	<!-- Header -->
	<div class="sticky top-0 z-10 border-b bg-background">
		<div class="flex items-center justify-between px-8 py-4">
			<div>
				<h1 class="text-2xl font-bold">Adv Upload</h1>
				<p class="text-sm text-muted-foreground">Upload advertisement videos (.mp4) to the server</p>
			</div>

			<div class="flex items-center gap-3">
				<Badge variant={isAdbConnected ? 'default' : 'secondary'}>
					{isAdbConnected ? 'Machine connected' : 'Machine offline'}
				</Badge>

				{#if files.length > 0}
					<Button
						variant="outline"
						onclick={clearAllFiles}
						disabled={uploading || pushingToMachine}
					>
						<X class="mr-2 h-4 w-4" />
						Clear All
					</Button>
				{/if}

				<!-- Step 1: push to the connected machine to preview -->
				<Button
					variant="outline"
					onclick={pushToMachine}
					disabled={pushingToMachine || uploading || !isAdbConnected || validVideoCount === 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 ({validVideoCount})
					{/if}
				</Button>

				<!-- Step 2: upload to server -->
				<Button
					onclick={uploadFiles}
					disabled={uploading || pushingToMachine || generatingManifest || uploadableCount === 0}
				>
					{#if uploading}
						<Spinner class="mr-2 h-4 w-4" />
						Uploading {uploadProgress.current}/{uploadProgress.total}...
					{:else}
						<Upload class="mr-2 h-4 w-4" />
						Upload ({uploadableCount})
					{/if}
				</Button>
			</div>
		</div>
	</div>

	<!-- Content -->
	<div class="flex-1 overflow-y-auto p-8">
		<div class="mx-auto max-w-6xl space-y-6">
			<!-- Settings -->
			<Card.Root>
				<Card.Header>
					<Card.Title>Upload Settings</Card.Title>
				</Card.Header>
				<Card.Content>
					<div class="space-y-2 md:max-w-sm">
						<Label>Country</Label>
						<Select.Root type="single" bind:value={selectedCountry}>
							<Select.Trigger class="w-full">
								{COUNTRIES.find((c) => c.value === selectedCountry)?.label || 'Select country'}
							</Select.Trigger>
							<Select.Content>
								{#each COUNTRIES as country}
									<Select.Item value={country.value}>{country.label}</Select.Item>
								{/each}
							</Select.Content>
						</Select.Root>
						<!-- <p class="text-xs text-muted-foreground">
							Machine push path: <code class="font-mono">{machineAdvDir(selectedCountry)}</code>
						</p> -->
					</div>

				</Card.Content>
			</Card.Root>

			<!-- Naming guide -->
			<Card.Root>
				<Card.Header>
					<Card.Title>Naming &amp; Size Rules</Card.Title>
				</Card.Header>
				<Card.Content>
					<div class="grid gap-4 md:grid-cols-2">
						<div class="rounded-lg border p-4">
							<p class="font-mono text-sm font-semibold">taobin_adv_menu_*.mp4</p>
							<p class="text-sm text-muted-foreground">Menu banner ad</p>
							<Badge variant="secondary" class="mt-2">1080 × 380</Badge>
						</div>
						<div class="rounded-lg border p-4">
							<p class="font-mono text-sm font-semibold">taobin_adv_*.mp4</p>
							<p class="text-sm text-muted-foreground">Fullscreen / Idle ad</p>
							<Badge variant="secondary" class="mt-2">1080 × 608</Badge>
						</div>
					</div>
				</Card.Content>
			</Card.Root>

			<!-- Drop Zone -->
			<Card.Root>
				<Card.Content class="p-6">
					<label
						class="flex min-h-[200px] cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors {dragOver
							? 'border-primary bg-primary/5'
							: 'border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/50'}"
						ondrop={handleDrop}
						ondragover={handleDragOver}
						ondragleave={handleDragLeave}
					>
						<input
							type="file"
							multiple
							accept=".mp4,video/mp4"
							class="hidden"
							onchange={handleFileSelect}
							disabled={uploading}
						/>
						<Video class="mb-4 h-12 w-12 text-muted-foreground" />
						<p class="mb-2 text-lg font-medium">Drop .mp4 videos here or click to browse</p>
						<p class="text-sm text-muted-foreground">
							Filenames must follow the taobin_adv_*.mp4 convention
						</p>
					</label>
				</Card.Content>
			</Card.Root>

			<!-- Push-to-Machine Progress -->
			{#if pushingToMachine}
				<div class="space-y-2">
					<Progress
						value={pushProgress.total > 0 ? (pushProgress.current / pushProgress.total) * 100 : 0}
						max={100}
						class="h-2"
					/>
					<p class="flex items-center justify-center gap-2 text-center text-sm text-muted-foreground">
						<Spinner class="h-3.5 w-3.5" />
						Sending to machine ({pushProgress.current}/{pushProgress.total}): {pushProgress.name}
					</p>
				</div>
			{/if}

			<!-- Upload Progress -->
			{#if uploading}
				<div class="space-y-2">
					<Progress
						value={uploadProgress.total > 0
							? (uploadProgress.current / uploadProgress.total) * 100
							: 0}
						max={100}
						class="h-2"
					/>
					<p class="text-center text-sm text-muted-foreground">
						Uploading: {uploadProgress.current} / {uploadProgress.total}
					</p>
				</div>
			{/if}

			<!-- Generating manifest from machine (method 2) -->
			{#if generatingManifest}
				<p class="flex items-center justify-center gap-2 text-center text-sm text-muted-foreground">
					<Spinner class="h-3.5 w-3.5" />
					Generating sync_1.file on machine and uploading...
				</p>
			{/if}

			<!-- File List -->
			{#if files.length > 0}
				<Card.Root>
					<Card.Header>
						<Card.Title class="flex items-center justify-between">
							<span>Selected Files ({files.length})</span>
							<div class="flex gap-2">
								{#if files.some((f) => f.status === 'success')}
									<Badge variant="default" class="bg-green-500">
										{files.filter((f) => f.status === 'success').length} uploaded
									</Badge>
								{/if}
								{#if files.some((f) => f.status === 'error')}
									<Badge variant="destructive">
										{files.filter((f) => f.status === 'error').length} invalid/failed
									</Badge>
								{/if}
							</div>
						</Card.Title>
					</Card.Header>
					<Card.Content>
						<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
							{#each files as item (item.id)}
								<div
									class="group relative overflow-hidden rounded-lg border bg-muted/30 transition-shadow hover:shadow-md"
								>
									<!-- Video preview -->
									<div class="relative aspect-video bg-black">
										<!-- svelte-ignore a11y_media_has_caption -->
										<video src={item.preview} class="h-full w-full object-contain" muted controls
										></video>

										{#if item.status === 'uploading'}
											<div class="absolute inset-0 flex items-center justify-center bg-black/50">
												<Spinner class="h-8 w-8 text-white" />
											</div>
										{:else if item.status === 'success'}
											<div
												class="absolute inset-0 flex items-center justify-center bg-green-500/20"
											>
												<CheckCircle class="h-10 w-10 text-green-500" />
											</div>
										{/if}

										<!-- Remove button -->
										{#if item.status !== 'uploading'}
											<button
												class="absolute top-2 right-2 rounded-full bg-black/60 p-1 text-white opacity-0 transition-opacity group-hover:opacity-100"
												onclick={() => removeFile(item.id)}
												disabled={uploading}
											>
												<X class="h-4 w-4" />
											</button>
										{/if}
									</div>

									<!-- File info -->
									<div class="space-y-1 p-3">
										<p class="truncate text-sm font-medium" title={item.file.name}>
											{item.file.name}
										</p>
										<div class="flex flex-wrap items-center gap-1.5">
											<Badge variant="outline" class="text-[11px]">
												{item.category === 'menu' ? 'Menu banner' : 'Fullscreen'}
											</Badge>
											{#if !item.dimChecked}
												<Badge variant="secondary" class="text-[11px]">Reading…</Badge>
											{:else if item.dimOk}
												<Badge variant="default" class="bg-green-500 text-[11px]">
													{item.width}×{item.height}
												</Badge>
											{:else}
												<Badge variant="destructive" class="text-[11px]">
													{item.width ?? '?'}×{item.height ?? '?'}
												</Badge>
											{/if}
											<span class="text-[11px] text-muted-foreground">
												{(item.file.size / (1024 * 1024)).toFixed(1)} MB
											</span>
										</div>
										<p class="text-[11px] text-muted-foreground">
											Expected {item.expectedWidth}×{item.expectedHeight}
										</p>
										{#if item.error}
											<p class="flex items-center gap-1 text-[11px] text-red-500" title={item.error}>
												<AlertCircle class="h-3 w-3 shrink-0" />
												<span class="truncate">{item.error}</span>
											</p>
										{/if}
									</div>
								</div>
							{/each}
						</div>
					</Card.Content>
				</Card.Root>
			{/if}
		</div>
	</div>
</div>
