import type { Transaction } from "kysely";
import { jsonBuildObject } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB, Tables } from "~/db/tables";
import { actorId } from "~/features/auth/core/user.server";
import { databaseTimestampNow } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import {
	concatUserSubmittedImagePrefix,
	customAvatarUrl,
	matchProfileWeapons,
} from "~/utils/kysely.server";
import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql";
import { randomTeamName } from "~/utils/team-name";

export function startLooking(teamId: number) {
	return db.transaction().execute(async (trx) => {
		await trx
			.updateTable("TournamentTeam")
			.set({ isLooking: 1 })
			.where("id", "=", teamId)
			.execute();

		return ensurePickupChatCode(teamId, trx);
	});
}

type CreatePlaceholderTeamArgs = {
	tournamentId: number;
	userId: number;
	isStayAsSub?: boolean;
	lfgNote?: string;
};
export function createPlaceholderTeam(args: CreatePlaceholderTeamArgs) {
	return db.transaction().execute(async (trx) => {
		const createdTeam = await trx
			.insertInto("TournamentTeam")
			.values({
				tournamentId: args.tournamentId,
				name: randomTeamName(),
				inviteCode: shortNanoid(),
				isPlaceholder: 1,
				isLooking: 1,
				lfgNote: args.lfgNote ?? null,
			})
			.returning("id")
			.executeTakeFirstOrThrow();

		await trx
			.insertInto("TournamentTeamMember")
			.values({
				tournamentTeamId: createdTeam.id,
				userId: args.userId,
				role: "OWNER",
				isStayAsSub: args.isStayAsSub ? 1 : 0,
			})
			.execute();

		return createdTeam;
	});
}

type TournamentLFGMemberObject = {
	id: Tables["User"]["id"];
	username: Tables["User"]["username"];
	discordId: Tables["User"]["discordId"];
	discordAvatar: Tables["User"]["discordAvatar"];
	customAvatarUrl: string | null;
	customUrl: Tables["User"]["customUrl"];
	languages: Tables["User"]["languages"];
	vc: Tables["User"]["vc"];
	pronouns: Tables["User"]["pronouns"];
	role: Tables["TournamentTeamMember"]["role"];
	isStayAsSub: Tables["TournamentTeamMember"]["isStayAsSub"];
	weapons:
		| (Pick<Tables["UserWeaponPool"], "weaponSplId" | "isFavorite"> & {
				isTenStar: number;
		  })[]
		| null;
	plusTier: Tables["PlusTier"]["tier"] | null;
};

export async function findLookingTeamsByTournamentId(tournamentId: number) {
	return db
		.selectFrom("TournamentTeam")
		.innerJoin(
			"TournamentTeamMember",
			"TournamentTeamMember.tournamentTeamId",
			"TournamentTeam.id",
		)
		.innerJoin("User", "User.id", "TournamentTeamMember.userId")
		.leftJoin("PlusTier", "PlusTier.userId", "User.id")
		.leftJoin(
			"UserSubmittedImage",
			"UserSubmittedImage.id",
			"TournamentTeam.avatarImgId",
		)
		.select(({ fn, eb }) => [
			"TournamentTeam.id",
			"TournamentTeam.isPlaceholder",
			"TournamentTeam.lfgNote as note",
			"TournamentTeam.name as teamName",
			concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
				"teamAvatarUrl",
			),
			fn
				.agg("json_group_array", [
					jsonBuildObject({
						id: eb.ref("User.id"),
						username: eb.ref("User.username"),
						discordId: eb.ref("User.discordId"),
						discordAvatar: eb.ref("User.discordAvatar"),
						customAvatarUrl: customAvatarUrl(eb),
						customUrl: eb.ref("User.customUrl"),
						languages: eb.ref("User.languages"),
						vc: eb.ref("User.vc"),
						pronouns: eb.ref("User.pronouns"),
						role: eb.ref("TournamentTeamMember.role"),
						isStayAsSub: eb.ref("TournamentTeamMember.isStayAsSub"),
						weapons: matchProfileWeapons(eb),
						plusTier: eb.ref("PlusTier.tier"),
					}),
				])
				.$castTo<TournamentLFGMemberObject[]>()
				.as("members"),
		])
		.where("TournamentTeam.tournamentId", "=", tournamentId)
		.where("TournamentTeam.isLooking", "=", 1)
		.groupBy("TournamentTeam.id")
		.execute();
}

