<script lang="ts">
	import { untrack } from 'svelte';
	import { ArrowRight, GitMerge, Plus, Undo2 } from '@lucide/svelte';

	import * as Dialog from '$lib/components/ui/dialog/index.js';
	import { Button } from '$lib/components/ui/button/index.js';
	import { Input } from '$lib/components/ui/input/index.js';
	import { Textarea } from '$lib/components/ui/textarea/index.js';
	import { Badge } from '$lib/components/ui/badge/index.js';
	import BookImage from '$lib/components/view/book-image.svelte';
	import GeneratedCover from '$lib/components/view/generated-cover.svelte';
	import type { Book } from '$lib/schema';

	import { mergeInto } from './merge';

	import {
		MERGE_FIELDS,
		applyAction,
		displayValue,
		isEmpty,
		isSame,
		readField,
		type FieldAction,
		type FieldSpec,
		type FieldValue
	} from './field-spec';

	let {
		books,
		libraryId,
		open = $bindable(),
		onmerged
	}: {
		books: Book[];
		libraryId: number | string;
		open: boolean;
		/** Given the record that survived and the ones folded into it and deleted. */
		onmerged?: (survivor: Book, folded: Book[]) => void;
	} = $props();

	// Seeded once per mount. The dialog is keyed on the group upstream, so a
	// different group gets a fresh workbench rather than the previous one's draft.
	let survivorId = $state(untrack(() => books[0]?.id));
	let candidateId = $state(untrack(() => books[1]?.id));
	let draft = $state<Record<string, FieldValue>>({});
	let busy = $state(false);

	const survivor = $derived(books.find((book) => book.id === survivorId) ?? books[0]);
	const candidates = $derived(books.filter((book) => book.id !== survivorId));
	/** The records that will be deleted — the same set, named for what happens to them. */
	const folded = $derived(candidates);
	const candidate = $derived(candidates.find((book) => book.id === candidateId) ?? candidates[0]);

	/** The survivor's stored value for a field, or the draft if it has been touched. */
	function current(field: FieldSpec): FieldValue {
		return field.key in draft ? draft[field.key] : readField(survivor, field.key);
	}

	function take(field: FieldSpec, action: FieldAction) {
		draft[field.key] = applyAction(
			action,
			field.kind,
			current(field),
			readField(candidate, field.key)
		);
	}

	function undo(field: FieldSpec) {
		delete draft[field.key];
		draft = { ...draft };
	}

	/**
	 * Fill only the fields the survivor has nothing in.
	 *
	 * The safe bulk action, and the one worth reaching for: it cannot overwrite a
	 * value, so it needs no per-field protection to be pressed without reading.
	 */
	function fillEmpty() {
		for (const field of MERGE_FIELDS) {
			const incoming = readField(candidate, field.key);
			if (isEmpty(current(field)) && !isEmpty(incoming)) draft[field.key] = incoming;
		}
	}

	const changed = $derived(Object.keys(draft));

	const differing = $derived(
		candidate
			? MERGE_FIELDS.filter((field) => !isSame(current(field), readField(candidate, field.key)))
			: []
	);

	/** Fields shown as a row: anything the two disagree on, plus anything edited. */
	const shown = $derived(
		MERGE_FIELDS.filter((field) => differing.includes(field) || field.key in draft)
	);

	const agreed = $derived(MERGE_FIELDS.filter((field) => !shown.includes(field)));

	const groups = $derived([...new Set(shown.map((field) => field.group))]);

	async function submit() {
		if (busy) return;
		busy = true;

		const merged = await mergeInto(libraryId, survivor, folded, changed.length ? draft : undefined);

		busy = false;

		if (merged) {
			open = false;
			onmerged?.(survivor, folded);
		}
	}
</script>

