/**
 * LiveKit client wrapper for Synops.
 *
 * Handles room connection, participant tracking, and Web Audio routing.
 * LiveKit's auto-attach of <audio> elements is disabled — all audio is
 * routed through the Web Audio API via the mixer module (mixer.ts).
 */

import {
	Room,
	RoomEvent,
	Track,
	ConnectionState,
	type RemoteTrack,
	type RemoteTrackPublication,
	type RemoteParticipant,
	type Participant,
} from 'livekit-client';

import { addChannel, removeChannel, destroyMixer, ensureAudioContext } from './mixer';

// ─── Types ──────────────────────────────────────────────────────────────────

export interface LiveKitParticipant {
	identity: string;
	displayName: string;
	isSpeaking: boolean;
	audioLevel: number;
	isMuted: boolean;
}

export type RoomStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';

// ─── State ──────────────────────────────────────────────────────────────────

let room: Room | null = null;

// Reactive state via callbacks
type StateListener = () => void;
const listeners = new Set<StateListener>();

let _status: RoomStatus = 'disconnected';
let _participants: LiveKitParticipant[] = [];
let _localIdentity: string = '';

export function getStatus(): RoomStatus { return _status; }
export function getParticipants(): LiveKitParticipant[] { return _participants; }
export function getLocalIdentity(): string { return _localIdentity; }

export function subscribe(fn: StateListener): () => void {
	listeners.add(fn);
	return () => { listeners.delete(fn); };
}

function notify() {
	for (const fn of listeners) fn();
}

function setStatus(s: RoomStatus) {
	_status = s;
	notify();
}

// ─── Participant tracking ───────────────────────────────────────────────────

function buildParticipantList(): LiveKitParticipant[] {
	if (!room) return [];
	const list: LiveKitParticipant[] = [];

	// Local participant
	const local = room.localParticipant;
	list.push({
		identity: local.identity,
		displayName: local.name || local.identity,
		isSpeaking: local.isSpeaking,
		audioLevel: local.audioLevel,
		isMuted: !local.isMicrophoneEnabled,
	});

	// Remote participants
	for (const [, p] of room.remoteParticipants) {
		list.push({
			identity: p.identity,
			displayName: p.name || p.identity,
			isSpeaking: p.isSpeaking,
			audioLevel: p.audioLevel,
			isMuted: isParticipantMuted(p),
		});
	}

	return list;
}

function isParticipantMuted(p: RemoteParticipant): boolean {
	for (const [, pub] of p.trackPublications) {
		if (pub.kind === Track.Kind.Audio) {
			return pub.isMuted;
		}
	}
	return true; // no audio track = effectively muted
}

function refreshParticipants() {
	_participants = buildParticipantList();
	notify();
}

// ─── Web Audio routing (delegated to mixer.ts) ────────────────────────────

/**
 * Route a remote participant's audio track through the mixer graph
 * instead of letting LiveKit auto-attach an <audio> element.
 */
function attachTrackToWebAudio(track: RemoteTrack, participant: RemoteParticipant) {
	if (track.kind !== Track.Kind.Audio) return;

	const mediaStream = track.mediaStream;
	if (!mediaStream) return;

	ensureAudioContext();
	addChannel(participant.identity, mediaStream);
}

function detachParticipantAudio(identity: string) {
	removeChannel(identity);
}

// ─── Room connection ────────────────────────────────────────────────────────

export async function connect(wsUrl: string, token: string): Promise<void> {
	if (room) {
		await disconnect();
	}

	const newRoom = new Room({
		// Disable auto-attach — we route audio through Web Audio API
		audioCaptureDefaults: {
			autoGainControl: true,
			echoCancellation: true,
			noiseSuppression: true,
		},
		adaptiveStream: true,
		dynacast: true,
	});

	// Set up event handlers before connecting
	newRoom
		.on(RoomEvent.Connected, () => {
			_localIdentity = newRoom.localParticipant.identity;
			setStatus('connected');
			refreshParticipants();
		})
		.on(RoomEvent.Reconnecting, () => {
			setStatus('reconnecting');
		})
		.on(RoomEvent.Reconnected, () => {
			setStatus('connected');
			refreshParticipants();
		})
		.on(RoomEvent.Disconnected, () => {
			cleanupAudio();
			setStatus('disconnected');
			_participants = [];
			notify();
		})
		.on(RoomEvent.ParticipantConnected, () => {
			refreshParticipants();
		})
		.on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {
			detachParticipantAudio(participant.identity);
			refreshParticipants();
		})
		.on(RoomEvent.TrackSubscribed, (track: RemoteTrack, pub: RemoteTrackPublication, participant: RemoteParticipant) => {
			attachTrackToWebAudio(track, participant);
			refreshParticipants();
		})
		.on(RoomEvent.TrackUnsubscribed, (_track: RemoteTrack, _pub: RemoteTrackPublication, participant: RemoteParticipant) => {
			detachParticipantAudio(participant.identity);
			refreshParticipants();
		})
		.on(RoomEvent.TrackMuted, () => {
			refreshParticipants();
		})
		.on(RoomEvent.TrackUnmuted, () => {
			refreshParticipants();
		})
		.on(RoomEvent.ActiveSpeakersChanged, () => {
			refreshParticipants();
		})
		.on(RoomEvent.DataReceived, (payload: Uint8Array, participant?: RemoteParticipant, kind?: DataPacket_Kind, topic?: string) => {
			const senderIdentity = participant?.identity;
			for (const handler of dataListeners) {
				handler(payload, senderIdentity, topic);
			}
		});

	room = newRoom;
	setStatus('connecting');

	await newRoom.connect(wsUrl, token, {
		autoSubscribe: true,
	});

	// Enable microphone after connecting
	await newRoom.localParticipant.setMicrophoneEnabled(true);

	refreshParticipants();
}

export async function disconnect(): Promise<void> {
	if (!room) return;

	cleanupAudio();
	await room.disconnect();
	room = null;
	_status = 'disconnected';
	_participants = [];
	_localIdentity = '';
	notify();
}

function cleanupAudio() {
	destroyMixer();
}

/** Toggle local microphone mute */
export async function toggleMute(): Promise<boolean> {
	if (!room) return false;
	const enabled = room.localParticipant.isMicrophoneEnabled;
	await room.localParticipant.setMicrophoneEnabled(!enabled);
	refreshParticipants();
	return !enabled;
}

// Mixer controls are now exported from mixer.ts directly.
// Use: import { getChannel, setChannelGain, ... } from './mixer';

export function isConnected(): boolean {
	return room?.state === ConnectionState.Connected;
}

// ─── Data Messages (for sound pad sync) ────────────────────────────────────

export type DataMessageHandler = (payload: Uint8Array, senderIdentity: string | undefined, topic: string | undefined) => void;

const dataListeners = new Set<DataMessageHandler>();

/**
 * Subscribe to incoming data messages from other participants.
 * Returns an unsubscribe function.
 */
export function onDataMessage(handler: DataMessageHandler): () => void {
	dataListeners.add(handler);
	return () => { dataListeners.delete(handler); };
}

/**
 * Send a reliable data message to all participants in the room.
 * Uses DataPacket_Kind.RELIABLE for guaranteed delivery.
 */
export async function sendDataMessage(payload: Uint8Array, topic?: string): Promise<void> {
	if (!room || room.state !== ConnectionState.Connected) return;
	await room.localParticipant.publishData(payload, {
		reliable: true,
		topic,
	});
}
