import { sql } from "kysely";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import * as R from "remeda";
import { db } from "~/db/sql";
import type { Tables } from "~/db/tables";
import type {
	MainWeaponId,
	ModeShort,
	StageId,
} from "~/modules/in-game-lists/types";
import { weaponIdToArrayWithAlts } from "~/modules/in-game-lists/weapon-ids";
import {
	dateToDatabaseTimestamp,
	dayMonthYearToDatabaseTimestamp,
} from "~/utils/dates";
import invariant from "~/utils/invariant";
import {
	type CommonUser,
	commonUserJsonObject,
	commonUserSelect,
} from "~/utils/kysely.server";
import { VODS_PAGE_BATCH_SIZE } from "./vods-constants";
import type { VideoBeingAdded, Vod } from "./vods-types";
import {
	extractYoutubeIdFromVideoUrl,
	hoursMinutesSecondsStringToSeconds,
} from "./vods-utils";

export async function findByUserId(userId: Tables["User"]["id"], limit = 100) {
	return findVods({ userId, limit });
}

export async function findVods({
	weapon,
	mode,
	stageId,
	type,
	userId,
	limit = VODS_PAGE_BATCH_SIZE,
	offset = 0,
}: {
	weapon?: MainWeaponId;
	mode?: ModeShort;
	stageId?: StageId;
	type?: Tables["Video"]["type"];
	userId?: number;
	limit?: number;
	offset?: number;
}) {
	let query = db
		.selectFrom("Video")
		.leftJoin("VideoMatch", "VideoMatch.videoId", "Video.id")
		.leftJoin(
			"VideoMatchPlayer",
			"VideoMatch.id",
			"VideoMatchPlayer.videoMatchId",
		)
		.selectAll("Video")
		.select(({ fn, ref, eb }) => [
			sql<
				Array<number>
			>`json_group_array(distinct ${ref("VideoMatchPlayer.weaponSplId")})`
				.$castTo<MainWeaponId[]>()
				.as("weapons"),
			fn
				.agg("json_group_array", ["VideoMatchPlayer.playerName"])
				.$castTo<string[]>()
				.as("playerNames"),
			jsonArrayFrom(
				eb
					.selectFrom("User")
					.select((playerEb) => commonUserSelect(playerEb))
					.whereRef("User.id", "=", "VideoMatchPlayer.playerUserId"),
			).as("players"),
		]);
	if (userId) {
		query = query.where("VideoMatchPlayer.playerUserId", "=", userId);
	} else {
		if (type) {
			query = query.where("Video.type", "=", type);
		}
		if (mode) {
			query = query.where("VideoMatch.mode", "=", mode);
		}
		if (stageId) {
			query = query.where("VideoMatch.stageId", "=", stageId);
		}
	}
	if (weapon) {
		query = query.where(
			"VideoMatchPlayer.weaponSplId",
			"in",
			weaponIdToArrayWithAlts(weapon),
		);
	}
	const result = await query
		.groupBy("Video.id")
		.orderBy("Video.youtubeDate", "desc")
		.limit(limit)
		.offset(offset)
		.execute();

	const vods = result.map((value) => {
		const { playerNames, players, ...vod } = value;
		return {
			...vod,
			pov: playerNames[0] ?? players[0],
		};
	});
	return vods;
}

export async function countVods({
	weapon,
	mode,
	stageId,
	type,
	userId,
}: {
	weapon?: MainWeaponId;
	mode?: ModeShort;
	stageId?: StageId;
	type?: Tables["Video"]["type"];
	userId?: number;
}) {
	let query = db
		.selectFrom("Video")
		.leftJoin("VideoMatch", "VideoMatch.videoId", "Video.id")
		.leftJoin(
			"VideoMatchPlayer",
			"VideoMatch.id",
			"VideoMatchPlayer.videoMatchId",
		)
		.select(({ fn }) => fn.count<number>("Video.id").distinct().as("count"));
	if (userId) {
		query = query.where("VideoMatchPlayer.playerUserId", "=", userId);
	} else {
		if (type) {
			query = query.where("Video.type", "=", type);
		}
		if (mode) {
			query = query.where("VideoMatch.mode", "=", mode);
		}
		if (stageId) {
			query = query.where("VideoMatch.stageId", "=", stageId);
		}
	}
	if (weapon) {
		query = query.where(
			"VideoMatchPlayer.weaponSplId",
			"in",
			weaponIdToArrayWithAlts(weapon),
		);
	}

	const result = await query.executeTakeFirstOrThrow();
	return result.count;
}

