import clsx from "clsx";
import {
	Link as LinkIcon,
	Minus,
	MousePointerClick,
	Plus,
	RefreshCcw,
	Unlink,
} from "lucide-react";
import * as React from "react";
import { useFilter } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { type FetcherWithComponents, Link, useFetcher } from "react-router";
import { SendouDialog } from "~/components/elements/Dialog";
import {
	SendouSelect,
	SendouSelectItem,
	SendouSelectItemSection,
} from "~/components/elements/Select";
import { ModeImage, StageImage } from "~/components/Image";
import { InfoPopover } from "~/components/InfoPopover";
import { Input } from "~/components/Input";
import { Label } from "~/components/Label";
import { LocaleTime } from "~/components/LocaleTime";
import { SubmitButton } from "~/components/SubmitButton";
import type { CustomPickBanFlow, TournamentRoundMaps } from "~/db/tables";
import {
	useTournament,
	useTournamentPreparedMaps,
} from "~/features/tournament/routes/to.$id";
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
import type { TournamentManagerDataSet } from "~/features/tournament-bracket/core/engine/types";
import * as PickBan from "~/features/tournament-bracket/core/PickBan";
import { modesShort } from "~/modules/in-game-lists/modes";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import { nullFilledArray } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import { assertUnreachable } from "~/utils/types";
import { calendarEditPage } from "~/utils/urls";
import { SendouButton } from "../../../components/elements/Button";
import { logger } from "../../../utils/logger";
import type { Bracket } from "../core/Bracket";
import * as Engine from "../core/engine";
import * as PreparedMaps from "../core/PreparedMaps";
import { getRounds } from "../core/rounds";
import type { Tournament } from "../core/Tournament";
import {
	type BracketMapCounts,
	generateTournamentRoundMaplist,
	type TournamentRoundMapList,
} from "../core/toMapList";
import styles from "./BracketMapListDialog.module.css";
import { CustomFlowBuilder } from "./CustomFlowBuilder";

