<!--
	One cross-folder duplicate group rendered as a card. Lists every on-disk
	copy of the same byte-identical file. The user picks one to keep; the
	rest are archived to `.duplicates/<timestamp>/` via the sidecar.

	Differences from StackGroupCard (which operates on PhotoPrism Files in
	a single Photo stack):
	- These photos are NOT in PhotoPrism's DB (PhotoPrism dropped them at
	  index time). They're files on disk only.
	- Thumbnails come via `thumbUrl(hash, ...)` — content-addressed, so we
	  can render every copy from the same hash even though only one Photo
	  entry exists.
	- Resolution moves files (reversible) rather than deletes (irreversible).

	Same keyboard contract as StackGroupCard: arrows pick the keeper,
	Enter commits.
-->
<script lang="ts">
	import { useQueryClient } from '@tanstack/svelte-query';
	import { toast } from 'svelte-sonner';
	import {
		archiveDuplicatePaths,
		type CrossFolderDuplicateGroup
	} from '$lib/services/photoprism';
	import { thumbUrl } from '$lib/stores/session.svelte';
	import { view } from '$lib/stores/view.svelte';

	interface Props {
		group: CrossFolderDuplicateGroup;
		/** First-card auto-focus, same pattern as StackGroupCard. */
		autoFocus?: boolean;
	}
	let { group, autoFocus = false }: Props = $props();

	const qc = useQueryClient();
	let keep = $state('');
	let busy = $state(false);
	let sectionEl: HTMLElement | undefined = $state();
	let gridEl: HTMLElement | undefined = $state();
	let cols = $state(1);

	// Seed `keep` from the indexed path when available; that's the safest
	// default because losing it would leave PhotoPrism with no copy. Fall
	// back to the first listed path.
	$effect(() => {
		const validPaths = new Set(group.files.map((f) => f.path));
		if (!keep || !validPaths.has(keep)) {
			keep =
				group.indexedPath && validPaths.has(group.indexedPath)
					? group.indexedPath
					: group.files[0]?.path ?? '';
		}
	});

	$effect(() => {
		if (autoFocus && sectionEl) sectionEl.focus({ preventScroll: true });
	});

	// Column-count tracking — identical pattern to StackGroupCard.
	$effect(() => {
		if (!gridEl) return;
		const measure = () => {
			if (!gridEl) return;
			const n = getComputedStyle(gridEl)
				.gridTemplateColumns.split(' ')
				.filter(Boolean).length;
			cols = Math.max(1, n);
		};
		measure();
		const ro = new ResizeObserver(measure);
		ro.observe(gridEl);
		return () => ro.disconnect();
	});
	$effect(() => {
		void view.thumbnailSize;
		queueMicrotask(() => {
			if (!gridEl) return;
			const n = getComputedStyle(gridEl)
				.gridTemplateColumns.split(' ')
				.filter(Boolean).length;
			cols = Math.max(1, n);
		});
	});

	function sizeLabel(bytes: number): string {
		if (bytes > 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
		return `${Math.round(bytes / 1024)} KB`;
	}

	function shortFolder(relPath: string): string {
		const segs = relPath.split('/').filter(Boolean);
		if (segs.length <= 1) return '(root)';
		return segs.slice(0, -1).join('/');
	}

	function moveKeep(delta: number) {
		const i = group.files.findIndex((f) => f.path === keep);
		if (i < 0) return;
		const next = Math.min(Math.max(0, i + delta), group.files.length - 1);
		keep = group.files[next].path;
	}

	function onKeydown(e: KeyboardEvent) {
		if (busy) return;
		switch (e.key) {
			case 'ArrowLeft':
				e.preventDefault();
				moveKeep(-1);
				return;
			case 'ArrowRight':
				e.preventDefault();
				moveKeep(1);
				return;
			case 'ArrowUp':
				e.preventDefault();
				moveKeep(-cols);
				return;
			case 'ArrowDown':
				e.preventDefault();
				moveKeep(cols);
				return;
			case 'Enter':
				e.preventDefault();
				void commit();
				return;
			case 'Escape':
				(e.target as HTMLElement)?.blur();
				return;
		}
	}

	async function commit() {
		if (busy || group.files.length < 2) return;
		// Defensive guard: never archive the indexed copy. The user can
		// pick a different "keeper" but the archive list is computed AFTER
		// resolving that into "everything except the keeper". If they pick
		// a non-indexed copy as keeper, the indexed one gets archived —
		// PhotoPrism will lose its photo entry on the cleanup reindex.
		// That's a legitimate user choice (they wanted to move the
		// canonical copy), just call it out in the toast.
		const losers = group.files.filter((f) => f.path !== keep);
		if (losers.length === 0) return;
		const losingIndexed =
			group.indexedPath && losers.some((f) => f.path === group.indexedPath);

		busy = true;
		try {
			const result = await archiveDuplicatePaths(losers.map((f) => f.path));
			if (result.errors.length > 0) {
				toast.error(
					`Archived ${result.moved.length}; ${result.errors.length} failed`,
					{
						description: result.errors[0].error
					}
				);
			} else {
				toast.success(
					`Archived ${result.moved.length} duplicate${result.moved.length === 1 ? '' : 's'}`,
					{
						description: losingIndexed
							? 'The previously-indexed copy was moved; the indexer will drop it on the next index pass.'
							: 'Files moved to .duplicates/ inside originals.'
					}
				);
			}
			void qc.invalidateQueries({ queryKey: ['duplicates-cross-folder'] });
			void qc.invalidateQueries({ queryKey: ['photos'] });
		} catch (err) {
			toast.error(err instanceof Error ? err.message : 'Archive failed');
		} finally {
			busy = false;
		}
	}
</script>

<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
	bind:this={sectionEl}
	tabindex="0"
	role="application"
	aria-label={`Duplicate group · ${group.files.length} copies`}
	onkeydown={onKeydown}
	class="space-y-2 rounded-md border border-border bg-card/30 p-3 outline-none
	       focus-visible:ring-2 focus-visible:ring-primary/50"
>
	<header class="flex items-center justify-between gap-3">
		<div class="min-w-0">
			<div class="text-sm font-medium text-foreground">
				{group.files.length} copies · {sizeLabel(group.size)} each
			</div>
			<div class="truncate text-[10px] font-mono text-muted-foreground">
				sha1 {group.hash.slice(0, 16)}…
			</div>
		</div>
		<button
			type="button"
			class="inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs hover:bg-accent disabled:opacity-50"
			disabled={busy || group.files.length < 2}
			onclick={commit}
			title="Move the unselected copies to .duplicates/ (reversible)"
		>
			Keep selected, archive rest
			<kbd
				class="rounded bg-muted px-1 py-0.5 text-[9px] font-medium text-muted-foreground"
				>Enter</kbd
			>
		</button>
	</header>

	<div
		bind:this={gridEl}
		class="grid gap-2"
		style="grid-template-columns: repeat(auto-fill, minmax({view.thumbnailSize}px, 1fr));"
	>
		{#each group.files as file (file.path)}
			{@const isKeep = file.path === keep}
			{@const isIndexed = file.path === group.indexedPath}
			<button
				type="button"
				onclick={() => (keep = file.path)}
				class:scale-95={isKeep}
				class:ring-2={isKeep}
				class:ring-blue-500={isKeep}
				class:ring-offset-2={isKeep}
				class:ring-offset-background={isKeep}
				class:transition-[transform,box-shadow]={isKeep}
				class:duration-300={isKeep}
				class:ease-[cubic-bezier(0.34,1.56,0.64,1)]={isKeep}
				class="group flex flex-col overflow-hidden rounded-md border border-border bg-secondary p-0 text-left outline-none focus:outline-none"
			>
				<div class="relative aspect-square w-full overflow-hidden">
					<img
						src={thumbUrl(group.hash, 'tile_500')}
						alt={file.path}
						loading="lazy"
						class="h-full w-full object-cover"
					/>
					{#if isKeep}
						<span
							class="absolute left-1.5 top-1.5 rounded bg-blue-500 px-1.5 py-0.5 text-[10px] font-semibold text-white"
						>
							Keep
						</span>
					{/if}
					{#if isIndexed}
						<span
							class="absolute right-1.5 top-1.5 rounded bg-emerald-600 px-1.5 py-0.5 text-[10px] font-semibold text-white"
							title="Currently in the library"
						>
							Indexed
						</span>
					{/if}
				</div>
				<div
					class="space-y-0.5 px-2 py-1.5 text-[10px] leading-tight text-muted-foreground"
					title={file.path}
				>
					<div class="truncate text-foreground/90">{shortFolder(file.path)}</div>
					<div class="truncate font-mono">{file.path.split('/').pop()}</div>
				</div>
			</button>
		{/each}
	</div>
</div>