export async function findVodById(id: Tables["Video"]["id"]) {
	const videoQuery = db
		.selectFrom("Video")
		.select([
			"id",
			"title",
			"youtubeDate",
			"youtubeId",
			"type",
			"submitterUserId",
		])
		.where("Video.id", "=", id);

	const video = await videoQuery.executeTakeFirst();

	if (video) {
		const videoMatchQuery = db
			.selectFrom("VideoMatch")
			.select([
				"VideoMatch.id",
				"VideoMatch.mode",
				"VideoMatch.stageId",
				"VideoMatch.startsAt",
			])
			.leftJoin(
				"VideoMatchPlayer",
				"VideoMatch.id",
				"VideoMatchPlayer.videoMatchId",
			)
			.leftJoin("User", "VideoMatchPlayer.playerUserId", "User.id")
			.select(({ fn, eb }) => [
				fn
					.agg("json_group_array", ["VideoMatchPlayer.weaponSplId"])
					.$castTo<MainWeaponId[]>()
					.as("weapons"),
				fn
					.agg("json_group_array", ["VideoMatchPlayer.playerName"])
					.filterWhere("VideoMatchPlayer.playerName", "is not", null)
					.$castTo<string[]>()
					.as("playerNames"),
				fn
					.agg("json_group_array", [commonUserJsonObject(eb)])
					.filterWhere("User.username", "is not", null)
					.$castTo<CommonUser[]>()
					.as("players"),
			])
			.where("VideoMatch.videoId", "=", id)
			.groupBy("VideoMatch.id")
			.orderBy("VideoMatch.startsAt", "asc")
			.orderBy("VideoMatchPlayer.player", "asc");

		const matches = await videoMatchQuery.execute();

		return {
			...video,
			pov: resolvePov(matches),
			matches: R.map(matches, R.omit(["players", "playerNames"])),
		};
	}
	return null;
}

function resolvePov(
	matches: Array<{ playerNames: string[]; players: CommonUser[] }>,
): Vod["pov"] {
	for (const match of matches) {
		if (match.playerNames.length > 0) {
			return match.playerNames[0];
		}

		if (match.players.length > 0) {
			return match.players[0];
		}
	}

	return;
}

export async function update(
	args: VideoBeingAdded & {
		submitterUserId: number;
		isValidated: boolean;
		id: number;
	},
) {
	return insert(args);
}

export async function insert(
	args: VideoBeingAdded & {
		submitterUserId: number;
		isValidated: boolean;
		id?: number;
	},
) {
	const youtubeId = extractYoutubeIdFromVideoUrl(args.youtubeUrl);
	invariant(youtubeId, "Invalid YouTube URL");
	return db.transaction().execute(async (trx) => {
		let videoId: number;
		const video = {
			title: args.title,
			type: args.type,
			youtubeDate: dayMonthYearToDatabaseTimestamp(args.date),
			eventId: args.eventId ?? null,
			youtubeId,
			submitterUserId: args.submitterUserId,
			validatedAt: args.isValidated
				? dateToDatabaseTimestamp(new Date())
				: null,
		};
		if (args.id) {
			await trx
				.deleteFrom("VideoMatch")
				.where("videoId", "=", args.id)
				.execute();

			await trx
				.updateTable("UnvalidatedVideo")
				.set(video)
				.where("id", "=", args.id)
				.execute();
			videoId = args.id;
		} else {
			const result = await trx
				.insertInto("UnvalidatedVideo")
				.values(video)
				.returning("UnvalidatedVideo.id")
				.executeTakeFirstOrThrow();
			videoId = result.id;
		}
		for (const match of args.matches) {
			const videoMatchResult = await trx
				.insertInto("VideoMatch")
				.values({
					videoId: videoId,
					startsAt: hoursMinutesSecondsStringToSeconds(match.startsAt),
					stageId: match.stageId,
					mode: match.mode,
				})
				.returning("VideoMatch.id")
				.executeTakeFirstOrThrow();
			const matchId = videoMatchResult.id;

			for (const [i, weaponSplId] of match.weapons.entries()) {
				await trx
					.insertInto("VideoMatchPlayer")
					.values({
						videoMatchId: matchId,
						playerUserId: args.pov?.type === "USER" ? args.pov.userId : null,
						playerName: args.pov?.type === "NAME" ? args.pov.name : null,
						weaponSplId,
						player: i + 1,
					})
					.executeTakeFirstOrThrow();
			}
		}
		return { ...video, id: videoId };
	});
}

export function deleteById(id: number) {
	return db.deleteFrom("UnvalidatedVideo").where("id", "=", id).execute();
}