export function BracketMapListDialog({
	close,
	bracket,
	bracketIdx,
	isPreparing,
}: {
	close: () => void;
	bracket: Bracket;
	bracketIdx: number;
	isPreparing?: boolean;
}) {
	const { t } = useTranslation(["common"]);
	const fetcher = useFetcher();
	const tournament = useTournament();
	const untrimmedPreparedMaps = useBracketPreparedMaps(bracketIdx);

	useCloseModalOnSubmit(fetcher, close);

	const bracketTeamsCount = bracket.participantTournamentTeamIds.length;

	const preparedMaps =
		!isPreparing &&
		(bracket.type === "single_elimination" ||
			bracket.type === "double_elimination")
			? // we are about to start bracket, "trim" prepared map for actual
				PreparedMaps.trimPreparedEliminationMaps({
					preparedMaps: untrimmedPreparedMaps,
					teamCount: bracketTeamsCount,
					bracket,
				})
			: untrimmedPreparedMaps;

	const [eliminationTeamCount, setEliminationTeamCount] = React.useState<
		number | null
	>(() => {
		if (preparedMaps?.eliminationTeamCount) {
			return preparedMaps.eliminationTeamCount;
		}

		if (isPreparing) {
			return null;
		}

		return PreparedMaps.eliminationTeamCountOptions(bracketTeamsCount)[0].max;
	});
	const [thirdPlaceMatchLinked, setThirdPlaceMatchLinked] = React.useState(
		() => {
			if (
				!Engine.hasThirdPlaceMatch({
					type: bracket.type,
					settings: bracket.settings,
					participantsCount: eliminationTeamCount ?? 2,
				})
			) {
				return true; // default to true if not applicable or elimination team count not yet set (initial state)
			}

			if (!preparedMaps?.maps) {
				return true;
			}

			// if maps were set before infer default from whether finals and third place match have different maps or not

			const finalsMaps = preparedMaps.maps
				.filter((map) => map.groupId === 0)
				.sort((a, b) => b.roundId - a.roundId)[0];
			const thirdPlaceMaps = preparedMaps.maps.find((map) => map.groupId === 1);

			if (!finalsMaps?.list || !thirdPlaceMaps?.list) {
				logger.error(
					"Expected both finals and third place match maps to be defined",
				);
				return true;
			}

			return (
				finalsMaps.count === thirdPlaceMaps.count &&
				finalsMaps.pickBan === thirdPlaceMaps.pickBan &&
				finalsMaps.list.every(
					(map, i) =>
						map.mode === thirdPlaceMaps.list![i].mode &&
						map.stageId === thirdPlaceMaps.list![i].stageId,
				)
			);
		},
	);
	const [patterns, setPatterns] = React.useState(new Map<number, string>());

	const bracketData = isPreparing
		? teamCountAdjustedBracketData({
				bracket,
				teamCount: eliminationTeamCount ?? 2,
			})
		: bracket.data;
	const rounds = bracketData.round;
	const defaultRoundBestOfs = bracket.defaultRoundBestOfs(bracketData);

	const [countType, setCountType] = React.useState<TournamentRoundMaps["type"]>(
		preparedMaps?.maps[0].type ?? "BEST_OF",
	);

	const [maps, setMaps] = React.useState(() => {
		if (preparedMaps) {
			return new Map(preparedMaps.maps.map((map) => [map.roundId, map]));
		}

		return generateTournamentRoundMaplist({
			mapCounts: defaultRoundBestOfs,
			roundsWithPickBan: new Set(),
			pool: tournament.ctx.toSetMapPool,
			rounds,
			type: bracket.type,
			pickBanStyle: null,
			patterns,
			countType,
		});
	});
	const [pickBanStyle, setPickBanStyle] = React.useState(
		Array.from(maps.values()).find((round) => round.pickBan)?.pickBan ??
			"COUNTERPICK",
	);
	const [customFlow, setCustomFlow] = React.useState<CustomPickBanFlow | null>(
		preparedMaps?.maps.find((m) => m.customFlow)?.customFlow ?? null,
	);
	const [hoveredMap, setHoveredMap] = React.useState<string | null>(null);

	const roundsWithPickBan = new Set(
		Array.from(maps.entries())
			.filter(([, round]) => round.pickBan)
			.map(([roundId]) => roundId),
	);

	const mapCounts = inferMapCounts({
		bracket,
		data: bracketData,
		tournamentRoundMapList: maps,
	});

	const roundsWithNames = React.useMemo(() => {
		if (bracket.type === "round_robin" || bracket.type === "swiss") {
			return Array.from(maps.keys()).map((roundId, i) => {
				return {
					id: roundId,
					name: `Round ${i + 1}`,
				};
			});
		}

		if (bracket.type === "double_elimination") {
			const winners = getRounds({ type: "winners", bracketData });
			const losers = getRounds({ type: "losers", bracketData });

			return [...winners, ...losers];
		}

		if (bracket.type === "single_elimination") {
			const rounds = getRounds({ type: "single", bracketData });

			const hasThirdPlaceMatch = rounds.some((round) => round.group_id === 1);

			if (!thirdPlaceMatchLinked || !hasThirdPlaceMatch) return rounds;

			return rounds
				.filter((round) => round.group_id !== 1)
				.map((round) =>
					round.name === "Finals"
						? {
								...round,
								name: TOURNAMENT.ROUND_NAMES.FINALS_THIRD_PLACE_MATCH_UNIFIED,
							}
						: round,
				);
		}

		assertUnreachable(bracket.type);
	}, [bracketData, maps, bracket.type, thirdPlaceMatchLinked]);

	const mapCountsWithGlobalCount = (newCount: number) => {
		const newMap = new Map(defaultRoundBestOfs);

		for (const [groupId, value] of newMap.entries()) {
			const newGroupMap: typeof value = new Map(value);
			for (const [roundNumber, roundValue] of value.entries()) {
				newGroupMap.set(roundNumber, { ...roundValue, count: newCount });
			}

			newMap.set(groupId, newGroupMap);
		}

		return newMap;
	};

	const mapCountsWithGlobalPickBanStyle = (
		newPickBanStyle: TournamentRoundMaps["pickBan"],
	): Set<number> => {
		if (!newPickBanStyle) {
			return new Set();
		}

		const newRoundsWithPickBan = new Set(roundsWithPickBan);

		for (const round of roundsWithNames) {
			newRoundsWithPickBan.add(round.id);
		}

		return newRoundsWithPickBan;
	};

	const validateNoDecreasingCount = () => {
		for (const groupCounts of mapCounts.values()) {
			let roundPreviousValue = 0;
			for (const [, roundValue] of Array.from(groupCounts.entries()).sort(
				// sort by round number
				(a, b) => a[0] - b[0],
			)) {
				if (roundPreviousValue > roundValue.count) {
					return false;
				}

				roundPreviousValue = roundValue.count;
			}
		}

		// check grands have at least as many maps as winners final (different groups)
		if (bracket.type === "double_elimination") {
			const grandsCounts = Array.from(mapCounts.get(2)?.values() ?? []);
			const winnersCounts = Array.from(mapCounts.get(0)?.values() ?? []);
			const maxWinnersCount = Math.max(...winnersCounts.map((c) => c.count));

			if (grandsCounts.some(({ count }) => count < maxWinnersCount)) {
				return false;
			}
		}

		return true;
	};

	const validateCustomFlow = () => {
		if (pickBanStyle !== "CUSTOM") return true;
		if (roundsWithPickBan.size === 0) return true;
		if (!customFlow) return false;

		return (
			PickBan.validateCustomFlowSection(customFlow.preSet, "preSet").length ===
				0 &&
			PickBan.validateCustomFlowSection(customFlow.postGame, "postGame")
				.length === 0
		);
	};

	const validateCustomFlowRoundsSelected = () => {
		if (globalSelections) return true;
		if (pickBanStyle !== "CUSTOM") return true;
		if (!customFlow) return true;

		return roundsWithPickBan.size > 0;
	};

	const lacksToSetMapPool =
		tournament.ctx.toSetMapPool.length === 0 &&
		tournament.ctx.mapPickingStyle === "TO";

	const globalSelections =
		bracket.type === "round_robin" || bracket.type === "swiss";

	const needsToPickEliminationTeamCount =
		(bracket.type === "single_elimination" ||
			bracket.type === "double_elimination") &&
		!eliminationTeamCount;

	return (
		<SendouDialog
			heading={`Maplist selection (${bracket.name})`}
			isOpen
			onClose={close}
			isFullScreen
		>
			<fetcher.Form method="post" className={styles.container}>
				<input type="hidden" name="bracketIdx" value={bracketIdx} />
				<input
					type="hidden"
					name="thirdPlaceMatchLinked"
					value={thirdPlaceMatchLinked ? "on" : "off"}
				/>
				<input
					type="hidden"
					name="maps"
					value={JSON.stringify(
						Array.from(maps.entries()).map(([key, value]) => ({
							...value,
							roundId: key,
							groupId: rounds.find((r) => r.id === key)?.group_id,
							type: countType,
							customFlow: value.pickBan === "CUSTOM" ? customFlow : undefined,
						})),
					)}
				/>
				{isPreparing &&
				(bracket.type === "single_elimination" ||
					bracket.type === "double_elimination") ? (
					<input
						type="hidden"
						name="eliminationTeamCount"
						value={eliminationTeamCount ?? -1}
					/>
				) : null}
				<div>
					{preparedMaps ? (
						<div className="text-xs text-center text-lighter">
							Prepared by{" "}
							{authorIdToUsername(tournament, preparedMaps.authorId)} @{" "}
							<LocaleTime
								date={preparedMaps.createdAt}
								options={{
									day: "numeric",
									month: "numeric",
									year: "numeric",
									hour: "numeric",
									minute: "2-digit",
								}}
								inline
							/>
						</div>
					) : null}
				</div>
				{lacksToSetMapPool ? (
					<div>
						You need to select map pool in the{" "}
						<Link to={calendarEditPage(tournament.ctx.eventId)}>
							tournament settings
						</Link>{" "}
						before bracket can be started
					</div>
				) : (
					<>
						<div className="stack horizontal items-center justify-between">
							<div className="stack horizontal lg flex-wrap">
								{isPreparing &&
								(bracket.type === "single_elimination" ||
									bracket.type === "double_elimination") ? (
									<EliminationTeamCountSelect
										count={eliminationTeamCount}
										realCount={bracketTeamsCount}
										setCount={(newCount) => {
											if (!newCount) {
												setEliminationTeamCount(null);
												return;
											}

											const newBracketData = teamCountAdjustedBracketData({
												bracket,
												teamCount: newCount,
											});

											setMaps(
												generateTournamentRoundMaplist({
													mapCounts:
														bracket.defaultRoundBestOfs(newBracketData),
													pool: tournament.ctx.toSetMapPool,
													rounds: newBracketData.round,
													type: bracket.type,
													roundsWithPickBan,
													pickBanStyle,
													patterns,
													countType,
												}),
											);
											setEliminationTeamCount(newCount);
										}}
									/>
								) : null}
								{!needsToPickEliminationTeamCount ? (
									<PickBanSelect
										pickBanStyle={pickBanStyle}
										isOneModeOnly={tournament.modesIncluded.length === 1}
										onPickBanStyleChange={(newPickBanStyle) => {
											let newRoundsWithPickBan = roundsWithPickBan;
											if (globalSelections) {
												newRoundsWithPickBan =
													mapCountsWithGlobalPickBanStyle(newPickBanStyle);
											}

											setPickBanStyle(newPickBanStyle);

											const noPickBanSetBeforeOrAfter =
												!roundsWithPickBan.size && !newRoundsWithPickBan.size;
											const switchedFromCounterpickToAnother =
												(pickBanStyle === "COUNTERPICK" &&
													newPickBanStyle === "COUNTERPICK_MODE_REPEAT_OK") ||
												(pickBanStyle === "COUNTERPICK_MODE_REPEAT_OK" &&
													newPickBanStyle === "COUNTERPICK");
											const shouldSkipRegenerateMaps =
												noPickBanSetBeforeOrAfter ||
												switchedFromCounterpickToAnother;

											if (!shouldSkipRegenerateMaps) {
												setMaps(
													generateTournamentRoundMaplist({
														mapCounts,
														pool: tournament.ctx.toSetMapPool,
														rounds,
														type: bracket.type,
														roundsWithPickBan: newRoundsWithPickBan,
														pickBanStyle: newPickBanStyle,
														patterns,
														countType,
													}),
												);
											}
										}}
									/>
								) : null}
								{globalSelections ? (
									<GlobalCountTypeSelect
										defaultValue={countType}
										onSetCountType={setCountType}
									/>
								) : null}
								{tournament.ctx.mapPickingStyle === "TO" &&
								tournament.modesIncluded.length > 1 &&
								!needsToPickEliminationTeamCount ? (
									<PatternInputs
										patterns={patterns}
										mapCounts={mapCounts}
										onPatternsChange={setPatterns}
									/>
								) : null}
							</div>
							{tournament.ctx.toSetMapPool.length > 0 &&
							!needsToPickEliminationTeamCount ? (
								<SendouButton
									size="small"
									icon={<RefreshCcw />}
									variant="outlined"
									onPress={() =>
										setMaps(
											generateTournamentRoundMaplist({
												mapCounts,
												pool: tournament.ctx.toSetMapPool,
												rounds,
												type: bracket.type,
												roundsWithPickBan,
												pickBanStyle,
												patterns,
												countType,
											}),
										)
									}
								>
									Reroll all maps
								</SendouButton>
							) : null}
						</div>
						{pickBanStyle === "CUSTOM" && !needsToPickEliminationTeamCount ? (
							<CustomFlowBuilder value={customFlow} onChange={setCustomFlow} />
						) : null}
						{needsToPickEliminationTeamCount ? (
							<div className="text-center text-lg font-bold my-24">
								Pick the expected teams count above to prepare maps
								<div className="text-lighter text-sm">
									Tip: if uncertain, overestimate the team count. <br /> The
									system can remove unnecessary rounds, but if you choose too
									few, you'll need to repick all the maps.
								</div>
							</div>
						) : (
							<>
								<div className={styles.roundsGrid}>
									{roundsWithNames.map((round) => {
										const roundMaps = maps.get(round.id);
										invariant(roundMaps, "Expected maps to be defined");

										const showUnlinkButton =
											bracket.type === "single_elimination" &&
											thirdPlaceMatchLinked === true &&
											round.name ===
												TOURNAMENT.ROUND_NAMES.FINALS_THIRD_PLACE_MATCH_UNIFIED;
										const showLinkButton =
											bracket.type === "single_elimination" &&
											thirdPlaceMatchLinked === false &&
											round.name === TOURNAMENT.ROUND_NAMES.THIRD_PLACE_MATCH;

										return (
											<RoundMapList
												key={round.id}
												name={round.name}
												maps={roundMaps}
												onHoverMap={setHoveredMap}
												unlink={
													showUnlinkButton
														? () => setThirdPlaceMatchLinked(false)
														: undefined
												}
												link={
													showLinkButton
														? () => setThirdPlaceMatchLinked(true)
														: undefined
												}
												hoveredMap={hoveredMap}
												onCountChange={(newCount) => {
													if (globalSelections) {
														const newMapCounts =
															mapCountsWithGlobalCount(newCount);
														setMaps(
															generateTournamentRoundMaplist({
																mapCounts: newMapCounts,
																pool: tournament.ctx.toSetMapPool,
																rounds,
																type: bracket.type,
																roundsWithPickBan,
																pickBanStyle,
																patterns,
																countType,
															}),
														);
														return;
													}

													const newMapCounts = new Map(mapCounts);
													const bracketRound = rounds.find(
														(r) => r.id === round.id,
													);
													invariant(
														bracketRound,
														"Expected round to be defined",
													);

													const groupInfo = newMapCounts.get(
														bracketRound.group_id,
													);
													invariant(
														groupInfo,
														"Expected group info to be defined",
													);
													const oldMapInfo = newMapCounts
														.get(bracketRound.group_id)
														?.get(bracketRound.number);
													invariant(
														oldMapInfo,
														"Expected map info to be defined",
													);

													groupInfo.set(bracketRound.number, {
														...oldMapInfo,
														count: newCount,
													});

													const newMap = generateTournamentRoundMaplist({
														mapCounts: newMapCounts,
														pool: tournament.ctx.toSetMapPool,
														rounds,
														type: bracket.type,
														roundsWithPickBan,
														pickBanStyle,
														patterns,
														countType,
													}).get(round.id);

													setMaps(new Map(maps).set(round.id, newMap!));
												}}
												onPickBanChange={(hasPickBan) => {
													if (globalSelections) {
														const newRoundsWithPickBan = hasPickBan
															? mapCountsWithGlobalPickBanStyle(pickBanStyle)
															: new Set<number>();

														setMaps(
															generateTournamentRoundMaplist({
																mapCounts,
																pool: tournament.ctx.toSetMapPool,
																rounds,
																type: bracket.type,
																roundsWithPickBan: newRoundsWithPickBan,
																pickBanStyle,
																patterns,
																countType,
															}),
														);
														return;
													}

													const newRoundsWithPickBan = new Set(
														roundsWithPickBan,
													);
													if (hasPickBan) {
														newRoundsWithPickBan.add(round.id);
													} else {
														newRoundsWithPickBan.delete(round.id);
													}

													const newMap = generateTournamentRoundMaplist({
														mapCounts,
														pool: tournament.ctx.toSetMapPool,
														rounds,
														type: bracket.type,
														roundsWithPickBan: newRoundsWithPickBan,
														pickBanStyle,
														patterns,
														countType,
													}).get(round.id);

													setMaps(new Map(maps).set(round.id, newMap!));
												}}
												onRoundMapListChange={(newRoundMaps) => {
													const newMaps = new Map(maps);
													newMaps.set(round.id, newRoundMaps);
													setMaps(newMaps);
												}}
											/>
										);
									})}
								</div>
								{!validateNoDecreasingCount() ? (
									<div className="mt-4 text-warning text-center">
										Invalid selection: tournament progression decreases in map
										count
									</div>
								) : !validateCustomFlow() ? (
									<div className="mt-4 text-warning text-center">
										Invalid selection: custom pick/ban flow is invalid
									</div>
								) : !validateCustomFlowRoundsSelected() ? (
									<div className="mt-4 text-warning text-center">
										Custom flow is configured but no rounds have pick/ban
										enabled
									</div>
								) : (
									<SubmitButton
										testId="confirm-finalize-bracket-button"
										_action={isPreparing ? "PREPARE_MAPS" : "START_BRACKET"}
										className="mx-auto mt-4"
									>
										{isPreparing
											? t("common:actions.save")
											: "Start the bracket"}
									</SubmitButton>
								)}
							</>
						)}
					</>
				)}
			</fetcher.Form>
		</SendouDialog>
	);
}

