<script lang="ts">
	import { tick, untrack } from 'svelte';
	import { toast } from 'svelte-sonner';
	import { Download, Trash2 } from '@lucide/svelte';

	import { uploadBookFiles } from '$lib/api';
	import type { Book, BookFile } from '$lib/schema';
	import { formatFileSize, getFileType } from '$lib/utils';
	import { getBookOperationsState } from '$lib/state/bookOperations.svelte';

	import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
	import * as Field from '$lib/components/ui/field/index.js';
	import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
	import { Checkbox } from '$lib/components/ui/checkbox/index.js';
	import { FileDropZone, type FileDropZoneProps } from '$lib/components/ui/file-drop-zone';
	import { Spinner } from '$lib/components/ui/spinner/index';

	let { book }: { book: Book } = $props();

	const bookOps = getBookOperationsState();

	/**
	 * The book's files, owned locally.
	 *
	 * `book` is a snapshot handed down from bookOperations rather than a live
	 * query, so invalidating after a delete does not reach it. Keeping the list
	 * here lets the rail reflect an add or a remove straight away. Seeded once per
	 * mount — edit-book.svelte keys the dialog on book.id.
	 */
	let files = $state<BookFile[]>(untrack(() => [...book.files]));

	// The field's value is a sparse-ish list until the form settles, so narrow it
	// before rendering rather than asserting at each use.
	let pending = $derived(
		(uploadBookFiles.fields.files.value() ?? []).filter((file): file is File => Boolean(file))
	);

	let fileToDelete = $state<BookFile>();
	let deleteFromDisk = $state(true);
	let confirmOpen = $state(false);
	let formEl = $state<HTMLFormElement>();

	const onUpload: FileDropZoneProps['onUpload'] = async (uploaded) => {
		uploadBookFiles.fields.files.set([...Array.from(pending), ...uploaded]);
		await tick();
		formEl?.requestSubmit();
	};

	const onFileRejected: FileDropZoneProps['onFileRejected'] = async ({ reason, file }) => {
		toast.error(`${file.name} was not added`, { description: reason });
	};

	/**
	 * The API's own words, when it has any.
	 *
	 * Adding a file the library already holds under another book is refused with a
	 * 409 naming it — far more use than "failed to add files". SvelteKit hands an
	 * `error()` back as an HttpError on the client, so the message sits on `body`.
	 */
	function apiMessage(error: unknown): string | undefined {
		if (typeof error !== 'object' || error === null) return undefined;

		const body = (error as { body?: { message?: string } }).body;
		if (typeof body?.message === 'string') return body.message;

		return error instanceof Error ? error.message : undefined;
	}

	function confirmDelete(file: BookFile) {
		fileToDelete = file;
		confirmOpen = true;
	}

	async function removeFile() {
		if (!fileToDelete) return;

		const target = fileToDelete;
		confirmOpen = false;

		// Drop it from the list first: the request invalidates the books query, but
		// this dialog holds its own copy of the book and would not see that.
		files = files.filter((file) => file.id !== target.id);

		await bookOps.deleteBookFiles(book.id, [target.id], deleteFromDisk);
		fileToDelete = undefined;
	}
</script>

<section class="flex min-w-0 flex-col gap-2">
	<h3 class="font-mono text-[10px] tracking-widest text-muted-foreground uppercase">Files</h3>

	{#if files.length === 0}
		<p class="text-xs text-muted-foreground">
			No files yet. Add one below so this book can be read or downloaded.
		</p>
	{/if}

	<ul class="flex flex-col gap-1.5">
		{#each files as file (file.id)}
			<li class="flex items-center gap-2 rounded-md border bg-background p-2">
				<span
					class="rounded-sm bg-accent px-1.5 py-0.5 font-mono text-[9px] font-semibold text-accent-foreground"
				>
					{getFileType(file.filename)}
				</span>

				<span class="min-w-0 flex-1">
					<span class="block truncate text-xs" title={file.filename}>{file.filename}</span>
					<span class="block font-mono text-[10px] text-muted-foreground tabular-nums">
						{formatFileSize(file.size)}
					</span>
				</span>

				<Button
					variant="ghost"
					size="icon"
					class="size-7 shrink-0"
					title="Download {file.filename}"
					onclick={() => bookOps.downloadBookFile(book.id, file.id, file.filename)}
				>
					<Download class="size-3.5" />
					<span class="sr-only">Download {file.filename}</span>
				</Button>

				<Button
					variant="ghost"
					size="icon"
					class="size-7 shrink-0 text-destructive hover:text-destructive"
					title="Remove {file.filename}"
					onclick={() => confirmDelete(file)}
				>
					<Trash2 class="size-3.5" />
					<span class="sr-only">Remove {file.filename}</span>
				</Button>
			</li>
		{/each}

		{#each pending as file (file.name)}
			<li
				class="flex items-center gap-2 rounded-md border border-dashed bg-background p-2 text-muted-foreground"
			>
				<Spinner class="size-3.5 shrink-0" />
				<span class="min-w-0 flex-1">
					<span class="block truncate text-xs">{file.name}</span>
					<span class="block font-mono text-[10px] tabular-nums">Uploading…</span>
				</span>
			</li>
		{/each}
	</ul>

	<form
		bind:this={formEl}
		{...uploadBookFiles.enhance(async ({ submit }) => {
			try {
				await submit();

				const issues = uploadBookFiles.fields.allIssues();
				if (issues && issues.length > 0) return;

				// The endpoint answers with the updated book, so the new files come
				// back with their ids rather than having to be guessed at.
				files = uploadBookFiles.result?.files ?? files;
				uploadBookFiles.fields.files.set([]);
				toast.success('Files added');
			} catch (error) {
				console.error('Failed to add files: ', error);
				toast.error(apiMessage(error) ?? 'Failed to add files');
				uploadBookFiles.fields.files.set([]);
			}
		})}
		enctype="multipart/form-data"
		class="flex flex-col gap-2"
	>
		<input {...uploadBookFiles.fields.book_id.as('hidden', book.id)} />
		<input class="hidden" {...uploadBookFiles.fields.files.as('file multiple')} />

		<FileDropZone
			{onUpload}
			{onFileRejected}
			accept=".pdf,.epub,.mobi,application/pdf,application/epub+zip,application/x-mobipocket-ebook"
			label="Add a file"
			sublabel="EPUB, PDF or MOBI"
		/>
	</form>
</section>

<AlertDialog.Root bind:open={confirmOpen}>
	<AlertDialog.Content>
		<AlertDialog.Header>
			<AlertDialog.Title>Remove {fileToDelete?.filename}?</AlertDialog.Title>
			<AlertDialog.Description>
				This cannot be undone. The other files on this book are not affected.
			</AlertDialog.Description>
		</AlertDialog.Header>

		<!-- The API takes these as separate outcomes: drop the record, or drop the
		     record and the file on disk. Leaving it implicit would mean deleting
		     someone's only copy without saying so. -->
		<div class="flex items-center gap-2">
			<Checkbox id="delete-from-disk" bind:checked={deleteFromDisk} />
			<Field.Label for="delete-from-disk" class="font-normal">
				Also delete the file from the filesystem
			</Field.Label>
		</div>

		<AlertDialog.Footer>
			<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
			<AlertDialog.Action class={buttonVariants({ variant: 'destructive' })} onclick={removeFile}>
				Remove
			</AlertDialog.Action>
		</AlertDialog.Footer>
	</AlertDialog.Content>
</AlertDialog.Root>
