import { cachified } from "@epic-web/cachified";
import { addDays } from "date-fns";
import { href } from "react-router";
import * as R from "remeda";
import * as ExternalStreamRepository from "~/features/admin/ExternalStreamRepository.server";
import { userIsBanned } from "~/features/ban/core/banned.server";
import type { ShowcaseCalendarEvent } from "~/features/calendar/calendar-types";
import {
	COMBINED_STREAMS_KEY,
	getLiveTournamentStreamerTwitchNames,
	getLiveTournamentStreams,
	type SidebarStream,
} from "~/features/core/streams/streams.server";
import * as FriendRepository from "~/features/friends/FriendRepository.server";
import {
	type FriendActivityType,
	isLiveFriendActivity,
} from "~/features/friends/friends-constants";
import {
	type FriendActivity,
	resolveFriendActivity,
} from "~/features/friends/friends-utils.server";
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server";
import type { SidebarScrim } from "~/features/scrims/ScrimPostRepository.server";
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
import { getSendouQSidebarStreams } from "~/features/sendouq-streams/core/streams.server";
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
import * as SavedCalendarEventRepository from "~/features/tournament/SavedCalendarEventRepository.server";
import { cache, ttl } from "~/utils/cache.server";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import {
	BLANK_IMAGE_URL,
	discordAvatarUrl,
	navIconUrl,
	twitchUrl,
	userPage,
} from "~/utils/urls";
import * as StreamRanking from "./StreamRanking";

export type SidebarEvent = {
	id: number;
	name: string;
	url: string;
	logoUrl: string | null;
	startTime: number;
	type: "tournament" | "scrim";
	scrimStatus?: "booked" | "looking" | "requestPending";
};

export type SidebarFriend = {
	id: number;
	name: string;
	discordId: string;
	discordAvatar: string | null;
	customAvatarUrl: string | null;
	url: string;
	subtitle: string;
	badge: string;
	activityType: FriendActivityType | null;
	matchId: number | null;
	tournamentId: number | null;
};

const MAX_EVENTS_VISIBLE = 5;
const MAX_FRIENDS_VISIBLE = 8;
const MAX_STREAMS_VISIBLE = 5;
const UPCOMING_TOURNAMENT_WINDOW_DAYS = 3;
const SENDOUQ_QUOTA = 2;
const TOURNAMENT_SUB_QUOTA = 2;

export async function resolveSidebarData(userId: number | null) {
	if (!userId) {
		return {
			events: [] as SidebarEvent[],
			friends: [] as SidebarFriend[],
			streams: await combinedStreamsCached(),
			savedTournamentIds: [] as number[],
			incomingFriendRequestIds: [] as number[],
		};
	}

	const [
		tournamentsData,
		scrimsData,
		friendsWithActivity,
		savedTournaments,
		incomingFriendRequestIds,
	] = await Promise.all([
		ShowcaseTournaments.categorizedTournamentsByUserId(userId),
		ScrimPostRepository.findUserScrims(userId),
		FriendRepository.findByUserIdWithActivity(userId),
		SavedCalendarEventRepository.upcoming(userId),
		FriendRepository.findPendingReceivedRequestIds(userId),
	]);

	const seenTournamentIds = new Set<number>();
	const tournamentEvents: SidebarEvent[] = [
		...tournamentsData.participatingFor,
		...tournamentsData.organizingFor,
	]
		.filter((t) => {
			if (seenTournamentIds.has(t.id)) return false;
			seenTournamentIds.add(t.id);
			return true;
		})
		.map(tournamentToSidebarEvent);

	const savedEvents: SidebarEvent[] = savedTournaments
		.filter((t) => !seenTournamentIds.has(t.id))
		.map((t) => {
			seenTournamentIds.add(t.id);
			return tournamentToSidebarEvent(t);
		});

	const scrimEvents: SidebarEvent[] = scrimsData.map(scrimToSidebarEvent);

	const events = [...tournamentEvents, ...savedEvents, ...scrimEvents]
		.sort((a, b) => a.startTime - b.startTime)
		.slice(0, MAX_EVENTS_VISIBLE);

	const friends = resolveFriends(friendsWithActivity);

	const savedTournamentIds = savedTournaments.map((t) => t.id);

	return {
		events,
		friends,
		streams: await combinedStreamsCached(),
		savedTournamentIds,
		incomingFriendRequestIds,
	};
}

function combinedStreamsCached(): Promise<SidebarStream[]> {
	return cachified({
		key: COMBINED_STREAMS_KEY,
		cache,
		ttl: ttl(10 * 60 * 1000),
		async getFreshValue() {
			return combinedStreams();
		},
	});
}

