import type { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import cheers from "cheers";
import commands, { specialAliasCommands } from "commands";
import { createCheerRecord } from "db/dbCheers";
import { getUserRecord } from "db/dbUser";
import { eventSub } from "index";
import { Item } from "items";
import { isAdmin } from "lib/admins";
import { type Command, sendMessage } from "lib/commandUtils";
import handleAnivMessage from "lib/handleAnivMessage";
import { isInvuln, removeInvuln, setTemporaryInvuln } from "lib/invuln";
import logger from "lib/logger";
import { redis } from "lib/redis";
import { chatterId, commandPrefix, streamerId, streamerUsers } from "main";
import User from "user";
import { addMessageToChatWidget } from "web/chatWidget/message";

eventSub.onChannelChatMessage(streamerId, chatterId, parseChatMessage);

async function parseChatMessage(msg: EventSubChannelChatMessageEvent) {
	addMessageToChatWidget(msg);

	const user = await User.initUsername(msg.chatterName);

	// Get user from cache or place user in cache
	// Given the fact that this is the user that chats, this user object always exists and cannot be null
	//
	// One of the flaws with the user object is solved by creating the object with the name.
	// This way, if a user changes their name, the original name stays in the cache for at least 1 hour (extendable by using that name as target for item)
	// and both are usable to target the same user (id is the same)
	// The only problem would be if a user changed their name and someone else took their name right after

	if (await redis.exists(`user:${user?.id}:bot`)) return; // Ignore all bot commands

	if (
		!(await redis.exists(`user:${user?.id}:haschatted`)) &&
		!msg.sourceMessageId
	) {
		// The msg.sourceMessageId checks if the message is from shared chat. shared chat should be ignored
		const message = await sendMessage(
			`Welcome ${user?.displayName}. Please note: This chat has PvP, if you get timed out that's part of the qwerinope experience. You have 10 minutes of invincibility. A full list of commands and items can be found here: https://gitlab.com/qwerinope/qweribot/#qweribot`,
		);
		await redis.set(`user:${user?.id}:haschatted`, "1");
		await redis.set(`user:${user?.id}:welcomemessageid`, message.id);
		await redis.expire(`user:${user?.id}:welcomemessageid`, 600);
		if (!(await isInvuln(msg.chatterId))) await setTemporaryInvuln(user?.id!); // This would set the invuln expiration lmao
	}

	if (!(await isInvuln(user?.id!))) user?.setVulnerable(); // Make the user vulnerable to explosions if not marked as invuln

	// Custom welcome messages
	const wcmessage = await redis.get(`user:${user?.id}:welcomemessagetext`);
	if (
		process.env.NODE_ENV === "production" && // when running prod DB
		wcmessage && // when chatter has a welcome message set
		(await redis.exists(`streamIsLive`)) && // when the stream is active
		!(await redis.exists(`user:${user?.id}:haschattedthisstream`)) // when the user hasn't chatted this stream
	)
		await sendMessage(wcmessage);

	await redis.set(`user:${user?.id}:haschattedthisstream`, "1");

	if (!msg.isCheer && !msg.isRedemption) await handleChatMessage(msg, user!);
	else if (msg.isCheer && !msg.isRedemption)
		await handleCheer(msg, msg.bits, user!);
}

async function handleChatMessage(
	msg: EventSubChannelChatMessageEvent,
	user: User,
) {
	// Aniv message filter
	handleAnivMessage(msg, user);

	// Parse commands:
	const selected = selectCommand(msg.messageText);
	if (!selected) return;
	const { cmd: selection, activation, isitem } = selected;
	if (await redis.sismember("disabledcommands", selection.name)) return;
	if (
		isitem &&
		(await isInvuln(msg.chatterId)) &&
		!streamerUsers.includes(msg.chatterId)
	) {
		await sendMessage(
			`You're no longer an invuln because you used an item.`,
			msg.messageId,
		);
		await removeInvuln(msg.chatterId);
	}

	switch (selection.usertype) {
		case "admin":
			if (!(await isAdmin(user.id))) return;
			break;
		case "streamer":
			if (!streamerUsers.includes(msg.chatterId)) return;
			break;
		case "moderator":
			if (
				!(
					(await redis.exists(`user:${user.id}:mod`)) ||
					(await isAdmin(user.id))
				)
			)
				return;
			break;
	}

	try {
		await selection.execute(msg, user, { activation });
	} catch (err) {
		logger.err(err as string);
		await sendMessage("ERROR: Something went wrong", msg.messageId);
		await user.clearLock();
	}
}

type selectedCommand = {
	cmd: Command;
	activation?: string;
	isitem: boolean;
};

function selectCommand(message: string): selectedCommand | false {
	const specialcmdselector = message.trim().toLowerCase().split(" ")[0]!;
	const specialcmd = specialAliasCommands.get(specialcmdselector);
	if (specialcmd)
		return {
			cmd: specialcmd,
			activation: specialcmdselector,
			isitem: specialcmd instanceof Item,
		};
	if (!message.startsWith(commandPrefix)) return false;
	const commandSelector = message
		.slice(commandPrefix.length)
		.trim()
		.toLowerCase()
		.split(" ")[0]!;
	const normalcmd = commands.get(commandSelector);
	if (normalcmd)
		return {
			cmd: normalcmd,
			activation: undefined,
			isitem: normalcmd instanceof Item,
		};
	return false;
}

export async function handleCheer(
	msg: EventSubChannelChatMessageEvent,
	bits: number,
	user: User,
) {
	if (msg.isCheer) {
		await getUserRecord(user); // ensure they exist in the database
		await createCheerRecord(user, bits);
	} // If this is not triggered it's because of the testcheer command. these fake bits should not be added to the database

	const selection = cheers.get(bits);
	if (!selection) return;

	if (await redis.sismember("disabledcheers", selection.name)) {
		await sendMessage(
			`The ${selection.name} cheer is disabled! Sorry!`,
			msg.messageId,
		);
		return;
	}
	if (
		selection.isItem &&
		(await isInvuln(user.id)) &&
		!streamerUsers.includes(user.id)
	) {
		await sendMessage(`${user.displayName} Is no longer an invuln`);
		await removeInvuln(user.id);
	}
	try {
		await selection.execute(msg, user);
	} catch (err) {
		await sendMessage(`[ERROR]: Something went wrong with cheer execution`);
		logger.err(err as string);
	}
}
