import { toast } from 'svelte-sonner';
import { batchEdit } from './batch';
import { getPhoto, updatePhoto, type UpdatePhotoBody } from './photoprism';
import { queryClient } from '$lib/queryClient';
import { push as pushUndo } from '$lib/stores/undo.svelte';
import type { PpPhoto } from '$lib/types/photoprism';

/**
 * Shared helpers for bulk metadata mutations across the timeline. Three call
 * sites: the keyboard culling layer (`gridKeyNav`), the `BulkActionBar` row,
 * and the bulk metadata sidebar shown on multi-select. Each needs the same
 * "fetch full photo body → deep-merge patch → PUT → invalidate" round-trip
 * (PhotoPrism's PUT only persists nested fields when the body is whole), so
 * the wiring lives here to keep the call sites declarative.
 */

/** Fetch the freshest photo body, seeding the per-photo cache. PhotoPrism's
 * PUT needs the full body to persist `Rating` / `Color` / `Details.*`; the
 * cache lookup means the subsequent patch pass reuses this fetch. */
export async function freshPhoto(uid: string): Promise<PpPhoto> {
	const cached = queryClient.getQueryData<PpPhoto>(['photo', uid]);
	if (cached) return cached;
	const p = await getPhoto(uid);
	queryClient.setQueryData(['photo', uid], p);
	return p;
}

export function invalidatePhotos(uids: string[]): void {
	void queryClient.invalidateQueries({ queryKey: ['photos'] });
	for (const id of uids) {
		void queryClient.invalidateQueries({ queryKey: ['photo', id] });
	}
}

/**
 * Apply a patch to every uid. The patch can be a static body or a per-photo
 * function (used by keyword merges which need to read each photo's current
 * Details.Keywords before extending it). When `inverseBuilder` is provided,
 * an undo entry is registered that restores each photo's pre-patch state.
 */
export async function patchTargets(
	ids: string[],
	patch: UpdatePhotoBody | ((p: PpPhoto) => UpdatePhotoBody),
	label: string,
	inverseBuilder?: (photo: PpPhoto) => UpdatePhotoBody
): Promise<void> {
	if (ids.length === 0) return;

	const inverses = inverseBuilder
		? new Map<string, UpdatePhotoBody>(
				await Promise.all(
					ids.map(async (id) => {
						const p = await freshPhoto(id);
						return [id, inverseBuilder(p)] as const;
					})
				)
			)
		: null;

	const { updated, errors } = await batchEdit(ids, async (id) => {
		const p = await freshPhoto(id);
		const body = typeof patch === 'function' ? patch(p) : patch;
		// An empty body is a no-op signal — e.g. "keyword already present".
		if (Object.keys(body).length === 0) return p;
		return updatePhoto(p, body);
	});

	invalidatePhotos(ids);

	if (errors.length) {
		toast.error(`${label} · ${updated.length} ok, ${errors.length} failed`);
	} else {
		toast.success(`${label} · ${ids.length}`);
	}

	if (inverses) {
		pushUndo(`${label} (${ids.length})`, async () => {
			await batchEdit(ids, async (id) => {
				const p = await freshPhoto(id);
				const inv = inverses.get(id) ?? {};
				if (Object.keys(inv).length === 0) return p;
				return updatePhoto(p, inv);
			});
			invalidatePhotos(ids);
		});
	}
}
