<!--
	Multi-select metadata panel. Mirrors mule-image's RightSidebar bulk mode:
	apply the same Note / Date / Keyword to every selected photo.

	Apply-button-driven (not blur-on-edit) so the user controls when the
	mutation fans out — accidental focus loss won't rewrite N photos.
-->
<script lang="ts">
	import { useQueryClient } from '@tanstack/svelte-query';
	import { toast } from 'svelte-sonner';
	import { Calendar, Star, Tag } from 'lucide-svelte';
	import {
		buildTakenAtPatch,
		bulkSetMarks,
		isValidISODate,
		type PhotoMark,
		type PhotoMarksMap,
		type UpdatePhotoBody
	} from '$lib/services/photoprism';
	import { patchTargets, invalidateFacets } from '$lib/services/bulk';
	import { startBulk, doneBulk, failBulk } from '$lib/stores/bulkAction.svelte';
	import { COLOR_SWATCHES } from '$lib/utils/tagGroups';

	const qc = useQueryClient();

	interface Props {
		ids: string[];
	}
	let { ids }: Props = $props();

	let noteDraft = $state('');
	let dateDraft = $state('');
	let keywordDraft = $state('');
	// `null` = nothing picked yet; `0` / `''` = explicit clear.
	let ratingDraft = $state<number | null>(null);
	let colorDraft = $state<string | null>(null);
	let busy = $state(false);

	// `label` drives the per-photo tile overlay (pending → done / error) via the
	// shared bulkAction store, so metadata applies show the same progress state
	// as the archive/keep actions in BulkActionBar.
	async function withBusy<T>(fn: () => Promise<T>, label?: string): Promise<T> {
		busy = true;
		if (label) startBulk(`${label}…`, ids);
		try {
			const result = await fn();
			if (label) doneBulk(label, ids);
			return result;
		} catch (e) {
			if (label) failBulk(ids);
			throw e;
		} finally {
			busy = false;
		}
	}

	async function applyNote() {
		if (busy) return;
		const value = noteDraft;
		const label = value ? `Note → ${ids.length}` : `Cleared note on ${ids.length}`;
		await withBusy(
			() =>
				patchTargets(
					ids,
					{ Caption: value, CaptionSrc: 'manual' },
					label,
					(p) => ({ Caption: p.Caption ?? '', CaptionSrc: 'manual' })
				),
			label
		);
		noteDraft = '';
	}

	const dateDraftValid = $derived(dateDraft === '' || isValidISODate(dateDraft));
	async function applyDate() {
		if (busy || !dateDraft || !isValidISODate(dateDraft)) return;
		// Date-only input — stamp midnight UTC and let PhotoPrism's backwrite
		// fill the local timezone field downstream.
		const iso = `${dateDraft}T00:00:00Z`;
		const label = `Date → ${ids.length}`;
		await withBusy(
			() =>
				patchTargets(
					ids,
					buildTakenAtPatch(iso),
					label,
					(p) =>
						p.TakenAt
							? buildTakenAtPatch(p.TakenAt)
							: ({ TakenSrc: '' } as UpdatePhotoBody)
				),
			label
		);
		dateDraft = '';
	}

	async function applyMarks(patch: PhotoMark, label: string) {
		if (busy) return;
		const tid = toast.loading(`${label}…`);
		startBulk(`${label}…`, ids);
		await withBusy(async () => {
			qc.setQueryData<PhotoMarksMap>(['marks'], (prev) => {
				const map = { ...(prev ?? {}) };
				for (const id of ids) {
					const merged: PhotoMark = { ...(map[id] ?? {}), ...patch };
					if (!merged.rating) delete merged.rating;
					if (!merged.color) delete merged.color;
					if (merged.rating == null && !merged.color) delete map[id];
					else map[id] = merged;
				}
				return map;
			});
			try {
				await bulkSetMarks(ids, patch);
				doneBulk(label, ids);
				// Refresh the Colors / Ratings facet panels — they sit on
				// `['marks']` + `['photos','marks-pool']`, not the optimistic write above.
				invalidateFacets();
				toast.success(`${label} · ${ids.length}`, { id: tid });
			} catch (err) {
				failBulk(ids);
				toast.error(err instanceof Error ? err.message : 'Save failed', { id: tid });
				void qc.invalidateQueries({ queryKey: ['marks'] });
			}
		});
	}

	async function applyRating() {
		if (ratingDraft === null) return;
		const value = ratingDraft;
		await applyMarks({ rating: value }, value === 0 ? 'Cleared score' : `★ ${value}`);
		ratingDraft = null;
	}

	async function applyColor() {
		if (colorDraft === null) return;
		const value = colorDraft;
		await applyMarks({ color: value }, value ? `Color ${value}` : 'Cleared color');
		colorDraft = null;
	}

	async function applyKeyword() {
		if (busy) return;
		const kw = keywordDraft.trim().replace(/,/g, '');
		if (!kw) return;
		keywordDraft = '';
		const label = `Tagged "${kw}" → ${ids.length}`;
		await withBusy(
			() =>
				patchTargets(
					ids,
					(p) => {
						const cur = (p.Details?.Keywords ?? '')
							.split(',')
							.map((k) => k.trim())
							.filter(Boolean);
						if (cur.includes(kw)) return {};
						const next = [...cur, kw].join(', ');
						return { Details: { Keywords: next, KeywordsSrc: 'manual' } };
					},
					label,
					(p) => ({
						Details: { Keywords: p.Details?.Keywords ?? '', KeywordsSrc: 'manual' }
					})
				),
			label
		);
	}

	function onKeywordKeydown(e: KeyboardEvent) {
		if (e.key === 'Enter' || e.key === ',') {
			e.preventDefault();
			void applyKeyword();
		}
	}