function useCloseModalOnSubmit(
	fetcher: FetcherWithComponents<unknown>,
	close: () => void,
) {
	React.useEffect(() => {
		if (fetcher.state !== "loading") return;

		close();
	}, [fetcher.state, close]);
}

function inferMapCounts({
	bracket,
	data,
	tournamentRoundMapList,
}: {
	bracket: Bracket;
	data: TournamentManagerDataSet;
	tournamentRoundMapList: TournamentRoundMapList;
}) {
	const result: BracketMapCounts = new Map();

	for (const [groupId, value] of bracket.defaultRoundBestOfs(data).entries()) {
		for (const roundNumber of value.keys()) {
			const roundId = data.round.find(
				(round) => round.group_id === groupId && round.number === roundNumber,
			)?.id;
			invariant(typeof roundId === "number", "Expected roundId to be defined");

			const count = tournamentRoundMapList.get(roundId)?.count;

			// skip rounds in RR and Swiss that don't have maps (only one group has maps)
			if (typeof count !== "number") {
				continue;
			}

			result.set(
				groupId,
				new Map(result.get(groupId)).set(roundNumber, {
					count,
					// currently "best of" / "play all" is defined per bracket but in future it might be per round
					// that's why there is this hardcoded default value for now
					type: "BEST_OF",
				}),
			);
		}
	}

	invariant(result.size > 0, "Expected result to be defined");

	return result;
}

