import type { Dispatch, SetStateAction } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
import {
	getCourtAssignments,
	getReceivingPlayer,
	getServiceCourt,
	getServingPlayer,
	getTeamDisplayName,
	hasWonGame,
} from '../lib/match'
import type {
	CourtSide,
	GroupTeam,
	PlayerSlot,
	RoundGroup,
	ScoreSide,
	ScoreState,
} from '../types'

type VoiceSettings = {
	announceScore: boolean
	announceServer: boolean
	rate: number
}

const VOICE_SETTINGS_STORAGE_KEY = 'badminton-scoreboard::voice-settings'
const defaultVoiceSettings: VoiceSettings = {
	announceScore: true,
	announceServer: true,
	rate: 1,
}
const SPEECH_NAME_MAP: Record<string, string> = {
	ruru: '嚕嚕',
}

type ScoreboardPageProps = {
	currentSelectionOrder: string[]
	finishDialogError: string
	finishDialogOpen: boolean
	finishDialogUploading: boolean
	groupSource: 'idle' | 'db' | 'manual'
	hasRecordedPoint: boolean
	leftTeam: GroupTeam | null
	liveRoomId: string | null
	nextMatchSignal: number
	rightTeam: GroupTeam | null
	scoreState: ScoreState
	selectedGroup: RoundGroup | null
	streakAnnouncement: {
		count: number
		key: number
		teamName: string
		title: string
	} | null
	victoryAnnouncement: {
		key: number
		scoreLabel: string
		teamName: string
		title: string
	} | null
	voiceAnnouncement: {
		key: number
		servingScore: number
		opponentScore: number
		serverName: string
		serverCourt: 'left' | 'right'
		matchPoint: boolean
		winnerTeamName: string | null
	} | null
	targetDate: string
	onApplyMatchup: (
		leftTeam: GroupTeam,
		rightTeam: GroupTeam,
		targetScore: number,
	) => void
	onCloseFinishDialog: () => void
	onConfirmUpload: () => void
	onOpenFinishDialog: () => void
	onRecordPoint: (side: ScoreSide) => void
	onSetServing: (side: ScoreSide) => void
	onSkipUpload: () => void
	onSwapMatchup: () => void
	onSwapTeamPlayers: (side: ScoreSide) => void
	onUndoLastPoint: () => void
}

