/**
 * Section visibility / feature flags — client-side mirror of
 * `public.section_enabled()` (Fase 12).
 *
 * Why mirror instead of round-trip per render: every navigation tile, redirect
 * guard, and tab-bar grid recompute would otherwise issue one PostgREST call
 * per section per render — three orders of magnitude too noisy. The server
 * function stays the source of truth (RLS-guarded writes still go through it
 * indirectly via the `users.feature_flags` / `collectives.feature_flags`
 * columns); this store just resolves the same precedence locally.
 *
 * Precedence (mirrors migration 023):
 *   collective override > user override > default true
 * Fase 13 will add a server layer on top; this file plus the SQL function
 * are the two places to touch.
 *
 * Persistence layer:
 *   - `currentUserFeatures`  — { feature_flags: FeatureFlags } for the
 *     logged-in user, populated by root `+layout.svelte`.
 *   - `currentCollective`    — already exposes `feature_flags` after the
 *     `loadUserCollectives` extension in this fase.
 *
 * Realtime: this module subscribes to the relevant rows so an admin in
 * another tab can flip a collective flag and the current tab reacts
 * within a second. The subscriptions are keyed off the active user / active
 * collective and torn down when those change (one channel each at a time).
 */
import { writable, derived, get } from 'svelte/store';
import type { RealtimeChannel } from '@supabase/supabase-js';
import type { FeatureFlags, SectionKey } from '@colectivo/types';
import { SECTION_KEYS } from '@colectivo/types';
import { getSupabase } from '$lib/supabase';
import { currentUser } from '$lib/stores/auth';
import { currentCollective } from '$lib/stores/collective';

// ── Stores ──────────────────────────────────────────────────────────────────

/**
 * The logged-in user's feature_flags row. Populated by root layout's
 * `loadCurrentUserFeatures()` after sign-in / token-refresh, and kept fresh
 * by the realtime subscription below. `null` until the first load resolves.
 */
export const currentUserFeatures = writable<{ feature_flags: FeatureFlags } | null>(null);

/**
 * Derived map { section → boolean } applying the same precedence as
 * `public.section_enabled()`. Layers, MOST restrictive wins:
 *   1. collective.feature_flags[section]   (admin override)
 *   2. user.feature_flags[section]         (per-user override)
 *   3. default true
 *
 * Unknown section keys are not part of SECTION_KEYS so they are never queried
 * here — that path is reserved for SQL-side callers (migrations 13+).
 */
export const enabledSections = derived(
	[currentUserFeatures, currentCollective],
	([$user, $collective]) => {
		const userFlags = ($user?.feature_flags ?? {}) as FeatureFlags;
		const collectiveFlags = ($collective?.feature_flags ?? {}) as FeatureFlags;
		const out: Record<SectionKey, boolean> = {} as Record<SectionKey, boolean>;
		for (const key of SECTION_KEYS) {
			const collectiveVal = collectiveFlags[key];
			const userVal = userFlags[key];
			out[key] = collectiveVal ?? userVal ?? true;
		}
		return out;
	}
);

/**
 * Lower-cardinality derived view: just the SectionKeys that are enabled,
 * preserved in the SECTION_KEYS order. Cheap to iterate in nav lists.
 *
 * Edge case: if every layer answers OFF for every section, force "lists" ON
 * (the user must always have somewhere to land). This is the only client-side
 * hard override and matches plan §12.3.4.
 */
export const enabledSectionList = derived(enabledSections, ($flags) => {
	const visible = SECTION_KEYS.filter((k) => $flags[k]);
	if (visible.length === 0) return ['lists' as SectionKey];
	return visible;
});

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

/**
 * Write the calling user's per-section flag. Patches the existing JSONB row
 * so we don't clobber other sections.
 */
export async function setUserFeature(section: SectionKey, value: boolean): Promise<void> {
	const user = get(currentUser);
	if (!user) return;

	const current = get(currentUserFeatures);
	const next: FeatureFlags = { ...(current?.feature_flags ?? {}), [section]: value };

	// Optimistic local update — realtime echo will deduplicate.
	currentUserFeatures.set({ feature_flags: next });

	const { error } = await getSupabase()
		.from('users')
		.update({ feature_flags: next })
		.eq('id', user.id);

	if (error) {
		// Roll back on failure so the toggle reflects ground truth.
		currentUserFeatures.set(current);
		throw error;
	}
}