function useBracketPreparedMaps(bracketIdx: number) {
	const prepared = useTournamentPreparedMaps();
	const tournament = useTournament();

	return PreparedMaps.resolvePreparedForTheBracket({
		bracketIdx,
		preparedByBracket: prepared,
		tournament,
	});
}

function authorIdToUsername(tournament: Tournament, authorId: number) {
	if (tournament.ctx.author.id === authorId) {
		return tournament.ctx.author.username;
	}

	return (
		tournament.ctx.staff.find((staff) => staff.id === authorId)?.username ??
		tournament.ctx.organization?.members.find(
			(member) => member.userId === authorId,
		)?.username ??
		"???"
	);
}

function teamCountAdjustedBracketData({
	bracket,
	teamCount,
}: {
	bracket: Bracket;
	teamCount: number;
}) {
	switch (bracket.type) {
		case "swiss":
			// always has the same amount of rounds even if 0 participants
			return bracket.data;
		case "round_robin":
			// ensure a full bracket (no bye round) gets generated even if registration is underway
			return bracket.generateMatchesData(
				nullFilledArray(
					bracket.settings?.teamsPerGroup ??
						TOURNAMENT.RR_DEFAULT_TEAM_COUNT_PER_GROUP,
				).map((_, i) => i + 1),
			);
		case "single_elimination":
		case "double_elimination":
			return bracket.generateMatchesData(
				nullFilledArray(teamCount).map((_, i) => i + 1),
			);
	}
}