export function ScoreboardPage({
	currentSelectionOrder,
	finishDialogError,
	finishDialogOpen,
	finishDialogUploading,
	groupSource,
	hasRecordedPoint,
	leftTeam,
	liveRoomId,
	nextMatchSignal,
	rightTeam,
	scoreState,
	selectedGroup,
	streakAnnouncement,
	victoryAnnouncement,
	voiceAnnouncement,
	targetDate,
	onApplyMatchup,
	onCloseFinishDialog,
	onConfirmUpload,
	onOpenFinishDialog,
	onRecordPoint,
	onSetServing,
	onSkipUpload,
	onSwapMatchup,
	onSwapTeamPlayers,
	onUndoLastPoint,
}: ScoreboardPageProps) {
	const FINISH_HOLD_DURATION = 1000
	const [pickerOpen, setPickerOpen] = useState(false)
	const [settingsOpen, setSettingsOpen] = useState(false)
	const [draftPlayers, setDraftPlayers] = useState<string[]>([])
	const [draftTargetScore, setDraftTargetScore] = useState(() =>
		String(scoreState.targetScore),
	)
	const [clock, setClock] = useState(() => formatClock())
	const [finishHoldActive, setFinishHoldActive] = useState(false)
	const [finishHoldProgress, setFinishHoldProgress] = useState(0)
	const [voiceSettings, setVoiceSettings] = useState<VoiceSettings>(() =>
		loadVoiceSettings(),
	)
	const finishHoldFrameRef = useRef<number | null>(null)
	const finishHoldTimerRef = useRef<number | null>(null)
	const finishHoldStartRef = useRef(0)
	const finishTriggeredRef = useRef(false)

	useEffect(() => {
		const timer = window.setInterval(() => {
			setClock(formatClock())
		}, 1000)

		return () => window.clearInterval(timer)
	}, [])

	useEffect(() => {
		window.localStorage.setItem(
			VOICE_SETTINGS_STORAGE_KEY,
			JSON.stringify(voiceSettings),
		)
	}, [voiceSettings])

	useEffect(() => {
		return () => {
			if (finishHoldFrameRef.current !== null) {
				window.cancelAnimationFrame(finishHoldFrameRef.current)
			}

			if (finishHoldTimerRef.current !== null) {
				window.clearTimeout(finishHoldTimerRef.current)
			}

			if ('speechSynthesis' in window) {
				window.speechSynthesis.cancel()
			}
		}
	}, [])

	const selectablePlayers = useMemo(() => {
		if (!selectedGroup) {
			return []
		}

		const seen = new Set<string>()
		const players: string[] = []

		selectedGroup.teams.forEach((team) => {
			if (!team.isPlaceholderA && !seen.has(team.playerA)) {
				seen.add(team.playerA)
				players.push(team.playerA)
			}

			if (!team.isPlaceholderB && !seen.has(team.playerB)) {
				seen.add(team.playerB)
				players.push(team.playerB)
			}
		})

		return players
	}, [selectedGroup])

	const presetTeams = useMemo(
		() =>
			selectedGroup?.teams.filter((team) => !team.isPlaceholderA && !team.isPlaceholderB) ?? [],
		[selectedGroup],
	)

	const matchWon = hasWonGame(scoreState)
	const canArrangeMatch = !hasRecordedPoint
	const canScore = scoreState.serving !== null && !matchWon
	const canFinishMatch = scoreState.scoreLeft > 0 || scoreState.scoreRight > 0

	const servingScore =
		scoreState.serving === 'left' ? scoreState.scoreLeft : scoreState.scoreRight
	const servingCourt =
		scoreState.serving === null ? null : getServiceCourt(servingScore)

	const leftAssignments = useMemo(
		() =>
			leftTeam ? getCourtAssignments(leftTeam, scoreState.leftRightCourtPlayer, true) : [],
		[leftTeam, scoreState.leftRightCourtPlayer],
	)
	const rightAssignments = useMemo(
		() =>
			rightTeam ? getCourtAssignments(rightTeam, scoreState.rightRightCourtPlayer, false) : [],
		[rightTeam, scoreState.rightRightCourtPlayer],
	)

	const currentServer =
		scoreState.serving === 'left'
			? leftTeam
				? getServingPlayer(leftTeam, scoreState.leftRightCourtPlayer, scoreState.scoreLeft)
				: null
			: scoreState.serving === 'right'
				? rightTeam
					? getServingPlayer(
						rightTeam,
						scoreState.rightRightCourtPlayer,
						scoreState.scoreRight,
					)
					: null
				: null

	const currentReceiver =
		scoreState.serving === 'left'
			? rightTeam
				? getReceivingPlayer(
					rightTeam,
					scoreState.rightRightCourtPlayer,
					scoreState.scoreLeft,
				)
				: null
			: scoreState.serving === 'right'
				? leftTeam
					? getReceivingPlayer(
						leftTeam,
						scoreState.leftRightCourtPlayer,
						scoreState.scoreRight,
					)
					: null
				: null

	useEffect(() => {
		if (!voiceAnnouncement) {
			return
		}

		if (voiceAnnouncement.winnerTeamName) {
			const winnerSpeech = voiceAnnouncement.winnerTeamName
				.split('/')
				.map((name) => getSpeechName(name.trim()))
				.filter(Boolean)
				.join('、')
			speakAnnouncement(`${winnerSpeech}贏得比賽`, voiceSettings.rate)
			return
		}

		const parts: string[] = []

		if (voiceSettings.announceScore) {
			// 報分只唸個位數：10→0、19→9、20→0；同分改唸「N 平」。
			const spokenServing = voiceAnnouncement.servingScore % 10
			const spokenOpponent = voiceAnnouncement.opponentScore % 10
			const scoreText =
				voiceAnnouncement.servingScore === voiceAnnouncement.opponentScore
					? `${spokenServing}平`
					: `${spokenServing}比${spokenOpponent}`
			parts.push(`${scoreText}${voiceAnnouncement.matchPoint ? ' 賽末點' : ''}`)
		}

		if (voiceSettings.announceServer && voiceAnnouncement.serverName) {
			parts.push(
				`${getSpeechName(voiceAnnouncement.serverName)}${
					voiceAnnouncement.serverCourt === 'left' ? '左邊' : '右邊'
				}發球`,
			)
		}

		if (parts.length > 0) {
			speakAnnouncement(parts.join('，'), voiceSettings.rate)
		}
	}, [
		voiceAnnouncement,
		voiceSettings.announceScore,
		voiceSettings.announceServer,
		voiceSettings.rate,
	])

	// 結算完成後（nextMatchSignal 遞增）自動打開選隊伍面板並清空已選，方便直接排下一場。
	// 採 React 官方「render 期間依 prop 變化調整 state」模式，避免在 effect 內 setState。
	const [handledNextMatchSignal, setHandledNextMatchSignal] = useState(nextMatchSignal)

	if (nextMatchSignal !== handledNextMatchSignal) {
		setHandledNextMatchSignal(nextMatchSignal)
		setDraftPlayers([])
		setDraftTargetScore(String(scoreState.targetScore))
		setPickerOpen(true)
	}

	if (!selectedGroup) {
		return (
			<section className="page-grid">
				<article className="panel panel-hero">
					<p className="panel-kicker">Step 3</p>
					<h2>請先回到選隊伍頁面</h2>
					<p className="panel-copy">
						目前還沒有帶入要上場的組別，先回去選擇分組，再進入記分板。
					</p>
					<Link className="primary-button inline-link" to="/teams">
						回到選隊伍
					</Link>
				</article>
			</section>
		)
	}

	const matchupLabel =
		leftTeam && rightTeam
			? `${getTeamDisplayName(leftTeam)} vs ${getTeamDisplayName(rightTeam)}`
			: '尚未設定對戰隊伍'

	const openPicker = () => {
		setDraftPlayers(currentSelectionOrder.filter((player) => selectablePlayers.includes(player)))
		setDraftTargetScore(String(scoreState.targetScore))
		setPickerOpen(true)
	}

	const stopFinishHold = () => {
		if (finishHoldFrameRef.current !== null) {
			window.cancelAnimationFrame(finishHoldFrameRef.current)
			finishHoldFrameRef.current = null
		}

		if (finishHoldTimerRef.current !== null) {
			window.clearTimeout(finishHoldTimerRef.current)
			finishHoldTimerRef.current = null
		}

		finishHoldStartRef.current = 0
		finishTriggeredRef.current = false
		setFinishHoldActive(false)
		setFinishHoldProgress(0)
	}

	const startFinishHold = () => {
		if (
			!canFinishMatch ||
			finishDialogOpen ||
			finishDialogUploading ||
			finishHoldActive
		) {
			return
		}

		finishTriggeredRef.current = false
		finishHoldStartRef.current = performance.now()
		setFinishHoldActive(true)
		setFinishHoldProgress(0)

		finishHoldTimerRef.current = window.setTimeout(() => {
			finishTriggeredRef.current = true
			setFinishHoldActive(false)
			setFinishHoldProgress(0)
			finishHoldTimerRef.current = null

			if (finishHoldFrameRef.current !== null) {
				window.cancelAnimationFrame(finishHoldFrameRef.current)
				finishHoldFrameRef.current = null
			}

			onOpenFinishDialog()
		}, FINISH_HOLD_DURATION)

		const tick = (now: number) => {
			const elapsed = now - finishHoldStartRef.current
			const progress = Math.min(elapsed / FINISH_HOLD_DURATION, 1)
			setFinishHoldProgress(progress)

			if (!finishHoldStartRef.current || finishTriggeredRef.current) {
				finishHoldFrameRef.current = null
				return
			}

			finishHoldFrameRef.current = window.requestAnimationFrame(tick)
		}

		finishHoldFrameRef.current = window.requestAnimationFrame(tick)
	}

	const cancelFinishHold = () => {
		if (finishTriggeredRef.current) {
			finishTriggeredRef.current = false
			return
		}

		stopFinishHold()
	}

	const toggleDraftPlayer = (playerName: string) => {
		setDraftPlayers((current) => {
			if (current.includes(playerName)) {
				return current.filter((value) => value !== playerName)
			}

			if (current.length >= 4) {
				return current
			}

			return [...current, playerName]
		})
	}

	const togglePresetTeam = (team: GroupTeam) => {
		setDraftPlayers((current) => {
			const removed = removePresetTeamFromDraft(current, team)

			if (removed.length !== current.length) {
				return removed
			}

			if (current.length >= 4 || current.length % 2 !== 0) {
				return current
			}

			if (current.includes(team.playerA) || current.includes(team.playerB)) {
				return current
			}

			return [...current, team.playerA, team.playerB]
		})
	}

	const confirmDraftTeams = () => {
		if (draftPlayers.length !== 4) {
			return
		}

		onApplyMatchup(
			{
				id: -1,
				playerA: draftPlayers[0],
				playerB: draftPlayers[1],
				isPlaceholderA: false,
				isPlaceholderB: false,
			},
			{
				id: -2,
				playerA: draftPlayers[3],
				playerB: draftPlayers[2],
				isPlaceholderA: false,
				isPlaceholderB: false,
			},
			sanitizeTargetScore(draftTargetScore),
		)
		setPickerOpen(false)
	}

	const autoPickDraftPlayers = () => {
		const shuffled = [...selectablePlayers]

		for (let index = shuffled.length - 1; index > 0; index -= 1) {
			const swapIndex = Math.floor(Math.random() * (index + 1))
				;[shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[index]]
		}

		setDraftPlayers(shuffled.slice(0, 4))
	}

	return (
		<>
			{streakAnnouncement ? (
				<div className="streak-banner" key={streakAnnouncement.key}>
					<span className="streak-banner-count">{streakAnnouncement.count} 連勝</span>
					<strong>{streakAnnouncement.title}</strong>
					<small>{streakAnnouncement.teamName}</small>
				</div>
			) : null}

			{victoryAnnouncement ? (
				<div className="victory-banner" key={victoryAnnouncement.key}>
					<span className="victory-banner-kicker">目標分數達成</span>
					<strong>{victoryAnnouncement.title}</strong>
					<small>{victoryAnnouncement.teamName}</small>
					<em>{victoryAnnouncement.scoreLabel}</em>
				</div>
			) : null}

			<section className="scoreboard-screen">
				<div className="scoreboard-court">
					<ScoreboardTeamPanel
						assignments={leftAssignments}
						canArrangeMatch={canArrangeMatch}
						canScore={canScore}
						canSetServing={canArrangeMatch}
						currentReceiver={scoreState.serving === 'right' ? currentReceiver?.name ?? null : null}
						currentServer={scoreState.serving === 'left' ? currentServer?.name ?? null : null}
						hasInitialServing={scoreState.initialServing === 'left'}
						onRecordPoint={() => onRecordPoint('left')}
						onSetServing={() => onSetServing('left')}
						onSwapPlayers={() => onSwapTeamPlayers('left')}
						onSwapTeams={onSwapMatchup}
						score={scoreState.scoreLeft}
						serviceCourt={scoreState.serving === 'left' ? servingCourt : null}
						showServingPrompt={scoreState.serving === null}
						team={leftTeam}
						teamSlot="top"
					/>

					<div className="scoreboard-center-banner">
						<p>{scoreState.serving === null ? '請先設定發球方' : '比賽進行中'}</p>
						<small>
							{scoreState.serving === null
								? `本場 ${scoreState.targetScore} 分獲勝`
								: `發球：${currentServer?.name ?? '-'}${currentReceiver ? ` / 接發：${currentReceiver.name}` : ''
								} / 目標 ${scoreState.targetScore} 分`}
						</small>
					</div>

					<ScoreboardTeamPanel
						assignments={rightAssignments}
						canArrangeMatch={canArrangeMatch}
						canScore={canScore}
						canSetServing={canArrangeMatch}
						currentReceiver={scoreState.serving === 'left' ? currentReceiver?.name ?? null : null}
						currentServer={scoreState.serving === 'right' ? currentServer?.name ?? null : null}
						hasInitialServing={scoreState.initialServing === 'right'}
						onRecordPoint={() => onRecordPoint('right')}
						onSetServing={() => onSetServing('right')}
						onSwapPlayers={() => onSwapTeamPlayers('right')}
						onSwapTeams={onSwapMatchup}
						score={scoreState.scoreRight}
						serviceCourt={scoreState.serving === 'right' ? servingCourt : null}
						showServingPrompt={scoreState.serving === null}
						team={rightTeam}
						teamSlot="bottom"
					/>
				</div>

				<aside className="scoreboard-rail">
					<div className="rail-icon-grid">
						{hasRecordedPoint ? (
							<button className="rail-square-button" type="button" onClick={onUndoLastPoint}>
								上一步
							</button>
						) : (
							<button className="rail-square-button" type="button" onClick={openPicker}>
								設定隊伍
							</button>
						)}
						<button
							aria-label="語音設定"
							className="rail-square-button"
							type="button"
							onClick={() => setSettingsOpen(true)}
						>
							設定
						</button>
					</div>

					<div className="rail-clock">{clock}</div>

					{liveRoomId ? <div className="rail-room-id">房號 {liveRoomId}</div> : null}

					<div
						className={
							finishHoldActive ? 'rail-pill-hold-wrap rail-pill-hold-wrap-active' : 'rail-pill-hold-wrap'
						}
					>
						<button
							className={
								finishHoldActive
									? 'rail-pill rail-pill-danger rail-pill-active-hold'
									: canFinishMatch
										? 'rail-pill rail-pill-danger'
										: 'rail-pill rail-pill-muted'
							}
							aria-disabled={!canFinishMatch}
							type="button"
							onClick={(event) => event.preventDefault()}
							onPointerDown={startFinishHold}
							onPointerUp={cancelFinishHold}
							onPointerLeave={cancelFinishHold}
							onPointerCancel={cancelFinishHold}
							onBlur={cancelFinishHold}
							onKeyDown={(event) => {
								if ((event.key === 'Enter' || event.key === ' ') && !event.repeat) {
									event.preventDefault()
									startFinishHold()
								}
							}}
							onKeyUp={(event) => {
								if (event.key === 'Enter' || event.key === ' ') {
									event.preventDefault()
									cancelFinishHold()
								}
							}}
						>
							比賽結算
						</button>

						{finishHoldActive ? (
							<div aria-hidden="true" className="rail-hold-progress">
								<span
									className="rail-hold-progress-bar"
									style={{ transform: `scaleX(${finishHoldProgress})` }}
								/>
							</div>
						) : null}
					</div>
				</aside>
			</section>

			{pickerOpen ? (
				<TeamPickerModal
					draftPlayers={draftPlayers}
					draftTargetScore={draftTargetScore}
					group={selectedGroup}
					presetTeams={presetTeams}
					selectablePlayers={selectablePlayers}
					selectionCount={draftPlayers.length}
					sourceLabel={groupSource === 'db' ? 'DB' : groupSource === 'manual' ? '手動' : '-'}
					targetDate={targetDate}
					onAutoPick={autoPickDraftPlayers}
					onClear={() => setDraftPlayers([])}
					onClose={() => setPickerOpen(false)}
					onConfirm={confirmDraftTeams}
					onDraftTargetScoreChange={setDraftTargetScore}
					onTogglePlayer={toggleDraftPlayer}
					onTogglePresetTeam={togglePresetTeam}
				/>
			) : null}

			{settingsOpen ? (
				<VoiceSettingsModal
					settings={voiceSettings}
					onClose={() => setSettingsOpen(false)}
					onUpdateSettings={setVoiceSettings}
				/>
			) : null}

			{finishDialogOpen ? (
				<FinishDialog
					error={finishDialogError}
					leftScore={scoreState.scoreLeft}
					leftTeamName={leftTeam ? getTeamDisplayName(leftTeam) : '未設定'}
					matchupLabel={matchupLabel}
					rightScore={scoreState.scoreRight}
					rightTeamName={rightTeam ? getTeamDisplayName(rightTeam) : '未設定'}
					uploading={finishDialogUploading}
					onClose={onCloseFinishDialog}
					onConfirm={onConfirmUpload}
					onSkip={onSkipUpload}
				/>
			) : null}
		</>
	)
}

