/**
 * Reactive Svelte 5 stores for nodes, edges, access og mixer-kanaler.
 *
 * Bruker Svelte 5 runes ($state) for finkornet reaktivitet.
 * Fylles fra portvokterens WebSocket (initial_sync + inkrementelle events).
 *
 * Usage:
 *   import { nodeStore, edgeStore } from '$lib/realtime/stores.svelte';
 *
 *   // I en .svelte-komponent:
 *   const nodes = $derived(nodeStore.all);
 *   const myEdges = $derived(edgeStore.bySource('user-123'));
 */

import type {
	Node,
	Edge,
	NodeAccess,
	MixerChannel,
	WsNodeChanged,
	WsEdgeChanged,
	WsAccessChanged,
	WsMixerChannelChanged,
	WsInitialSync,
} from './types';

// ---------------------------------------------------------------------------
// Node store
// ---------------------------------------------------------------------------

let _nodes = $state<Map<string, Node>>(new Map());
let _nodeVersion = $state(0);

function createNodeStore() {
	return {
		get all(): Node[] {
			void _nodeVersion;
			return [..._nodes.values()];
		},

		get count(): number {
			void _nodeVersion;
			return _nodes.size;
		},

		get(id: string): Node | undefined {
			void _nodeVersion;
			return _nodes.get(id);
		},

		byKind(kind: string): Node[] {
			void _nodeVersion;
			return [..._nodes.values()].filter((n) => n.nodeKind === kind);
		},

		byCreator(userId: string): Node[] {
			void _nodeVersion;
			return [..._nodes.values()].filter((n) => n.createdBy === userId);
		},

		_handleEvent(msg: WsNodeChanged) {
			if (msg.op === 'DELETE') {
				_nodes.delete(msg.id);
			} else if (msg.node) {
				_nodes.set(msg.id, msg.node);
			}
			_nodeVersion++;
		},

		_loadInitial(nodes: Node[]) {
			_nodes = new Map();
			for (const n of nodes) {
				_nodes.set(n.id, n);
			}
			_nodeVersion++;
		},

		_clear() {
			_nodes = new Map();
			_nodeVersion++;
		},
	};
}

export const nodeStore = createNodeStore();

// ---------------------------------------------------------------------------
// Edge store
// ---------------------------------------------------------------------------

let _edges = $state<Map<string, Edge>>(new Map());
let _edgeVersion = $state(0);
let _edgesBySource = $state<Map<string, Set<string>>>(new Map());
let _edgesByTarget = $state<Map<string, Set<string>>>(new Map());

function addToIndex(index: Map<string, Set<string>>, key: string, edgeId: string) {
	let set = index.get(key);
	if (!set) {
		set = new Set();
		index.set(key, set);
	}
	set.add(edgeId);
}

function removeFromIndex(index: Map<string, Set<string>>, key: string, edgeId: string) {
	const set = index.get(key);
	if (set) {
		set.delete(edgeId);
		if (set.size === 0) index.delete(key);
	}
}

function indexEdge(edge: Edge) {
	addToIndex(_edgesBySource, edge.sourceId, edge.id);
	addToIndex(_edgesByTarget, edge.targetId, edge.id);
}

function unindexEdge(edge: Edge) {
	removeFromIndex(_edgesBySource, edge.sourceId, edge.id);
	removeFromIndex(_edgesByTarget, edge.targetId, edge.id);
}

