import { useEffect, useMemo, useRef, useState } from 'react'
import { NavLink, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import './App.css'
import {
  createLiveRoom,
  loadHistoryPlayCounts,
  loadMatchResults,
  loadMemberSkills,
  releaseLiveRoom,
  rememberCertNotAfter,
  saveMatchHistory,
  sendLiveRoomHeartbeat,
  updateLiveRoom,
} from './lib/api'
import {
  buildCourtGroups,
  convertDateToKey,
  convertDbRecordToGroups,
  formatDateInputValue,
  getMirroredCourt,
  getServiceCourt,
  getServingPlayer,
  getTeamDisplayName,
  getWinnerName,
  hasWonGame,
  parseRoster,
  swapCourtPositions,
} from './lib/match'
import { ChangelogPage } from './pages/ChangelogPage'
import { HistoryPage } from './pages/HistoryPage'
import { RoomListPage } from './pages/RoomListPage'
import { RoomSpectatorPage } from './pages/RoomSpectatorPage'
import { ScoreboardPage } from './pages/ScoreboardPage'
import { TeamSelectionPage } from './pages/TeamSelectionPage'
import type {
  ActiveMatchup,
  GroupTeam,
  HistoryUploadPayload,
  LiveRoomSession,
  LoadStatus,
  MatchHistoryItem,
  PointHistoryEntry,
  RoundGroup,
  ScoreSide,
  ScoreSnapshot,
  ScoreState,
} from './types'

const STORAGE_KEYS = {
  areaA: 'badminton-scoreboard::area-a',
  areaB: 'badminton-scoreboard::area-b',
  areaOther: 'badminton-scoreboard::area-other',
  history: 'badminton-scoreboard::history',
  playCounts: 'badminton-scoreboard::play-counts',
  roster: 'badminton-scoreboard::roster-today',
} as const

// 首次啟用今日出席名單時，把舊版 A / B 區的儲存內容合併過來。
function loadInitialRoster() {
  const stored = loadStoredText(STORAGE_KEYS.roster, '')

  if (stored.trim()) {
    return stored
  }

  const legacy = [
    loadStoredText(STORAGE_KEYS.areaA, ''),
    loadStoredText(STORAGE_KEYS.areaB, ''),
  ]
    .join('\n')
    .trim()

  return legacy
}

const initialScoreState: ScoreState = {
  scoreLeft: 0,
  scoreRight: 0,
  gamesLeft: 0,
  gamesRight: 0,
  currentGame: 1,
  targetScore: 21,
  initialServing: null,
  serving: null,
  leftRightCourtPlayer: 'playerA',
  rightRightCourtPlayer: 'playerA',
}

type SettlementState = {
  error: string
  open: boolean
  uploading: boolean
}

type StreakAnnouncement = {
  count: number
  key: number
  teamName: string
  title: string
}

type VictoryAnnouncement = {
  key: number
  scoreLabel: string
  teamName: string
  title: string
}

type VoiceAnnouncement = {
  key: number
  servingScore: number
  opponentScore: number
  serverName: string
  serverCourt: 'left' | 'right'
  matchPoint: boolean
  winnerTeamName: string | null
}

const STREAK_TITLES: Record<number, string> = {
  3: '大殺特殺',
  4: '暴走',
  5: '無人能擋',
  6: '主宰比賽',
  7: '像神一般的',
  8: '成為傳說',
}
const PWA_UPDATE_EVENT = 'badminton-scoreboard:pwa-update-ready'
const APP_VERSION_POLL_MS = 30000
const LIVE_ROOM_HEARTBEAT_MS = 10_000
// 憑證到期前 7 天開始提醒，避免到期當天才突然全部連不上。
const CERT_EXPIRY_WARNING_MS = 7 * 24 * 60 * 60 * 1000

function App() {
  const location = useLocation()
  const navigate = useNavigate()
  const isScoreboardRoute = location.pathname === '/scoreboard'

  const [targetDate, setTargetDate] = useState(() => formatDateInputValue())
  // 今日出席名單：手動排賽程的對象，取代舊版 A / B 區。
  const [rosterInput, setRosterInput] = useState(() => loadInitialRoster())
  // 全體成員實力表（tg_members.skill），排賽程時做強弱互補與兩隊平衡。
  const [skillMap, setSkillMap] = useState<Record<string, number>>({})
  const [skillsReady, setSkillsReady] = useState(false)
  // 其他區：不參與分組配對，但會帶進記分板讓臨時上場的人可以被選到。
  const [areaOtherInput, setAreaOtherInput] = useState(() =>
    loadStoredText(STORAGE_KEYS.areaOther, ''),
  )
  const [groups, setGroups] = useState<RoundGroup[]>([])
  const [groupSource, setGroupSource] = useState<'idle' | 'db' | 'manual'>('idle')
  const [loadStatus, setLoadStatus] = useState<LoadStatus>('idle')
  const [loadMessage, setLoadMessage] = useState('')
  const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null)
  const [activeMatchup, setActiveMatchup] = useState<ActiveMatchup>({
    leftTeam: null,
    rightTeam: null,
  })
  const [scoreState, setScoreState] = useState<ScoreState>(initialScoreState)
  const [scoreHistory, setScoreHistory] = useState<ScoreSnapshot[]>([])
  const [pointLog, setPointLog] = useState<PointHistoryEntry[]>([])
  const [history, setHistory] = useState<MatchHistoryItem[]>(() =>
    loadStoredHistory(STORAGE_KEYS.history),
  )
  // 當天每位球員打過的場次（結算一次算一場），跨日自動歸零。
  const [playCounts, setPlayCounts] = useState<Record<string, number>>(() =>
    loadStoredPlayCounts(STORAGE_KEYS.playCounts),
  )
  const [settlement, setSettlement] = useState<SettlementState>({
    error: '',
    open: false,
    uploading: false,
  })
  const [streakAnnouncement, setStreakAnnouncement] = useState<StreakAnnouncement | null>(null)
  const [victoryAnnouncement, setVictoryAnnouncement] = useState<VictoryAnnouncement | null>(null)
  const [voiceAnnouncement, setVoiceAnnouncement] = useState<VoiceAnnouncement | null>(null)
  const [pwaUpdateReady, setPwaUpdateReady] = useState(false)
  const [certExpiryWarning, setCertExpiryWarning] = useState('')
  const certWarningDismissedRef = useRef(false)
  const [liveRoomSession, setLiveRoomSession] = useState<LiveRoomSession | null>(null)
  const [navigationLockMessage, setNavigationLockMessage] = useState('')
  // 結算完成後遞增，通知記分板自動打開選隊伍面板讓人選下一場。
  const [nextMatchSignal, setNextMatchSignal] = useState(0)
  const currentAppVersionRef = useRef<string | null>(null)
  const creatingRoomRef = useRef(false)
  const lastSyncedRoomSignatureRef = useRef('')

  const parsedRoster = useMemo(() => parseRoster(rosterInput), [rosterInput])
  const parsedAreaOther = useMemo(() => parseRoster(areaOtherInput), [areaOtherInput])
  const selectedGroup = groups.find((group) => group.id === selectedGroupId) ?? null
  const leftTeam = activeMatchup.leftTeam
  const rightTeam = activeMatchup.rightTeam
  const liveRoomId = liveRoomSession?.roomId ?? null
  const isNavigationLocked = Boolean(leftTeam && rightTeam && scoreState.serving !== null)

  useEffect(() => {
    window.localStorage.setItem(STORAGE_KEYS.roster, rosterInput)
  }, [rosterInput])

  // 開站先載入實力表，載入完成前不能手動產生賽程（跟 match-hub 一致）。
  useEffect(() => {
    void (async () => {
      try {
        const skills = await loadMemberSkills()
        setSkillMap(skills)
        setSkillsReady(true)
      } catch (error) {
        console.error('skills load error:', error)
      }
    })()
  }, [])

  useEffect(() => {
    window.localStorage.setItem(STORAGE_KEYS.areaOther, areaOtherInput)
  }, [areaOtherInput])

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

  useEffect(() => {
    window.localStorage.setItem(
      STORAGE_KEYS.playCounts,
      JSON.stringify({ date: formatDateInputValue(), counts: playCounts }),
    )
  }, [playCounts])

  useEffect(() => {
    if (loadStatus !== 'loaded' || !loadMessage) {
      return
    }

    const timer = window.setTimeout(() => {
      setLoadMessage('')
    }, 1000)

    return () => window.clearTimeout(timer)
  }, [loadMessage, loadStatus])

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

    const timer = window.setTimeout(() => {
      setStreakAnnouncement(null)
    }, 1800)

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

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

    const timer = window.setTimeout(() => {
      setVictoryAnnouncement(null)
    }, 2200)

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

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

    const timer = window.setTimeout(() => {
      setNavigationLockMessage('')
    }, 1400)

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

  useEffect(() => {
    document.body.classList.toggle('body-scoreboard', isScoreboardRoute)

    return () => {
      document.body.classList.remove('body-scoreboard')
    }
  }, [isScoreboardRoute])

  useEffect(() => {
    const handlePwaUpdateReady = () => {
      setPwaUpdateReady(true)
    }

    window.addEventListener(PWA_UPDATE_EVENT, handlePwaUpdateReady)

    return () => {
      window.removeEventListener(PWA_UPDATE_EVENT, handlePwaUpdateReady)
    }
  }, [])

  useEffect(() => {
    let active = true

    const checkAppVersion = async () => {
      try {
        const response = await fetch('/api/version', {
          cache: 'no-store',
          headers: {
            'cache-control': 'no-cache',
          },
        })

        if (!response.ok) {
          return
        }

        const payload = (await response.json()) as {
          certNotAfter?: string | null
          ok?: boolean
          version?: string
        }
        const nextVersion = payload.version?.trim()

        if (!active) {
          return
        }

        rememberCertNotAfter(payload.certNotAfter)

        const certNotAfterTime = payload.certNotAfter ? Date.parse(payload.certNotAfter) : NaN

        if (
          Number.isFinite(certNotAfterTime) &&
          certNotAfterTime - Date.now() < CERT_EXPIRY_WARNING_MS &&
          !certWarningDismissedRef.current
        ) {
          const expiryLabel = new Date(certNotAfterTime).toLocaleString('zh-TW', {
            hour12: false,
          })
          setCertExpiryWarning(
            certNotAfterTime < Date.now()
              ? `SSL 憑證已於 ${expiryLabel} 過期，請盡快更新伺服器憑證。`
              : `SSL 憑證將於 ${expiryLabel} 到期，請記得更新伺服器憑證。`,
          )
        }

        if (!nextVersion) {
          return
        }

        if (!currentAppVersionRef.current) {
          currentAppVersionRef.current = nextVersion
          return
        }

        if (currentAppVersionRef.current !== nextVersion) {
          currentAppVersionRef.current = nextVersion
          setPwaUpdateReady(true)
        }
      } catch {
        // Ignore transient version-check failures and retry on next poll.
      }
    }

    void checkAppVersion()
    const timer = window.setInterval(() => {
      void checkAppVersion()
    }, APP_VERSION_POLL_MS)

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

  const resetScoring = (
    nextState: ScoreState = initialScoreState,
    options?: {
      releaseLiveRoom?: boolean
    },
  ) => {
    const shouldReleaseLiveRoom = options?.releaseLiveRoom ?? true

    if (shouldReleaseLiveRoom && liveRoomSession?.status === 'live') {
      void releaseLiveRoom(liveRoomSession.roomId, liveRoomSession.hostToken).catch(() => {})
    }

    setScoreState(nextState)
    setScoreHistory([])
    setPointLog([])
    setStreakAnnouncement(null)
    setVictoryAnnouncement(null)
    setVoiceAnnouncement(null)
    setSettlement({
      error: '',
      open: false,
      uploading: false,
    })
    creatingRoomRef.current = false
    setLiveRoomSession(null)
    lastSyncedRoomSignatureRef.current = ''
  }

  const finalizeLiveRoom = async () => {
    if (!liveRoomSession || !leftTeam || !rightTeam) {
      return
    }

    const winnerTeamName = getWinnerName(
      getTeamDisplayName(leftTeam),
      getTeamDisplayName(rightTeam),
      scoreState,
    )

    const payload = buildLiveRoomPayload({
      groupId: selectedGroup?.id ?? null,
      leftTeam,
      pointLog,
      rightTeam,
      scoreState,
      targetDate,
    })

    try {
      await updateLiveRoom(liveRoomSession.roomId, {
        ...payload,
        hostToken: liveRoomSession.hostToken,
        status: 'finished',
        winnerTeamName,
      })

      setLiveRoomSession((current) =>
        current
          ? {
              ...current,
              status: 'finished',
            }
          : current,
      )
    } catch (error) {
      console.error('finalize live room error:', error)
    }
  }

  const selectGroup = (groupId: number, nextGroups = groups) => {
    const nextGroup = nextGroups.find((group) => group.id === groupId)
    const firstTeam = nextGroup?.teams[0] ?? null
    const secondTeam = nextGroup?.teams[1] ?? null

    setSelectedGroupId(nextGroup?.id ?? null)
    setActiveMatchup({
      leftTeam: firstTeam,
      rightTeam: secondTeam,
    })
    resetScoring()
  }

  const applyMatchup = (
    leftTeam: GroupTeam,
    rightTeam: GroupTeam,
    targetScore: number,
  ) => {
    setActiveMatchup({
      leftTeam,
      rightTeam,
    })
    resetScoring({
      ...initialScoreState,
      targetScore,
    })
  }

  const refreshForPwaUpdate = () => {
    const registrationPromise = navigator.serviceWorker?.getRegistration
      ? navigator.serviceWorker.getRegistration()
      : Promise.resolve(undefined)

    void registrationPromise.then((registration) => {
      if (registration?.waiting) {
        registration.waiting.postMessage({ type: 'SKIP_WAITING' })
        return
      }

      window.location.reload()
    })
  }

  useEffect(() => {
    if (
      !isScoreboardRoute ||
      !leftTeam ||
      !rightTeam ||
      liveRoomSession ||
      creatingRoomRef.current
    ) {
      return
    }

    let cancelled = false

    const createRoom = async () => {
      try {
        creatingRoomRef.current = true
        const session = await createLiveRoom(
          buildLiveRoomPayload({
            groupId: selectedGroup?.id ?? null,
            leftTeam,
            pointLog,
            rightTeam,
            scoreState,
            targetDate,
          }),
        )

        if (!cancelled) {
          setLiveRoomSession(session)
        }
      } catch (error) {
        console.error('create live room error:', error)
      } finally {
        creatingRoomRef.current = false
      }
    }

    void createRoom()

    return () => {
      cancelled = true
    }
  }, [
    leftTeam,
    liveRoomSession,
    pointLog,
    rightTeam,
    scoreState,
    selectedGroup?.id,
    targetDate,
    isScoreboardRoute,
  ])

  useEffect(() => {
    if (!isScoreboardRoute || !liveRoomSession || !leftTeam || !rightTeam) {
      return
    }

    const winnerTeamName =
      hasWonGame(scoreState) && scoreState.scoreLeft > scoreState.scoreRight
        ? getTeamDisplayName(leftTeam)
        : hasWonGame(scoreState) && scoreState.scoreRight > scoreState.scoreLeft
          ? getTeamDisplayName(rightTeam)
          : null
    const nextStatus = winnerTeamName ? 'finished' : 'live'
    const payload = buildLiveRoomPayload({
      groupId: selectedGroup?.id ?? null,
      leftTeam,
      pointLog,
      rightTeam,
      scoreState,
      targetDate,
    })
    const signature = JSON.stringify({
      payload,
      roomId: liveRoomSession.roomId,
      status: nextStatus,
      winnerTeamName,
    })

    if (signature === lastSyncedRoomSignatureRef.current) {
      return
    }

    lastSyncedRoomSignatureRef.current = signature

    void updateLiveRoom(liveRoomSession.roomId, {
      ...payload,
      hostToken: liveRoomSession.hostToken,
      status: nextStatus,
      winnerTeamName,
    })
      .then((room) => {
        setLiveRoomSession((current) =>
          current
            ? {
                ...current,
                status: room.status,
              }
            : current,
        )
      })
      .catch((error) => {
        console.error('update live room error:', error)
      })
  }, [
    leftTeam,
    liveRoomSession,
    pointLog,
    rightTeam,
    scoreState,
    selectedGroup?.id,
    targetDate,
    isScoreboardRoute,
  ])

  useEffect(() => {
    if (!isNavigationLocked || isScoreboardRoute) {
      return
    }

    navigate('/scoreboard', { replace: true })
    setNavigationLockMessage('比賽進行中，請先完成結算。')
  }, [isNavigationLocked, isScoreboardRoute, navigate])

  useEffect(() => {
    if (!isScoreboardRoute || !liveRoomSession || liveRoomSession.status !== 'live') {
      return
    }

    let active = true

    const syncHeartbeat = async () => {
      try {
        await sendLiveRoomHeartbeat(liveRoomSession.roomId, liveRoomSession.hostToken)
      } catch (error) {
        if (active) {
          console.error('live room heartbeat error:', error)
        }
      }
    }

    void syncHeartbeat()
    const timer = window.setInterval(() => {
      void syncHeartbeat()
    }, LIVE_ROOM_HEARTBEAT_MS)

    return () => {
      active = false
      window.clearInterval(timer)
    }
  }, [isScoreboardRoute, liveRoomSession])

  useEffect(() => {
    if (!liveRoomSession || liveRoomSession.status !== 'live') {
      return
    }

    const { hostToken, roomId } = liveRoomSession
    let released = false

    const release = () => {
      if (released) {
        return
      }

      released = true
      void releaseLiveRoom(roomId, hostToken).catch(() => {})
    }

    const handleBeforeUnload = () => {
      if (released) {
        return
      }

      released = true
      if (navigator.sendBeacon) {
        const payload = new Blob([JSON.stringify({ hostToken })], {
          type: 'application/json',
        })
        navigator.sendBeacon(`/api/rooms/${roomId}/release`, payload)
        return
      }

      void fetch(`/api/rooms/${roomId}/release`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ hostToken }),
        keepalive: true,
      }).catch(() => {})
    }

    window.addEventListener('beforeunload', handleBeforeUnload)

    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload)
      if (!isScoreboardRoute) {
        release()
      }
    }
  }, [isScoreboardRoute, liveRoomSession])

  const loadGroupsFromDb = async () => {
    if (!targetDate) {
      setLoadStatus('error')
      setLoadMessage('請先選擇日期。')
      return
    }

    setLoadStatus('loading')
    setLoadMessage('正在讀取指定日期的分組資料...')

    try {
      const record = await loadMatchResults(convertDateToKey(targetDate))

      if (!record) {
        setGroups([])
        setSelectedGroupId(null)
        setActiveMatchup({ leftTeam: null, rightTeam: null })
        setGroupSource('idle')
        setLoadStatus('empty')
        setLoadMessage('指定日期沒有資料，請改用手動配對。')
        return
      }

      const nextData = convertDbRecordToGroups(record)
      setRosterInput([...nextData.areaA, ...nextData.areaB].join('\n'))
      setGroups(nextData.groups)
      setGroupSource('db')
      setLoadStatus('loaded')
      setLoadMessage(`已載入 ${convertDateToKey(targetDate)} 的分組資料。`)
      selectGroup(nextData.groups[0]?.id ?? 1, nextData.groups)
    } catch (error) {
      setGroups([])
      setSelectedGroupId(null)
      setActiveMatchup({ leftTeam: null, rightTeam: null })
      setGroupSource('idle')
      setLoadStatus('error')
      setLoadMessage(error instanceof Error ? error.message : '讀取資料失敗。')
    }
  }

  const generateManualGroups = () => {
    if (parsedRoster.length < 4) {
      setGroups([])
      setSelectedGroupId(null)
      setActiveMatchup({ leftTeam: null, rightTeam: null })
      setGroupSource('idle')
      setLoadStatus('error')
      setLoadMessage('今日出席至少需要 4 位成員才能排賽程。')
      return
    }

    if (!skillsReady) {
      setLoadStatus('error')
      setLoadMessage('實力資料還沒載入完成，請稍候再產生賽程（或重新整理頁面）。')
      return
    }

    const nextGroups = buildCourtGroups(parsedRoster, skillMap)
    setGroups(nextGroups)
    setGroupSource('manual')
    setLoadStatus('loaded')
    setLoadMessage(
      nextGroups.length === 2
        ? '已依實力產生 2、3 號場地各 8 局賽程，挑一個場地進記分板。'
        : '人數不足 8 人，只排 2 號場地 8 局賽程。',
    )
    selectGroup(nextGroups[0]?.id ?? 1, nextGroups)
  }

  const swapMatchupSides = () => {
    if (scoreHistory.length > 0) {
      return
    }

    setActiveMatchup((current) => ({
      leftTeam: current.rightTeam,
      rightTeam: current.leftTeam,
    }))

    setScoreState((current) => ({
      ...current,
      scoreLeft: current.scoreRight,
      scoreRight: current.scoreLeft,
      gamesLeft: current.gamesRight,
      gamesRight: current.gamesLeft,
      serving:
        current.serving === 'left'
          ? 'right'
          : current.serving === 'right'
            ? 'left'
            : null,
      initialServing:
        current.initialServing === 'left'
          ? 'right'
          : current.initialServing === 'right'
            ? 'left'
            : null,
      leftRightCourtPlayer: current.rightRightCourtPlayer,
      rightRightCourtPlayer: current.leftRightCourtPlayer,
    }))
  }

  const swapTeamPlayers = (side: ScoreSide) => {
    if (scoreHistory.length > 0) {
      return
    }

    setScoreState((current) => ({
      ...current,
      leftRightCourtPlayer:
        side === 'left'
          ? swapCourtPositions(current.leftRightCourtPlayer)
          : current.leftRightCourtPlayer,
      rightRightCourtPlayer:
        side === 'right'
          ? swapCourtPositions(current.rightRightCourtPlayer)
          : current.rightRightCourtPlayer,
    }))
  }

  const setServing = (side: ScoreSide) => {
    if (scoreHistory.length > 0) {
      return
    }

    setScoreState((current) => ({
      ...current,
      initialServing: current.initialServing === side ? null : side,
      serving: current.initialServing === side ? null : side,
    }))
  }

  const recordPoint = (side: ScoreSide) => {
    if (!leftTeam || !rightTeam || scoreState.serving === null) {
      return
    }

    // 已分出勝負就不再計分，需先結算才能開始下一場。
    if (hasWonGame(scoreState)) {
      return
    }

    const starter = getServerHistoryIndex(scoreState, leftTeam, rightTeam)

    if (starter === null) {
      return
    }

    const winner: 0 | 1 = side === 'left' ? 0 : 1
    const previousPoint = pointLog.at(-1)
    const winCount = previousPoint?.winner === winner ? previousPoint.winCount + 1 : 0
    const streakCount = winCount + 1
    const streakTitle = STREAK_TITLES[streakCount]

    const nextPointLog = [
      ...pointLog,
      {
        round: pointLog.length,
        starter,
        winCount,
        winner,
      },
    ]

    const nextScoreState: ScoreState = {
      ...scoreState,
      scoreLeft: side === 'left' ? scoreState.scoreLeft + 1 : scoreState.scoreLeft,
      scoreRight: side === 'right' ? scoreState.scoreRight + 1 : scoreState.scoreRight,
      serving: side,
      leftRightCourtPlayer:
        side === 'left' && side === scoreState.serving
          ? swapCourtPositions(scoreState.leftRightCourtPlayer)
          : scoreState.leftRightCourtPlayer,
      rightRightCourtPlayer:
        side === 'right' && side === scoreState.serving
          ? swapCourtPositions(scoreState.rightRightCourtPlayer)
          : scoreState.rightRightCourtPlayer,
    }

    const reachedTarget = hasWonGame(nextScoreState)
    const winnerTeamName = reachedTarget
      ? side === 'left'
        ? getTeamDisplayName(leftTeam)
        : getTeamDisplayName(rightTeam)
      : null

    // 得分方接著發球，報分以發球方分數為先；發球區一律用實際球場左右，不做鏡像。
    const servingScore = side === 'left' ? nextScoreState.scoreLeft : nextScoreState.scoreRight
    const opponentScore = side === 'left' ? nextScoreState.scoreRight : nextScoreState.scoreLeft
    const serverCourt = getServiceCourt(servingScore)
    // 只有發球方(剛得分那隊)再得 1 分就能贏,才算賽末點。
    const matchPoint =
      !reachedTarget &&
      hasWonGame(
        side === 'left'
          ? { ...nextScoreState, scoreLeft: nextScoreState.scoreLeft + 1 }
          : { ...nextScoreState, scoreRight: nextScoreState.scoreRight + 1 },
      )

    setScoreHistory((current) => [...current, { pointLog, scoreState }])
    setPointLog(nextPointLog)
    setScoreState(nextScoreState)
    setVoiceAnnouncement({
      key: Date.now(),
      servingScore,
      opponentScore,
      serverName: getNextServerName(nextScoreState, leftTeam, rightTeam, side),
      serverCourt,
      matchPoint,
      winnerTeamName,
    })

    if (streakTitle) {
      setStreakAnnouncement({
        count: streakCount,
        key: Date.now(),
        teamName: side === 'left' ? getTeamDisplayName(leftTeam) : getTeamDisplayName(rightTeam),
        title: streakTitle,
      })
    }

    if (reachedTarget) {
      setVictoryAnnouncement({
        key: Date.now() + 1,
        scoreLabel: `${nextScoreState.scoreLeft} : ${nextScoreState.scoreRight}`,
        teamName: side === 'left' ? getTeamDisplayName(leftTeam) : getTeamDisplayName(rightTeam),
        title: '拿下勝利',
      })
    }
  }

  const undoLastPoint = () => {
    const previous = scoreHistory.at(-1)

    if (!previous) {
      return
    }

    setScoreHistory((current) => current.slice(0, -1))
    setPointLog(previous.pointLog)
    setScoreState(previous.scoreState)
    setStreakAnnouncement(null)
    setVictoryAnnouncement(null)
  }

  const openSettlementDialog = () => {
    if (!leftTeam || !rightTeam || pointLog.length === 0) {
      return
    }

    setSettlement({
      error: '',
      open: true,
      uploading: false,
    })
  }

  const closeSettlementDialog = () => {
    if (settlement.uploading) {
      return
    }

    setSettlement((current) => ({
      ...current,
      error: '',
      open: false,
    }))
  }

  // 結算完成時幫這場上場的球員各記一場（輪空不計）。
  const recordPlayedMatch = () => {
    if (!leftTeam || !rightTeam) {
      return
    }

    const playedNames = [leftTeam, rightTeam].flatMap((team) => [
      team.isPlaceholderA ? null : team.playerA,
      team.isPlaceholderB ? null : team.playerB,
    ])

    setPlayCounts((current) => {
      const next = { ...current }
      playedNames.forEach((name) => {
        if (name) {
          next[name] = (next[name] ?? 0) + 1
        }
      })
      return next
    })
  }

  // 從 DB 的 history 表撈今天（本機時區）的比賽，把每個人的今日場次覆蓋成 DB 的數字。
  // 跨裝置記分時，其他裝置上傳的場次本機沒有紀錄，靠這個補齊。
  const syncPlayCountsFromHistory = async () => {
    const dayStart = new Date()
    dayStart.setHours(0, 0, 0, 0)
    const from = Math.floor(dayStart.getTime() / 1000)
    const to = from + 24 * 60 * 60

    const result = await loadHistoryPlayCounts(from, to)
    setPlayCounts(result.counts)

    return result
  }

  const skipUpload = () => {
    void finalizeLiveRoom().finally(() => {
      recordPlayedMatch()
      setSettlement({
        error: '',
        open: false,
        uploading: false,
      })
      resetScoring(initialScoreState, { releaseLiveRoom: false })
      setNextMatchSignal((current) => current + 1)
    })
  }

  const uploadSettledMatch = async () => {
    if (!leftTeam || !rightTeam || !selectedGroup || pointLog.length === 0) {
      return
    }

    setSettlement((current) => ({
      ...current,
      error: '',
      uploading: true,
    }))

    try {
      const payload = buildHistoryPayload({
        leftTeam,
        pointLog,
        rightTeam,
        scoreState,
      })

      const result = await saveMatchHistory(payload)

      const historyItem: MatchHistoryItem = {
        id: String(result.id),
        playedAt: formatPlayedAt(payload.time),
        matchDate: targetDate,
        source: groupSource,
        groupId: selectedGroup.id,
        leftTeamName: getTeamDisplayName(leftTeam),
        rightTeamName: getTeamDisplayName(rightTeam),
        scoreLeft: scoreState.scoreLeft,
        scoreRight: scoreState.scoreRight,
        winner: getWinnerName(
          getTeamDisplayName(leftTeam),
          getTeamDisplayName(rightTeam),
          scoreState,
        ),
      }

      setHistory((current) => [historyItem, ...current])
      recordPlayedMatch()
      await finalizeLiveRoom()
      setSettlement({
        error: '',
        open: false,
        uploading: false,
      })
      resetScoring(initialScoreState, { releaseLiveRoom: false })
      setNextMatchSignal((current) => current + 1)
    } catch (error) {
      setSettlement({
        error: error instanceof Error ? error.message : '上傳戰績失敗。',
        open: true,
        uploading: false,
      })
    }
  }

  const handleNavAttempt = (targetPath: string) => (event: React.MouseEvent<HTMLAnchorElement>) => {
    if (!isNavigationLocked || targetPath === '/scoreboard') {
      return
    }

    event.preventDefault()
    setNavigationLockMessage('比賽進行中，請先完成結算。')
  }

  return (
    <div className={isScoreboardRoute ? 'app-shell app-shell-scoreboard app-shell-scoreboard-fit' : 'app-shell'}>
      <header className={isScoreboardRoute ? 'topbar topbar-compact' : 'topbar'}>
        <div className="branding">
          <p className="eyebrow">Badminton Scoreboard</p>
          <h1>{isScoreboardRoute ? '羽毛球記分板' : '羽毛球分組與記分板'}</h1>
          {!isScoreboardRoute ? (
            <p className="intro-copy">
              先選日期或手動建立組別，再從該組挑選要對打的兩隊進入記分板。比賽結束後可直接上傳戰績到
              DB。
            </p>
          ) : null}
        </div>

        <nav className="topnav" aria-label="主要導覽">
          <NavLink
            className={({ isActive }) => (isActive ? 'nav-pill nav-pill-active' : 'nav-pill')}
            onClick={handleNavAttempt('/teams')}
            to="/teams"
          >
            選隊伍
          </NavLink>
          <NavLink
            className={({ isActive }) => (isActive ? 'nav-pill nav-pill-active' : 'nav-pill')}
            onClick={handleNavAttempt('/scoreboard')}
            to="/scoreboard"
          >
            記分板
          </NavLink>
          <NavLink
            className={({ isActive }) => (isActive ? 'nav-pill nav-pill-active' : 'nav-pill')}
            onClick={handleNavAttempt('/history')}
            to="/history"
          >
            歷史戰績
          </NavLink>
          <NavLink
            className={({ isActive }) => (isActive ? 'nav-pill nav-pill-active' : 'nav-pill')}
            onClick={handleNavAttempt('/rooms')}
            to="/rooms"
          >
            房間列表
          </NavLink>
          <NavLink
            className={({ isActive }) => (isActive ? 'nav-pill nav-pill-active' : 'nav-pill')}
            onClick={handleNavAttempt('/changelog')}
            to="/changelog"
          >
            更新紀錄
          </NavLink>
        </nav>
      </header>

      <Routes>
        <Route
          path="/"
          element={
            <TeamSelectionPage
              areaOtherInput={areaOtherInput}
              groups={groups}
              groupSource={groupSource}
              loadMessage={loadMessage}
              loadStatus={loadStatus}
              rosterInput={rosterInput}
              targetDate={targetDate}
              onAreaOtherInputChange={setAreaOtherInput}
              onRosterInputChange={setRosterInput}
              onGenerateManualGroups={generateManualGroups}
              onLoadGroupsFromDb={() => void loadGroupsFromDb()}
              onTargetDateChange={setTargetDate}
              onUseGroup={selectGroup}
            />
          }
        />
        <Route
          path="/teams"
          element={
            <TeamSelectionPage
              areaOtherInput={areaOtherInput}
              groups={groups}
              groupSource={groupSource}
              loadMessage={loadMessage}
              loadStatus={loadStatus}
              rosterInput={rosterInput}
              targetDate={targetDate}
              onAreaOtherInputChange={setAreaOtherInput}
              onRosterInputChange={setRosterInput}
              onGenerateManualGroups={generateManualGroups}
              onLoadGroupsFromDb={() => void loadGroupsFromDb()}
              onTargetDateChange={setTargetDate}
              onUseGroup={selectGroup}
            />
          }
        />
        <Route
          path="/scoreboard"
          element={
            <ScoreboardPage
              currentSelectionOrder={getSelectionOrder(leftTeam, rightTeam)}
              extraPlayers={parsedAreaOther}
              finishDialogError={settlement.error}
              finishDialogOpen={settlement.open}
              finishDialogUploading={settlement.uploading}
              groupSource={groupSource}
              hasRecordedPoint={pointLog.length > 0}
              leftTeam={leftTeam}
              liveRoomId={liveRoomId}
              nextMatchSignal={nextMatchSignal}
              pointLog={pointLog}
              rightTeam={rightTeam}
              scoreState={scoreState}
              playCounts={playCounts}
              selectedGroup={selectedGroup}
              streakAnnouncement={streakAnnouncement}
              victoryAnnouncement={victoryAnnouncement}
              voiceAnnouncement={voiceAnnouncement}
              targetDate={targetDate}
              onApplyMatchup={applyMatchup}
              onSyncPlayCounts={syncPlayCountsFromHistory}
              onCloseFinishDialog={closeSettlementDialog}
              onConfirmUpload={uploadSettledMatch}
              onOpenFinishDialog={openSettlementDialog}
              onRecordPoint={recordPoint}
              onSetServing={setServing}
              onSkipUpload={skipUpload}
              onSwapMatchup={swapMatchupSides}
              onSwapTeamPlayers={swapTeamPlayers}
              onUndoLastPoint={undoLastPoint}
            />
          }
        />
        <Route path="/history" element={<HistoryPage />} />
        <Route path="/changelog" element={<ChangelogPage />} />
        <Route path="/rooms" element={<RoomListPage />} />
        <Route
          path="/rooms/:roomId"
          element={<RoomSpectatorPage onConfirmFinished={() => navigate('/rooms')} />}
        />
      </Routes>

      {pwaUpdateReady ? (
        <div className="pwa-update-toast" role="status" aria-live="polite">
          <div className="pwa-update-copy">
            <strong>有新版本可更新</strong>
            <span>點重新整理後套用最新版本。</span>
          </div>
          <button className="pwa-update-button" onClick={refreshForPwaUpdate} type="button">
            重新整理
          </button>
        </div>
      ) : null}

      {navigationLockMessage ? (
        <div className="floating-status-bubble" role="status" aria-live="polite">
          {navigationLockMessage}
        </div>
      ) : null}

      {certExpiryWarning && !pwaUpdateReady ? (
        <div className="pwa-update-toast" role="alert">
          <div className="pwa-update-copy">
            <strong>SSL 憑證提醒</strong>
            <span>{certExpiryWarning}</span>
          </div>
          <button
            className="pwa-update-button"
            onClick={() => {
              certWarningDismissedRef.current = true
              setCertExpiryWarning('')
            }}
            type="button"
          >
            知道了
          </button>
        </div>
      ) : null}
    </div>
  )
}