type ScoreboardTeamPanelProps = {
	assignments: Array<{ slot: PlayerSlot; name: string; court: CourtSide }>
	canArrangeMatch: boolean
	canScore: boolean
	canSetServing: boolean
	currentReceiver: string | null
	currentServer: string | null
	hasInitialServing: boolean
	onRecordPoint: () => void
	onSetServing: () => void
	onSwapPlayers: () => void
	onSwapTeams: () => void
	score: number
	serviceCourt: CourtSide | null
	showServingPrompt: boolean
	team: GroupTeam | null
	teamSlot: 'top' | 'bottom'
}

function ScoreboardTeamPanel({
	assignments,
	canArrangeMatch,
	canScore,
	canSetServing,
	currentReceiver,
	currentServer,
	hasInitialServing,
	onRecordPoint,
	onSetServing,
	onSwapPlayers,
	onSwapTeams,
	score,
	serviceCourt,
	showServingPrompt,
	team,
	teamSlot,
}: ScoreboardTeamPanelProps) {
	const orderedAssignments = [...assignments].sort((left, right) => {
		if (left.court === right.court) {
			return 0
		}

		return left.court === 'left' ? -1 : 1
	})

	const header = (
		<div className="scoreboard-team-head">
			<div className="team-head-main">
				{orderedAssignments.map((assignment) => (
					<div
						className={
							assignment.name === currentServer
								? 'scoreboard-name-chip scoreboard-name-chip-serving'
								: 'scoreboard-name-chip'
						}
						key={assignment.slot}
					>
						<span className="team-number">{getPlayerNumber(teamSlot, assignment.court)}</span>
						<strong>{assignment.name}</strong>
					</div>
				))}
			</div>

			<div className="team-head-buttons">
				<button
					aria-label="交換上下隊伍"
					className="team-icon-button"
					disabled={!canArrangeMatch}
					type="button"
					onClick={onSwapTeams}
				>
					↕
				</button>
				<button
					aria-label="交換左右位置"
					className="team-icon-button"
					disabled={!canArrangeMatch}
					type="button"
					onClick={onSwapPlayers}
				>
					↔
				</button>
			</div>
		</div>
	)

	const serveBar = (
		<button
			className={
				hasInitialServing && !canSetServing
					? 'serve-lane serve-lane-locked'
					: showServingPrompt
						? 'serve-lane serve-lane-prompt'
						: 'serve-lane'
			}
			disabled={!canSetServing || !team}
			type="button"
			onClick={onSetServing}
		>
			<span className={hasInitialServing ? 'serve-lane-box serve-lane-box-checked' : 'serve-lane-box'} />
			<span>先攻</span>
			{currentServer ? (
				<small>
					發球區：{serviceCourt === 'left' ? '左' : serviceCourt === 'right' ? '右' : '-'}
					{currentReceiver ? ` / 接發：${currentReceiver}` : ''}
				</small>
			) : hasInitialServing ? (
				<small>本局先攻</small>
			) : (
				<small>點擊設定這一隊先攻</small>
			)}
		</button>
	)

	const scoreBoard = (
		<button
			className={canScore ? 'score-panel-surface score-panel-surface-live' : 'score-panel-surface'}
			disabled={!canScore || !team}
			type="button"
			onClick={onRecordPoint}
		>
			<span className="score-panel-value">{score}</span>
		</button>
	)

	return (
		<section className="scoreboard-team-section">
			{teamSlot === 'top' ? (
				<>
					{header}
					{serveBar}
					{scoreBoard}
				</>
			) : (
				<>
					{scoreBoard}
					{serveBar}
					{header}
				</>
			)}
		</section>
	)
}