export async function findSubGroups(tournamentId: number) {
	const rows = await db
		.selectFrom("TournamentTeam")
		.innerJoin(
			"TournamentTeamMember",
			"TournamentTeamMember.tournamentTeamId",
			"TournamentTeam.id",
		)
		.innerJoin("User", "User.id", "TournamentTeamMember.userId")
		.leftJoin("PlusTier", "PlusTier.userId", "User.id")
		.select(({ fn, eb }) => [
			"TournamentTeam.id",
			"TournamentTeam.lfgNote as message",
			fn
				.agg("json_group_array", [
					jsonBuildObject({
						id: eb.ref("User.id"),
						username: eb.ref("User.username"),
						discordId: eb.ref("User.discordId"),
						discordAvatar: eb.ref("User.discordAvatar"),
						customAvatarUrl: customAvatarUrl(eb),
						customUrl: eb.ref("User.customUrl"),
						languages: eb.ref("User.languages"),
						vc: eb.ref("User.vc"),
						pronouns: eb.ref("User.pronouns"),
						role: eb.ref("TournamentTeamMember.role"),
						isStayAsSub: eb.ref("TournamentTeamMember.isStayAsSub"),
						weapons: matchProfileWeapons(eb),
						plusTier: eb.ref("PlusTier.tier"),
					}),
				])
				.$castTo<TournamentLFGMemberObject[]>()
				.as("members"),
		])
		.where("TournamentTeam.tournamentId", "=", tournamentId)
		.where("TournamentTeam.isPlaceholder", "=", 1)
		.where("TournamentTeamMember.isStayAsSub", "=", 1)
		.groupBy("TournamentTeam.id")
		.execute();

	return rows;
}

export function mergeTeams({
	survivingTeamId,
	otherTeamId,
	maxGroupSize,
}: {
	survivingTeamId: number;
	otherTeamId: number;
	maxGroupSize: number;
}) {
	return db.transaction().execute(async (trx) => {
		const otherTeam = await trx
			.selectFrom("TournamentTeam")
			.select("chatCode")
			.where("id", "=", otherTeamId)
			.executeTakeFirst();

		const otherMembers = await trx
			.selectFrom("TournamentTeamMember")
			.select(["TournamentTeamMember.userId", "TournamentTeamMember.role"])
			.where("TournamentTeamMember.tournamentTeamId", "=", otherTeamId)
			.execute();

		for (const member of otherMembers) {
			await trx
				.updateTable("TournamentTeamMember")
				.set({
					role: member.role === "OWNER" ? "MANAGER" : member.role,
					tournamentTeamId: survivingTeamId,
					// reset so the merged-in members sort after the surviving team's original members
					createdAt: databaseTimestampNow(),
				})
				.where("TournamentTeamMember.tournamentTeamId", "=", otherTeamId)
				.where("TournamentTeamMember.userId", "=", member.userId)
				.execute();
		}

		await deleteLikesByTeamId(survivingTeamId, trx);

		await trx
			.deleteFrom("TournamentTeam")
			.where("TournamentTeam.id", "=", otherTeamId)
			.execute();

		const memberCount = await getMemberCount(survivingTeamId, trx);

		invariant(
			memberCount <= maxGroupSize,
			"Group has too many members after merge",
		);

		await trx
			.updateTable("TournamentTeam")
			.set({
				isLooking: memberCount >= maxGroupSize ? 0 : undefined,
				isPlaceholder: 0,
			})
			.where("id", "=", survivingTeamId)
			.execute();

		const survivor = await ensurePickupChatCode(survivingTeamId, trx);

		return {
			survivor,
			removedChatCode: otherTeam?.chatCode ?? null,
		};
	});
}

export async function addLike({
	likerTeamId,
	targetTeamId,
}: {
	likerTeamId: number;
	targetTeamId: number;
}) {
	try {
		await db
			.insertInto("TournamentLFGLike")
			.values({ likerTeamId, targetTeamId })
			.onConflict((oc) =>
				oc.columns(["likerTeamId", "targetTeamId"]).doNothing(),
			)
			.execute();
	} catch (error) {
		if (errorIsSqliteForeignKeyConstraintFailure(error)) {
			return;
		}
		throw error;
	}
}

export function deleteLike({
	likerTeamId,
	targetTeamId,
}: {
	likerTeamId: number;
	targetTeamId: number;
}) {
	return db
		.deleteFrom("TournamentLFGLike")
		.where("likerTeamId", "=", likerTeamId)
		.where("targetTeamId", "=", targetTeamId)
		.execute();
}

export async function allLikesByTeamId(teamId: number) {
	const rows = await db
		.selectFrom("TournamentLFGLike")
		.select(["TournamentLFGLike.likerTeamId", "TournamentLFGLike.targetTeamId"])
		.where((eb) =>
			eb.or([
				eb("TournamentLFGLike.likerTeamId", "=", teamId),
				eb("TournamentLFGLike.targetTeamId", "=", teamId),
			]),
		)
		.execute();

	return {
		given: rows
			.filter((row) => row.likerTeamId === teamId)
			.map((row) => ({ teamId: row.targetTeamId })),
		received: rows
			.filter((row) => row.targetTeamId === teamId)
			.map((row) => ({ teamId: row.likerTeamId })),
	};
}

