<script lang="ts">
	import { page } from '$app/state';
	import { createQuery, useQueryClient } from '@tanstack/svelte-query';
	import { toast } from 'svelte-sonner';
	import {
		addToHeap,
		approvePhoto,
		batchArchive,
		batchDelete,
		batchRestore,
		listHeaps,
		removeFromHeap,
		type PpAlbum
	} from '$lib/services/photoprism';
	import { batchEdit } from '$lib/services/batch';
	import { acceptDateAndKeep, cachedPhoto } from '$lib/services/photoActions';
	import { suggestDateFromPath } from '$lib/utils/suggestDateFromPath';
	import { photoNameAndDir } from '$lib/types/photoprism';
	import {
		clearBulkToFirst,
		clearSelection,
		focusAfter,
		selection,
		setFocused
	} from '$lib/stores/selection.svelte';
	import { filters } from '$lib/stores/filters.svelte';
	import { push as pushUndo } from '$lib/stores/undo.svelte';
	import { isAuthenticated } from '$lib/stores/session.svelte';
	import {
		startBulk,
		setDetail,
		doneBulk,
		failBulk,
		markRemoved,
		clearRemoved
	} from '$lib/stores/bulkAction.svelte';
	import { EmptyState, InlineLoader } from '$lib/components/feedback';
	import { Layers } from 'lucide-svelte';

	const qc = useQueryClient();
	let busy = $state(false);
	let heapPickerOpen = $state(false);

	const heapsQuery = createQuery<PpAlbum[]>(() => ({
		queryKey: ['heaps'],
		queryFn: listHeaps,
		enabled: isAuthenticated()
	}));

	/**
	 * Targets of an action: the multi-selected set when one exists, else the
	 * focused tile alone. Mule-image's design treats focus as "implicit single
	 * selection" so the bar's actions always have something to operate on.
	 */
	function snapshotIds(): string[] {
		if (selection.ids.size > 0) return Array.from(selection.ids);
		if (selection.focused) return [selection.focused];
		return [];
	}

	const targetCount = $derived(
		selection.ids.size > 0 ? selection.ids.size : selection.focused ? 1 : 0
	);
	const isBulk = $derived(selection.ids.size > 0);
	// Filename for the single-focus label. Re-derives whenever
	// selection.focused flips — cachedPhoto reads from the same query
	// cache that drives the visible tiles, so the name resolves on the
	// same tick the tile renders.
	const focusedPhoto = $derived(selection.focused ? cachedPhoto(selection.focused) : undefined);
	const focusedName = $derived(
		focusedPhoto ? photoNameAndDir(focusedPhoto).fileName : ''
	);
	// Review section uses a two-button decision flow (Keep / Archive) —
	// every other action is hidden so the choice can't be confused with
	// heap-adding / restoring. The S keybinding is rerouted to approve
	// from gridKeyNav for the same reason.
	const isReview = $derived(filters.section === 'review');
	// "Accept date & Keep" is scoped to the EXIF Stripped review tab —
	// that's where path-derived dates are the most useful fix. Outside the
	// tab the button stays hidden even if a selected photo would otherwise
	// have a path-parseable date, to keep other tabs uncluttered.
	const onExifStrippedTab = $derived(
		isReview && page.url.searchParams.get('tab') === 'stripped_exif'
	);
	// Surface the button only when EVERY targeted photo has a derivable
	// suggestion — otherwise clicking it would silently approve some
	// photos without a date fix, which contradicts the verb. A uid not in
	// any cache also counts as "no suggestion" so we don't promise
	// something we can't verify.
	const allHaveSuggestion = $derived.by(() => {
		if (!onExifStrippedTab) return false;
		const ids =
			selection.ids.size > 0
				? Array.from(selection.ids)
				: selection.focused
					? [selection.focused]
					: [];
		if (ids.length === 0) return false;
		for (const id of ids) {
			const p = cachedPhoto(id);
			if (!p) return false;
			const { fileName, path } = photoNameAndDir(p);
			if (!suggestDateFromPath({ fileName, originalName: p.OriginalName, path })) {
				return false;
			}
		}
		return true;
	});
	// Archive section is the parallel two-button flow: Keep (restore back
	// to the timeline) or Delete (permanent, no undo). X is repurposed
	// from "archive" to "delete" since the photo is already archived;
	// gridKeyNav mirrors the rerouting.
	const isArchive = $derived(filters.section === 'archive');

	function clearAll() {
		// Mirror gridKeyNav's Esc: a multi-selection collapses back to its
		// first member (the user keeps a single-focus reference) before
		// the next Clear/Esc fully dismisses focus.
		if (clearBulkToFirst()) return;
		clearSelection();
		setFocused(null);
	}

	const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));

	interface BulkConfig {
		ids: string[];
		label: string;
		doneLabel: string;
	}

	async function withBusy<T>(fn: () => Promise<T>, bulk?: BulkConfig): Promise<T> {
		busy = true;
		if (bulk) startBulk(`${bulk.label}…`, bulk.ids);
		try {
			const result = await fn();
			if (bulk) {
				doneBulk(bulk.doneLabel, bulk.ids);
				await delay(1000);
			}
			return result;
		} catch (e) {
			if (bulk) failBulk(bulk.ids);
			throw e;
		} finally {
			busy = false;
			const settled = Promise.all([
				qc.invalidateQueries({ queryKey: ['photos'] }),
				qc.invalidateQueries({ queryKey: ['marks'] }),
				qc.invalidateQueries({ queryKey: ['review-groups'] })
			]);
			// Clear the optimistic-removal overlay only once the refetch has
			// landed, so tiles never flash back in before the fresh (archived-
			// filtered) page replaces the old one.
			if (bulk) void settled.then(() => clearRemoved(bulk.ids));
		}
	}

	async function onApprove() {
		const ids = snapshotIds();
		if (ids.length === 0) return;
		const tid = toast.loading(`Keeping ${ids.length}…`);
		await withBusy(async () => {
			const { updated, errors } = await batchEdit(ids, (id) => approvePhoto(id), {
				onProgress: (_done, _total, completedId) => {
					const p = cachedPhoto(completedId);
					setDetail(p?.FileName ?? completedId);
				}
			});
			if (errors.length) {
				toast.error(`Kept ${updated.length}; ${errors.length} failed`, { id: tid });
			} else {
				toast.success(`Kept ${ids.length}`, { id: tid });
			}
			// Approved photos leave the review section — hide them immediately.
			markRemoved(ids);
			focusAfter(ids);
			clearSelection();
		}, { ids, label: 'Keeping', doneLabel: `Kept ${ids.length}` });
	}

	async function onAcceptDateAndKeep() {
		const ids = snapshotIds();
		if (ids.length === 0) return;
		await withBusy(() => acceptDateAndKeep(ids), {
			ids,
			label: 'Updating',
			doneLabel: `Updated ${ids.length}`
		});
	}

	async function onArchive() {
		const ids = snapshotIds();
		if (ids.length === 0) return;
		const tid = toast.loading(`Archiving ${ids.length}…`);
		await withBusy(async () => {
			try {
				await batchArchive(ids);
				markRemoved(ids);
				pushUndo(`Archived ${ids.length}`, async () => {
					await batchRestore(ids);
					void qc.invalidateQueries({ queryKey: ['photos'] });
				});
				focusAfter(ids);
				clearSelection();
				toast.success(`Archived ${ids.length}`, { id: tid });
			} catch (err) {
				toast.error(err instanceof Error ? err.message : 'Archive failed', { id: tid });
			}
		}, { ids, label: 'Archiving', doneLabel: `Archived ${ids.length}` });
	}

	async function onDelete() {
		const ids = snapshotIds();
		if (ids.length === 0) return;
		const msg =
			ids.length === 1
				? 'Permanently delete this photo? This cannot be undone.'
				: `Permanently delete ${ids.length} photos? This cannot be undone.`;
		if (!confirm(msg)) return;
		const tid = toast.loading(`Deleting ${ids.length}…`);
		await withBusy(async () => {
			try {
				await batchDelete(ids);
				markRemoved(ids);
				focusAfter(ids);
				clearSelection();
				toast.success(`Deleted ${ids.length}`, { id: tid });
			} catch (err) {
				toast.error(err instanceof Error ? err.message : 'Delete failed', { id: tid });
			}
		}, { ids, label: 'Deleting', doneLabel: `Deleted ${ids.length}` });
	}

	async function onRestore() {
		const ids = snapshotIds();
		if (ids.length === 0) return;
		const tid = toast.loading(`Restoring ${ids.length}…`);
		await withBusy(async () => {
			try {
				await batchRestore(ids);
				markRemoved(ids);
				pushUndo(`Restored ${ids.length}`, async () => {
					await batchArchive(ids);
					void qc.invalidateQueries({ queryKey: ['photos'] });
				});
				focusAfter(ids);
				clearSelection();
				toast.success(`Restored ${ids.length}`, { id: tid });
			} catch (err) {
				toast.error(err instanceof Error ? err.message : 'Restore failed', { id: tid });
			}
		}, { ids, label: 'Restoring', doneLabel: `Restored ${ids.length}` });
	}

	async function onAddToHeap(heap: PpAlbum) {
		const ids = snapshotIds();
		if (!ids.length) return;
		heapPickerOpen = false;
		const tid = toast.loading(`Adding ${ids.length} → ${heap.Title}…`);
		startBulk(`Adding to ${heap.Title}…`, ids);
		await withBusy(async () => {
			try {
				const { added } = await addToHeap(heap.UID, ids);
				qc.invalidateQueries({ queryKey: ['heaps'] });
				if (added.length === 0) {
					failBulk(ids);
					toast.error(`Nothing added to ${heap.Title}`, {
						id: tid,
						description: `The server rejected all ${ids.length} UIDs (already in heap, or not indexed).`
					});
					return;
				}
				doneBulk(`Added ${added.length} → ${heap.Title}`, ids);
				await delay(400);
				if (added.length < ids.length) {
					toast.success(`Added ${added.length}/${ids.length} → ${heap.Title}`, {
						id: tid,
						description: 'The rest were already in this heap.'
					});
				} else {
					toast.success(`Added ${added.length} → ${heap.Title}`, { id: tid });
				}
				pushUndo(`Added ${added.length} to ${heap.Title}`, async () => {
					await removeFromHeap(heap.UID, added);
					qc.invalidateQueries({ queryKey: ['heaps'] });
				});
				clearSelection();
			} catch (err) {
				failBulk(ids);
				toast.error(err instanceof Error ? err.message : 'Add-to-heap failed', { id: tid });
			}
		});
	}