</script>

<aside class="space-y-4 p-3 text-xs">
	<header class="border-b border-border pb-2">
		<div class="text-sm font-medium text-foreground">{ids.length} selected</div>
		<p class="mt-0.5 text-[10px] text-muted-foreground">
			Edits apply to every selected photo.
		</p>
	</header>

	<!-- Note (Caption) -->
	<section class="space-y-1">
		<div
			class="flex items-center justify-between text-[10px] uppercase tracking-wide text-muted-foreground"
		>
			<span>Note</span>
			<span class="font-normal normal-case text-muted-foreground/70">
				Overwrites each photo
			</span>
		</div>
		<textarea
			rows="3"
			placeholder="Add a note for all selected…"
			class="w-full resize-y rounded border border-input bg-background px-1.5 py-1 text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
			bind:value={noteDraft}
			disabled={busy}
		></textarea>
		<button
			class="w-full rounded-md border border-border px-2 py-1 text-xs hover:bg-accent disabled:opacity-50"
			disabled={busy}
			onclick={applyNote}
		>
			Apply note to {ids.length}
		</button>
	</section>

	<!-- Date (TakenAt) -->
	<section class="space-y-1">
		<div
			class="flex items-center gap-1 text-[10px] uppercase tracking-wide text-muted-foreground"
		>
			<Calendar class="h-3 w-3" /> Date taken
		</div>
		<input
			type="text"
			inputmode="numeric"
			placeholder="YYYY-MM-DD"
			pattern="\d{4}-\d{2}-\d{2}"
			aria-invalid={!dateDraftValid}
			class="w-full rounded border border-input bg-background px-1.5 py-1 text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-ring aria-invalid:border-destructive aria-invalid:text-destructive aria-invalid:focus:ring-destructive"
			bind:value={dateDraft}
			disabled={busy}
		/>
		<button
			class="w-full rounded-md border border-border px-2 py-1 text-xs hover:bg-accent disabled:opacity-50"
			disabled={busy || !dateDraft || !dateDraftValid}
			onclick={applyDate}
		>
			Apply date to {ids.length}
		</button>
	</section>

	<!-- Score (rating) — pick a value with the stars, then Apply. The "Clear"
	     button picks `0` so the Apply step explicitly wipes the score across
	     the selection. Stored on the mule-sidecar; PhotoPrism's PUT can't
	     persist these. -->
	<section class="space-y-1">
		<div class="text-[10px] uppercase tracking-wide text-muted-foreground">Score</div>
		<div class="flex items-center gap-0.5" role="group" aria-label="Score">
			{#each [1, 2, 3, 4, 5] as n (n)}
				<button
					type="button"
					class="p-0.5 transition-colors disabled:opacity-50"
					class:text-yellow-400={ratingDraft !== null && ratingDraft >= n}
					class:text-muted-foreground={!(ratingDraft !== null && ratingDraft >= n)}
					disabled={busy}
					onclick={() => (ratingDraft = n)}
					title={`Pick ★ ${n}`}
					aria-label={`Score ${n}`}
				>
					<Star
						class="h-4 w-4"
						fill={ratingDraft !== null && ratingDraft >= n ? 'currentColor' : 'none'}
					/>
				</button>
			{/each}
			<button
				type="button"
				class="ml-1 rounded px-1 text-[10px] text-muted-foreground hover:bg-accent disabled:opacity-50"
				class:bg-accent={ratingDraft === 0}
				class:text-foreground={ratingDraft === 0}
				disabled={busy}
				onclick={() => (ratingDraft = 0)}
				title="Pick: clear score"
			>
				✕
			</button>
		</div>
		<button
			class="w-full rounded-md border border-border px-2 py-1 text-xs hover:bg-accent disabled:opacity-50"
			disabled={busy || ratingDraft === null}
			onclick={applyRating}
		>
			{#if ratingDraft === null}
				Pick a score
			{:else if ratingDraft === 0}
				Clear score on {ids.length}
			{:else}
				Apply ★ {ratingDraft} to {ids.length}
			{/if}
		</button>
	</section>

	<!-- Colors — same pattern as Score. -->
	<section class="space-y-1">
		<div class="text-[10px] uppercase tracking-wide text-muted-foreground">Colors</div>
		<div class="flex flex-wrap items-center gap-1.5" role="group" aria-label="Colors">
			{#each COLOR_SWATCHES as c (c.key)}
				{@const picked = colorDraft === c.key}
				<button
					type="button"
					class="h-4 w-4 rounded-full border-2 transition-all disabled:opacity-50 {c.border} {picked
						? c.bg
						: 'bg-transparent'}"
					aria-pressed={picked}
					disabled={busy}
					onclick={() => (colorDraft = c.key)}
					title={`Pick ${c.title}`}
					aria-label={`Color ${c.key}`}
				></button>
			{/each}
			<button
				type="button"
				class="ml-0.5 rounded px-1 text-[10px] text-muted-foreground hover:bg-accent disabled:opacity-50"
				class:bg-accent={colorDraft === ''}
				class:text-foreground={colorDraft === ''}
				disabled={busy}
				onclick={() => (colorDraft = '')}
				title="Pick: clear color"
			>
				✕
			</button>
		</div>
		<button
			class="w-full rounded-md border border-border px-2 py-1 text-xs hover:bg-accent disabled:opacity-50"
			disabled={busy || colorDraft === null}
			onclick={applyColor}
		>
			{#if colorDraft === null}
				Pick a color
			{:else if colorDraft === ''}
				Clear color on {ids.length}
			{:else}
				Apply {colorDraft} to {ids.length}
			{/if}
		</button>
	</section>

	<!-- Keywords (additive — merge into each photo's existing list) -->
	<section class="space-y-1">
		<div
			class="flex items-center gap-1 text-[10px] uppercase tracking-wide text-muted-foreground"
		>
			<Tag class="h-3 w-3" /> Add keyword
		</div>
		<input
			type="text"
			placeholder="tag name + Enter"
			class="w-full rounded border border-input bg-background px-1.5 py-1 text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
			bind:value={keywordDraft}
			disabled={busy}
			onkeydown={onKeywordKeydown}
		/>
		<button
			class="w-full rounded-md border border-border px-2 py-1 text-xs hover:bg-accent disabled:opacity-50"
			disabled={busy || !keywordDraft.trim()}
			onclick={applyKeyword}
		>
			Add to {ids.length}
		</button>
		<p class="text-[10px] text-muted-foreground/80">
			Adds to existing keywords; doesn't replace them.
		</p>
	</section>

	{#if busy}
		<div class="text-[10px] text-muted-foreground">Applying…</div>
	{/if}
</aside>