type TeamPickerModalProps = {
	draftPlayers: string[]
	draftTargetScore: string
	group: RoundGroup
	presetTeams: GroupTeam[]
	selectablePlayers: string[]
	selectionCount: number
	sourceLabel: string
	targetDate: string
	onAutoPick: () => void
	onClear: () => void
	onClose: () => void
	onConfirm: () => void
	onDraftTargetScoreChange: (value: string) => void
	onTogglePlayer: (playerName: string) => void
	onTogglePresetTeam: (team: GroupTeam) => void
}

function TeamPickerModal({
	draftPlayers,
	draftTargetScore,
	group,
	presetTeams,
	selectablePlayers,
	selectionCount,
	sourceLabel,
	targetDate,
	onAutoPick,
	onClear,
	onClose,
	onConfirm,
	onDraftTargetScoreChange,
	onTogglePlayer,
	onTogglePresetTeam,
}: TeamPickerModalProps) {
	return (
		<div className="team-picker-overlay" role="presentation" onClick={onClose}>
			<div
				aria-label="設定隊伍"
				aria-modal="true"
				className="team-picker-shell"
				onClick={(event) => event.stopPropagation()}
				role="dialog"
			>
				<button aria-label="關閉設定隊伍" className="team-picker-close" type="button" onClick={onClose}>
					×
				</button>

				<div className="team-picker-ribbon">
					<span>{selectionCount >= 4 ? '已完成選擇' : '請選滿 4 位球員'}</span>
				</div>

				<div className="team-picker-layout">
					<section className="team-picker-panel team-picker-list-panel">
						<div className="team-picker-title">
							<span className="team-picker-count">{selectionCount}/4</span>
							<div>
								<strong>依序選擇球員</strong>
								<p>
									第 {group.id} 組 / {sourceLabel} / {targetDate || '-'}
								</p>
							</div>
						</div>

						<div className="team-picker-config-row">
							<label className="team-picker-config team-picker-config-compact">
								<span>獲勝分數</span>
								<input
									className="team-picker-score-input team-picker-score-input-compact"
									inputMode="numeric"
									maxLength={2}
									type="text"
									value={draftTargetScore}
									onChange={(event) => onDraftTargetScoreChange(event.target.value)}
								/>
							</label>
						</div>

						<div className="team-picker-list">
							{selectablePlayers.map((playerName) => {
								const checked = draftPlayers.includes(playerName)
								const selectedOrder = checked ? draftPlayers.indexOf(playerName) + 1 : null

								return (
									<button
										className={
											checked
												? 'team-picker-option team-picker-option-active'
												: 'team-picker-option'
										}
										key={`player-option-${playerName}`}
										type="button"
										onClick={() => onTogglePlayer(playerName)}
									>
										<span className="team-picker-checkbox">
											{checked ? String(selectedOrder) : ''}
										</span>
										<div className="team-picker-option-text">
											<strong>{playerName}</strong>
											<small>
												{selectedOrder ? `已選為第 ${selectedOrder} 位` : '尚未加入上場名單'}
											</small>
										</div>
									</button>
								)
							})}
						</div>

						<div className="team-picker-actions">
							<button className="team-picker-ghost" type="button" onClick={onAutoPick}>
								自動選擇
							</button>
							<button
								className="team-picker-confirm"
								disabled={draftPlayers.length !== 4}
								type="button"
								onClick={onConfirm}
							>
								確認
							</button>
						</div>
					</section>

					<aside className="team-picker-panel team-picker-side-panel">
						<div className="preset-team-block">
							<div className="preset-team-head">
								<strong>快速選預設隊伍</strong>
								<small>可直接點一整隊，或和左側逐一選人混用。</small>
							</div>

							<div className="preset-team-list">
								{presetTeams.map((team) => {
									const selectedSlot = getPresetTeamSelectionSlot(draftPlayers, team)

									return (
										<button
											className={
												selectedSlot === null
													? 'preset-team-card'
													: 'preset-team-card preset-team-card-active'
											}
											key={`preset-team-${team.id}`}
											type="button"
											onClick={() => onTogglePresetTeam(team)}
										>
											<span className="preset-team-index">{team.id}</span>
											<div>
												<strong>{getTeamDisplayName(team)}</strong>
												<small>
													{selectedSlot === null
														? '點擊套用這一隊'
														: `已放入第 ${selectedSlot === 0 ? '1' : '2'} 隊`}
												</small>
											</div>
										</button>
									)
								})}
							</div>
						</div>

						<button className="team-picker-clear" type="button" onClick={onClear}>
							清空
						</button>

						<p className="picker-side-hint">
							第 1、2 位會自動成為上方隊伍，第 3、4 位會成為下方隊伍。
						</p>
					</aside>
				</div>
			</div>
		</div>
	)
}

