/**
 * Real-time sync for the shopping-list detail view.
 *
 * Subscribes to `postgres_changes` on `shopping_items` filtered by `list_id`,
 * and calls user-provided handlers for INSERT / UPDATE / DELETE. The page owns
 * the items array; this store only brokers events.
 *
 * The supabase-js client is created with `lock: passThroughLock` (see
 * $lib/supabase) so Realtime subscriptions inside a component lifecycle do not
 * deadlock on GoTrue's auth lock.
 *
 * Presence is intentionally NOT implemented here yet — see Fase 2b plan and
 * `packages/test-utils/tests/realtime-presence.test.ts` for the upstream bug
 * that currently crashes presence_diff handlers in the Realtime service.
 */
import { getSupabase } from '$lib/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js';
import type { ShoppingItem } from '@colectivo/types';

export type ItemEvent =
	| { type: 'INSERT'; row: ShoppingItem }
	| { type: 'UPDATE'; row: ShoppingItem }
	| { type: 'DELETE'; id: string };

export interface RealtimeSubscription {
	channel: RealtimeChannel;
	unsubscribe: () => Promise<void>;
}

/**
 * Subscribe to item-level changes for a single list. The caller provides a
 * handler that receives each change after the SUBSCRIBED handshake completes.
 *
 * Typical usage in a +page.svelte:
 *
 *   let items = $state<ShoppingItem[]>([]);
 *   let sub: RealtimeSubscription | null = null;
 *
 *   onMount(async () => {
 *     items = await loadItems(listId);
 *     sub = await subscribeToList(listId, (evt) => applyEvent(evt));
 *   });
 *
 *   onDestroy(async () => {
 *     await sub?.unsubscribe();
 *   });
 */
export async function subscribeToList(
	listId: string,
	onEvent: (evt: ItemEvent) => void
): Promise<RealtimeSubscription> {
	const supabase = getSupabase();
	const channel = supabase.channel(`list:${listId}:items`);

	channel.on(
		// @ts-expect-error — supabase-js types require a literal-union event,
		// but the broader filter options are runtime strings.
		'postgres_changes',
		{
			event: '*',
			schema: 'public',
			table: 'shopping_items',
			filter: `list_id=eq.${listId}`
		},
		(payload: {
			eventType: 'INSERT' | 'UPDATE' | 'DELETE';
			new: ShoppingItem;
			old: ShoppingItem | { id: string };
		}) => {
			if (payload.eventType === 'INSERT') {
				onEvent({ type: 'INSERT', row: payload.new });
			} else if (payload.eventType === 'UPDATE') {
				onEvent({ type: 'UPDATE', row: payload.new });
			} else if (payload.eventType === 'DELETE') {
				// With REPLICA IDENTITY FULL the OLD row contains all columns,
				// including the id. Without it we'd only have PK.
				onEvent({ type: 'DELETE', id: (payload.old as { id: string }).id });
			}
		}
	);

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

	return {
		channel,
		async unsubscribe() {
			await channel.unsubscribe();
			await supabase.removeChannel(channel);
		}
	};
}

/**
 * Apply an ItemEvent to an items array, idempotently. INSERT skips rows that
 * already exist (optimistic local insert has already placed them), UPDATE
 * replaces by id, DELETE removes by id.
 *
 * Returns the next items array. Callers should treat it as immutable input
 * (do not mutate the return value in place).
 */
export function applyItemEvent(items: ShoppingItem[], evt: ItemEvent): ShoppingItem[] {
	switch (evt.type) {
		case 'INSERT': {
			if (items.some((i) => i.id === evt.row.id)) return items;
			return [...items, evt.row];
		}
		case 'UPDATE': {
			let changed = false;
			const next = items.map((i) => {
				if (i.id !== evt.row.id) return i;
				// last-write-wins: remote wins if its checked_at is newer or different
				// from the local one. For other fields we always take the remote.
				changed = true;
				return { ...i, ...evt.row };
			});
			return changed ? next : items;
		}
		case 'DELETE': {
			const next = items.filter((i) => i.id !== evt.id);
			return next.length === items.length ? items : next;
		}
	}
}
