import { type InferResult, sql } from "kysely";
import * as R from "remeda";
import { db } from "~/db/sql";
import type { Tables, TablesInsertable } from "~/db/tables";
import * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server";
import {
	type MonthYear,
	nextNonCompletedVoting,
	rangeToMonthYear,
} from "~/features/plus-voting/core";
import invariant from "~/utils/invariant";
import { commonUserSelect } from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
import * as PlusVoting from "./core/PlusVoting";

const resultsByMonthYearQuery = (args: MonthYear) =>
	db
		.selectFrom("PlusVotingResult")
		.innerJoin("User", "PlusVotingResult.votedId", "User.id")
		.select((eb) => [
			...commonUserSelect(eb),
			"PlusVotingResult.wasSuggested",
			"PlusVotingResult.tier",
			"PlusVotingResult.score",
			"PlusVotingResult.votedId",
		])
		.where("PlusVotingResult.month", "=", args.month)
		.where("PlusVotingResult.year", "=", args.year)
		.orderBy(sql`"User"."username" collate nocase`, "asc");
type ResultsByMonthYearQueryReturnType = InferResult<
	ReturnType<typeof resultsByMonthYearQuery>
>;

export async function allPlusTiersFromLatestVoting() {
	// resolving month & year separately first allows SQLite to push the
	// filtering down into the PlusVotingResult view's aggregation
	const latestVoting = await db
		.selectFrom("PlusVote")
		.select(["PlusVote.year", "PlusVote.month"])
		.where("PlusVote.validAfter", "<", sql<number>`strftime('%s', 'now')`)
		.orderBy("PlusVote.year", "desc")
		.orderBy("PlusVote.month", "desc")
		.limit(1)
		.executeTakeFirst();

	const rows = latestVoting
		? await db
				.selectFrom("PlusVotingResult")
				.select([
					"PlusVotingResult.votedId",
					"PlusVotingResult.tier",
					"PlusVotingResult.score",
					"PlusVotingResult.wasSuggested",
				])
				.where("PlusVotingResult.year", "=", latestVoting.year)
				.where("PlusVotingResult.month", "=", latestVoting.month)
				.execute()
		: [];

	const withPassed = PlusVoting.computePassedVoting(rows);
	return PlusVoting.computeFreshPlusTiers(withPassed);
}

export type ResultsByMonthYearItem = Unwrapped<typeof resultsByMonthYear>;
export async function resultsByMonthYear(args: MonthYear) {
	const rows = await resultsByMonthYearQuery(args).execute();

	const passedMap = new Map<
		string,
		{ passedVoting: number; wasSuggested: number }
	>();
	const rawForVoting = rows.map((row) => ({
		votedId: row.votedId,
		tier: row.tier,
		score: row.score,
		wasSuggested: row.wasSuggested,
	}));
	for (const r of PlusVoting.computePassedVoting(rawForVoting)) {
		passedMap.set(`${r.votedId}-${r.tier}`, {
			passedVoting: r.passedVoting,
			wasSuggested: r.wasSuggested,
		});
	}

	const enrichedRows = rows.map((row) => {
		const computed = passedMap.get(`${row.votedId}-${row.tier}`);
		return {
			...row,
			passedVoting: computed?.passedVoting ?? 0,
		};
	});

	return groupPlusVotingResults(enrichedRows);
}

type EnrichedRow = ResultsByMonthYearQueryReturnType[number] & {
	passedVoting: number;
};

function groupPlusVotingResults(rows: EnrichedRow[]) {
	const grouped: Record<
		number,
		{
			passed: EnrichedRow[];
			failed: EnrichedRow[];
		}
	> = {};

	for (const row of rows) {
		const playersOfTier = grouped[row.tier] ?? {
			passed: [],
			failed: [],
		};
		grouped[row.tier] = playersOfTier;

		playersOfTier[row.passedVoting ? "passed" : "failed"].push(row);
	}

	return Object.entries(grouped)
		.map(([tier, { passed, failed }]) => ({
			tier: Number(tier),
			passed,
			failed,
		}))
		.sort((a, b) => a.tier - b.tier);
}

export type UsersForVoting = {
	user: Pick<
		Tables["User"],
		"id" | "discordId" | "username" | "discordAvatar" | "bio"
	> & { customAvatarUrl: string | null };
	suggestion?: PlusSuggestionRepository.FindAllByMonthItem;
}[];

export async function usersForVoting(loggedInUser: {
	id: number;
	plusTier: number;
}) {
	const members = await db
		.selectFrom("User")
		.innerJoin("PlusTier", "PlusTier.userId", "User.id")
		.select((eb) => [...commonUserSelect(eb), "User.bio"])
		.where("PlusTier.tier", "=", loggedInUser.plusTier)
		.execute();

	const votingRange = nextNonCompletedVoting(new Date());
	invariant(votingRange, "No next voting found");

	const suggestedUsers = (
		await PlusSuggestionRepository.findAllByMonth(rangeToMonthYear(votingRange))
	).filter((suggestion) => suggestion.tier === loggedInUser.plusTier);

	const result: UsersForVoting = [];

	for (const member of members) {
		result.push({
			user: {
				id: member.id,
				discordId: member.discordId,
				username: member.username,
				discordAvatar: member.discordAvatar,
				customAvatarUrl: member.customAvatarUrl,
				bio: member.bio,
			},
		});
	}

	for (const suggestion of suggestedUsers) {
		result.push({
			user: {
				id: suggestion.suggested.id,
				discordId: suggestion.suggested.discordId,
				username: suggestion.suggested.username,
				discordAvatar: suggestion.suggested.discordAvatar,
				customAvatarUrl: suggestion.suggested.customAvatarUrl,
				bio: suggestion.suggested.bio,
			},
			suggestion,
		});
	}

	return R.shuffle(result.filter(({ user }) => user.id !== loggedInUser.id));
}

export async function hasVoted(args: {
	authorId: number;
	month: number;
	year: number;
}) {
	const rows = await db
		.selectFrom("PlusVote")
		.select(({ eb }) => eb.lit(1).as("one"))
		.where("PlusVote.authorId", "=", args.authorId)
		.where("PlusVote.month", "=", args.month)
		.where("PlusVote.year", "=", args.year)
		.execute();

	return rows.length > 0;
}

export type UpsertManyPlusVotesArgs = Pick<
	TablesInsertable["PlusVote"],
	"month" | "year" | "tier" | "authorId" | "votedId" | "score" | "validAfter"
>[];
export function upsertMany(votes: UpsertManyPlusVotesArgs) {
	const firstVote = votes[0];

	return db.transaction().execute(async (trx) => {
		await trx
			.deleteFrom("PlusVote")
			.where("PlusVote.authorId", "=", firstVote.authorId)
			.where("PlusVote.month", "=", firstVote.month)
			.where("PlusVote.year", "=", firstVote.year)
			.execute();

		await trx.insertInto("PlusVote").values(votes).execute();
	});
}