type VoiceSettingsModalProps = {
	settings: VoiceSettings
	onClose: () => void
	onUpdateSettings: Dispatch<SetStateAction<VoiceSettings>>
}

function VoiceSettingsModal({
	settings,
	onClose,
	onUpdateSettings,
}: VoiceSettingsModalProps) {
	return (
		<div className="voice-settings-overlay" role="presentation" onClick={onClose}>
			<div
				aria-label="語音設定"
				aria-modal="true"
				className="voice-settings-panel"
				role="dialog"
				onClick={(event) => event.stopPropagation()}
			>
				<button
					aria-label="關閉語音設定"
					className="voice-settings-close"
					type="button"
					onClick={onClose}
				>
					×
				</button>

				<p className="panel-kicker">語音設定</p>
				<h3>播報內容</h3>

				<label className="voice-setting-row">
					<span>播報比分</span>
					<input
						checked={settings.announceScore}
						type="checkbox"
						onChange={(event) =>
							onUpdateSettings((current) => ({
								...current,
								announceScore: event.target.checked,
							}))
						}
					/>
				</label>

				<label className="voice-setting-row">
					<span>播報誰發球</span>
					<input
						checked={settings.announceServer}
						type="checkbox"
						onChange={(event) =>
							onUpdateSettings((current) => ({
								...current,
								announceServer: event.target.checked,
							}))
						}
					/>
				</label>

				<label className="voice-setting-slider">
					<span>語速</span>
					<input
						max="10"
						min="0.7"
						step="0.1"
						type="range"
						value={settings.rate}
						onChange={(event) =>
							onUpdateSettings((current) => ({
								...current,
								rate: Number(event.target.value),
							}))
						}
					/>
					<strong>{settings.rate.toFixed(1)}x</strong>
				</label>
			</div>
		</div>
	)
}