function buildHistoryPayload({
  leftTeam,
  pointLog,
  rightTeam,
  scoreState,
}: {
  leftTeam: GroupTeam
  pointLog: PointHistoryEntry[]
  rightTeam: GroupTeam
  scoreState: ScoreState
}): HistoryUploadPayload {
  const players = [
    leftTeam.playerA,
    leftTeam.playerB,
    rightTeam.playerB,
    rightTeam.playerA,
  ]

  return {
    dayOfWeek: new Date().getDay(),
    players,
    score: [scoreState.scoreLeft, scoreState.scoreRight],
    scoreList: pointLog.map((point) => [
      point.round,
      point.starter,
      point.winCount,
      point.winner,
    ]),
    team: [
      [leftTeam.playerA, leftTeam.playerB],
      [rightTeam.playerB, rightTeam.playerA],
    ],
    time: Math.floor(Date.now() / 1000),
    type: 0,
    winScore: scoreState.targetScore,
  }
}

function getServerHistoryIndex(
  state: ScoreState,
  leftTeam: GroupTeam,
  rightTeam: GroupTeam,
) {
  if (state.serving === 'left') {
    const server = getServingPlayer(leftTeam, state.leftRightCourtPlayer, state.scoreLeft)

    if (!server) {
      return null
    }

    return getMirroredCourt(getServiceCourt(state.scoreLeft)) === 'left' ? 0 : 1
  }

  if (state.serving === 'right') {
    const server = getServingPlayer(rightTeam, state.rightRightCourtPlayer, state.scoreRight)

    if (!server) {
      return null
    }

    return getServiceCourt(state.scoreRight) === 'right' ? 2 : 3
  }

  return null
}

