import { dispatchSocket } from "dispatch/socket";
import {
	handleDisconnect,
	handleLocalTrackUnpublished,
	handleTrackSubscribed,
	handleTrackUnsubscribed,
} from "_helpers/liveKitEventHandler";
import { ConnectionQuality, Participant, Room, RoomEvent, RpcInvocationData } from "livekit-client";
import { pilotSocket } from "pilot/socket";
import { create } from "zustand";
import axios from "axios";

let interval: NodeJS.Timeout;

type TalkState = {
	micDeviceId: string | null;
	micVolume: number;
	isTalking: boolean;
	removeMessage: () => void;
	state: "connecting" | "connected" | "disconnected" | "error";
	message: string | null;
	connectionQuality: ConnectionQuality;
	remoteParticipants: number;
	toggleTalking: () => void;
	setMic: (micDeviceId: string | null, volume: number) => void;
	connect: (roomName: string, role: string) => void;
	disconnect: () => void;
	speakingParticipants: Participant[];
	addSpeakingParticipant: (participant: Participant) => void;
	removeSpeakingParticipant: (speakingParticipants: Participant) => void;
	room: Room | null;
};
const getToken = async (roomName: string) => {
	const response = await axios.get(`/api/livekit-token?roomName=${roomName}`);
	const data = response.data;
	return data.token;
};

export const useAudioStore = create<TalkState>((set, get) => ({
	isTalking: false,
	message: null,
	micDeviceId: null,
	speakingParticipants: [],
	micVolume: 1,
	state: "disconnected",
	source: "",
	remoteParticipants: 0,
	connectionQuality: ConnectionQuality.Unknown,
	room: null,
	addSpeakingParticipant: (participant) => {
		set((state) => {
			if (!state.speakingParticipants.some((p) => p.identity === participant.identity)) {
				return { speakingParticipants: [...state.speakingParticipants, participant] };
			}
			return state;
		});
	},
	removeMessage: () => {
		set({ message: null });
	},
	removeSpeakingParticipant: (participant) => {
		set((state) => ({
			speakingParticipants: state.speakingParticipants.filter(
				(p) => !(p.identity === participant.identity),
			),
		}));
	},
	setMic: (micDeviceId, micVolume) => {
		set({ micDeviceId, micVolume });
	},
	toggleTalking: () => {
		const { room, isTalking, micDeviceId, micVolume } = get();
		if (!room) return;
		// Todo: use micVolume
		room.localParticipant.setMicrophoneEnabled(!isTalking, {
			deviceId: micDeviceId ?? undefined,
		});

		if (!isTalking) {
			// If old status was not talking, we need to emit the PTT event
			if (pilotSocket.connected) {
				pilotSocket.emit("ptt", {
					shouldTransmit: true,
					channel: room.name,
				});
			}
			if (dispatchSocket.connected)
				dispatchSocket.emit("ptt", {
					shouldTransmit: true,
					channel: room.name,
				});
		}

		set((state) => ({ isTalking: !state.isTalking }));
	},
	connect: async (roomName, role) => {
		set({ state: "connecting" });
		console.log("Connecting to room: ", roomName);
		try {
			// Clean old room
			const connectedRoom = get().room;
			if (interval) clearInterval(interval);
			if (connectedRoom) {
				connectedRoom.disconnect();
				connectedRoom.removeAllListeners();
			}

			const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
			if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set");

			const token = await getToken(roomName);
			if (!token) throw new Error("Fehlende Berechtigung");
			const room = new Room({});
			await room.prepareConnection(url, token);
			room
				// Connection events
				.on(RoomEvent.Connected, () => {
					set({ state: "connected", room, message: null });
				})
				.on(RoomEvent.Disconnected, () => {
					set({ state: "disconnected" });

					handleDisconnect();
				})
				.on(RoomEvent.ConnectionQualityChanged, (connectionQuality) => set({ connectionQuality }))

				// Track events
				.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
				.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
				.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished);
			await room.connect(url, token, {});

			room.localParticipant.setAttributes({
				role,
			});
			set({ room });

			room.registerRpcMethod("force-mute", async (data: RpcInvocationData) => {
				const { by } = JSON.parse(data.payload);
				room.localParticipant.setMicrophoneEnabled(false);
				useAudioStore.setState({
					isTalking: false,
					message: `Ruf beendet durch ${by || "eine unsichtbare Macht"}`,
				});
				return `Hello, ${data.callerIdentity}!`;
			});

			interval = setInterval(() => {
				set({
					remoteParticipants: room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed
				});
			}, 500);
		} catch (error: Error | unknown) {
			console.error("Error occured: ", error);
			if (error instanceof Error) {
				set({ state: "error", message: error.message });
			} else {
				set({ state: "error", message: "Unknown error" });
			}
		}
	},
	disconnect: () => {
		get().room?.disconnect();
	},
}));

interface PTTData {
	shouldTransmit: boolean;
	source: string;
}

const handlePTT = (data: PTTData) => {
	const { shouldTransmit, source } = data;
	const { room } = useAudioStore.getState();
	if (!room) return;

	useAudioStore.setState({
		isTalking: shouldTransmit,
	});

	if (shouldTransmit) {
		room.localParticipant.setMicrophoneEnabled(true);
	} else {
		room.localParticipant.setMicrophoneEnabled(false);
	}
};

pilotSocket.on("ptt", handlePTT);
dispatchSocket.on("ptt", handlePTT);
