import clsx from "clsx";
import { Eye } from "lucide-react";
import type * as React from "react";
import { Link } from "react-router";
import { Avatar } from "~/components/Avatar";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import { useUser } from "~/features/auth/core/user";
import { TournamentStream } from "~/features/tournament/components/TournamentStream";
import {
	useTournament,
	useTournamentVods,
} from "~/features/tournament/routes/to.$id";
import { matchEndedEarly } from "~/features/tournament-match/tournament-match-utils";
import { useAutoRerender } from "~/hooks/useAutoRerender";
import { databaseTimestampToDate } from "~/utils/dates";
import type { Unpacked } from "~/utils/types";
import {
	tournamentMatchPage,
	tournamentStreamsPage,
	vodUrl,
} from "~/utils/urls";
import type { Bracket } from "../../core/Bracket";
import * as Deadline from "../../core/Deadline";
import type { TournamentData } from "../../core/Tournament.server";
import parentStyles from "../../tournament-bracket.module.css";
import styles from "./bracket.module.css";

type LineType = "none" | "straight" | "curve-up" | "curve-down";

interface MatchProps {
	match: Unpacked<TournamentData["data"]["match"]>;
	isPreview?: boolean;
	type?: "winners" | "losers" | "grands" | "groups";
	group?: string;
	roundNumber: number;
	showSimulation: boolean;
	bracket: Bracket;
	hideMatchTimer?: boolean;
	lineType?: LineType;
	lineVerticalExtend?: number;
	spoilerCensor?: "full" | "score-only";
}

export function Match(props: MatchProps) {
	const isBye = !props.match.opponent1 || !props.match.opponent2;

	if (isBye) {
		return (
			<div className={clsx(styles.matchBye, styles.matchWrapper)}>
				<MatchLine
					lineType={props.lineType}
					verticalExtend={props.lineVerticalExtend}
				/>
			</div>
		);
	}

	return (
		<div className={styles.matchWrapper}>
			<MatchHeader {...props} />
			<MatchContent {...props}>
				<MatchRow {...props} side={1} />
				<div className={styles.matchSeparator} />
				<MatchRow {...props} side={2} />
			</MatchContent>
			{!props.hideMatchTimer ? (
				<MatchTimer match={props.match} bracket={props.bracket} />
			) : null}
			<MatchLine
				lineType={props.lineType}
				verticalExtend={props.lineVerticalExtend}
			/>
		</div>
	);
}

function MatchHeader({ match, type, roundNumber, group }: MatchProps) {
	const tournament = useTournament();
	const vods = useTournamentVods();
	const streamingParticipants = tournament.streamingParticipantIds ?? [];

	const prefix = () => {
		if (type === "winners") return "WB ";
		if (type === "losers") return "LB ";
		if (type === "grands") return "GF ";
		if (type === "groups") return `${group}`;
		return "";
	};

	const isOver =
		match.opponent1?.result === "win" || match.opponent2?.result === "win";
	const matchVods = isOver ? vods.filter((v) => v.matchId === match.id) : [];
	const hasStreams = () => {
		if (isOver || !match.opponent1?.id || !match.opponent2?.id) return false;
		if (
			tournament.ctx.castedMatchesInfo?.castedMatches.some(
				(cm) => cm.matchId === match.id,
			)
		) {
			return true;
		}

		const matchParticipants = [match.opponent1.id, match.opponent2.id].flatMap(
			(teamId) =>
				tournament.teamById(teamId)?.members.map((m) => m.userId) ?? [],
		);

		return streamingParticipants.some((p) => matchParticipants.includes(p));
	};
	const toBeCasted =
		!isOver &&
		tournament.ctx.castedMatchesInfo?.lockedMatches?.some(
			(lm) => lm.matchId === match.id,
		);

	return (
		<div className={styles.matchHeader}>
			<div className={styles.matchHeaderBox}>
				{prefix()}
				{roundNumber}.{match.number}
			</div>
			{toBeCasted ? (
				<SendouPopover
					trigger={
						<SendouButton
							className={clsx(
								styles.matchHeaderBox,
								styles.matchHeaderBoxButton,
							)}
						>
							🔒 CAST
						</SendouButton>
					}
				>
					Match is scheduled to be casted
				</SendouPopover>
			) : hasStreams() && match.startedAt ? (
				<SendouPopover
					placement="top"
					popoverClassName="w-max"
					trigger={
						<SendouButton
							className={clsx(
								styles.matchHeaderBox,
								styles.matchHeaderBoxButton,
							)}
						>
							🔴 LIVE
						</SendouButton>
					}
				>
					<MatchStreams match={match} />
				</SendouPopover>
			) : matchVods.length > 0 ? (
				<SendouPopover
					placement="top"
					popoverClassName="w-max"
					trigger={
						<SendouButton
							className={clsx(
								styles.matchHeaderBox,
								styles.matchHeaderBoxButton,
							)}
						>
							📺 VOD
						</SendouButton>
					}
				>
					<MatchVods vods={matchVods} />
				</SendouPopover>
			) : null}
		</div>
	);
}