async function combinedStreams(): Promise<SidebarStream[]> {
	const tournamentStreams = getLiveTournamentStreams();
	const [sendouQEntries, xRankRows, upcomingTournaments, externalStreams] =
		await Promise.all([
			getSendouQSidebarStreams(),
			LiveStreamRepository.findXRankStreams(),
			ShowcaseTournaments.upcomingTournaments(),
			ExternalStreamRepository.forSidebar(),
		]);

	const seenUsernames = new Set([
		...getLiveTournamentStreamerTwitchNames(),
		...sendouQEntries.flatMap((e) =>
			e.twitchUsernames.map((t) => t.toLowerCase()),
		),
	]);

	const ranked: { stream: SidebarStream; score: number }[] = [];

	for (const externalStream of externalStreams) {
		ranked.push({
			stream: {
				id: `external-${externalStream.id}`,
				name: externalStream.name,
				imageUrl: externalStream.avatarUrl ?? BLANK_IMAGE_URL,
				url: externalStream.url,
				subtitle: "",
				startsAt: externalStream.startTime,
				tier: null,
			},
			score: StreamRanking.EXTERNAL_STREAM_SCORE,
		});
	}

	for (const stream of tournamentStreams) {
		ranked.push({
			stream,
			score: StreamRanking.tournamentTierToScore(
				stream.tier,
				stream.membersPerTeam,
			),
		});
	}

	for (const { sidebarStream, tier } of sendouQEntries) {
		const score = tier ? StreamRanking.sendouQTierToScore(tier) : 9;
		ranked.push({ stream: sidebarStream, score });
	}

	const xRankByUser = new Map<number, (typeof xRankRows)[number]>();
	for (const row of xRankRows) {
		const existing = xRankByUser.get(row.id);
		if (!existing || (row.peakXp ?? 0) > (existing.peakXp ?? 0)) {
			xRankByUser.set(row.id, row);
		}
	}

	for (const row of xRankByUser.values()) {
		if (userIsBanned(row.id)) continue;

		if (
			row.twitchUsername &&
			seenUsernames.has(row.twitchUsername.toLowerCase())
		) {
			continue;
		}

		const score = StreamRanking.xpToScore(row.peakXp ?? 0);
		if (score === null) continue;

		ranked.push({
			stream: {
				id: `xrank-${row.id}`,
				name: row.username,
				imageUrl: row.customAvatarUrl
					? row.customAvatarUrl
					: row.discordAvatar
						? discordAvatarUrl({
								discordId: row.discordId,
								discordAvatar: row.discordAvatar,
								size: "sm",
							})
						: BLANK_IMAGE_URL,
				url: row.twitchUsername
					? twitchUrl(row.twitchUsername)
					: userPage({ discordId: row.discordId, customUrl: row.customUrl }),
				subtitle: "",
				startsAt: Math.floor(Date.now() / 1000),
				tier: null,
				peakXp: row.peakXp ?? undefined,
			},
			score,
		});
	}

	const nowTimestamp = dateToDatabaseTimestamp(new Date());
	const threeDaysFromNow = dateToDatabaseTimestamp(
		addDays(new Date(), UPCOMING_TOURNAMENT_WINDOW_DAYS),
	);
	for (const event of upcomingTournaments) {
		const effectiveTier = event.tier ?? event.tentativeTier;
		if (effectiveTier === null) continue;
		if (event.startTime < nowTimestamp) continue;
		if (event.startTime > threeDaysFromNow) continue;
		if (event.hidden) continue;

		const membersPerTeam = event.minMembersPerTeam ?? 4;

		ranked.push({
			stream: {
				id: `upcoming-${event.id}`,
				name: event.name,
				imageUrl: event.logoUrl ?? BLANK_IMAGE_URL,
				url: event.url,
				subtitle: "",
				startsAt: event.startTime,
				tier: (event.tier as TournamentTierNumber) ?? null,
				membersPerTeam,
				tentativeTier: event.tentativeTier ?? undefined,
			},
			score: StreamRanking.upcomingTournamentTierToScore(
				effectiveTier,
				membersPerTeam,
			),
		});
	}

	return StreamRanking.rank(ranked, MAX_STREAMS_VISIBLE);
}

type FriendWithActivity = Awaited<
	ReturnType<typeof FriendRepository.findByUserIdWithActivity>
>[number];