type FinishDialogProps = {
	error: string
	leftScore: number
	leftTeamName: string
	matchupLabel: string
	rightScore: number
	rightTeamName: string
	uploading: boolean
	onClose: () => void
	onConfirm: () => void
	onSkip: () => void
}

function FinishDialog({
	error,
	leftScore,
	leftTeamName,
	matchupLabel,
	rightScore,
	rightTeamName,
	uploading,
	onClose,
	onConfirm,
	onSkip,
}: FinishDialogProps) {
	return (
		<div className="finish-dialog-overlay" role="presentation">
			<div aria-modal="true" className="finish-dialog" role="dialog">
				<button
					aria-label="關閉比賽結算"
					className="finish-dialog-close"
					disabled={uploading}
					type="button"
					onClick={onClose}
				>
					×
				</button>

				<p className="panel-kicker">比賽結算</p>
				<h3>{matchupLabel}</h3>

				<div className="finish-score">
					<div>
						<strong>{leftScore}</strong>
						<span>{leftTeamName}</span>
					</div>
					<div className="finish-score-divider">:</div>
					<div>
						<strong>{rightScore}</strong>
						<span>{rightTeamName}</span>
					</div>
				</div>

				<p className="finish-dialog-copy">要不要把這場比賽戰績上傳到資料庫？</p>

				{error ? <p className="finish-dialog-error">{error}</p> : null}

				<div className="finish-dialog-actions">
					<button
						className="team-picker-ghost"
						disabled={uploading}
						type="button"
						onClick={onSkip}
					>
						不上傳
					</button>
					<button
						className="team-picker-confirm"
						disabled={uploading}
						type="button"
						onClick={onConfirm}
					>
						{uploading ? '上傳中...' : '確認上傳'}
					</button>
				</div>
			</div>
		</div>
	)
}

