/**
 * Tag store — collective-scoped item tags (Fase 11).
 *
 * Loads + caches the tags for the active collective and subscribes to
 * `item_tags` postgres-changes so concurrent rename / create / delete from
 * another tab or device propagates without a reload.
 *
 * Per CLAUDE.md "Supabase JS v2 + SvelteKit deadlock" gotcha: this store is
 * created from `onMount`-style component lifecycles, never from inside
 * `onAuthStateChange`. The Realtime subscription itself does not call
 * `getAccessToken()`, so the lock-defer pattern is not needed here.
 */
import { writable, get } from 'svelte/store';
import { getSupabase } from '$lib/supabase';
import type { ItemTag, ItemTagColor } from '@colectivo/types';
import type { RealtimeChannel } from '@supabase/supabase-js';

export const tags = writable<ItemTag[]>([]);
export const tagsLoading = writable(false);

let activeChannel: RealtimeChannel | null = null;
let activeCollectiveId: string | null = null;

export async function loadTags(collectiveId: string): Promise<void> {
	tagsLoading.set(true);
	try {
		const { data, error } = await getSupabase()
			.from('item_tags')
			.select('*')
			.eq('collective_id', collectiveId)
			.order('name', { ascending: true });

		if (!error && data) {
			tags.set(data as ItemTag[]);
		}
	} finally {
		tagsLoading.set(false);
	}

	// Re-subscribe whenever the active collective changes. Cheap: one channel
	// per collective + tab.
	if (activeCollectiveId !== collectiveId) {
		await unsubscribe();
		activeCollectiveId = collectiveId;
		await subscribe(collectiveId);
	}
}

async function subscribe(collectiveId: string): Promise<void> {
	const channel = getSupabase().channel(`tags:${collectiveId}`);

	channel.on(
		// @ts-expect-error — postgres_changes is a runtime string event
		'postgres_changes',
		{
			event: '*',
			schema: 'public',
			table: 'item_tags',
			filter: `collective_id=eq.${collectiveId}`
		},
		(payload: {
			eventType: 'INSERT' | 'UPDATE' | 'DELETE';
			new: ItemTag;
			old: ItemTag | { id: string };
		}) => {
			if (payload.eventType === 'INSERT') {
				tags.update((arr) =>
					arr.some((t) => t.id === payload.new.id) ? arr : sortByName([...arr, payload.new])
				);
			} else if (payload.eventType === 'UPDATE') {
				tags.update((arr) =>
					sortByName(arr.map((t) => (t.id === payload.new.id ? payload.new : t)))
				);
			} else {
				const id = (payload.old as { id: string }).id;
				tags.update((arr) => arr.filter((t) => t.id !== id));
			}
		}
	);

	await new Promise<void>((resolve, reject) => {
		const timer = setTimeout(() => reject(new Error('tags channel subscribe timeout')), 10_000);
		channel.subscribe((status) => {
			if (status === 'SUBSCRIBED') {
				clearTimeout(timer);
				resolve();
			} else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT' || status === 'CLOSED') {
				clearTimeout(timer);
				reject(new Error(`tags channel subscribe failed: ${status}`));
			}
		});
	});

	activeChannel = channel;
}

async function unsubscribe(): Promise<void> {
	if (!activeChannel) return;
	const supabase = getSupabase();
	try {
		await activeChannel.unsubscribe();
		await supabase.removeChannel(activeChannel);
	} catch {
		// idempotent cleanup
	}
	activeChannel = null;
}

export async function teardownTags(): Promise<void> {
	await unsubscribe();
	activeCollectiveId = null;
	tags.set([]);
}

function sortByName(arr: ItemTag[]): ItemTag[] {
	return [...arr].sort((a, b) => a.name.localeCompare(b.name));
}

// ── Mutations ─────────────────────────────────────────────────────────────────

export async function createTag(
	collectiveId: string,
	name: string,
	color: ItemTagColor = 'slate'
): Promise<ItemTag | null> {
	const trimmed = name.trim();
	if (!trimmed) return null;

	const { data, error } = await getSupabase()
		.from('item_tags')
		.insert({ collective_id: collectiveId, name: trimmed, color })
		.select()
		.single();

	if (error || !data) return null;
	const row = data as ItemTag;
	// Optimistic local insert — the Realtime echo is deduped by id.
	tags.update((arr) => (arr.some((t) => t.id === row.id) ? arr : sortByName([...arr, row])));
	return row;
}

export async function renameTag(id: string, name: string): Promise<void> {
	const trimmed = name.trim();
	if (!trimmed) return;
	tags.update((arr) => sortByName(arr.map((t) => (t.id === id ? { ...t, name: trimmed } : t))));
	await getSupabase().from('item_tags').update({ name: trimmed }).eq('id', id);
}

export async function recolorTag(id: string, color: ItemTagColor): Promise<void> {
	tags.update((arr) => arr.map((t) => (t.id === id ? { ...t, color } : t)));
	await getSupabase().from('item_tags').update({ color }).eq('id', id);
}

export async function deleteTag(id: string): Promise<void> {
	tags.update((arr) => arr.filter((t) => t.id !== id));
	await getSupabase().from('item_tags').delete().eq('id', id);
}

// ── Attach / detach on a shopping_item ───────────────────────────────────────

export async function attachTag(itemId: string, tagId: string): Promise<boolean> {
	const { error } = await getSupabase()
		.from('shopping_item_tags')
		.insert({ item_id: itemId, tag_id: tagId });
	return !error;
}

export async function detachTag(itemId: string, tagId: string): Promise<boolean> {
	const { error } = await getSupabase()
		.from('shopping_item_tags')
		.delete()
		.eq('item_id', itemId)
		.eq('tag_id', tagId);
	return !error;
}

// ── Local-only helpers ───────────────────────────────────────────────────────

export function findTagByName(name: string): ItemTag | undefined {
	const lookup = name.trim().toLowerCase();
	return get(tags).find((t) => t.name.toLowerCase() === lookup);
}