export function updateTeamNote({
	teamId,
	value,
}: {
	teamId: number;
	value: string | null;
}) {
	return db
		.updateTable("TournamentTeam")
		.set({ lfgNote: value })
		.where("id", "=", teamId)
		.execute();
}

export function updateMemberRole({
	userId,
	teamId,
	role,
}: {
	userId: number;
	teamId: number;
	role: Tables["TournamentTeamMember"]["role"];
}) {
	if (role === "OWNER") {
		throw new Error("Can't set role to OWNER with this function");
	}

	return db
		.updateTable("TournamentTeamMember")
		.set({ role })
		.where("userId", "=", userId)
		.where("tournamentTeamId", "=", teamId)
		.execute();
}

export function updateOwnStayAsSub({
	teamId,
	value,
}: {
	teamId: number;
	value: boolean;
}) {
	return db
		.updateTable("TournamentTeamMember")
		.set({ isStayAsSub: value ? 1 : 0 })
		.where("tournamentTeamId", "=", teamId)
		.where("userId", "=", actorId())
		.execute();
}

export function leaveLfg({
	userId,
	tournamentId,
}: {
	userId: number;
	tournamentId: number;
}) {
	return db.transaction().execute(async (trx) => {
		const userTeam = await trx
			.selectFrom("TournamentTeamMember")
			.innerJoin(
				"TournamentTeam",
				"TournamentTeam.id",
				"TournamentTeamMember.tournamentTeamId",
			)
			.select([
				"TournamentTeamMember.tournamentTeamId",
				"TournamentTeam.isPlaceholder",
			])
			.where("TournamentTeamMember.userId", "=", userId)
			.where("TournamentTeam.tournamentId", "=", tournamentId)
			.where("TournamentTeam.isLooking", "=", 1)
			.executeTakeFirst();

		if (!userTeam) return;

		if (!userTeam.isPlaceholder) {
			await trx
				.updateTable("TournamentTeam")
				.set({ isLooking: 0 })
				.where("id", "=", userTeam.tournamentTeamId)
				.execute();
			await trx
				.updateTable("TournamentTeamMember")
				.set({ isStayAsSub: 0 })
				.where("tournamentTeamId", "=", userTeam.tournamentTeamId)
				.execute();
			await deleteLikesByTeamId(userTeam.tournamentTeamId, trx);
			return;
		}

		await trx
			.deleteFrom("TournamentTeam")
			.where("id", "=", userTeam.tournamentTeamId)
			.execute();
	});
}

export async function getSubsForTournament(tournamentId: number) {
	const rows = await db
		.selectFrom("TournamentTeamMember")
		.innerJoin(
			"TournamentTeam",
			"TournamentTeam.id",
			"TournamentTeamMember.tournamentTeamId",
		)
		.select("TournamentTeamMember.userId")
		.where("TournamentTeam.tournamentId", "=", tournamentId)
		.where("TournamentTeamMember.isStayAsSub", "=", 1)
		.execute();

	return rows.map((row) => row.userId);
}

function deleteLikesByTeamId(teamId: number, trx: Transaction<DB>) {
	return trx
		.deleteFrom("TournamentLFGLike")
		.where((eb) =>
			eb.or([
				eb("TournamentLFGLike.likerTeamId", "=", teamId),
				eb("TournamentLFGLike.targetTeamId", "=", teamId),
			]),
		)
		.execute();
}

async function getMemberCount(
	teamId: number,
	trx: Transaction<DB>,
): Promise<number> {
	const members = await trx
		.selectFrom("TournamentTeamMember")
		.select("TournamentTeamMember.userId")
		.where("TournamentTeamMember.tournamentTeamId", "=", teamId)
		.execute();

	return members.length;
}

export type PickupChatTeam = {
	chatCode: string;
	name: string;
	memberUserIds: number[];
};

async function ensurePickupChatCode(
	teamId: number,
	trx: Transaction<DB>,
): Promise<PickupChatTeam | null> {
	const team = await trx
		.selectFrom("TournamentTeam")
		.select(["name", "chatCode"])
		.where("id", "=", teamId)
		.executeTakeFirstOrThrow();

	const members = await trx
		.selectFrom("TournamentTeamMember")
		.select("userId")
		.where("tournamentTeamId", "=", teamId)
		.execute();

	if (members.length < 2) return null;

	let chatCode = team.chatCode;
	if (!chatCode) {
		chatCode = shortNanoid();
		await trx
			.updateTable("TournamentTeam")
			.set({ chatCode })
			.where("id", "=", teamId)
			.execute();
	}

	return {
		chatCode,
		name: team.name,
		memberUserIds: members.map((m) => m.userId),
	};
}