/**
 * Admin-only: write the active collective's per-section flag. Patches the
 * existing JSONB row. RLS (collectives_update) returns 0 rows updated for
 * non-admins; we surface that as a thrown Error so the UI can show a hint
 * instead of silently no-op-ing.
 */
export async function setCollectiveFeature(
	section: SectionKey,
	value: boolean
): Promise<void> {
	const collective = get(currentCollective);
	if (!collective) return;

	const currentFlags = (collective.feature_flags ?? {}) as FeatureFlags;
	const next: FeatureFlags = { ...currentFlags, [section]: value };

	// Optimistic local update for the active collective in both stores.
	const optimistic = { ...collective, feature_flags: next };
	currentCollective.set(optimistic);

	const { data, error } = await getSupabase()
		.from('collectives')
		.update({ feature_flags: next })
		.eq('id', collective.id)
		.select('id, feature_flags');

	if (error) {
		currentCollective.set(collective);
		throw error;
	}
	if (!data || data.length === 0) {
		// RLS denied — restore.
		currentCollective.set(collective);
		throw new Error('forbidden');
	}
}

// ── Realtime ────────────────────────────────────────────────────────────────

let userChannel: RealtimeChannel | null = null;
let userChannelUserId: string | null = null;
let collectiveChannel: RealtimeChannel | null = null;
let collectiveChannelId: string | null = null;

/**
 * Initial load + realtime subscription for the logged-in user's row. Called
 * by root `+layout.svelte` after the auth callback resolves a session.
 *
 * Per CLAUDE.md gotcha "loadUserCollectives ... inside onAuthStateChange must
 * be deferred" — callers MUST already have done the setTimeout(..., 0)
 * unwrap. We do not re-defer here.
 */
export async function loadCurrentUserFeatures(userId: string): Promise<void> {
	const supabase = getSupabase();
	const { data } = await supabase
		.from('users')
		.select('feature_flags')
		.eq('id', userId)
		.single();

	currentUserFeatures.set({
		feature_flags: ((data?.feature_flags ?? {}) as FeatureFlags) || {}
	});

	if (userChannelUserId !== userId) {
		await teardownUserChannel();
		userChannelUserId = userId;
		const channel = supabase.channel(`features:user:${userId}`);
		channel.on(
			'postgres_changes',
			{
				event: 'UPDATE',
				schema: 'public',
				table: 'users',
				filter: `id=eq.${userId}`
			} as never,
			((payload: { new: { feature_flags?: FeatureFlags | null } }) => {
				const next = (payload.new?.feature_flags ?? {}) as FeatureFlags;
				currentUserFeatures.set({ feature_flags: next });
			}) as never
		);
		await subscribeWithTimeout(channel, `features:user:${userId}`);
		userChannel = channel;
	}
}

/**
 * Realtime subscription for the active collective's row. Called by root
 * layout whenever `$currentCollective.id` changes.
 */
export async function subscribeCollectiveFeatures(collectiveId: string): Promise<void> {
	const supabase = getSupabase();
	if (collectiveChannelId === collectiveId) return;
	await teardownCollectiveChannel();
	collectiveChannelId = collectiveId;
	const channel = supabase.channel(`features:collective:${collectiveId}`);
	channel.on(
		'postgres_changes',
		{
			event: 'UPDATE',
			schema: 'public',
			table: 'collectives',
			filter: `id=eq.${collectiveId}`
		} as never,
		((payload: {
			new: { id: string; name: string; emoji: string; created_at: string; feature_flags?: FeatureFlags | null };
		}) => {
			const incoming = payload.new;
			const flags = (incoming.feature_flags ?? {}) as FeatureFlags;
			// Patch the active-collective store if the event refers to it.
			const active = get(currentCollective);
			if (active && active.id === incoming.id) {
				currentCollective.set({
					...active,
					name: incoming.name,
					emoji: incoming.emoji,
					feature_flags: flags
				});
			}
		}) as never
	);
	await subscribeWithTimeout(channel, `features:collective:${collectiveId}`);
	collectiveChannel = channel;
}

export async function teardownFeatureSubscriptions(): Promise<void> {
	await teardownUserChannel();
	await teardownCollectiveChannel();
	currentUserFeatures.set(null);
}

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

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

async function subscribeWithTimeout(channel: RealtimeChannel, label: string): Promise<void> {
	await new Promise<void>((resolve, reject) => {
		const timer = setTimeout(() => reject(new Error(`${label} 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(`${label} subscribe failed: ${status}`));
			}
		});
	});
}