function EliminationTeamCountSelect({
	count,
	realCount,
	setCount,
}: {
	count: number | null;
	realCount: number;
	setCount: (count: number | null) => void;
}) {
	return (
		<div>
			<Label htmlFor="elimination-team-count">Expected teams</Label>
			<select
				id="elimination-team-count"
				onChange={(e) =>
					setCount(e.target.value === "" ? null : Number(e.target.value))
				}
				defaultValue={count ?? ""}
			>
				<option value="">Select count</option>
				{PreparedMaps.eliminationTeamCountOptions(realCount).map(
					(teamCountRange) => {
						const label =
							teamCountRange.min === teamCountRange.max
								? teamCountRange.min
								: `${teamCountRange.min}-${teamCountRange.max}`;

						return (
							<option key={teamCountRange.max} value={teamCountRange.max}>
								{label}
							</option>
						);
					},
				)}
			</select>
		</div>
	);
}

function GlobalCountTypeSelect({
	defaultValue,
	onSetCountType,
}: {
	defaultValue: TournamentRoundMaps["type"];
	onSetCountType: (type: TournamentRoundMaps["type"]) => void;
}) {
	return (
		<div>
			<Label htmlFor="count-type">Count type</Label>
			<select
				id="count-type"
				onChange={(e) =>
					onSetCountType(e.target.value as TournamentRoundMaps["type"])
				}
				defaultValue={defaultValue}
			>
				<option value="BEST_OF">Best of</option>
				<option value="PLAY_ALL">Play all</option>
			</select>
		</div>
	);
}