function MatchContent({
	match,
	isPreview,
	children,
}: MatchProps & { children: React.ReactNode }) {
	const tournament = useTournament();

	if (!isPreview) {
		return (
			<Link
				className={styles.match}
				to={tournamentMatchPage({
					tournamentId: tournament.ctx.id,
					matchId: match.id,
				})}
				data-match-id={match.id}
			>
				{children}
			</Link>
		);
	}

	return <div className={styles.match}>{children}</div>;
}

function MatchRow({
	match,
	side,
	isPreview,
	showSimulation,
	bracket,
	spoilerCensor,
}: MatchProps & { side: 1 | 2 }) {
	const user = useUser();
	const tournament = useTournament();

	const opponentKey = `opponent${side}` as const;
	const opponent = match[`opponent${side}`];

	const score = () => {
		if (spoilerCensor) return null;
		if (!match.opponent1?.id || !match.opponent2?.id || isPreview) return null;

		const opponentScore = opponent!.score;
		const opponentResult = opponent!.result;

		// Display W/L as the score might not reflect the winner set in the early ending
		const round = bracket.data.round.find((r) => r.id === match.round_id);
		if (
			round?.maps &&
			matchEndedEarly({
				opponentOne: match.opponent1,
				opponentTwo: match.opponent2,
				count: round.maps.count,
				countType: round.maps.type,
			})
		) {
			if (opponentResult === "win") return "W";
			if (opponentResult === "loss") return "L";
		}

		return opponentScore ?? 0;
	};

	const isLoser = spoilerCensor ? false : opponent?.result === "loss";

	const { team, simulated } = (() => {
		if (opponent?.id) {
			return { team: tournament.teamById(opponent.id), simulated: false };
		}

		const simulated = showSimulation
			? bracket.simulatedMatch(match.id)
			: undefined;
		const simulatedOpponent = simulated?.[opponentKey];

		return simulatedOpponent?.id
			? { team: tournament.teamById(simulatedOpponent.id), simulated: true }
			: { team: null, simulated: true };
	})();

	const ownTeam = tournament.teamMemberOfByUser(user);

	const logoSrc = team ? tournament.tournamentTeamLogoSrc(team) : null;
	const showAvatar = spoilerCensor === "full" ? false : !simulated && team;

	const isBigSeedNumber =
		spoilerCensor === "full" ? false : team?.seed && team.seed > 99;

	const displayedSeed = spoilerCensor === "full" ? null : team?.seed;
	const displayedName =
		spoilerCensor === "full" ? "???" : (team?.name ?? "???");

	return (
		<div
			className={clsx("stack horizontal", { "text-lighter": isLoser })}
			data-participant-id={team?.id}
			title={
				spoilerCensor === "full"
					? undefined
					: team?.members.map((m) => m.username).join(", ")
			}
		>
			<div
				className={clsx(styles.matchSeed, {
					"text-lighter italic opaque": simulated,
					[styles.matchSeedWide]: isBigSeedNumber,
				})}
			>
				{displayedSeed}
			</div>
			{showAvatar ? (
				<Avatar
					size="xxxs"
					url={logoSrc}
					identiconInput={team!.name}
					className="mr-1"
				/>
			) : null}
			<div
				className={clsx(styles.matchTeamName, {
					"text-theme-secondary":
						!simulated && ownTeam && ownTeam?.id === team?.id,
					"text-lighter italic opaque": simulated,
					[styles.matchTeamNameNarrow]:
						// either but not both
						(showAvatar || isBigSeedNumber) && !(showAvatar && isBigSeedNumber),
					// both
					[styles.matchTeamNameNarrowest]: showAvatar && isBigSeedNumber,
					invisible: !team,
				})}
			>
				{displayedName}
			</div>{" "}
			<div className={styles.matchScore}>{score()}</div>
		</div>
	);
}

function MatchStreams({ match }: Pick<MatchProps, "match">) {
	const tournament = useTournament();

	if (!match.opponent1?.id || !match.opponent2?.id) {
		return null;
	}

	const castingAccount = tournament.ctx.castedMatchesInfo?.castedMatches.find(
		(cm) => cm.matchId === match.id,
	)?.twitchAccount;

	const matchParticipants = [match.opponent1.id, match.opponent2.id].flatMap(
		(teamId) => tournament.teamById(teamId)?.members.map((m) => m.userId) ?? [],
	);

	const streamsOfThisMatch = tournament.streams.filter(
		(stream) =>
			(stream.userId && matchParticipants.includes(stream.userId)) ||
			stream.twitchUserName === castingAccount,
	);

	if (streamsOfThisMatch.length === 0) {
		return (
			<div className={parentStyles.streamPopover}>
				After all there seems to be no streams of this match. Check the{" "}
				<Link to={tournamentStreamsPage(tournament.ctx.id)}>streams page</Link>{" "}
				for all the available streams.
			</div>
		);
	}

	return (
		<div
			className={clsx("stack md justify-center", parentStyles.streamPopover)}
			data-testid="stream-popover"
		>
			{streamsOfThisMatch.map((stream) => (
				<TournamentStream
					key={stream.twitchUserName}
					stream={stream}
					withThumbnail={false}
				/>
			))}
		</div>
	);
}

