import cachified from "@epic-web/cachified";
import type { LoaderFunctionArgs } from "react-router";
import { getUser } from "~/features/auth/core/user.server";
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
import { chatAccessible } from "~/features/chat/chat-utils";
import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { isLeagueRoundLocked } from "~/features/tournament/tournament-utils";
import { MatchStatus as Status } from "~/features/tournament-bracket/core/engine/types";
import * as PickBan from "~/features/tournament-bracket/core/PickBan";
import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server";
import { matchPageParamsSchema } from "~/features/tournament-bracket/tournament-bracket-schemas.server";
import { tournamentTeamToActiveRosterUserIds } from "~/features/tournament-bracket/tournament-bracket-utils";
import * as UserCardRepository from "~/features/user-card/UserCardRepository.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
import { IS_E2E_TEST_RUN } from "~/utils/e2e";
import { logger } from "~/utils/logger";
import type { SerializeFrom } from "~/utils/remix";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { tournamentMatchPage } from "~/utils/urls";
import { executeRoll } from "../core/executeRoll.server";
import { mapListFromResults, resolveMapList } from "../core/mapList.server";
import * as TournamentMatchRepository from "../TournamentMatchRepository.server";
import { matchEndedEarly } from "../tournament-match-utils";

export type TournamentMatchLoaderData = SerializeFrom<typeof loader>;