function PickBanSelect({
	pickBanStyle,
	isOneModeOnly,
	onPickBanStyleChange,
}: {
	pickBanStyle: NonNullable<TournamentRoundMaps["pickBan"]>;
	isOneModeOnly: boolean;
	onPickBanStyleChange: (
		pickBanStyle: NonNullable<TournamentRoundMaps["pickBan"]>,
	) => void;
}) {
	const pickBanSelectText: Record<PickBan.Type, string> = {
		COUNTERPICK: "Counterpick",
		COUNTERPICK_MODE_REPEAT_OK: "Counterpick (mode repeat allowed)",
		BAN_2: "Ban 2",
		CUSTOM: "Custom",
	};

	// selection doesn't make sense for one mode only tournaments as you have to repeat the mode
	const availableTypes = PickBan.types.filter(
		(type) => !isOneModeOnly || type !== "COUNTERPICK_MODE_REPEAT_OK",
	);

	return (
		<div>
			<div className="stack horizontal xs items-center">
				<MousePointerClick className="w-4" />
				<Label htmlFor="pick-ban-style">Pick/ban style</Label>
			</div>
			<select
				className={styles.pickBanSelect}
				id="pick-ban-style"
				value={pickBanStyle}
				onChange={(e) =>
					onPickBanStyleChange(
						e.target.value as NonNullable<TournamentRoundMaps["pickBan"]>,
					)
				}
			>
				{availableTypes.map((type) => (
					<option key={type} value={type}>
						{pickBanSelectText[type]}
					</option>
				))}
			</select>
		</div>
	);
}

const serializedMapMode = (
	map: NonNullable<TournamentRoundMaps["list"]>[number],
) => `${map.mode}-${map.stageId}`;