{#snippet cover(book: Book, size: string)}
	<span class="{size} shrink-0 overflow-hidden rounded-sm border bg-muted">
		{#if book.cover_image}
			<BookImage src="/api/{book.cover_image}" class="h-full w-full object-cover" />
		{:else}
			<!-- Drawn rather than left blank, so the rail tells two coverless books
			     apart the same way the shelves do. -->
			<GeneratedCover {book} />
		{/if}
	</span>
{/snippet}

<!-- The inert reference column: the same shape as the control opposite it, with
     nothing that invites a click. -->
{#snippet reference(field: FieldSpec, book: Book)}
	{@const value = readField(book, field.key)}
	<div class="flex min-w-0 flex-col gap-1">
		<span class="text-[11px] text-muted-foreground">{field.label}</span>
		{#if isEmpty(value)}
			<span class="min-h-8 py-1 text-sm text-muted-foreground italic">empty</span>
		{:else if field.kind === 'list'}
			<span class="flex min-h-8 flex-wrap items-center gap-1 py-0.5">
				{#each value as string[] as item (item)}
					<Badge variant="secondary" class="font-normal">{item}</Badge>
				{/each}
			</span>
		{:else}
			<span class="min-h-8 py-1 text-sm break-words">{displayValue(value)}</span>
		{/if}
	</div>
{/snippet}

<Dialog.Root bind:open>
	<Dialog.Content
		class="flex h-[85vh] max-w-[min(72rem,95vw)] flex-col gap-0 overflow-hidden p-0 sm:max-w-[min(72rem,95vw)]"
	>
		<Dialog.Header class="shrink-0 space-y-0 border-b px-5 py-3 text-left">
			<Dialog.Title class="font-serif text-base font-normal">
				Merge {books.length} books
			</Dialog.Title>
			<Dialog.Description class="text-xs">
				{candidates.length}
				{candidates.length === 1 ? 'record is' : 'records are'} deleted. Their files move onto the book
				you keep — nothing is removed from disk.
			</Dialog.Description>
		</Dialog.Header>

		<div class="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[15rem_1fr]">
			<!-- Rail: the books being folded in, one open at a time -->
			<aside
				class="flex min-w-0 flex-col overflow-y-auto border-b bg-sidebar md:border-r md:border-b-0"
			>
				<p
					class="px-3 pt-3 pb-1 font-mono text-[10px] tracking-widest text-muted-foreground uppercase"
				>
					Taking from · {candidates.length}
				</p>
				{#each candidates as book (book.id)}
					{@const count = MERGE_FIELDS.filter(
						(field) => !isSame(readField(survivor, field.key), readField(book, field.key))
					).length}
					<button
						type="button"
						onclick={() => (candidateId = book.id)}
						class="flex items-start gap-2 border-l-2 px-3 py-2 text-left transition-colors hover:bg-muted/50 {book.id ===
						candidate?.id
							? 'border-l-primary bg-background'
							: 'border-l-transparent'}"
					>
						{@render cover(book, 'h-10 w-7')}
						<span class="min-w-0 flex-1">
							<span class="line-clamp-2 font-serif text-[13px]">{book.title}</span>
							<span class="block text-[10px] text-muted-foreground">
								#{book.id} · {book.files.length}
								{book.files.length === 1 ? 'file' : 'files'}
							</span>
							{#if count === 0}
								<Badge variant="secondary" class="mt-1 text-[10px] font-normal">
									nothing to take
								</Badge>
							{/if}
						</span>
					</button>
				{/each}
			</aside>

			<div class="flex min-h-0 flex-col">
				<!-- Which record survives -->
				<div class="flex shrink-0 flex-wrap items-center gap-2 border-b px-5 py-2.5">
					<span class="text-[11px] text-muted-foreground">Keeping</span>
					{#each books as book (book.id)}
						<button
							type="button"
							onclick={() => (survivorId = book.id)}
							class="flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs transition-colors {book.id ===
							survivorId
								? 'border-primary bg-accent text-accent-foreground'
								: 'text-muted-foreground hover:bg-muted'}"
						>
							{@render cover(book, 'h-6 w-4')}
							<span class="max-w-32 truncate">#{book.id}</span>
						</button>
					{/each}

					<div class="ml-auto flex gap-2">
						<Button variant="outline" size="sm" onclick={fillEmpty} disabled={!candidate}>
							Fill empty fields
						</Button>
					</div>
				</div>

				<div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
					{#if !candidate}
						<p class="text-sm text-muted-foreground">Nothing left to fold in.</p>
					{:else if shown.length === 0}
						<p class="text-sm text-muted-foreground">
							These records agree on every field. Merging keeps
							<span class="font-medium text-foreground">#{survivor.id}</span> and moves the others' files
							onto it.
						</p>
					{:else}
						{#each groups as group (group)}
							<h3
								class="mt-5 border-b pb-1 font-mono text-[10px] tracking-widest text-primary uppercase first:mt-0"
							>
								{group}
							</h3>

							{#each shown.filter((field) => field.group === group) as field (field.key)}
								{@const value = current(field)}
								{@const incoming = readField(candidate, field.key)}
								{@const edited = field.key in draft}
								<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-3 border-b py-2">
									{@render reference(field, candidate)}

									<!-- Actions, on the row rather than stacked beside it -->
									<div class="flex items-center gap-1">
										{#if edited}
											<Button
												variant="ghost"
												size="icon"
												class="size-7"
												title="Undo"
												onclick={() => undo(field)}
											>
												<Undo2 class="size-3.5" />
												<span class="sr-only">Undo {field.label}</span>
											</Button>
										{/if}

										{#if !isSame(value, incoming)}
											{#if isEmpty(value)}
												<!-- Nothing to weigh, so it is an offer rather than a choice -->
												<Button
													size="sm"
													class="h-7 gap-1 px-2 text-[11px]"
													onclick={() => take(field, 'replace')}
												>
													<ArrowRight class="size-3.5" />
													Take
												</Button>
											{:else}
												<Button
													variant="outline"
													size="icon"
													class="size-7"
													title="Replace"
													onclick={() => take(field, 'replace')}
												>
													<ArrowRight class="size-3.5" />
													<span class="sr-only">Replace {field.label}</span>
												</Button>
												{#each field.extra as action (action)}
													<Button
														variant="outline"
														size="icon"
														class="size-7"
														title={action === 'merge' ? 'Merge' : 'Append'}
														onclick={() => take(field, action)}
													>
														{#if action === 'merge'}
															<GitMerge class="size-3.5" />
														{:else}
															<Plus class="size-3.5" />
														{/if}
														<span class="sr-only">{action} {field.label}</span>
													</Button>
												{/each}
											{/if}
										{/if}
									</div>

									<!-- The survivor's side: the edit form, live -->
									<div class="flex min-w-0 flex-col gap-1">
										<span class="flex items-center gap-1.5 text-[11px] text-muted-foreground">
											{field.label}
											{#if edited}
												<Badge class="h-4 px-1.5 text-[9px] font-semibold">taken</Badge>
											{/if}
										</span>

										{#if field.kind === 'longtext'}
											<Textarea
												rows={4}
												class="text-sm"
												value={(value as string) ?? ''}
												oninput={(event) => (draft[field.key] = event.currentTarget.value)}
											/>
										{:else if field.kind === 'list' || field.kind === 'keyed'}
											<!-- Edited through the take actions; typing here would need the
											     tags and key/value editors, which belong to the edit form. -->
											<div
												class="flex min-h-8 flex-wrap items-center gap-1 rounded-md border bg-background px-2 py-1"
											>
												{#if isEmpty(value)}
													<span class="text-sm text-muted-foreground">—</span>
												{:else if field.kind === 'list'}
													{#each value as string[] as item (item)}
														<Badge variant="secondary" class="font-normal">{item}</Badge>
													{/each}
												{:else}
													{#each Object.entries(value as Record<string, string>) as [name, id] (name)}
														<Badge variant="secondary" class="font-mono text-[10px] font-normal">
															{name}: {id}
														</Badge>
													{/each}
												{/if}
											</div>
										{:else}
											<Input
												type={field.kind === 'number'
													? 'number'
													: field.kind === 'date'
														? 'date'
														: 'text'}
												class="h-8 text-sm"
												value={(value as string | number) ?? ''}
												oninput={(event) =>
													(draft[field.key] =
														field.kind === 'number'
															? Number(event.currentTarget.value) || null
															: event.currentTarget.value)}
											/>
										{/if}
									</div>
								</div>
							{/each}
						{/each}

						{#if agreed.length > 0}
							<p class="pt-3 text-center text-[11px] text-muted-foreground italic">
								{agreed.map((field) => field.label).join(', ')} — identical in both
							</p>
						{/if}
					{/if}
				</div>
			</div>
		</div>

		<Dialog.Footer class="shrink-0 items-center gap-2 border-t px-5 py-3 sm:justify-between">
			<p class="text-xs text-muted-foreground">
				{#if changed.length}
					{changed.length}
					{changed.length === 1 ? 'change' : 'changes'} pending · this cannot be undone
				{:else}
					Metadata is left as #{survivor.id} has it · this cannot be undone
				{/if}
			</p>
			<div class="flex gap-2">
				<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
				<Button onclick={submit} disabled={busy || candidates.length === 0}>
					Merge into “{survivor.title}”
				</Button>
			</div>
		</Dialog.Footer>
	</Dialog.Content>
</Dialog.Root>
