import { skipToken } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ChatsChatid } from "~/schemas/public/Chats";
import type { MessagesMessageid } from "~/schemas/public/Messages";
import type { Session } from "~/server/api/trpc";
import type { ChatMessage } from "~/server/services/messages.service";
import type { OnlineStatus, WhoIsTyping } from "~/server/sse";
import { api } from "~/utils/api";

type EditEvent = {
	eventType: "edit";
	data: { messageId: MessagesMessageid; message: string };
};

type DeleteEvent = {
	eventType: "delete";
	data: MessagesMessageid;
};

type MessageUpdate = EditEvent | DeleteEvent;

/**
 * Set isTyping with a throttle of 1s
 * Triggers immediately if state changes
 */
export function useThrottledIsTypingMutation({
	chatId,
	username,
}: {
	chatId: ChatsChatid;
	username: string;
}) {
	const { mutate } = api.chatSSE.type.useMutation();

	return useMemo(() => {
		let state = false;
		let timeout: ReturnType<typeof setTimeout> | null;
		function trigger() {
			if (timeout) {
				clearTimeout(timeout);
				timeout = null;
			}

			mutate({ chatId, typing: state, username });
		}

		return (nextState: boolean) => {
			const shouldTriggerImmediately = nextState !== state;

			state = nextState;
			if (shouldTriggerImmediately) {
				trigger();
			}
			if (state) {
				// If the user is typing, reset the timeout
				if (timeout) {
					clearTimeout(timeout);
				}
				timeout = setTimeout(() => {
					// If the user is inactive for 3 seconds, send an update
					state = false;
					trigger();
				}, 3000); // 3 seconds timeout
			}
		};
	}, [chatId]);
}

export type LiveChat = ReturnType<typeof useLiveChat>;
export const useLiveChat = (chatId: ChatsChatid, userData: Session) => {
	//console.log("useLiveChat", chatId);

	const [, query] = api.messagesSSE.infinite.useSuspenseInfiniteQuery(
		{ chatId },
		{
			getNextPageParam: (d) => d.nextCursor,
			refetchOnMount: false,
			// No need to refetch as we have a subscription
			refetchOnReconnect: false,
			refetchOnWindowFocus: false,
		},
	);

	const [messages, setMessages] = useState<ChatMessage[]>(() => {
		return query.data ? query.data.pages.flatMap((page) => page.items) : [];
	});

	/**
	 * fn to add and dedupe new messages onto state
	 */
	const addMessages = useCallback((incoming?: ChatMessage[]) => {
		setMessages((current) => {
			const map: Record<ChatMessage["messageid"], ChatMessage> = {};
			for (const msg of current ?? []) {
				map[msg.messageid] = msg;
			}
			for (const msg of incoming ?? []) {
				map[msg.messageid] = msg;
			}
			return Object.values(map).sort(
				(a, b) => a.time.getTime() - b.time.getTime(),
			);
		});
	}, []);

	// Edit a message from the chat
	const updateMessage = useCallback(
		(msgId: MessagesMessageid, data: Partial<ChatMessage>) => {
			setMessages((current) => {
				const newMessages = current.map((msg) => {
					if (msg.messageid === msgId) {
						return { ...msg, ...data };
					}
					return msg;
				});
				return newMessages;
			});
		},
		[],
	);

	// Delete a message from the chat
	const deleteMessage = useCallback((msgId: MessagesMessageid) => {
		setMessages((prev) => prev.filter((msg) => msg.messageid !== msgId));
	}, []);

	/**
	 * when new data from `useInfiniteQuery`, merge with current state
	 */
	useEffect(() => {
		const msgs = query.data?.pages.flatMap((page) => page.items);
		addMessages(msgs);
	}, [query.data?.pages, addMessages]);

	const [lastEventId, setLastEventId] = useState<
		// Query has not been run yet
		| false
		// Empty list
		| null
		// Event id
		| MessagesMessageid
	>(false);

	if (messages && lastEventId === false) {
		// We should only set the lastEventId once, if the SSE-connection is lost, it will automatically reconnect and continue from the last event id
		// Changing this value will trigger a new subscription
		setLastEventId(messages.at(-1)?.messageid ?? null);
	}
	const { mutate: read } = api.messagesSSE.read.useMutation();

	api.messagesSSE.onAdd.useSubscription(
		lastEventId === false ? skipToken : { chatId, lastEventId },
		{
			onData(event) {
				const someoneElseInChat =
					onlineUsers.length > 1 &&
					onlineUsers.some(
						(user) =>
							user.userId !== userData.id && user.isAdmin !== userData.isAdmin,
					);
				addMessages([
					{ ...event.data, isread: event.data.isread || someoneElseInChat },
				]);
				read({ chatId });
			},

			onError(err) {
				console.error("onAdd Subscription client error:", err);

				const lastMessageEventId = messages?.at(-1)?.messageid;
				if (lastMessageEventId) {
					// We've lost the connection, let's resubscribe from the last message
					setLastEventId(lastMessageEventId);
				}
			},
		},
	);

	api.messagesSSE.onMessageEvent.useSubscription(
		{ chatId },
		{
			onData(data: { data: MessageUpdate }) {
				switch (data.data.eventType) {
					case "edit":
						handleEdit(data.data.data);
						break;
					case "delete":
						handleDelete(data.data.data);
						break;
				}
			},
			onError(err) {
				console.error("Subscription error:", err);
			},
		},
	);

	const handleEdit = (data: {
		messageId: MessagesMessageid;
		message: string;
	}) => {
		updateMessage(data.messageId, { message: data.message });
	};
	const handleDelete = (data: MessagesMessageid) => {
		deleteMessage(data);
	};

	const [onlineUsers, setOnlineUsers] = useState<OnlineStatus[]>([]);
	const [typingStatus, setTypingStatus] = useState<WhoIsTyping[]>([]);

	api.chatSSE.onChatEvent.useSubscription(
		{ chatId },
		{
			onComplete: () => {
				leave({ chatId });
			},
			onData: (data) => {
				switch (data.data.eventType) {
					case "online":
						console.log("Online event", data.data.data);
						handleOnlineEvent(data.data.data);
						break;
					case "typing":
						console.log("Typing event", data.data.data);
						handleTypingEvent(data.data.data);
						break;
				}
			},
			onStarted: () => {
				enter({ chatId });
			},
		},
	);

	const handleOnlineEvent = (data: OnlineStatus[]) => {
		setOnlineUsers(data);

		// Filter users who are not the current user and have a different `isAdmin` property
		const someoneElse = data.filter(
			(u) => u.userId !== userData.id && u.isAdmin !== userData.isAdmin,
		);
		if (someoneElse.length > 0) {
			// If there is someone else in chat and if thet are not the same usertype as me, update the read status of all messages
			setMessages((current) => {
				const newMessages = current.map((msg) => {
					return { ...msg, isread: true };
				});
				return newMessages;
			});
		}
	};

	const { mutate: enter } = api.chatSSE.enterChat.useMutation();
	const { mutate: leave } = api.chatSSE.leaveChat.useMutation();

	useEffect(() => {
		return () => {
			// Cleanup: leave the chat when the component unmounts
			console.log("Leaving chat", chatId);
			leave({ chatId });
		};
	}, []);

	const handleTypingEvent = (data: WhoIsTyping[]) => {
		setTypingStatus(data);
	};

	return {
		messages,
		onlineUsers,
		query,
		typingStatus,
	};
};