function RoundMapList({
	name,
	maps,
	onHoverMap,
	onCountChange,
	onPickBanChange,
	onRoundMapListChange,
	unlink,
	link,
	hoveredMap,
}: {
	name: string;
	maps: Omit<TournamentRoundMaps, "type">;
	onHoverMap: (map: string | null) => void;
	onCountChange: (count: number) => void;
	onPickBanChange: (hasPickBan: boolean) => void;
	onRoundMapListChange: (maps: Omit<TournamentRoundMaps, "type">) => void;
	unlink?: () => void;
	link?: () => void;
	hoveredMap: string | null;
}) {
	const tournament = useTournament();

	const minCount = TOURNAMENT.AVAILABLE_BEST_OF[0];
	const maxCount = TOURNAMENT.AVAILABLE_BEST_OF.at(-1)!;

	return (
		<div>
			<h3>{name}</h3>
			<div className={styles.roundControls}>
				<button
					type="button"
					className={styles.roundButton}
					onClick={() => onCountChange(Math.max(minCount, maps.count - 2))}
					disabled={maps.count <= minCount}
				>
					<Minus />
				</button>
				<div className={clsx(styles.roundButton, styles.roundButtonNumber)}>
					{maps.count}
				</div>
				<button
					type="button"
					className={styles.roundButton}
					onClick={() => onCountChange(Math.min(maxCount, maps.count + 2))}
					disabled={maps.count >= maxCount}
					data-testid="increase-map-count-button"
				>
					<Plus />
				</button>
				<div className={styles.roundControlsDivider} />
				<button
					type="button"
					className={clsx(styles.roundButton, {
						[styles.roundButtonActive]: maps.pickBan,
					})}
					onClick={() => onPickBanChange(!maps.pickBan)}
					title="Toggle counterpick/ban"
				>
					<MousePointerClick />
				</button>
				{unlink ? (
					<button
						type="button"
						className={styles.roundButton}
						onClick={unlink}
						title="Enter finals and 3rd place match separately"
						data-testid="unlink-finals-3rd-place-match-button"
					>
						<Unlink />
					</button>
				) : null}
				{link ? (
					<button
						type="button"
						className={styles.roundButton}
						onClick={link}
						title="Link finals and 3rd place match to use the same maps"
						data-testid="link-finals-3rd-place-match-button"
					>
						<LinkIcon />
					</button>
				) : null}
			</div>
			<ol className="pl-0">
				{maps.pickBan === "CUSTOM"
					? nullFilledArray(maps.count).map((_, i) => (
							<MysteryRow
								key={i}
								number={i + 1}
								isCounterpicks={false}
								isTiebreaker={false}
								isCustomFlow
							/>
						))
					: nullFilledArray(
							maps.pickBan === "BAN_2" ? maps.count + 2 : maps.count,
						).map((_, i) => {
							const map = maps.list?.[i];

							if (map) {
								return (
									<MapListRow
										key={i}
										map={map}
										number={i + 1}
										onHoverMap={onHoverMap}
										hoveredMap={hoveredMap}
										onMapChange={(map) => {
											onRoundMapListChange({
												...maps,
												list: maps.list?.map((m, j) => (i === j ? map : m)),
											});
										}}
									/>
								);
							}

							const isTeamsPick = !maps.list && i === 0;
							const isLast =
								i ===
								(maps.pickBan === "BAN_2" ? maps.count + 2 : maps.count) - 1;

							return (
								<MysteryRow
									key={i}
									number={i + 1}
									isCounterpicks={
										!isTeamsPick && maps.pickBan === "COUNTERPICK"
									}
									isTiebreaker={
										tournament.ctx.mapPickingStyle === "AUTO_ALL" && isLast
									}
								/>
							);
						})}
			</ol>
		</div>
	);
}

function MapListRow({
	map,
	number,
	onHoverMap,
	hoveredMap,
	onMapChange,
}: {
	map: NonNullable<TournamentRoundMaps["list"]>[number];
	number: number;
	onHoverMap: (map: string | null) => void;
	hoveredMap: string | null;
	onMapChange: (map: NonNullable<TournamentRoundMaps["list"]>[number]) => void;
}) {
	const { t } = useTranslation(["common", "game-misc"]);
	const { contains } = useFilter({ sensitivity: "base" });
	const tournament = useTournament();

	const items = modesShort.flatMap((mode) => {
		const mapsForMode = tournament.ctx.toSetMapPool.filter(
			(m) => m.mode === mode,
		);

		if (mapsForMode.length === 0) return [];

		return [
			{
				key: mode,
				modeLabel: t(`game-misc:MODE_LONG_${mode}`),
				maps: mapsForMode.map((m) => ({
					id: serializedMapMode(m),
					mode,
					stageId: m.stageId,
					name: t(`game-misc:STAGE_${m.stageId}`),
				})),
			},
		];
	});

	return (
		<li
			className={clsx(styles.mapListRow, {
				"text-theme-secondary underline": serializedMapMode(map) === hoveredMap,
			})}
			onMouseEnter={() => onHoverMap(serializedMapMode(map))}
		>
			<span className="text-sm text-lighter font-semi-bold">{number}.</span>
			<SendouSelect
				aria-label="Map"
				items={items}
				selectedKey={serializedMapMode(map)}
				onSelectionChange={(key) => {
					if (key === null) return;
					const [mode, stageId] = String(key).split("-");
					onMapChange({
						mode: mode as ModeShort,
						stageId: Number(stageId) as StageId,
					});
				}}
				search={{
					placeholder: t("common:forms.stageSearch.search.placeholder"),
				}}
				filter={(textValue, inputValue) =>
					mapSearchFilter(textValue, inputValue, contains)
				}
				className={styles.mapRowSelect}
				popoverClassName={styles.mapRowSelectPopover}
			>
				{(group) => (
					<SendouSelectItemSection key={group.key} heading={group.modeLabel}>
						{group.maps.map((m) => (
							<SendouSelectItem
								key={m.id}
								id={m.id}
								textValue={`${m.mode} ${m.name}`}
							>
								<div className={styles.mapSelectItem}>
									<ModeImage mode={m.mode} size={20} />
									<StageImage
										stageId={m.stageId}
										height={20}
										className="rounded-sm"
									/>
									<span>{m.name}</span>
								</div>
							</SendouSelectItem>
						))}
					</SendouSelectItemSection>
				)}
			</SendouSelect>
		</li>
	);
}