function createEdgeStore() {
	return {
		get all(): Edge[] {
			void _edgeVersion;
			return [..._edges.values()];
		},

		get count(): number {
			void _edgeVersion;
			return _edges.size;
		},

		get(id: string): Edge | undefined {
			void _edgeVersion;
			return _edges.get(id);
		},

		bySource(sourceId: string): Edge[] {
			void _edgeVersion;
			const ids = _edgesBySource.get(sourceId);
			if (!ids) return [];
			return [...ids].map((id) => _edges.get(id)!).filter(Boolean);
		},

		byTarget(targetId: string): Edge[] {
			void _edgeVersion;
			const ids = _edgesByTarget.get(targetId);
			if (!ids) return [];
			return [...ids].map((id) => _edges.get(id)!).filter(Boolean);
		},

		byType(edgeType: string): Edge[] {
			void _edgeVersion;
			return [..._edges.values()].filter((e) => e.edgeType === edgeType);
		},

		between(nodeA: string, nodeB: string): Edge[] {
			void _edgeVersion;
			return [..._edges.values()].filter(
				(e) =>
					(e.sourceId === nodeA && e.targetId === nodeB) ||
					(e.sourceId === nodeB && e.targetId === nodeA),
			);
		},

		_handleEvent(msg: WsEdgeChanged) {
			if (msg.op === 'DELETE') {
				const old = _edges.get(msg.id);
				if (old) unindexEdge(old);
				_edges.delete(msg.id);
			} else if (msg.edge) {
				const old = _edges.get(msg.id);
				if (old) unindexEdge(old);
				_edges.set(msg.id, msg.edge);
				indexEdge(msg.edge);
			}
			_edgeVersion++;
		},

		_loadInitial(edges: Edge[]) {
			_edges = new Map();
			_edgesBySource = new Map();
			_edgesByTarget = new Map();
			for (const e of edges) {
				_edges.set(e.id, e);
				indexEdge(e);
			}
			_edgeVersion++;
		},

		_clear() {
			_edges = new Map();
			_edgesBySource = new Map();
			_edgesByTarget = new Map();
			_edgeVersion++;
		},
	};
}

export const edgeStore = createEdgeStore();

// ---------------------------------------------------------------------------
// NodeAccess store
// ---------------------------------------------------------------------------

let _access = $state<Map<string, NodeAccess>>(new Map());
let _accessVersion = $state(0);
let _accessBySubject = $state<Map<string, Set<string>>>(new Map());

function createNodeAccessStore() {
	return {
		objectsForSubject(subjectId: string): Set<string> {
			void _accessVersion;
			return _accessBySubject.get(subjectId) ?? new Set();
		},

		hasAccess(subjectId: string, objectId: string): boolean {
			void _accessVersion;
			const key = `${subjectId}:${objectId}`;
			return _access.has(key);
		},

		getAccess(subjectId: string, objectId: string): string | undefined {
			void _accessVersion;
			const key = `${subjectId}:${objectId}`;
			return _access.get(key)?.access;
		},

		get count(): number {
			void _accessVersion;
			return _access.size;
		},

		_handleEvent(msg: WsAccessChanged) {
			const key = `${msg.subject_id}:${msg.object_id}`;
			if (msg.op === 'DELETE') {
				_access.delete(key);
				const set = _accessBySubject.get(msg.subject_id);
				if (set) {
					set.delete(msg.object_id);
					if (set.size === 0) _accessBySubject.delete(msg.subject_id);
				}
			} else if (msg.row) {
				_access.set(key, msg.row);
				let set = _accessBySubject.get(msg.subject_id);
				if (!set) {
					set = new Set();
					_accessBySubject.set(msg.subject_id, set);
				}
				set.add(msg.object_id);
			}
			_accessVersion++;
		},

		_loadInitial(rows: NodeAccess[]) {
			_access = new Map();
			_accessBySubject = new Map();
			for (const row of rows) {
				const key = `${row.subjectId}:${row.objectId}`;
				_access.set(key, row);
				let set = _accessBySubject.get(row.subjectId);
				if (!set) {
					set = new Set();
					_accessBySubject.set(row.subjectId, set);
				}
				set.add(row.objectId);
			}
			_accessVersion++;
		},

		_clear() {
			_access = new Map();
			_accessBySubject = new Map();
			_accessVersion++;
		},
	};
}

export const nodeAccessStore = createNodeAccessStore();

// ---------------------------------------------------------------------------
// MixerChannel store
// ---------------------------------------------------------------------------

let _mixerChannels = $state<Map<string, MixerChannel>>(new Map());
let _mixerVersion = $state(0);
let _mixerByRoom = $state<Map<string, Set<string>>>(new Map());

