<script lang="ts">
	import { fly } from 'svelte/transition';
	import { prefersReducedMotion } from 'svelte/motion';
	import { resolve } from '$app/paths';
	import { ChevronDown, CircleAlert, Copy, RotateCcw, X } from '@lucide/svelte';

	import { Button } from '$lib/components/ui/button/index.js';
	import { Spinner } from '$lib/components/ui/spinner/index';
	import { getUploadQueueState } from '$lib/state/upload-queue.svelte';
	import type { DuplicateBook, DuplicateFile } from '$lib/schema';
	import { formatFileSize } from '$lib/utils';

	const queue = getUploadQueueState();

	// A clean run clears itself after a few seconds, so it needs to leave rather
	// than blink out. Nothing to animate for anyone who asked not to see it.
	const motion = $derived(prefersReducedMotion.current ? 0 : 200);

	const percent = $derived(queue.total === 0 ? 0 : Math.round((queue.settled / queue.total) * 100));

	const heading = $derived.by(() => {
		const noun = queue.total === 1 ? 'book' : 'books';
		if (queue.active) return `Adding ${queue.settled} of ${queue.total} ${noun}`;
		if (queue.failed === 0 && queue.skipped === 0) return `Added ${queue.done} ${noun}`;
		if (queue.done === 0 && queue.skipped === 0) return `Couldn't add ${queue.failed} ${noun}`;

		const parts = [];
		if (queue.done > 0) parts.push(`Added ${queue.done}`);
		if (queue.skipped > 0) parts.push(`${queue.skipped} already here`);
		if (queue.failed > 0) parts.push(`${queue.failed} failed`);
		return parts.join(', ');
	});

	/**
	 * What the reader needs to know about files that were not stored.
	 *
	 * One duplicate can name where its bytes already live; several would be a list,
	 * and the row has no room for one, so they collapse to a count.
	 */
	function duplicateNote(duplicates: DuplicateFile[]) {
		if (duplicates.length > 1) return `${duplicates.length} files are already in your library`;

		const [only] = duplicates;
		// No book to name: it matched another file in this same upload.
		return only.book_title ? `Already in ${only.book_title}` : 'Already added by this upload';
	}

	/** The book a note can link to, when a single duplicate points at exactly one. */
	function noteTarget(duplicates: DuplicateFile[]) {
		return duplicates.length === 1 ? duplicates[0].book_id : null;
	}

	/**
	 * What the reader needs to know about a book that went in and may already be here.
	 *
	 * Worded weaker than the file-level "Already in …" on purpose. That one means the
	 * library holds these exact bytes; this one means the metadata agrees, which a
	 * second edition, a translation and a re-scan all do. Nothing was refused.
	 */
	function possibleNote(candidates: DuplicateBook[]) {
		if (candidates.length > 1)
			return `Might already be in your library, ${candidates.length} times`;

		return `Might already be in your library — ${candidates[0].title}`;
	}
</script>