/**
 * Filter for the map search input that supports an optional game mode prefix.
 *
 * - `"sz"` matches every Splat Zones map
 * - `"sz crab"` matches Splat Zones maps whose stage name contains "crab"
 * - input without a recognized mode prefix matches against the stage name as before
 *
 * Expects `textValue` to be formatted as `"${modeShort} ${stageName}"`.
 */
export function mapSearchFilter(
	textValue: string,
	inputValue: string,
	contains: (string: string, substring: string) => boolean,
) {
	const separatorIndex = textValue.indexOf(" ");
	const mode = textValue.slice(0, separatorIndex);
	const stageName = textValue.slice(separatorIndex + 1);

	const trimmedInput = inputValue.trim();
	const firstSpaceIndex = trimmedInput.indexOf(" ");
	const firstWord =
		firstSpaceIndex === -1
			? trimmedInput
			: trimmedInput.slice(0, firstSpaceIndex);

	const matchedMode = modesShort.find(
		(m) => m.toLowerCase() === firstWord.toLowerCase(),
	);

	if (!matchedMode) {
		return contains(stageName, trimmedInput);
	}

	if (mode !== matchedMode) return false;

	const restQuery =
		firstSpaceIndex === -1
			? ""
			: trimmedInput.slice(firstSpaceIndex + 1).trim();

	if (restQuery === "") return true;

	return contains(stageName, restQuery);
}

function MysteryRow({
	number,
	isCounterpicks,
	isTiebreaker,
	isCustomFlow,
}: {
	number: number;
	isCounterpicks: boolean;
	isTiebreaker: boolean;
	isCustomFlow?: boolean;
}) {
	return (
		<li className={styles.mapListRow}>
			<div
				className={clsx("stack horizontal items-center xs text-lighter", {
					"text-accent-high": isCounterpicks,
				})}
			>
				<span className="text-lg">{number}.</span>
				{isCustomFlow
					? "Custom flow"
					: isCounterpicks
						? "Counterpick"
						: isTiebreaker
							? "Tiebreaker"
							: "Team's pick"}
			</div>
		</li>
	);
}

function PatternInputs({
	patterns,
	mapCounts,
	onPatternsChange,
}: {
	patterns: Map<number, string>;
	mapCounts: BracketMapCounts;
	onPatternsChange: (patterns: Map<number, string>) => void;
}) {
	const uniqueCounts = new Set<number>();
	for (const groupCounts of mapCounts.values()) {
		for (const { count } of groupCounts.values()) {
			uniqueCounts.add(count);
		}
	}

	const sortedCounts = Array.from(uniqueCounts).sort((a, b) => a - b);

	return (
		<div>
			<div className="stack horizontal xs items-center">
				<Label>Mode patterns</Label>
				<InfoPopover tiny className={styles.infoPopover}>
					<div>Control the mode selection with a pattern. Examples:</div>
					<div className={styles.patternExample}>
						<code className={styles.patternCode}>*SZ*</code>
						<span className={styles.patternExplanation}>Any, SZ, any mode</span>
					</div>
					<div className={styles.patternExample}>
						<code className={styles.patternCode}>SZ*RM</code>
						<span className={styles.patternExplanation}>SZ, any, RM</span>
					</div>
					<div className={styles.patternExample}>
						<code className={styles.patternCode}>[TC]</code>
						<span className={styles.patternExplanation}>Must include TC</span>
					</div>
					<div className={styles.patternExample}>
						<code className={styles.patternCode}>[RM!]</code>
						<span className={styles.patternExplanation}>
							RM in guaranteed spots
						</span>
					</div>
					<div className={styles.patternExample}>
						<code className={styles.patternCode}>[TC]*SZ*</code>
						<span className={styles.patternExplanation}>
							TC once + every 2nd is SZ
						</span>
					</div>
				</InfoPopover>
			</div>
			<div className="stack horizontal xs">
				{sortedCounts.map((count) => (
					<Input
						key={count}
						className={styles.patternInput}
						leftAddon={`Bo${count}`}
						value={patterns.get(count) ?? ""}
						placeholder="[TC]*SZ*"
						onChange={(e) => {
							const newPatterns = new Map(patterns);
							if (e.target.value) {
								newPatterns.set(count, e.target.value);
							} else {
								newPatterns.delete(count);
							}
							onPatternsChange(newPatterns);
						}}
					/>
				))}
			</div>
		</div>
	);
}