interface MatchVodsProps {
	vods: Array<{
		matchId: number;
		userId: number | null;
		platform: string;
		account: string;
		platformVideoId: string;
		timestampSeconds: number;
		viewCount: number;
	}>;
}

function MatchVods({ vods }: MatchVodsProps) {
	const tournament = useTournament();

	return (
		<div className={parentStyles.vodGrid}>
			{vods.map((vod) => {
				const team = vod.userId
					? tournament.ctx.teams.find((t) =>
							t.members.some((m) => m.userId === vod.userId),
						)
					: null;
				const user = team?.members.find((m) => m.userId === vod.userId);

				return (
					<a
						key={`${vod.platformVideoId}-${vod.account}`}
						href={vodUrl(vod)}
						target="_blank"
						rel="noopener noreferrer"
					>
						<span className={parentStyles.vodUser}>
							{user ? (
								<>
									<Avatar size="xxs" user={user} />
									<span className="font-semi-bold">{user.username}</span>
								</>
							) : (
								<span className="font-semi-bold">{vod.account}</span>
							)}
						</span>
						<span
							className={clsx("text-theme-secondary", parentStyles.vodTeamName)}
						>
							{user ? team?.name : null}
						</span>
						<span className="text-lighter stack horizontal xs items-center">
							<Eye size={12} />
							{vod.viewCount.toLocaleString()}
						</span>
					</a>
				);
			})}
		</div>
	);
}

function MatchTimer({ match, bracket }: Pick<MatchProps, "match" | "bracket">) {
	const tournament = useTournament();

	if (tournament.isLeagueDivision) return null;
	if (!match.startedAt) return null;

	const isOver =
		match.opponent1?.result === "win" || match.opponent2?.result === "win";
	if (isOver) return null;

	const isLocked = tournament.ctx.castedMatchesInfo?.lockedMatches?.some(
		(lm) => lm.matchId === match.id,
	);
	if (isLocked) return null;

	const round = bracket.data.round.find((r) => r.id === match.round_id);
	const bestOf = round?.maps?.count;
	if (!bestOf) return null;

	return (
		<MatchTimerInner
			startedAt={match.startedAt}
			gamesCompleted={
				(match.opponent1?.score ?? 0) + (match.opponent2?.score ?? 0)
			}
			bestOf={bestOf}
		/>
	);
}

interface MatchTimerInnerProps {
	startedAt: number;
	gamesCompleted: number;
	bestOf: number;
}

function MatchTimerInner({
	startedAt,
	gamesCompleted,
	bestOf,
}: MatchTimerInnerProps) {
	const startedAtDate = databaseTimestampToDate(startedAt);
	const now = useAutoRerender("minute", { alignTo: startedAtDate });

	const elapsedMinutes = Math.floor(
		(now.getTime() - startedAtDate.getTime()) / 60_000,
	);
	const status = Deadline.matchStatus({
		elapsedMinutes,
		gamesCompleted,
		maxGamesCount: bestOf,
	});

	const displayText = elapsedMinutes >= 60 ? "1h+" : `${elapsedMinutes}m`;

	return (
		<div className={styles.matchTimer} data-testid="bracket-match-timer">
			<div
				className={clsx(styles.matchHeaderBox, styles.matchHeaderBoxButton, {
					[styles.matchTimerWarning]: status === "warning",
					[styles.matchTimerError]: status === "error",
				})}
			>
				{displayText}
			</div>
		</div>
	);
}

interface MatchLineProps {
	lineType?: LineType;
	verticalExtend?: number;
}

function MatchLine({ lineType, verticalExtend }: MatchLineProps) {
	if (!lineType || lineType === "none") return null;

	const lineClass =
		lineType === "straight"
			? styles.matchLineStraight
			: lineType === "curve-up"
				? styles.matchLineCurveUp
				: styles.matchLineCurveDown;

	const style = verticalExtend
		? ({
				"--bracket-vertical-extend": `${verticalExtend}px`,
			} as React.CSSProperties)
		: undefined;

	return (
		<div className={clsx(styles.matchLineContainer, lineClass)} style={style}>
			{lineType === "curve-down" ? (
				<div className={styles.matchLineConnectorDown} style={style} />
			) : null}
			{lineType === "curve-up" ? (
				<div className={styles.matchLineConnectorUp} style={style} />
			) : null}
		</div>
	);
}