{#if queue.total > 0}
	<!--
		Docked rather than a toast. This runs for minutes and carries a row per
		book, which a notification surface is not built to hold still for.
	-->
	<aside
		aria-label="Uploads"
		transition:fly={{ y: 12, duration: motion }}
		onmouseenter={() => queue.hold()}
		onmouseleave={() => queue.release()}
		onfocusin={() => queue.hold()}
		onfocusout={() => queue.release()}
		class="fixed right-4 bottom-4 z-50 w-80 max-w-[calc(100vw-2rem)] overflow-hidden rounded-lg border bg-card shadow-lg"
	>
		<div class="flex items-center gap-2 border-b px-3 py-2">
			{#if queue.active}
				<Spinner class="size-4 shrink-0" />
			{:else if queue.failed > 0}
				<CircleAlert class="size-4 shrink-0 text-destructive" />
			{:else if queue.skipped > 0}
				<!-- Muted, not alarming: nothing went wrong, the books were already here. -->
				<Copy class="size-4 shrink-0 text-muted-foreground" />
			{/if}

			<span class="min-w-0 flex-1 truncate text-sm font-medium">{heading}</span>

			<Button
				variant="ghost"
				size="icon"
				class="size-7 shrink-0"
				onclick={() => (queue.collapsed = !queue.collapsed)}
			>
				<ChevronDown class="size-4 transition-transform {queue.collapsed ? '' : 'rotate-180'}" />
				<span class="sr-only">{queue.collapsed ? 'Show' : 'Hide'} the list</span>
			</Button>

			<!-- Only once nothing is in flight: dismissing mid-run would suggest it
			     stopped the upload, which it does not. -->
			{#if !queue.active}
				<Button variant="ghost" size="icon" class="size-7 shrink-0" onclick={() => queue.dismiss()}>
					<X class="size-4" />
					<span class="sr-only">Dismiss</span>
				</Button>
			{/if}
		</div>

		<div class="h-1 bg-muted">
			<div
				class="h-full bg-primary transition-[width] duration-300"
				style="width: {percent}%"
			></div>
		</div>

		{#if !queue.collapsed}
			<ul class="flex max-h-64 flex-col divide-y overflow-y-auto">
				{#each queue.jobs as job (job.id)}
					{@const duplicates = job.duplicates ?? []}
					{@const target = noteTarget(duplicates)}
					{@const possible = job.possibleDuplicates ?? []}
					<li class="flex min-w-0 items-center gap-2 px-3 py-2">
						<span class="min-w-0 flex-1">
							<span class="block truncate text-sm" title={job.label}>{job.label}</span>

							{#if job.error}
								<span class="block font-mono text-[10px] text-muted-foreground tabular-nums">
									{job.error}
								</span>
							{:else if duplicates.length > 0}
								<!--
									Reported, with no offer to override. Storing the same bytes twice
									splits reading progress and shelves across two records that can
									never converge, and there is no version of that the reader wants.
									`allow_duplicates` stays on the API for when the match is wrong.
								-->
								<span class="block min-w-0 text-[10px] text-muted-foreground">
									{#if target}
										<a
											href={resolve('/(root)/(library)/book/[bookId]', { bookId: String(target) })}
											class="truncate underline underline-offset-2"
										>
											{duplicateNote(duplicates)}
										</a>
									{:else}
										<span class="truncate">{duplicateNote(duplicates)}</span>
									{/if}
								</span>
							{:else if possible.length > 0}
								<!--
									A guess, not a fact, so it says so and stops there: the book was
									stored, and the library's duplicates screen is where a reader
									decides what to do about it.
								-->
								<span class="block min-w-0 text-[10px] text-muted-foreground">
									{#if possible.length === 1}
										<a
											href={resolve('/(root)/(library)/book/[bookId]', {
												bookId: String(possible[0].book_id)
											})}
											class="truncate underline underline-offset-2"
										>
											{possibleNote(possible)}
										</a>
									{:else}
										<span class="truncate">{possibleNote(possible)}</span>
									{/if}
								</span>
							{:else}
								<span class="block font-mono text-[10px] text-muted-foreground tabular-nums">
									{formatFileSize(job.size)}
								</span>
							{/if}
						</span>

						<span
							class="shrink-0 font-mono text-[10px] tracking-wider uppercase
								{job.status === 'done' ? 'text-primary' : ''}
								{job.status === 'failed' ? 'text-destructive' : ''}
								{job.status !== 'done' && job.status !== 'failed' ? 'text-muted-foreground' : ''}"
						>
							{#if job.status === 'uploading'}
								Adding
							{:else if job.status === 'done'}
								Done
							{:else if job.status === 'skipped'}
								Skipped
							{:else if job.status === 'failed'}
								Failed
							{:else}
								Queued
							{/if}
						</span>
					</li>
				{/each}
			</ul>

			{#if !queue.active && queue.failed > 0}
				<div class="border-t p-2">
					<Button variant="outline" size="sm" class="w-full" onclick={() => queue.retryFailed()}>
						<RotateCcw class="size-4" />
						Retry {queue.failed} failed
					</Button>
				</div>
			{/if}
		{/if}
	</aside>
{/if}