export const loader = async ({ params }: LoaderFunctionArgs) => {
	const { mid: matchId, id: tournamentId } = parseParams({
		params,
		schema: matchPageParamsSchema,
	});
	const user = getUser();
	const tournament = await tournamentFromDBCached({
		tournamentId,
		user: undefined,
	});

	const match = notFoundIfFalsy(
		await TournamentMatchRepository.findMatchById(matchId),
	);

	if (match.tournamentId !== tournamentId) {
		throw new Response(null, { status: 404 });
	}

	const isBye = !match.opponentOne || !match.opponentTwo;
	if (isBye) {
		throw new Response(null, { status: 404 });
	}

	let pickBanEvents = match.roundMaps?.pickBan
		? await TournamentRepository.pickBanEventsByMatchId(match.id)
		: [];

	const results = await TournamentMatchRepository.findResultsByMatchId(matchId);

	const reportedWeapons =
		await ReportedWeaponRepository.findByTournamentMatchId(matchId);

	const matchIsOver =
		match.opponentOne?.result === "win" || match.opponentTwo?.result === "win";

	if (
		!matchIsOver &&
		match.roundMaps?.pickBan === "CUSTOM" &&
		match.roundMaps.customFlow &&
		match.opponentOne?.id &&
		match.opponentTwo?.id
	) {
		const currentStep = PickBan.resolveCurrentStep({
			eventCount: pickBanEvents.length,
			preSet: match.roundMaps.customFlow.preSet,
			postGame: match.roundMaps.customFlow.postGame,
			resultsCount: results.length,
		});
		if (currentStep?.action === "ROLL") {
			const teamOne = tournament.teamById(match.opponentOne.id);
			const teamTwo = tournament.teamById(match.opponentTwo.id);
			if (teamOne && teamTwo) {
				const rollExecuted = await executeRoll({
					matchId,
					maps: match.roundMaps,
					pickBanEvents,
					results,
					tournamentId,
					teams: [teamOne, teamTwo],
					tieBreakerMapPool: tournament.ctx.tieBreakerMapPool,
				});
				if (rollExecuted) {
					pickBanEvents = await TournamentRepository.pickBanEventsByMatchId(
						match.id,
					);
				}
			}
		}
	}

	// cached so that some user changing their noScreen preference doesn't
	// change the selection once the match has started
	const noScreen =
		match.opponentOne?.id && match.opponentTwo?.id
			? await cachified({
					key: `no-screen-mid-${matchId}-${match.opponentOne.id}-${match.opponentTwo.id}`,
					cache,
					// avoid preferences from other test runs leaking in
					ttl: IS_E2E_TEST_RUN ? -1 : ttl(IN_MILLISECONDS.TWO_DAYS),
					async getFreshValue() {
						return UserRepository.anyUserPrefersNoScreen(
							match.players.map((p) => p.id),
						);
					},
				})
			: null;

	const mapList =
		match.opponentOne?.id && match.opponentTwo?.id
			? matchIsOver
				? mapListFromResults(results)
				: resolveMapList({
						tournamentId,
						matchId,
						teams: [match.opponentOne.id, match.opponentTwo.id],
						mapPoolByTeamId: (teamId) =>
							tournament.teamById(teamId)?.mapPool ?? [],
						mapPickingStyle: match.mapPickingStyle,
						maps: match.roundMaps,
						tieBreakerMapPool: tournament.ctx.tieBreakerMapPool,
						pickBanEvents,
						recentlyPlayedMaps:
							match.mapPickingStyle !== "TO"
								? await TournamentTeamRepository.findRecentlyPlayedMapsByIds({
										teamIds: [match.opponentOne.id, match.opponentTwo.id],
									}).catch((error) => {
										logger.error("Failed to fetch recently played maps", error);
										return [];
									})
								: undefined,
					})
			: null;

	const endedEarly = matchIsOver
		? matchEndedEarly({
				opponentOne: {
					score: match.opponentOne?.score,
					result: match.opponentOne?.result,
				},
				opponentTwo: {
					score: match.opponentTwo?.score,
					result: match.opponentTwo?.result,
				},
				count: match.roundMaps.count,
				countType: match.roundMaps.type,
			})
		: false;

	if (
		match.chatCode &&
		!matchIsOver &&
		match.opponentOne?.id &&
		match.opponentTwo?.id &&
		match.status > Status.Locked
	) {
		// only add global chat for active roster (or all if not yet set i.e. first match)
		// if roster changed mid-set the subs can still see the chat on the match page
		const teamAlpha = tournament.teamById(match.opponentOne.id)!;
		const teamAlphaActiveRoster =
			tournamentTeamToActiveRosterUserIds(
				teamAlpha,
				tournament.minMembersPerTeam,
			) ?? teamAlpha.members.map((m) => m.userId);
		const teamBravo = tournament.teamById(match.opponentTwo.id)!;
		const teamBravoActiveRoster =
			tournamentTeamToActiveRosterUserIds(
				teamBravo,
				tournament.minMembersPerTeam,
			) ?? teamBravo.members.map((m) => m.userId);

		const playerIds = [...teamAlphaActiveRoster, ...teamBravoActiveRoster];

		const matchContext = tournament.matchContextNamesById(matchId);

		ChatSystemMessage.setMetadata({
			chatCode: match.chatCode,
			header: matchContext.roundName ?? `Match #${matchId}`,
			subtitle: tournament.ctx.name,
			url: tournamentMatchPage({ tournamentId, matchId }),
			imageUrl: tournament.ctx.logoUrl,
			participantUserIds: playerIds,
			expiresAfter: tournament.isLeagueDivision ? { days: 30 } : { hours: 2 },
		});
	}

	const hasPermsToSeeChat =
		tournament.isOrganizerOrStreamer(user) ||
		match.players.some((p) => p.id === user?.id);

	const isSiteStaff = user?.roles.includes("STAFF") ?? false;
	const isTournamentStaff = tournament.isOrganizer(user);
	const chatCodeExpired =
		tournament.ctx.isFinalized && !isSiteStaff && !isTournamentStaff
			? true
			: !chatAccessible({
					expiresAfterDays: tournament.isLeagueDivision ? 30 : 7,
					comparedTo: tournament.ctx.startTime,
				});

	const visibleChatCode =
		hasPermsToSeeChat && !chatCodeExpired ? match.chatCode : undefined;

	const isParticipant = match.players.some((p) => p.id === user?.id);
	const canJoin =
		!matchIsOver &&
		match.opponentOne?.id != null &&
		match.opponentTwo?.id != null &&
		(isParticipant || tournament.isOrganizerOrStreamer(user)) &&
		!isLeagueRoundLocked(tournament, match.roundId);

	return {
		...(await UserCardRepository.userCards({
			userIds: match.players.map((p) => p.id),
			include: {
				friendCode: isParticipant || isSiteStaff || isTournamentStaff,
			},
		})),
		match: hasPermsToSeeChat ? match : { ...match, chatCode: undefined },
		results,
		reportedWeapons,
		mapList,
		matchIsOver,
		endedEarly,
		noScreen,
		chatCode: visibleChatCode,
		canJoin,
		pickBanEventCount: pickBanEvents.length,
		pickBanEvents: pickBanEvents.map((e) => ({
			type: e.type,
			stageId: e.stageId,
			mode: e.mode,
			createdAt: e.createdAt,
		})),
	};
};