function resolveFriends(friendsWithActivity: FriendWithActivity[]) {
	const unique = R.uniqueBy(friendsWithActivity, (f) => f.id);
	const friendRows = unique.filter((f) => f.friendshipId !== null);
	const teamMemberRows = unique.filter((f) => f.friendshipId === null);

	const activeFriends: SidebarFriend[] = [];
	const sendouqFriends: SidebarFriend[] = [];
	const tournamentSubFriends: SidebarFriend[] = [];
	const inactiveFriends: FriendWithActivity[] = [];

	for (const friend of friendRows) {
		const activity = activityForRow(friend);

		if (!activity.type) {
			inactiveFriends.push(friend);
			continue;
		}

		const sidebarFriend = rowToSidebarFriend(friend, activity);

		if (isLiveFriendActivity(activity.type)) {
			activeFriends.push(sidebarFriend);
		} else if (activity.type === "SENDOUQ") {
			sendouqFriends.push(sidebarFriend);
		} else {
			tournamentSubFriends.push(sidebarFriend);
		}
	}

	const result: SidebarFriend[] = [];

	const sendouqToShow = sendouqFriends.slice(0, SENDOUQ_QUOTA);
	const tournamentToShow = tournamentSubFriends.slice(0, TOURNAMENT_SUB_QUOTA);

	result.push(...sendouqToShow, ...tournamentToShow);

	const remaining = MAX_FRIENDS_VISIBLE - result.length;
	if (remaining > 0) {
		const extraSendouq = sendouqFriends.slice(SENDOUQ_QUOTA);
		const extraTournament = tournamentSubFriends.slice(TOURNAMENT_SUB_QUOTA);
		result.push(...[...extraSendouq, ...extraTournament].slice(0, remaining));
	}

	if (result.length < MAX_FRIENDS_VISIBLE) {
		result.push(...activeFriends.slice(0, MAX_FRIENDS_VISIBLE - result.length));
	}

	if (result.length < MAX_FRIENDS_VISIBLE) {
		const shownIds = new Set(result.map((f) => f.id));
		const inactiveTeamMembers: FriendWithActivity[] = [];

		for (const tm of teamMemberRows) {
			if (result.length >= MAX_FRIENDS_VISIBLE) break;
			if (shownIds.has(tm.id)) continue;

			const activity = activityForRow(tm);
			if (!activity.type) {
				inactiveTeamMembers.push(tm);
				continue;
			}

			result.push(rowToSidebarFriend(tm, activity));
			shownIds.add(tm.id);
		}

		for (const friend of inactiveFriends) {
			if (result.length >= MAX_FRIENDS_VISIBLE) break;
			if (shownIds.has(friend.id)) continue;

			result.push(rowToSidebarFriend(friend, null));
			shownIds.add(friend.id);
		}

		for (const tm of inactiveTeamMembers) {
			if (result.length >= MAX_FRIENDS_VISIBLE) break;

			result.push(rowToSidebarFriend(tm, null));
		}
	}

	return result.slice(0, MAX_FRIENDS_VISIBLE);
}

function activityForRow(row: FriendWithActivity): FriendActivity {
	return resolveFriendActivity({
		friendId: row.id,
		tournamentId: row.tournamentId,
		tournamentName: row.tournamentName,
		teamMemberCount: row.teamMemberCount,
		tournamentMinTeamSize: row.tournamentMinTeamSize,
	});
}

function rowToSidebarFriend(
	row: FriendWithActivity,
	activity: FriendActivity | null,
): SidebarFriend {
	return {
		id: row.id,
		name: row.username,
		discordId: row.discordId,
		discordAvatar: row.discordAvatar,
		customAvatarUrl: row.customAvatarUrl,
		url: userPage({ discordId: row.discordId, customUrl: row.customUrl }),
		subtitle: activity?.subtitle ?? "",
		badge: activity?.badge ?? "",
		activityType: activity?.type ?? null,
		matchId: activity?.matchId ?? null,
		tournamentId: activity?.tournamentId ?? row.tournamentId,
	};
}

export function tournamentToSidebarEvent(
	t: ShowcaseCalendarEvent,
): SidebarEvent {
	return {
		id: t.id,
		name: t.name,
		url: t.url,
		logoUrl: t.logoUrl,
		startTime: t.startTime,
		type: "tournament" as const,
	};
}

const SCRIMS_ICON_URL = `${navIconUrl("scrims")}.avif`;

export function scrimToSidebarEvent(s: SidebarScrim): SidebarEvent {
	return {
		id: s.id,
		name: s.opponentName ?? "Scrim",
		url:
			s.status === "booked"
				? href("/scrims/:id", { id: String(s.id) })
				: s.status === "requestPending"
					? `${href("/scrims")}?pendingRequestPostId=${s.id}`
					: href("/scrims"),
		logoUrl: s.opponentAvatarUrl ?? SCRIMS_ICON_URL,
		startTime: s.at,
		type: "scrim" as const,
		scrimStatus: s.status,
	};
}
