<script lang="ts">
	import { resolve } from '$app/paths';
	import BookCover from './book-cover.svelte';
	import BookActionsMenu from './book-actions-menu.svelte';
	import * as Tooltip from '$lib/components/ui/tooltip/index';
	import { Badge, badgeVariants } from '$lib/components/ui/badge/index';
	import { buttonVariants } from '$lib/components/ui/button/index.js';
	import { getBookSelectionState } from '$lib/state/bookSelection.svelte';
	import { getBookOperationsState } from '$lib/state/bookOperations.svelte';
	import { getLibraryState } from '$lib/state/library.svelte';
	import { getFileType } from '$lib/utils';
	import { BookOpenText, Check, Download } from '@lucide/svelte';
	import type { Book, BookFile } from '$lib/schema';

	let { books }: { books: Book[] } = $props();

	const selectionState = getBookSelectionState();
	const bookOps = getBookOperationsState();
	const libraryState = getLibraryState();

	/** At most three, so cards with a heavy tag list stay the same height as the rest. */
	const TAG_LIMIT = 3;

	function authors(book: Book) {
		return book.authors.map((a) => a.name).join(', ');
	}

	function year(book: Book) {
		return book.published_date ? new Date(book.published_date).getFullYear() : null;
	}

	function formats(book: Book) {
		return [...new Set(book.files.map((f) => getFileType(f.filename)))].join(' + ');
	}

	/**
	 * BookProgressRead does not expose a timestamp yet, though BookProgress
	 * extends BigIntAuditBase so updated_at exists in the database. Reading it
	 * defensively means the date appears on its own once the schema catches up.
	 * See TODO.md.
	 */
	function finishedOn(book: Book) {
		const value = (book.progress as { updated_at?: string } | null | undefined)?.updated_at;
		return value
			? new Date(value).toLocaleDateString(undefined, {
					day: 'numeric',
					month: 'short',
					year: 'numeric'
				})
			: null;
	}

	/**
	 * One line where the progress bar used to be — the cover already draws the
	 * bar, so this says something the bar cannot. Unread books report their
	 * length rather than announcing that they are unread.
	 */
	function readState(book: Book): { text: string; tone: 'done' | 'reading' | 'idle' } {
		const progress = book.progress;
		const pages = book.pages ?? null;

		if (progress?.completed) {
			const on = finishedOn(book);
			return { text: on ? `Finished ${on}` : 'Finished', tone: 'done' };
		}

		if (progress?.percentage) {
			// pdf_page is a real page number when the reader recorded one;
			// otherwise infer it from the percentage.
			const current = progress.pdf_page ?? (pages ? Math.round(progress.percentage * pages) : null);

			return {
				text:
					pages && current
						? `Reading · ${current} of ${pages}`
						: `Reading · ${Math.round(progress.percentage * 100)}%`,
				tone: 'reading'
			};
		}

		return { text: pages ? `${pages} pages` : '', tone: 'idle' };
	}

	function openInReader(book: Book, file: BookFile) {
		const type = getFileType(file.filename);
		if (type === 'EPUB' || type === 'PDF')
			window.open(`/book/${book.id}/read/${type.toLowerCase()}/${file.id}`, '_blank', 'noopener');
	}

	/**
	 * Once a selection exists, selecting is the primary interaction — so the
	 * whole card toggles, not just the checkbox. preventDefault also stops the
	 * links inside from navigating, since the click bubbles through them here.
	 */
	function handleCardClick(event: MouseEvent, book: Book) {
		if (!selectionState.selectionModeActive) return;
		if ((event.target as HTMLElement).closest('[data-row-control]')) return;

		event.preventDefault();
		selectionState.toggleSelection(book);
	}
</script>

<!--
	Detail cards: a browsable cover beside real metadata, sitting between the
	grid (covers, no data) and the table (data, no covers). The column count
	comes from the container rather than a breakpoint, so it reflows from one to
	three as the sidebar opens and closes.
