import type { ExpressionBuilder, NotNull } from "kysely";
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB } from "~/db/tables";
import { peakXpOverallSql } from "~/features/top-search/XRankPlacementRepository.server";
import { sortBadgesByFavorites } from "~/features/user-page/core/badge-sorting.server";
import invariant from "~/utils/invariant";
import { commonUserSelect } from "~/utils/kysely.server";
import { SPLATOON_3_XP_BADGE_VALUES } from "./badges-constants";
import { findSplatoon3XpBadgeValue } from "./badges-utils";

const addPermissions = <T extends { managers: { userId: number }[] }>(
	row: T,
) => ({
	...row,
	permissions: {
		MANAGE: row.managers.map((m) => m.userId),
	},
});

const withAuthor = (eb: ExpressionBuilder<DB, "Badge">) => {
	return jsonObjectFrom(
		eb
			.selectFrom("User")
			.select((eb) => commonUserSelect(eb))
			.whereRef("User.id", "=", "Badge.authorId"),
	).as("author");
};

const withManagers = (eb: ExpressionBuilder<DB, "Badge">) => {
	return jsonArrayFrom(
		eb
			.selectFrom("BadgeManager")
			.innerJoin("User", "BadgeManager.userId", "User.id")
			.select((eb) => ["userId", ...commonUserSelect(eb)])
			.whereRef("BadgeManager.badgeId", "=", "Badge.id"),
	).as("managers");
};

// takes badgeId as a constant instead of correlating to "Badge"."id" so that
// SQLite can push the predicate down into both arms of the BadgeOwner view
const withOwners = (eb: ExpressionBuilder<DB, "Badge">, badgeId: number) => {
	return jsonArrayFrom(
		eb
			.selectFrom("BadgeOwner")
			.innerJoin("User", "BadgeOwner.userId", "User.id")
			.select(({ fn }) => [
				fn.sum<number>("BadgeOwner.count").as("count"),
				"User.id",
				"User.discordId",
				"User.username",
			])
			.where("BadgeOwner.badgeId", "=", badgeId)
			.groupBy("User.id")
			.orderBy("count", "desc"),
	).as("owners");
};

export async function all() {
	const rows = await db
		.selectFrom("Badge")
		.select(({ eb }) => [
			"id",
			"displayName",
			"code",
			"hue",
			withManagers(eb),
			withAuthor(eb),
		])
		.execute();

	return rows.map(addPermissions);
}

export async function findById(badgeId: number) {
	const row = await db
		.selectFrom("Badge")
		.select((eb) => [
			"Badge.id",
			"Badge.displayName",
			"Badge.code",
			"Badge.hue",
			withAuthor(eb),
			withManagers(eb),
			withOwners(eb, badgeId),
		])
		.where("id", "=", badgeId)
		.executeTakeFirst();

	if (!row) {
		return null;
	}

	return addPermissions(row);
}

export function findByManagersList(userIds: number[]) {
	return db
		.selectFrom("Badge")
		.select(["Badge.id", "Badge.code", "Badge.displayName", "Badge.hue"])
		.innerJoin("BadgeManager", "Badge.id", "BadgeManager.badgeId")
		.where("BadgeManager.userId", "in", userIds)
		.orderBy("Badge.id", "asc")
		.groupBy("Badge.id")
		.execute();
}

export function findManagedByUserId(userId: number) {
	return db
		.selectFrom("BadgeManager")
		.innerJoin("Badge", "Badge.id", "BadgeManager.badgeId")
		.select(["Badge.id", "Badge.code", "Badge.displayName", "Badge.hue"])
		.where("BadgeManager.userId", "=", userId)
		.execute();
}

export async function findByOwnerUserId(userId: number) {
	const rows = await db
		.selectFrom("BadgeOwner")
		.innerJoin("Badge", "Badge.id", "BadgeOwner.badgeId")
		.innerJoin("User", "User.id", "BadgeOwner.userId")
		.select(({ fn }) => [
			fn.sum<number>("BadgeOwner.count").as("count"),
			"Badge.id",
			"Badge.displayName",
			"Badge.code",
			"Badge.hue",
			"User.favoriteBadgeIds",
			"User.patronTier",
		])
		.where("BadgeOwner.userId", "=", userId)
		.groupBy("BadgeOwner.badgeId")
		.execute();

	if (rows.length === 0) return [];

	const { favoriteBadgeIds, patronTier } = rows[0];

	return sortBadgesByFavorites({
		favoriteBadgeIds,
		badges: rows.map(
			({ favoriteBadgeIds: _, patronTier: __, ...badge }) => badge,
		),
		patronTier,
	}).badges;
}

export function findByAuthorUserId(userId: number) {
	return db
		.selectFrom("Badge")
		.select(["Badge.id", "Badge.displayName", "Badge.code", "Badge.hue"])
		.where("Badge.authorId", "=", userId)
		.groupBy("Badge.id")
		.execute();
}

export function replaceManagers({
	badgeId,
	managerIds,
}: {
	badgeId: number;
	managerIds: number[];
}) {
	return db.transaction().execute(async (trx) => {
		await trx
			.deleteFrom("BadgeManager")
			.where("badgeId", "=", badgeId)
			.execute();

		if (managerIds.length > 0) {
			await trx
				.insertInto("BadgeManager")
				.values(
					managerIds.map((userId) => ({
						badgeId,
						userId,
					})),
				)
				.execute();
		}
	});
}

export function replaceOwners({
	badgeId,
	ownerIds,
}: {
	badgeId: number;
	ownerIds: number[];
}) {
	return db.transaction().execute(async (trx) => {
		await trx
			.deleteFrom("TournamentBadgeOwner")
			.where("badgeId", "=", badgeId)
			.execute();

		if (ownerIds.length > 0) {
			const counts = new Map<number, number>();
			for (const userId of ownerIds) {
				counts.set(userId, (counts.get(userId) ?? 0) + 1);
			}

			await trx
				.insertInto("TournamentBadgeOwner")
				.values(
					Array.from(counts, ([userId, count]) => ({
						badgeId,
						userId,
						count,
					})),
				)
				.execute();
		}
	});
}

export async function syncXPBadges() {
	return db.transaction().execute(async (trx) => {
		for (const value of SPLATOON_3_XP_BADGE_VALUES) {
			const badge = await trx
				.selectFrom("Badge")
				.select("id")
				.where("code", "=", String(value))
				.executeTakeFirst();

			invariant(badge, `Badge ${value} not found`);

			await trx
				.deleteFrom("TournamentBadgeOwner")
				.where("badgeId", "=", badge.id)
				.execute();
		}

		const userTopXPowers = await trx
			.selectFrom("SplatoonPlayer")
			.select(["userId", peakXpOverallSql().as("peakXp")])
			.where("userId", "is not", null)
			.where("peakXp", "is not", null)
			.$narrowType<{ userId: NotNull; peakXp: NotNull }>()
			.execute();

		for (const { userId, peakXp } of userTopXPowers) {
			const badgeValue = findSplatoon3XpBadgeValue(peakXp!);
			if (!badgeValue) continue;

			await trx
				.insertInto("TournamentBadgeOwner")
				.values((eb) => ({
					badgeId: eb
						.selectFrom("Badge")
						.select("id")
						.where("code", "=", String(badgeValue)),
					userId,
				}))
				.execute();
		}
	});
}
