<script lang="ts">
	import { SvelteSet } from "svelte/reactivity";
	import { now as tidNow } from "@atcute/tid";
	import { putReaction, deleteReaction } from "$lib/api/reaction";
	import { getAuth } from "$lib/auth.svelte";
	import type { ReactionRecord } from "$lib/api/records";
	import Reaction from "./Reaction.svelte";
	import ReactionPicker from "./ReactionPicker.svelte";
	import {
		withViewerReaction,
		withoutViewerReaction,
		upsertViewerReaction,
		type ReactionGroup,
		type ReactionKind
	} from "./reactions";

	interface Props {
		reactions: ReactionGroup[];
		subjectUri: string;
		class?: string;
		// controlled mode: report changes up instead of keeping them only in local state
		onchange?: (next: ReactionGroup[]) => void;
		showPicker?: boolean;
		// keep the bar mounted even with no reactions yet
		alwaysShow?: boolean;
	}

	let {
		reactions,
		subjectUri,
		class: className = "",
		onchange,
		showPicker = true,
		alwaysShow = false
	}: Props = $props();

	const auth = getAuth();

	// seeded from the prop, reassigned optimistically, reset by svelte on fresh reactions
	let groups = $derived(reactions);
	// kinds with an in-flight PDS write, disabled until it settles
	const pending = new SvelteSet<ReactionKind>();

	const commit = (next: ReactionGroup[]) => {
		groups = next;
		onchange?.(next);
	};

	const setPending = (kind: ReactionKind, on: boolean) => {
		if (on) pending.add(kind);
		else pending.delete(kind);
	};

	const toggle = async (group: ReactionGroup) => {
		const agent = auth.agent;
		const viewer = auth.currentUser;
		if (!agent || !viewer || pending.has(group.kind)) return;

		setPending(group.kind, true);
		try {
			if (group.isReacted) {
				if (group.viewerRkey) await deleteReaction(agent, group.viewerRkey);
				commit(
					groups
						.map((g) => (g.kind === group.kind ? withoutViewerReaction(g, viewer.handle) : g))
						.filter((g) => g.count > 0)
				);
			} else {
				const rkey = tidNow();
				const record: ReactionRecord = {
					$type: "sh.tangled.feed.reaction",
					subject: subjectUri as ReactionRecord["subject"],
					reaction: group.kind,
					createdAt: new Date().toISOString()
				};
				await putReaction(agent, rkey, record);
				commit(
					groups.map((g) =>
						g.kind === group.kind ? withViewerReaction(g, viewer.handle, rkey) : g
					)
				);
			}
		} catch {
			// leave the rendered state untouched on failure; a reload reconciles it
		} finally {
			setPending(group.kind, false);
		}
	};

	const canReact = $derived(auth.currentUser !== null);

	// kinds the viewer reacted with, mapped to their record's rkey so the picker can toggle
	const reacted = $derived(
		new Map(
			groups
				.filter((g) => g.isReacted && g.viewerRkey)
				.map((g) => [g.kind, g.viewerRkey!] as [ReactionKind, string])
		)
	);

	const onreacted = (kind: ReactionKind, rkey: string) => {
		const viewer = auth.currentUser;
		if (!viewer) return;
		commit(upsertViewerReaction(groups, kind, viewer.handle, rkey));
	};

	const onunreacted = (kind: ReactionKind) => {
		const viewer = auth.currentUser;
		if (!viewer) return;
		commit(
			groups
				.map((g) => (g.kind === kind ? withoutViewerReaction(g, viewer.handle) : g))
				.filter((g) => g.count > 0)
		);
	};

	const pickerAlways = $derived(alwaysShow && canReact && showPicker);
</script>

{#if groups.length || pickerAlways}
	<div class={`flex flex-wrap items-center gap-2 ${className}`}>
		{#each groups as reaction (reaction.kind)}
			<Reaction
				{reaction}
				ontoggle={canReact ? () => toggle(reaction) : undefined}
				pending={pending.has(reaction.kind)}
			/>
		{/each}
		{#if canReact && showPicker}
			<ReactionPicker {subjectUri} {reacted} {onreacted} {onunreacted} />
		{/if}
	</div>
{/if}