-->
<div class="grid grid-cols-[repeat(auto-fill,minmax(330px,1fr))] gap-3.5 p-1">
	{#each books as book (book.id)}
		{@const selected = selectionState.isSelected(book.id)}
		{@const state = readState(book)}
		<!-- svelte-ignore a11y_click_events_have_key_events -->
		<!-- svelte-ignore a11y_no_static_element_interactions -->
		<div
			onclick={(event) => handleCardClick(event, book)}
			class="group flex gap-3.5 rounded-lg border bg-card p-3 transition-colors {selected
				? 'border-primary bg-accent'
				: 'border-border/60 hover:border-border'} {selectionState.selectionModeActive
				? 'cursor-pointer'
				: ''}"
		>
			<a href={resolve('/(root)/(library)/book/[bookId]', { bookId: String(book.id) })} class="shrink-0">
				<BookCover {book} height={110} />
			</a>

			<div class="flex min-w-0 flex-1 flex-col">
				<div class="flex items-start gap-2">
					<div class="min-w-0 flex-1">
						<a
							href={resolve('/(root)/(library)/book/[bookId]', { bookId: String(book.id) })}
							class="line-clamp-2 font-serif text-sm leading-snug hover:underline"
						>
							{book.title}
						</a>
						<p class="mt-0.5 truncate text-xs text-muted-foreground">{authors(book)}</p>
					</div>

					<!-- Hidden until hover, pinned once selected — as in the grid -->
					<button
						type="button"
						data-row-control
						role="checkbox"
						aria-checked={selected}
						aria-label="Select {book.title}"
						onclick={() => selectionState.toggleSelection(book)}
						class="grid size-4 shrink-0 place-items-center rounded-sm border transition-opacity {selected
							? 'border-primary bg-primary text-primary-foreground opacity-100'
							: 'border-muted-foreground opacity-0 group-hover:opacity-100 focus-visible:opacity-100'}"
					>
						{#if selected}
							<Check class="size-3" />
						{/if}
					</button>
				</div>

				<div class="mt-2 flex flex-wrap items-center gap-1">
					{#if year(book)}
						<Badge variant="secondary" class="font-mono text-[10px] tabular-nums">
							{year(book)}
						</Badge>
					{/if}
					{#if book.files.length > 0}
						<Badge variant="outline" class="font-mono text-[10px]">{formats(book)}</Badge>
					{/if}
					{#each book.tags.slice(0, TAG_LIMIT) as tag (tag.id)}
						<a
							data-row-control
							href="{resolve('/(root)/(library)/library/[libraryId]/view', {
								libraryId: String(libraryState.activeLibrary?.id ?? '')
							})}?tags={tag.id}"
							class={badgeVariants({ variant: 'secondary' })}>{tag.name}</a
						>
					{/each}
					{#if book.tags.length > TAG_LIMIT}
						<span class="font-mono text-[10px] text-muted-foreground">
							+{book.tags.length - TAG_LIMIT}
						</span>
					{/if}
				</div>

				<div class="mt-auto flex items-center gap-2 pt-2.5">
					<!-- Actions on the left, reading state anchored bottom-right -->
					<div
						data-row-control
						class="flex shrink-0 gap-1 opacity-0 transition-opacity group-hover:opacity-100"
					>
						{#if book.files.length > 0}
							<Tooltip.Provider>
								<Tooltip.Root ignoreNonKeyboardFocus>
									<Tooltip.Trigger
										class="{buttonVariants({ variant: 'ghost', size: 'icon' })} size-7"
										onclick={() => openInReader(book, book.files[0])}
									>
										<BookOpenText class="size-4" />
									</Tooltip.Trigger>
									<Tooltip.Content side="bottom"><p>Read</p></Tooltip.Content>
								</Tooltip.Root>
							</Tooltip.Provider>

							<Tooltip.Provider>
								<Tooltip.Root ignoreNonKeyboardFocus>
									<Tooltip.Trigger
										class="{buttonVariants({ variant: 'ghost', size: 'icon' })} size-7"
										onclick={() =>
											bookOps.downloadBookFile(book.id, book.files[0].id, book.files[0].filename)}
									>
										<Download class="size-4" />
									</Tooltip.Trigger>
									<Tooltip.Content side="bottom"><p>Download</p></Tooltip.Content>
								</Tooltip.Root>
							</Tooltip.Provider>
						{/if}

						<BookActionsMenu {book} class="size-7" />
					</div>

					{#if state.text}
						<span
							class="ml-auto truncate font-mono text-xs tabular-nums {state.tone === 'done'
								? 'font-semibold text-success'
								: state.tone === 'reading'
									? 'font-semibold text-flag'
									: 'text-muted-foreground'}"
						>
							{state.text}
						</span>
					{/if}
				</div>
			</div>
		</div>
	{/each}
</div>