function getNextServerName(
  state: ScoreState,
  leftTeam: GroupTeam,
  rightTeam: GroupTeam,
  side: ScoreSide,
) {
  if (side === 'left') {
    return getServingPlayer(leftTeam, state.leftRightCourtPlayer, state.scoreLeft)?.name ?? ''
  }

  return getServingPlayer(rightTeam, state.rightRightCourtPlayer, state.scoreRight)?.name ?? ''
}

function formatPlayedAt(timestamp: number) {
  return new Date(timestamp * 1000).toLocaleString('zh-TW', { hour12: false })
}

function getSelectionOrder(leftTeam: GroupTeam | null, rightTeam: GroupTeam | null) {
  if (!leftTeam || !rightTeam) {
    return []
  }

  return [
    leftTeam.playerA,
    leftTeam.playerB,
    rightTeam.playerB,
    rightTeam.playerA,
  ].filter((name) => name.trim().length > 0)
}

function loadStoredText(storageKey: string, fallback: string) {
  const value = window.localStorage.getItem(storageKey)
  return value && value.trim() ? value : fallback
}

function loadStoredHistory(storageKey: string) {
  const value = window.localStorage.getItem(storageKey)

  if (!value) {
    return []
  }

  try {
    const parsed = JSON.parse(value) as MatchHistoryItem[]
    return Array.isArray(parsed) ? parsed : []
  } catch {
    return []
  }
}

// 讀取當天的球員場次紀錄，日期不是今天就重新歸零。
function loadStoredPlayCounts(storageKey: string): Record<string, number> {
  const value = window.localStorage.getItem(storageKey)

  if (!value) {
    return {}
  }

  try {
    const parsed = JSON.parse(value) as {
      date?: string
      counts?: Record<string, number>
    }

    if (parsed.date !== formatDateInputValue() || !parsed.counts) {
      return {}
    }

    return parsed.counts
  } catch {
    return {}
  }
}

function buildLiveRoomPayload({
  groupId,
  leftTeam,
  pointLog,
  rightTeam,
  scoreState,
  targetDate,
}: {
  groupId: number | null
  leftTeam: GroupTeam
  pointLog: PointHistoryEntry[]
  rightTeam: GroupTeam
  scoreState: ScoreState
  targetDate: string
}) {
  return {
    groupId,
    leftTeamName: getTeamDisplayName(leftTeam),
    matchupLabel: `${getTeamDisplayName(leftTeam)} vs ${getTeamDisplayName(rightTeam)}`,
    pointLog,
    rightTeamName: getTeamDisplayName(rightTeam),
    scoreState,
    targetDate,
  }
}

export default App