function createMixerChannelStore() {
	return {
		get all(): MixerChannel[] {
			void _mixerVersion;
			return [..._mixerChannels.values()];
		},

		get(id: string): MixerChannel | undefined {
			void _mixerVersion;
			return _mixerChannels.get(id);
		},

		byRoom(roomId: string): MixerChannel[] {
			void _mixerVersion;
			const ids = _mixerByRoom.get(roomId);
			if (!ids) return [];
			return [...ids].map((id) => _mixerChannels.get(id)!).filter(Boolean);
		},

		byParticipant(roomId: string, userId: string): MixerChannel | undefined {
			void _mixerVersion;
			return _mixerChannels.get(`${roomId}:${userId}`);
		},

		get count(): number {
			void _mixerVersion;
			return _mixerChannels.size;
		},

		_handleEvent(msg: WsMixerChannelChanged) {
			const id = `${msg.room_id}:${msg.target_user_id}`;
			if (msg.op === 'DELETE') {
				_mixerChannels.delete(id);
				const set = _mixerByRoom.get(msg.room_id);
				if (set) {
					set.delete(id);
					if (set.size === 0) _mixerByRoom.delete(msg.room_id);
				}
			} else if (msg.channel) {
				_mixerChannels.set(id, msg.channel);
				let set = _mixerByRoom.get(msg.room_id);
				if (!set) {
					set = new Set();
					_mixerByRoom.set(msg.room_id, set);
				}
				set.add(id);
			}
			_mixerVersion++;
		},

		_loadInitial(channels: MixerChannel[]) {
			_mixerChannels = new Map();
			_mixerByRoom = new Map();
			for (const ch of channels) {
				const id = ch.id || `${ch.roomId}:${ch.targetUserId}`;
				_mixerChannels.set(id, ch);
				let set = _mixerByRoom.get(ch.roomId);
				if (!set) {
					set = new Set();
					_mixerByRoom.set(ch.roomId, set);
				}
				set.add(id);
			}
			_mixerVersion++;
		},

		_clear() {
			_mixerChannels = new Map();
			_mixerByRoom = new Map();
			_mixerVersion++;
		},
	};
}

export const mixerChannelStore = createMixerChannelStore();

// ---------------------------------------------------------------------------
// Visibility filter
// ---------------------------------------------------------------------------

export function nodeVisibility(
	node: Node,
	userId: string | undefined,
): 'full' | 'discoverable' | 'hidden' {
	if (!userId) {
		if (node.visibility === 'readable' || node.visibility === 'open') return 'full';
		if (node.visibility === 'discoverable') return 'discoverable';
		return 'hidden';
	}

	if (node.createdBy === userId) return 'full';
	if (nodeAccessStore.hasAccess(userId, node.id)) return 'full';

	for (const edge of edgeStore.bySource(node.id)) {
		if (edge.edgeType === 'belongs_to') {
			const parent = nodeStore.get(edge.targetId);
			if (parent && (parent.createdBy === userId || nodeAccessStore.hasAccess(userId, parent.id))) {
				return 'full';
			}
		}
	}

	if (node.visibility === 'readable' || node.visibility === 'open') return 'full';
	if (node.visibility === 'discoverable') return 'discoverable';

	return 'hidden';
}

// ---------------------------------------------------------------------------
// WebSocket-meldingshåndtering
// ---------------------------------------------------------------------------

/**
 * Prosesser en initial_sync-melding og fyll alle stores.
 */
export function handleInitialSync(msg: WsInitialSync) {
	nodeStore._loadInitial(msg.nodes);
	edgeStore._loadInitial(msg.edges);
	nodeAccessStore._loadInitial(msg.access);
	mixerChannelStore._loadInitial(msg.mixer_channels ?? []);
}

/**
 * Tøm alle stores (kalles ved disconnect).
 */
export function clearAllStores() {
	nodeStore._clear();
	edgeStore._clear();
	nodeAccessStore._clear();
	mixerChannelStore._clear();
}