function getPlayerNumber(teamSlot: 'top' | 'bottom', court: CourtSide) {
	if (teamSlot === 'top') {
		return court === 'left' ? 1 : 2
	}

	return court === 'right' ? 3 : 4
}

function sanitizeTargetScore(value: string) {
	const numeric = Number.parseInt(value.replace(/[^\d]/g, ''), 10)

	if (Number.isNaN(numeric)) {
		return 21
	}

	return Math.min(99, Math.max(1, numeric))
}

function removePresetTeamFromDraft(players: string[], team: GroupTeam) {
	const firstPairSelected = players[0] === team.playerA && players[1] === team.playerB
	const secondPairSelected = players[2] === team.playerA && players[3] === team.playerB

	if (firstPairSelected) {
		return players.slice(2)
	}

	if (secondPairSelected) {
		return players.slice(0, 2)
	}

	return players
}

function getPresetTeamSelectionSlot(players: string[], team: GroupTeam) {
	if (players[0] === team.playerA && players[1] === team.playerB) {
		return 0
	}

	if (players[2] === team.playerA && players[3] === team.playerB) {
		return 1
	}

	return null
}

function formatClock() {
	return new Date().toLocaleTimeString('zh-TW', {
		hour: '2-digit',
		minute: '2-digit',
		hour12: false,
	})
}