</script>

{#if targetCount > 0}
	<!--
		Inline row at the bottom of the main content column (NOT fixed) so
		the sidebars stay reachable. Matches the Toolbar's h-9 / px-3 /
		bg-background/80 backdrop-blur visual so it reads as the timeline's
		own footer.
	-->
	<div
		class="flex min-h-9 shrink-0 items-center gap-2 border-t border-border bg-background px-3 py-1"
	>
		{#if isBulk}
			<span class="shrink-0 text-[11px] font-medium text-foreground">
				{targetCount} selected
			</span>
		{:else}
			<span class="shrink-0 text-[11px] font-medium text-muted-foreground">Focused</span>
			<span
				class="min-w-0 truncate text-[11px] font-medium text-foreground"
				title={focusedName || undefined}
			>
				{focusedName || 'photo'}
			</span>
		{/if}

		<!--
			`overflow-x-auto` would clip the heap-picker dropdown — CSS
			forces `overflow-y: auto` whenever `overflow-x` is non-visible,
			so the dropdown's `bottom-full` placement is clipped to zero
			pixels above the 36px bar (it renders but is invisible). We
			use `flex-wrap` instead so very narrow viewports get a second
			row rather than a horizontal scroll, and the dropdown stays
			free to escape upward.
		-->
		<div class="ml-auto flex min-w-0 flex-wrap items-center justify-end gap-1">
			{#if isReview}
				<!-- Review pile = binary decision. Keep approves (Quality →
				     3+, lands in the main timeline); Archive batches into
				     the archive section. Everything else (heap, restore)
				     is hidden so the choice reads as decisive. -->
				<button
					class="inline-flex items-center gap-1 rounded bg-primary px-2 py-0.5 text-[11px] font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
					disabled={busy}
					onclick={onApprove}
					title="Keep — accept into timeline"
				>
					✓ Keep
					<kbd class="rounded bg-primary-foreground/15 px-1 text-[9px] font-medium text-primary-foreground/90">S</kbd>
				</button>
				{#if allHaveSuggestion}
					<!-- Visible only when every selected photo has a path-
					     derivable date. Clicking applies each photo's
					     suggestion then approves it; mirrored by the bare
					     `a` shortcut in gridKeyNav. -->
					<button
						class="inline-flex items-center gap-1 rounded border border-amber-400/60 bg-amber-100/40 px-2 py-0.5 text-[11px] text-amber-800 hover:bg-amber-100 disabled:opacity-50 dark:border-amber-400/40 dark:bg-amber-500/15 dark:text-amber-200 dark:hover:bg-amber-500/25"
						disabled={busy}
						onclick={onAcceptDateAndKeep}
						title="Accept the date suggested from the file/folder path, then keep"
					>
						📅 Accept date & Keep
						<kbd class="rounded bg-amber-200/40 px-1 text-[9px] font-medium text-amber-900 dark:bg-amber-500/30 dark:text-amber-100">A</kbd>
					</button>
				{/if}
				<button
					class="inline-flex items-center gap-1 rounded border border-border bg-background px-2 py-0.5 text-[11px] hover:bg-accent disabled:opacity-50"
					disabled={busy}
					onclick={onArchive}
					title="Archive"
				>
					Archive
					<kbd class="rounded bg-muted px-1 text-[9px] font-medium text-muted-foreground">X</kbd>
				</button>
			{:else if isArchive}
				<!-- Archive section = mirror of review: Keep restores back
				     to the timeline; Delete is permanent and can't be
				     undone. X is repurposed from archive→delete since the
				     photo is already archived; the destructive styling
				     reinforces the irreversibility. -->
				<button
					class="inline-flex items-center gap-1 rounded bg-primary px-2 py-0.5 text-[11px] font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
					disabled={busy}
					onclick={onRestore}
					title="Keep — restore to timeline"
				>
					✓ Keep
					<kbd class="rounded bg-primary-foreground/15 px-1 text-[9px] font-medium text-primary-foreground/90">S</kbd>
				</button>
				<button
					class="inline-flex items-center gap-1 rounded border border-destructive/40 bg-destructive/5 px-2 py-0.5 text-[11px] text-destructive hover:bg-destructive/10 disabled:opacity-50"
					disabled={busy}
					onclick={onDelete}
					title="Permanently delete (no undo)"
				>
					Delete
					<kbd class="rounded bg-muted px-1 text-[9px] font-medium text-muted-foreground">X</kbd>
				</button>
			{:else}
				<div class="relative">
					<button
						class="inline-flex items-center gap-1 rounded bg-primary px-2 py-0.5 text-[11px] font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
						disabled={busy}
						onclick={() => (heapPickerOpen = !heapPickerOpen)}
						title="Add to heap (S then 1–9 picks a heap)"
					>
						＋ Add to heap
						<kbd class="rounded bg-primary-foreground/15 px-1 text-[9px] font-medium text-primary-foreground/90"
							>S&nbsp;N</kbd
						>
					</button>
					{#if heapPickerOpen}
						<div
							class="absolute bottom-full right-0 mb-2 max-h-72 w-56 overflow-y-auto rounded-md border border-border bg-background p-1 text-xs shadow-lg"
						>
							{#if heapsQuery.isPending}
								<InlineLoader size="sm" label="Loading heaps…" />
							{:else if (heapsQuery.data ?? []).length === 0}
								<EmptyState size="compact" icon={Layers} title="No heaps yet" />
							{:else}
								{#each heapsQuery.data ?? [] as heap, i (heap.UID)}
									<button
										class="flex w-full items-center gap-2 rounded px-2 py-1 text-left hover:bg-accent"
										onclick={() => onAddToHeap(heap)}
									>
										{#if i < 9}
											<kbd
												class="shrink-0 rounded bg-muted px-1 py-0.5 text-[9px] font-medium text-muted-foreground"
												title={`S ${i + 1}`}
											>
												{i + 1}
											</kbd>
										{:else}
											<span class="w-3 shrink-0"></span>
										{/if}
										<span class="flex-1 truncate">{heap.Title}</span>
										<span class="shrink-0 text-muted-foreground">
											({heap.PhotoCount ?? 0})
										</span>
									</button>
								{/each}
							{/if}
						</div>
					{/if}
				</div>
				<button
					class="inline-flex items-center gap-1 rounded border border-border bg-background px-2 py-0.5 text-[11px] hover:bg-accent disabled:opacity-50"
					disabled={busy}
					onclick={onArchive}
					title="Archive"
				>
					Archive
					<kbd class="rounded bg-muted px-1 text-[9px] font-medium text-muted-foreground">X</kbd>
				</button>
			{/if}
			<button
				class="inline-flex items-center gap-1 rounded px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
				onclick={clearAll}
				title={isBulk ? 'Clear selection' : 'Clear focus'}
			>
				{isBulk ? 'Clear' : 'Dismiss'}
				<kbd class="rounded bg-muted px-1 text-[9px] font-medium text-muted-foreground">Esc</kbd>
			</button>
		</div>
	</div>
{/if}