function loadVoiceSettings(): VoiceSettings {
	try {
		const raw = window.localStorage.getItem(VOICE_SETTINGS_STORAGE_KEY)

		if (!raw) {
			return defaultVoiceSettings
		}

		const parsed = JSON.parse(raw) as Partial<VoiceSettings>

		return {
			announceScore: parsed.announceScore ?? defaultVoiceSettings.announceScore,
			announceServer: parsed.announceServer ?? defaultVoiceSettings.announceServer,
			rate:
				typeof parsed.rate === 'number'
					? Math.min(10, Math.max(0.7, parsed.rate))
					: defaultVoiceSettings.rate,
		}
	} catch {
		return defaultVoiceSettings
	}
}

function getSpeechName(name: string) {
	return SPEECH_NAME_MAP[name.trim().toLowerCase()] ?? name
}

function speakAnnouncement(message: string, rate: number) {
	if (!('speechSynthesis' in window)) {
		return
	}

	const synthesis = window.speechSynthesis
	const utterance = new SpeechSynthesisUtterance(message)
	const voices = synthesis.getVoices()
	const zhVoice =
		voices.find((voice) => voice.lang.toLowerCase().startsWith('zh-tw')) ??
		voices.find((voice) => voice.lang.toLowerCase().startsWith('zh'))

	utterance.lang = zhVoice?.lang ?? 'zh-TW'
	utterance.rate = rate
	utterance.pitch = 1
	utterance.volume = 1

	if (zhVoice) {
		utterance.voice = zhVoice
	}

	synthesis.cancel()
	synthesis.speak(utterance)
}
