﻿<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import deskImage from '../assets/images/desk/desk_01_1920_945.png'
import robotIcon from '../assets/images/icons/robot.svg'
import exitIcon from '../assets/images/icons/exit.svg'
import '../assets/styles/room.css'
import TopPlayerCard from '../components/game/TopPlayerCard.vue'
import RightPlayerCard from '../components/game/RightPlayerCard.vue'
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
import LeftPlayerCard from '../components/game/LeftPlayerCard.vue'
import WindSquare from '../components/game/WindSquare.vue'
import eastWind from '../assets/images/direction/dong.png'
import southWind from '../assets/images/direction/nan.png'
import westWind from '../assets/images/direction/xi.png'
import northWind from '../assets/images/direction/bei.png'
import type {SeatPlayerCardModel} from '../components/game/seat-player-card'
import type {SeatKey} from '../game/seat'
import type {GameAction, PlayerTurnPayload, RoomPlayerUpdatePayload, RoomTrusteePayload} from '../game/actions'
import {dispatchGameAction} from '../game/dispatcher'
import {refreshAccessToken} from '../api/auth'
import {AuthExpiredError, type AuthSession} from '../api/authed-request'
import {getUserInfo} from '../api/user'
import {clearAuth, readStoredAuth, writeStoredAuth} from '../utils/auth-storage'
import type {WsStatus} from '../ws/client'
import {wsClient} from '../ws/client'
import {sendWsMessage} from '../ws/sender'
import {buildWsUrl} from '../ws/url'
import {useGameStore} from '../store/gameStore'
import {clearActiveRoom, setActiveRoom, useActiveRoomState} from '../store'
import type {ClaimOptionState, MeldState, PendingClaimState, PlayerState} from '../types/state'
import type {Tile} from '../types/tile'
import {getTileImage as getBottomTileImage} from '../config/bottomTileMap.ts'
import {getTileImage as getTopTileImage} from '../config/topTileMap.ts'
import {getTileImage as getRightTileImage} from '../config/rightTileMap.ts'
import {getTileImage as getLeftTileImage} from '../config/leftTileMap.ts'

const gameStore = useGameStore()
const activeRoom = useActiveRoomState()
const route = useRoute()
const router = useRouter()
const auth = ref(readStoredAuth())

type DisplayPlayer = PlayerState & {
  displayName?: string
  missingSuit?: string | null
}

type GameActionPayload<TType extends GameAction['type']> = Extract<GameAction, { type: TType }>['payload']
type HandSuitLabel = '万' | '筒' | '条'
type TableTileImageType = 'hand' | 'exposed' | 'covered'

interface WallTileItem {
  key: string
  src: string
  alt: string
  imageType: TableTileImageType
  isGroupStart?: boolean
  showLackTag?: boolean
  suit?: Tile['suit']
  tile?: Tile
}

interface WallSeatState {
  tiles: WallTileItem[]
}

interface DeskSeatState {
  tiles: WallTileItem[]
  hasHu: boolean
}

interface SeatViewModel {
  key: SeatKey
  player?: DisplayPlayer
  isSelf: boolean
  isTurn: boolean
}

interface PlayerActionTimer {
  playerIds: string[]
  actionDeadlineAt?: string | null
  countdownSeconds: number
  duration: number
  remaining: number
}

const now = ref(Date.now())
const wsStatus = ref<WsStatus>('idle')
const wsMessages = ref<string[]>([])
const wsError = ref('')
const roomCountdown = ref<PlayerActionTimer | null>(null)
const leaveRoomPending = ref(false)
const readyTogglePending = ref(false)
const startGamePending = ref(false)
const dingQuePending = ref(false)
const discardPending = ref(false)
const claimActionPending = ref(false)
const turnActionPending = ref(false)
const selfTurnAllowActions = ref<string[]>([])
const nextRoundPending = ref(false)
const settlementOverlayDismissed = ref(false)
const settlementDeadlineMs = ref<number | null>(null)
const selectedDiscardTileId = ref<number | null>(null)
let clockTimer: number | null = null
let discardPendingTimer: number | null = null
let turnActionPendingTimer: number | null = null
let unsubscribe: (() => void) | null = null
let needsInitialRoomInfo = false

const menuOpen = ref(false)
const isTrustMode = ref(false)
const menuTriggerActive = ref(false)
let menuTriggerTimer: number | null = null
let menuOpenTimer: number | null = null
let refreshingWsToken = false
let lastForcedRefreshAt = 0

const loggedInUserId = computed(() => {
  const source = auth.value?.user as Record<string, unknown> | undefined
  const rawId =
      source?.id ??
      source?.userID ??
      source?.user_id

  if (typeof rawId === 'string' && rawId.trim()) {
    return rawId
  }
  if (typeof rawId === 'number') {
    return String(rawId)
  }
  return ''
})

const loggedInUserName = computed(() => {
  return auth.value?.user?.nickname || auth.value?.user?.username || ''
})

const localCachedAvatarUrl = computed(() => {
  const source = auth.value?.user as Record<string, unknown> | undefined
  if (!source) {
    return ''
  }

  const avatarCandidates = [
    source.avatar,
    source.avatar_url,
    source.avatarUrl,
    source.head_img,
    source.headImg,
    source.profile_image,
    source.profileImage,
  ]

  for (const candidate of avatarCandidates) {
    if (typeof candidate === 'string' && candidate.trim()) {
      return candidate
    }
  }

  return ''
})

const myPlayer = computed(() => {
  return gameStore.players[loggedInUserId.value]
})

const myHandTiles = computed(() => {
  return myPlayer.value?.handTiles ?? []
})

const handSuitOrder: Record<Tile['suit'], number> = {
  W: 0,
  T: 1,
  B: 2,
}

const handSuitLabelMap: Record<Tile['suit'], HandSuitLabel> = {
  W: '万',
  T: '筒',
  B: '条',
}

const visibleHandTileGroups = computed(() => {
  const grouped = new Map<HandSuitLabel, Tile[]>()

  myHandTiles.value
      .slice()
      .sort((left, right) => {
        const suitDiff = handSuitOrder[left.suit] - handSuitOrder[right.suit]
        if (suitDiff !== 0) {
          return suitDiff
        }

        const valueDiff = left.value - right.value
        if (valueDiff !== 0) {
          return valueDiff
        }

        return left.id - right.id
      })
      .forEach((tile) => {
        const label = handSuitLabelMap[tile.suit]
        const current = grouped.get(label) ?? []
        current.push(tile)
        grouped.set(label, current)
      })

  return (['万', '筒', '条'] as HandSuitLabel[])
      .map((suit) => ({
        suit,
        tiles: grouped.get(suit) ?? [],
      }))
      .filter((group) => group.tiles.length > 0)
})

const sortedVisibleHandTiles = computed(() => {
  return visibleHandTileGroups.value.flatMap((group) => group.tiles)
})

const remainingTiles = computed(() => {
  return gameStore.remainingTiles
})

const gamePlayers = computed<DisplayPlayer[]>(() => {
  return Object.values(gameStore.players).sort((a, b) => a.seatIndex - b.seatIndex) as DisplayPlayer[]
})

const roomName = computed(() => {
  const queryRoomName = typeof route.query.roomName === 'string' ? route.query.roomName : ''
  const activeRoomName =
      activeRoom.value && activeRoom.value.roomId === gameStore.roomId
          ? activeRoom.value.roomName
          : ''
  return queryRoomName || activeRoomName || `房间 ${gameStore.roomId || '--'}`
})

const roomState = computed(() => {
  const status = gameStore.phase === 'waiting' ? 'waiting' : gameStore.phase === 'settlement' ? 'finished' : 'playing'
  const wall = Array.from({length: remainingTiles.value}, (_, index) => `wall-${index}`)
  const maxPlayers =
      activeRoom.value && activeRoom.value.roomId === gameStore.roomId
          ? activeRoom.value.maxPlayers
          : 4

  return {
    roomId: gameStore.roomId,
    name: roomName.value,
    playerCount: gamePlayers.value.length,
    maxPlayers,
    status,
    game: {
      state: {
        wall,
        dealerIndex: gameStore.dealerIndex,
        currentTurn: gameStore.currentTurn,
        phase: gameStore.phase,
      },
    },
  }
})

const seatViews = computed<SeatViewModel[]>(() => {
  const players = gamePlayers.value
  const tableOrder: SeatKey[] = ['bottom', 'right', 'top', 'left']
  const selfSeatIndex = myPlayer.value?.seatIndex ?? players.find((player) => player.playerId === loggedInUserId.value)?.seatIndex ?? 0
  const currentTurn = gameStore.currentTurn

  return players.slice(0, 4).map((player) => {
    const relativeIndex = (selfSeatIndex - player.seatIndex + 4) % 4
    const seatKey = tableOrder[relativeIndex] ?? 'top'
    return {
      key: seatKey,
      player,
      isSelf: player.playerId === loggedInUserId.value,
      isTurn: player.seatIndex === currentTurn,
    }
  })
})

const seatWinds = computed<Record<SeatKey, string>>(() => {
  const tableOrder: SeatKey[] = ['bottom', 'right', 'top', 'left']
  const players = gamePlayers.value
  const selfSeatIndex = myPlayer.value?.seatIndex ?? players.find((player) => player.playerId === loggedInUserId.value)?.seatIndex ?? 0

  const directionBySeatIndex = [eastWind, southWind, westWind, northWind]
  const result: Record<SeatKey, string> = {
    top: northWind,
    right: eastWind,
    bottom: southWind,
    left: westWind,
  }

  for (let absoluteSeat = 0; absoluteSeat < 4; absoluteSeat += 1) {
    const relativeIndex = (selfSeatIndex - absoluteSeat + 4) % 4
    const seatKey = tableOrder[relativeIndex] ?? 'top'
    result[seatKey] = directionBySeatIndex[absoluteSeat] ?? northWind
  }

  return result
})

const currentTurnSeat = computed<SeatKey | ''>(() => {
  const turnSeat = seatViews.value.find((seat) => seat.isTurn)
  return turnSeat?.key ?? ''
})

const currentPhaseText = computed(() => {
  const map: Record<string, string> = {
    waiting: '等待中',
    dealing: '发牌中',
    playing: '对局中',
    action: '操作中',
    settlement: '已结算',
  }
  return map[gameStore.phase] ?? gameStore.phase
})

const roomStatusText = computed(() => {
  const map: Record<string, string> = {
    waiting: '等待玩家',
    playing: '游戏中',
    finished: '已结束',
  }
  const status = roomState.value.status
  return map[status] ?? status ?? '--'
})

const roundText = computed(() => {
  if (gameStore.totalRounds > 0) {
    return `${gameStore.currentRound}/${gameStore.totalRounds}`
  }
  return ''
})

const showSettlementOverlay = computed(() => {
  return gameStore.phase === 'settlement' && !settlementOverlayDismissed.value
})

const isLastRound = computed(() => {
  return gameStore.currentRound >= gameStore.totalRounds && gameStore.totalRounds > 0
})

const settlementPlayers = computed(() => {
  const players = Object.values(gameStore.players)
  const winnerSet = new Set(gameStore.winners)
  return players
      .map((player) => ({
        playerId: player.playerId,
        displayName: player.displayName || `玩家${player.seatIndex + 1}`,
        score: gameStore.scores[player.playerId] ?? 0,
        isWinner: winnerSet.has(player.playerId),
        seatIndex: player.seatIndex,
      }))
      .sort((a, b) => b.score - a.score)
})

const settlementCountdown = computed(() => {
  if (!showSettlementOverlay.value || !settlementDeadlineMs.value) {
    return null
  }
  const remaining = Math.max(0, Math.ceil((settlementDeadlineMs.value - now.value) / 1000))
  return remaining
})

const myReadyState = computed(() => {
  return Boolean(myPlayer.value?.isReady)
})

const isRoomOwner = computed(() => {
  const room = activeRoom.value
  return Boolean(
      room &&
      room.roomId === gameStore.roomId &&
      room.ownerId &&
      loggedInUserId.value &&
      room.ownerId === loggedInUserId.value,
  )
})

const allPlayersReady = computed(() => {
  return (
      gamePlayers.value.length === 4 &&
      gamePlayers.value.every((player) => Boolean(player.isReady))
  )
})

const hasRoundStarted = computed(() => {
  return gamePlayers.value.some((player) => {
    return (
        player.handCount > 0 ||
        player.handTiles.length > 0 ||
        player.melds.length > 0 ||
        player.discardTiles.length > 0
    )
  })
})

const showStartGameButton = computed(() => {
  return gameStore.phase === 'waiting' && allPlayersReady.value && !hasRoundStarted.value
})

const showWaitingOwnerTip = computed(() => {
  return showStartGameButton.value && !isRoomOwner.value
})

const canStartGame = computed(() => {
  return showStartGameButton.value && isRoomOwner.value && !startGamePending.value
})

const showReadyToggle = computed(() => {
  if (gameStore.phase !== 'waiting' || !gameStore.roomId || hasRoundStarted.value) {
    return false
  }

  if (showStartGameButton.value) {
    return !isRoomOwner.value
  }

  return true
})

const showDingQueChooser = computed(() => {
  const player = myPlayer.value
  if (!player) {
    return false
  }

  if (gameStore.phase === 'settlement') {
    return false
  }

  return player.handTiles.length > 0 && !player.missingSuit
})

const selectedDiscardTile = computed(() => {
  const player = myPlayer.value
  if (!player || selectedDiscardTileId.value === null) {
    return null
  }

  return player.handTiles.find((tile) => tile.id === selectedDiscardTileId.value) ?? null
})

const hasMissingSuitTiles = computed(() => {
  const player = myPlayer.value
  const missingSuit = player?.missingSuit as Tile['suit'] | null | undefined
  if (!player || !missingSuit) {
    return false
  }

  return player.handTiles.some((tile) => tile.suit === missingSuit)
})

const discardBlockedReason = computed(() => {
  const player = myPlayer.value
  if (!player || !gameStore.roomId) {
    return '未进入房间'
  }

  if (wsStatus.value !== 'connected') {
    return 'WebSocket 未连接'
  }

  if (showDingQueChooser.value) {
    return '请先完成定缺'
  }

  if (gameStore.phase !== 'playing') {
    return '当前不是出牌阶段'
  }

  if (player.seatIndex !== gameStore.currentTurn) {
    return '未轮到你出牌'
  }

  if (gameStore.needDraw) {
    return '请先摸牌'
  }

  if (gameStore.pendingClaim) {
    return '等待当前操作结算'
  }

  if (player.handTiles.length === 0) {
    return '当前没有可出的手牌'
  }

  if (discardPending.value) {
    return '正在提交出牌'
  }

  return ''
})

function discardTileBlockedReason(tile: Tile): string {
  if (discardBlockedReason.value) {
    return discardBlockedReason.value
  }

  const player = myPlayer.value
  const missingSuit = player?.missingSuit as Tile['suit'] | null | undefined
  if (player && missingSuit && hasMissingSuitTiles.value && tile.suit !== missingSuit) {
    return `当前必须先打${missingSuitLabel(missingSuit)}牌`
  }

  return ''
}

const canConfirmDiscard = computed(() => {
  const tile = selectedDiscardTile.value
  if (!tile) {
    return false
  }
  return !discardTileBlockedReason(tile)
})

const confirmDiscardLabel = computed(() => {
  const tile = selectedDiscardTile.value
  return tile ? `出牌 ${formatTile(tile)}` : '出牌'
})

const canDrawTile = computed(() => {
  const player = myPlayer.value
  if (!player || !gameStore.roomId) {
    return false
  }

  if (gameStore.phase !== 'playing') {
    return false
  }

  if (!gameStore.needDraw) {
    return false
  }

  return player.seatIndex === gameStore.currentTurn
})

const myClaimState = computed<PendingClaimState | undefined>(() => {
  return gameStore.pendingClaim
})

const visibleClaimOptions = computed<ClaimOptionState[]>(() => {
  const options = myClaimState.value?.options ?? []
  const order: ClaimOptionState[] = ['hu', 'gang', 'peng', 'pass']
  return order.filter((option) => options.includes(option))
})

const showClaimActions = computed(() => {
  return visibleClaimOptions.value.length > 0
})

const canSelfHu = computed(() => {
  const player = myPlayer.value
  if (!player || !gameStore.roomId || wsStatus.value !== 'connected') {
    return false
  }

  if (showDingQueChooser.value || gameStore.phase !== 'playing' || gameStore.needDraw || gameStore.pendingClaim) {
    return false
  }

  if (player.seatIndex !== gameStore.currentTurn || turnActionPending.value) {
    return false
  }

  return selfTurnAllowActions.value.includes('hu')
})

const canSelfGang = computed(() => {
  const player = myPlayer.value
  if (!player || !gameStore.roomId || wsStatus.value !== 'connected') {
    return false
  }

  if (showDingQueChooser.value || gameStore.phase !== 'playing' || gameStore.needDraw || gameStore.pendingClaim) {
    return false
  }

  if (player.seatIndex !== gameStore.currentTurn || turnActionPending.value) {
    return false
  }

  return selfTurnAllowActions.value.includes('gang')
})

const actionCountdown = computed(() => {
  const countdown = roomCountdown.value
  if (!countdown) {
    return null
  }

  const deadlineAt = countdown.actionDeadlineAt ? Date.parse(countdown.actionDeadlineAt) : Number.NaN
  const fallbackRemaining = countdown.remaining > 0 ? countdown.remaining : countdown.countdownSeconds
  const derivedRemaining = Number.isFinite(deadlineAt)
      ? Math.ceil((deadlineAt - now.value) / 1000)
      : fallbackRemaining
  const remaining = Math.max(0, derivedRemaining)

  if (remaining <= 0) {
    return null
  }

  const targetPlayerIds = countdown.playerIds.filter((playerId) => typeof playerId === 'string' && playerId.trim())
  if (targetPlayerIds.length === 0) {
    return null
  }

  const playerLabel = targetPlayerIds
      .map((playerId) => {
        if (playerId === loggedInUserId.value) {
          return '你'
        }

        const targetPlayer = gameStore.players[playerId]
        if (targetPlayer?.displayName) {
          return targetPlayer.displayName
        }
        if (targetPlayer) {
          return `玩家${targetPlayer.seatIndex + 1}`
        }
        return '玩家'
      })
      .join('、')
  const duration = countdown.duration > 0 ? countdown.duration : Math.max(remaining, fallbackRemaining, 1)
  const includesSelf = targetPlayerIds.includes(loggedInUserId.value)

  return {
    playerLabel,
    remaining,
    duration,
    isSelf: includesSelf,
    progress: Math.max(0, Math.min(100, (remaining / duration) * 100)),
  }
})

function applyPlayerReadyState(playerId: string, ready: boolean): void {
  const player = gameStore.players[playerId]
  if (player) {
    player.isReady = ready
  }

  const room = activeRoom.value
  if (!room || room.roomId !== gameStore.roomId) {
    return
  }

  const roomPlayer = room.players.find((item) => item.playerId === playerId)
  if (roomPlayer) {
    roomPlayer.ready = ready
  }
}

function syncReadyStatesFromRoomUpdate(payload: RoomPlayerUpdatePayload): void {
  if (!Array.isArray(payload.players)) {
    return
  }

  for (const item of payload.players) {
    const playerId =
        (typeof item.PlayerID === 'string' && item.PlayerID) ||
        (typeof item.player_id === 'string' && item.player_id) ||
        ''
    const ready =
        typeof item.Ready === 'boolean'
            ? item.Ready
            : typeof item.ready === 'boolean'
                ? item.ready
                : undefined

    if (!playerId || typeof ready !== 'boolean') {
      continue
    }

    applyPlayerReadyState(playerId, ready)
  }
}

function normalizeWsType(type: string): string {
  return type.replace(/[-\s]/g, '_').toUpperCase()
}

function asRecord(value: unknown): Record<string, unknown> | null {
  return value && typeof value === 'object' ? value as Record<string, unknown> : null
}

function readString(source: Record<string, unknown>, ...keys: string[]): string {
  for (const key of keys) {
    const value = source[key]
    if (typeof value === 'string' && value.trim()) {
      return value
    }
  }
  return ''
}

function readNumber(source: Record<string, unknown>, ...keys: string[]): number | null {
  for (const key of keys) {
    const value = source[key]
    if (typeof value === 'number' && Number.isFinite(value)) {
      return value
    }
  }
  return null
}

function normalizeTimestampMs(value: number | null): number | null {
  if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
    return null
  }

  return value >= 1_000_000_000_000 ? value : value * 1000
}

function readStringArray(source: Record<string, unknown>, ...keys: string[]): string[] {
  for (const key of keys) {
    const value = source[key]
    if (Array.isArray(value)) {
      return value.filter((item): item is string => typeof item === 'string')
    }
  }
  return []
}

function readBoolean(source: Record<string, unknown>, ...keys: string[]): boolean | null {
  for (const key of keys) {
    const value = source[key]
    if (typeof value === 'boolean') {
      return value
    }
    if (typeof value === 'number') {
      if (value === 1) {
        return true
      }
      if (value === 0) {
        return false
      }
    }
    if (typeof value === 'string') {
      const normalized = value.trim().toLowerCase()
      if (normalized === 'true' || normalized === '1') {
        return true
      }
      if (normalized === 'false' || normalized === '0') {
        return false
      }
    }
  }
  return null
}

function readPlayerTurnPlayerId(payload: PlayerTurnPayload): string {
  return (
      (typeof payload.player_id === 'string' && payload.player_id) ||
      (typeof payload.playerId === 'string' && payload.playerId) ||
      (typeof payload.PlayerID === 'string' && payload.PlayerID) ||
      ''
  )
}

function readPlayerTurnAllowActions(payload: PlayerTurnPayload): string[] {
  const source =
      payload.allow_actions ??
      payload.allowActions ??
      payload.AllowActions ??
      payload.available_actions ??
      payload.availableActions ??
      payload.AvailableActions
  if (!Array.isArray(source)) {
    return []
  }

  const actions = source
      .filter((item): item is string => typeof item === 'string')
      .map((item) => item.trim().toLowerCase())
      .filter((item) => item.length > 0)
  return Array.from(new Set(actions))
}

function readMissingSuit(source: Record<string, unknown> | null | undefined): string | null {
  if (!source) {
    return null
  }

  return readString(source, 'missing_suit', 'MissingSuit', 'ding_que', 'dingQue', 'suit', 'Suit') || null
}

function readMissingSuitWithPresence(
  source: Record<string, unknown> | null | undefined,
): { present: boolean, value: string | null } {
  if (!source) {
    return { present: false, value: null }
  }

  const keys = ['missing_suit', 'MissingSuit', 'ding_que', 'dingQue', 'suit', 'Suit']
  const hasMissingSuitField = keys.some((key) => Object.prototype.hasOwnProperty.call(source, key))
  if (!hasMissingSuitField) {
    return { present: false, value: null }
  }

  return { present: true, value: readMissingSuit(source) }
}

function tileToText(tile: Tile): string {
  return `${tile.suit}${tile.value}`
}

function clearTurnActionPending(): void {
  turnActionPending.value = false
  if (turnActionPendingTimer !== null) {
    window.clearTimeout(turnActionPendingTimer)
    turnActionPendingTimer = null
  }
}

function markTurnActionPending(kind: 'gang' | 'hu'): void {
  clearTurnActionPending()
  turnActionPending.value = true
  turnActionPendingTimer = window.setTimeout(() => {
    turnActionPending.value = false
    turnActionPendingTimer = null
    wsError.value = `${kind === 'gang' ? '杠牌' : '胡牌'}未收到服务器确认`
  }, 2500)
}

function normalizeTile(tile: unknown): Tile | null {
  const source = asRecord(tile)
  if (!source) {
    return null
  }

  const id = readNumber(source, 'id')
  const suit = readString(source, 'suit') as Tile['suit'] | ''
  const value = readNumber(source, 'value')
  if (typeof id !== 'number' || !suit || typeof value !== 'number') {
    return null
  }

  if (suit !== 'W' && suit !== 'T' && suit !== 'B') {
    return null
  }

  return {
    id,
    suit,
    value,
  }
}

function normalizeTiles(value: unknown): Tile[] {
  if (!Array.isArray(value)) {
    return []
  }

  return value
      .map((item) => normalizeTile(item))
      .filter((item): item is Tile => Boolean(item))
}

function normalizePendingClaim(gameState: Record<string, unknown> | null | undefined): PendingClaimState | undefined {
  if (!gameState || !loggedInUserId.value) {
    return undefined
  }

  const pendingClaim = asRecord(gameState.pending_claim ?? gameState.pendingClaim)
  if (!pendingClaim) {
    return undefined
  }

  const selfOptions = asRecord(pendingClaim[loggedInUserId.value])
  if (!selfOptions) {
    return undefined
  }

  const options: ClaimOptionState[] = []
  if (readBoolean(selfOptions, 'hu')) {
    options.push('hu')
  }
  if (readBoolean(selfOptions, 'gang')) {
    options.push('gang')
  }
  if (readBoolean(selfOptions, 'peng')) {
    options.push('peng')
  }
  if (options.length === 0) {
    return undefined
  }

  options.push('pass')

  return {
    tile: normalizeTile(gameState.last_discard_tile ?? gameState.lastDiscardTile) ?? undefined,
    fromPlayerId: readString(gameState, 'last_discard_by', 'lastDiscardBy') || undefined,
    options,
  }
}

function normalizeMeldType(value: unknown, concealed = false): MeldState['type'] | null {
  if (typeof value !== 'string') {
    return concealed ? 'an_gang' : null
  }

  const normalized = value.replace(/[-\s]/g, '_').toLowerCase()
  if (normalized === 'peng') {
    return 'peng'
  }
  if (normalized === 'ming_gang' || normalized === 'gang' || normalized === 'gang_open') {
    return concealed ? 'an_gang' : 'ming_gang'
  }
  if (normalized === 'an_gang' || normalized === 'angang' || normalized === 'concealed_gang') {
    return 'an_gang'
  }

  return concealed ? 'an_gang' : null
}

function normalizeMelds(value: unknown): PlayerState['melds'] {
  if (!Array.isArray(value)) {
    return []
  }

  return value
      .map((item) => {
        if (Array.isArray(item)) {
          const tiles = normalizeTiles(item)
          if (tiles.length === 3) {
            return {
              type: 'peng',
              tiles,
              fromPlayerId: '',
            } satisfies MeldState
          }
          if (tiles.length === 4) {
            return {
              type: 'ming_gang',
              tiles,
              fromPlayerId: '',
            } satisfies MeldState
          }
          return null
        }

        const source = asRecord(item)
        if (!source) {
          return null
        }

        const tiles = normalizeTiles(
            source.tiles ??
            source.meld_tiles ??
            source.meldTiles ??
            source.cards ??
            source.card_list,
        )
        if (tiles.length === 0) {
          return null
        }

        const concealed =
            readBoolean(source, 'concealed', 'is_concealed', 'isConcealed', 'hidden', 'is_hidden') ?? false
        const explicitType = normalizeMeldType(
            source.type ?? source.meld_type ?? source.meldType ?? source.kind,
            concealed,
        )
        const type = explicitType ?? (tiles.length === 4
            ? (concealed ? 'an_gang' : 'ming_gang')
            : tiles.length === 3
                ? 'peng'
                : null)

        if (type === 'peng') {
          return {
            type,
            tiles,
            fromPlayerId: readString(source, 'from_player_id', 'fromPlayerId'),
          } satisfies MeldState
        }

        if (type === 'ming_gang') {
          return {
            type,
            tiles,
            fromPlayerId: readString(source, 'from_player_id', 'fromPlayerId'),
          } satisfies MeldState
        }

        if (type === 'an_gang') {
          return {
            type,
            tiles,
          } satisfies MeldState
        }

        return null
      })
      .filter((item): item is MeldState => Boolean(item))
}

function requestRoomInfo(): void {
  const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
  const roomId = routeRoomId || gameStore.roomId || activeRoom.value?.roomId || ''
  if (!roomId) {
    return
  }

  if (wsStatus.value !== 'connected') {
    return
  }

  needsInitialRoomInfo = false
  wsMessages.value.push(`[client] get_room_info ${roomId}`)
  sendWsMessage({
    type: 'get_room_info',
    roomId,
    payload: {
      room_id: roomId,
    },
  })
}

function handleRoomStateResponse(message: unknown): void {
  const source = asRecord(message)
  if (!source || typeof source.type !== 'string') {
    return
  }

  if (normalizeWsType(source.type) !== 'ROOM_STATE') {
    return
  }

  const payload = asRecord(source.payload)
  if (!payload) {
    return
  }

  const roomId =
      readString(payload, 'room_id', 'roomId') ||
      readString(source, 'roomId') ||
      gameStore.roomId
  if (!roomId) {
    return
  }
  if (gameStore.roomId && roomId !== gameStore.roomId) {
    return
  }

  const previousPlayers = gameStore.players
  const nextPlayers: typeof gameStore.players = {}
  const gamePlayers = Array.isArray(payload.players) ? payload.players : []

  gamePlayers.forEach((item, fallbackIndex) => {
    const player = asRecord(item)
    if (!player) {
      return
    }

    const playerId = readString(player, 'player_id', 'PlayerID')
    if (!playerId) {
      return
    }

    const previous = previousPlayers[playerId]
    const seatIndex = previous?.seatIndex ?? fallbackIndex
    const handCount = readNumber(player, 'hand_count', 'handCount') ?? previous?.handCount ?? 0
    const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles)
    const melds = normalizeMelds(
        player.melds ??
        player.exposed_melds ??
        player.exposedMelds ??
        player.claims,
    )
    const hasHu = Boolean(player.has_hu ?? player.hasHu)
    const dingQue = readMissingSuitWithPresence(player)
    const scores = asRecord(payload.scores)
    const score = scores?.[playerId]

    nextPlayers[playerId] = {
      playerId,
      seatIndex,
      displayName: previous?.displayName ?? playerId,
      avatarURL: previous?.avatarURL,
      isTrustee: previous?.isTrustee ?? false,
      missingSuit: dingQue.present ? dingQue.value : (previous?.missingSuit ?? null),
      handTiles: previous?.handTiles ?? [],
      handCount,
      melds,
      discardTiles: outTiles,
      hasHu,
      score: typeof score === 'number' ? score : previous?.score ?? 0,
      isReady: previous?.isReady ?? false,
    }
  })

  if (Object.keys(nextPlayers).length > 0) {
    gameStore.players = nextPlayers
  }

  gameStore.roomId = roomId

  const phase =
      readString(payload, 'phase') ||
      readString(payload, 'status') ||
      'waiting'
  const phaseMap: Record<string, typeof gameStore.phase> = {
    waiting: 'waiting',
    dealing: 'dealing',
    ding_que: 'playing',
    playing: 'playing',
    action: 'action',
    settlement: 'settlement',
    finished: 'settlement',
  }
  gameStore.phase = phaseMap[phase] ?? gameStore.phase

  const wallCount = readNumber(payload, 'wall_count', 'wallCount')
  if (typeof wallCount === 'number') {
    gameStore.remainingTiles = wallCount
  }
  gameStore.needDraw = readBoolean(payload, 'need_draw', 'needDraw') ?? false

  const currentTurnSeat = readNumber(payload, 'current_turn', 'currentTurn')
  const currentTurnPlayerId = readString(payload, 'current_turn_player', 'currentTurnPlayer')
  const currentTurn =
      currentTurnSeat ??
      (currentTurnPlayerId && gameStore.players[currentTurnPlayerId]
          ? gameStore.players[currentTurnPlayerId].seatIndex
          : null)
  if (typeof currentTurn === 'number') {
    gameStore.currentTurn = currentTurn
  }

  const scores = asRecord(payload.scores)
  if (scores) {
    gameStore.scores = Object.fromEntries(
        Object.entries(scores).filter(([, value]) => typeof value === 'number'),
    ) as Record<string, number>
  }
  gameStore.winners = readStringArray(payload, 'winners')
  const currentRound = readNumber(payload, 'current_round', 'currentRound')
  const totalRounds = readNumber(payload, 'total_rounds', 'totalRounds')
  if (typeof currentRound === 'number') {
    gameStore.currentRound = currentRound
  }
  if (typeof totalRounds === 'number') {
    gameStore.totalRounds = totalRounds
  }
  const sdMs = readNumber(payload, 'settlement_deadline_ms', 'settlementDeadlineMs')
  if (typeof sdMs === 'number' && sdMs > 0) {
    settlementDeadlineMs.value = sdMs
  } else if (phase !== 'settlement') {
    settlementDeadlineMs.value = null
  }
  gameStore.pendingClaim = normalizePendingClaim(payload)
  if (!gameStore.pendingClaim) {
    claimActionPending.value = false
  }
  clearTurnActionPending()

  const previousRoom = activeRoom.value
  const roomPlayers = Object.values(gameStore.players)
      .sort((left, right) => left.seatIndex - right.seatIndex)
      .map((player) => {
        const previousPlayer = previousRoom?.players.find((item) => item.playerId === player.playerId)
        return {
          index: player.seatIndex,
          playerId: player.playerId,
          displayName: player.displayName,
          missingSuit: player.missingSuit,
          ready: previousPlayer?.ready ?? player.isReady,
          hand: player.playerId === loggedInUserId.value
              ? player.handTiles.map((tile) => tileToText(tile))
              : Array.from({length: player.handCount}, () => ''),
          melds: player.melds.map((meld) => meld.type),
          outTiles: player.discardTiles.map((tile) => tileToText(tile)),
          hasHu: player.hasHu,
        }
      })

  setActiveRoom({
    roomId,
    roomName: previousRoom?.roomName || roomName.value,
    gameType: previousRoom?.gameType || 'chengdu',
    ownerId: previousRoom?.ownerId || '',
    maxPlayers: previousRoom?.maxPlayers ?? 4,
    playerCount: roomPlayers.length,
    status: phase === 'settlement' ? 'finished' : phase === 'waiting' ? 'waiting' : 'playing',
    createdAt: previousRoom?.createdAt || '',
    updatedAt: previousRoom?.updatedAt || '',
    players: roomPlayers,
    myHand: myHandTiles.value.map((tile) => tileToText(tile)),
    game: {
      state: {
        wall: Array.from({length: wallCount ?? 0}, (_, index) => `wall-${index}`),
        scores: gameStore.scores,
        dealerIndex: previousRoom?.game?.state?.dealerIndex ?? gameStore.dealerIndex,
        currentTurn: typeof currentTurn === 'number' ? currentTurn : previousRoom?.game?.state?.currentTurn ?? -1,
        phase,
      },
    },
  })

  if (phase !== 'waiting') {
    startGamePending.value = false
  }
  if (phase !== 'settlement') {
    nextRoundPending.value = false
    settlementOverlayDismissed.value = false
    settlementDeadlineMs.value = null
  }
  if (phase !== 'playing' || currentTurnPlayerId !== loggedInUserId.value) {
    selfTurnAllowActions.value = []
  }
  if (currentTurnPlayerId && currentTurnPlayerId !== loggedInUserId.value) {
    markDiscardCompleted()
  }
}

function handleRoomInfoResponse(message: unknown): void {
  const source = asRecord(message)
  if (!source || typeof source.type !== 'string') {
    return
  }

  const normalizedType = normalizeWsType(source.type)
  if (normalizedType !== 'GET_ROOM_INFO' && normalizedType !== 'ROOM_INFO') {
    return
  }

  const payload = asRecord(source.payload) ?? source
  syncCurrentUserID(readString(source, 'target'))
  const room = asRecord(payload.room)
  const gameState = asRecord(payload.game_state)
  const playerView = asRecord(payload.player_view)

  if (!room && !gameState && !playerView) {
    clearActiveRoom()
    gameStore.resetGame()
    wsClient.close()
    void router.push('/hall')
    return
  }

  const roomId =
      readString(room ?? {}, 'room_id', 'roomId') ||
      readString(gameState ?? {}, 'room_id', 'roomId') ||
      readString(playerView ?? {}, 'room_id', 'roomId') ||
      readString(payload, 'room_id', 'roomId') ||
      readString(source, 'roomId')
  if (!roomId) {
    return
  }

  const roomPlayers = Array.isArray(room?.players) ? room.players : []
  const gamePlayers = Array.isArray(gameState?.players) ? gameState.players : []
  const playerMap = new Map<string, {
    roomPlayer: {
      index: number
      playerId: string
      displayName?: string
      missingSuit?: string | null
      ready: boolean
      trustee: boolean
      hand: string[]
      melds: string[]
      outTiles: string[]
      hasHu: boolean
    }
    gamePlayer: {
      playerId: string
      seatIndex: number
      displayName?: string
      avatarURL?: string
      missingSuit?: string | null
      isReady: boolean
      isTrustee: boolean
      handTiles: Tile[]
      handCount: number
      melds: PlayerState['melds']
      discardTiles: Tile[]
      hasHu: boolean
      score: number
    }
  }>()

  roomPlayers.forEach((item, fallbackIndex) => {
    const player = asRecord(item)
    if (!player) {
      return
    }

    const playerId = readString(player, 'player_id', 'PlayerID', 'id', 'user_id')
    if (!playerId) {
      return
    }

    const seatIndex = readNumber(player, 'index', 'Index', 'seat_index', 'seatIndex') ?? fallbackIndex
    const displayName =
        readString(player, 'player_name', 'PlayerName', 'display_name', 'displayName', 'nickname', 'username') ||
        (playerId === loggedInUserId.value ? loggedInUserName.value : '')
    const ready = readBoolean(player, 'ready', 'Ready') ?? false
    const missingSuit = readMissingSuit(player)

    playerMap.set(playerId, {
      roomPlayer: {
        index: seatIndex,
        playerId,
        displayName: displayName || undefined,
        missingSuit,
        ready,
        trustee: false,
        hand: [],
        melds: [],
        outTiles: [],
        hasHu: false,
      },
      gamePlayer: {
        playerId,
        seatIndex,
        displayName: displayName || undefined,
        avatarURL: readString(player, 'avatar_url', 'AvatarUrl', 'avatar', 'avatarUrl') || undefined,
        missingSuit,
        isReady: ready,
        isTrustee: false,
        handTiles: [],
        handCount: 0,
        melds: [],
        discardTiles: [],
        hasHu: false,
        score: 0,
      },
    })
  })

  gamePlayers.forEach((item, fallbackIndex) => {
    const player = asRecord(item)
    if (!player) {
      return
    }

    const playerId = readString(player, 'player_id', 'PlayerID')
    if (!playerId) {
      return
    }

    const existing = playerMap.get(playerId)
    const seatIndex =
        existing?.gamePlayer.seatIndex ??
        readNumber(player, 'index', 'Index', 'seat_index', 'seatIndex') ??
        fallbackIndex
    const displayName = existing?.gamePlayer.displayName || (playerId === loggedInUserId.value ? loggedInUserName.value : '')
    const missingSuit = readMissingSuitWithPresence(player)
    const handCount = readNumber(player, 'hand_count', 'handCount') ?? 0
    const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles)
    const melds = normalizeMelds(
        player.melds ??
        player.exposed_melds ??
        player.exposedMelds ??
        player.claims,
    )
    const hasHu = Boolean(player.has_hu ?? player.hasHu)

    playerMap.set(playerId, {
      roomPlayer: {
        index: seatIndex,
        playerId,
        displayName: displayName || undefined,
        missingSuit: missingSuit.present ? missingSuit.value : (existing?.gamePlayer.missingSuit ?? null),
        ready: existing?.roomPlayer.ready ?? false,
        trustee: existing?.roomPlayer.trustee ?? false,
        hand: Array.from({length: handCount}, () => ''),
        melds: melds.map((meld) => meld.type),
        outTiles: outTiles.map((tile) => tileToText(tile)),
        hasHu,
      },
      gamePlayer: {
        playerId,
        seatIndex,
        displayName: displayName || undefined,
        avatarURL: existing?.gamePlayer.avatarURL,
        missingSuit: missingSuit.present ? missingSuit.value : (existing?.gamePlayer.missingSuit ?? null),
        isReady: existing?.gamePlayer.isReady ?? false,
        isTrustee: existing?.gamePlayer.isTrustee ?? false,
        handTiles: existing?.gamePlayer.handTiles ?? [],
        handCount,
        melds: melds.length > 0 ? melds : existing?.gamePlayer.melds ?? [],
        discardTiles: outTiles,
        hasHu,
        score: existing?.gamePlayer.score ?? 0,
      },
    })
  })

  const privateHand = normalizeTiles(playerView?.hand)
  if (loggedInUserId.value && playerMap.has(loggedInUserId.value)) {
    const current = playerMap.get(loggedInUserId.value)
    if (current) {
      const selfMissingSuit = readMissingSuitWithPresence(playerView)
      current.roomPlayer.hand = privateHand.map((tile) => tileToText(tile))
      if (selfMissingSuit.present) {
        current.roomPlayer.missingSuit = selfMissingSuit.value
      }
      current.gamePlayer.handTiles = privateHand
      current.gamePlayer.handCount = privateHand.length
      if (selfMissingSuit.present) {
        current.gamePlayer.missingSuit = selfMissingSuit.value
      }
    }
  }

  const players = Array.from(playerMap.values()).sort((a, b) => a.gamePlayer.seatIndex - b.gamePlayer.seatIndex)

  const previousPlayers = gameStore.players
  const nextPlayers: typeof gameStore.players = {}
  players.forEach(({gamePlayer}) => {
    const previous = previousPlayers[gamePlayer.playerId]
    const score = (gameState?.scores && typeof gameState.scores === 'object'
        ? (gameState.scores as Record<string, unknown>)[gamePlayer.playerId]
        : undefined)
    nextPlayers[gamePlayer.playerId] = {
      playerId: gamePlayer.playerId,
      seatIndex: gamePlayer.seatIndex,
      displayName: gamePlayer.displayName ?? previous?.displayName,
      avatarURL: gamePlayer.avatarURL ?? previous?.avatarURL,
      missingSuit: typeof gamePlayer.missingSuit === 'undefined' ? (previous?.missingSuit ?? null) : gamePlayer.missingSuit,
      isTrustee: previous?.isTrustee ?? gamePlayer.isTrustee,
      handTiles: gamePlayer.handTiles.length > 0 ? gamePlayer.handTiles : previous?.handTiles ?? [],
      handCount: gamePlayer.handCount > 0
          ? gamePlayer.handCount
          : gamePlayer.handTiles.length > 0
              ? gamePlayer.handTiles.length
              : (previous?.handCount ?? 0),
      melds: gamePlayer.melds.length > 0 ? gamePlayer.melds : previous?.melds ?? [],
      discardTiles: gamePlayer.discardTiles.length > 0 ? gamePlayer.discardTiles : previous?.discardTiles ?? [],
      hasHu: gamePlayer.hasHu || previous?.hasHu || false,
      score: typeof score === 'number' ? score : previous?.score ?? gamePlayer.score ?? 0,
      isReady: gamePlayer.isReady,
    }
  })

  const status =
      readString(gameState ?? {}, 'status') ||
      readString(room ?? {}, 'status') ||
      readString(gameState ?? {}, 'phase') ||
      'waiting'
  const phase =
      readString(gameState ?? {}, 'phase') ||
      readString(room ?? {}, 'status') ||
      'waiting'
  const wallCount = readNumber(gameState ?? {}, 'wall_count', 'wallCount')
  const dealerIndex = readNumber(gameState ?? {}, 'dealer_index', 'dealerIndex')
  const currentTurnSeat = readNumber(gameState ?? {}, 'current_turn', 'currentTurn')
  const currentTurnPlayerId = readString(gameState ?? {}, 'current_turn_player', 'currentTurnPlayer')
  const currentTurn =
      currentTurnSeat ??
      (currentTurnPlayerId && nextPlayers[currentTurnPlayerId]
          ? nextPlayers[currentTurnPlayerId].seatIndex
          : null)

  gameStore.roomId = roomId
  if (Object.keys(nextPlayers).length > 0) {
    gameStore.players = nextPlayers
    dingQuePending.value = false
  }

  const phaseMap: Record<string, typeof gameStore.phase> = {
    waiting: 'waiting',
    dealing: 'dealing',
    ding_que: 'playing',
    playing: 'playing',
    action: 'action',
    settlement: 'settlement',
    finished: 'settlement',
  }
  gameStore.phase = phaseMap[phase] ?? gameStore.phase
  if (typeof wallCount === 'number') {
    gameStore.remainingTiles = wallCount
  }
  if (typeof dealerIndex === 'number') {
    gameStore.dealerIndex = dealerIndex
  }
  if (typeof currentTurn === 'number') {
    gameStore.currentTurn = currentTurn
  }
  gameStore.needDraw = readBoolean(gameState ?? {}, 'need_draw', 'needDraw') ?? false
  gameStore.pendingClaim = normalizePendingClaim(gameState)
  if (!gameStore.pendingClaim) {
    claimActionPending.value = false
  }
  clearTurnActionPending()
  const scores = asRecord(gameState?.scores)
  if (scores) {
    gameStore.scores = Object.fromEntries(
        Object.entries(scores).filter(([, value]) => typeof value === 'number'),
    ) as Record<string, number>
  }
  gameStore.winners = readStringArray(gameState ?? {}, 'winners')
  const infoCurrentRound = readNumber(gameState ?? {}, 'current_round', 'currentRound')
  const infoTotalRounds = readNumber(gameState ?? {}, 'total_rounds', 'totalRounds')
  if (typeof infoCurrentRound === 'number') {
    gameStore.currentRound = infoCurrentRound
  }
  if (typeof infoTotalRounds === 'number') {
    gameStore.totalRounds = infoTotalRounds
  }
  const infoSdMs = readNumber(gameState ?? {}, 'settlement_deadline_ms', 'settlementDeadlineMs')
  if (typeof infoSdMs === 'number' && infoSdMs > 0) {
    settlementDeadlineMs.value = infoSdMs
  }
  if (gameStore.phase !== 'playing' || currentTurnPlayerId !== loggedInUserId.value) {
    selfTurnAllowActions.value = []
  }

  setActiveRoom({
    roomId,
    roomName: readString(room ?? {}, 'name', 'room_name') || activeRoom.value?.roomName || roomName.value,
    gameType: readString(room ?? {}, 'game_type') || activeRoom.value?.gameType || 'chengdu',
    ownerId: readString(room ?? {}, 'owner_id') || activeRoom.value?.ownerId || '',
    maxPlayers: readNumber(room ?? {}, 'max_players') ?? activeRoom.value?.maxPlayers ?? 4,
    playerCount: readNumber(room ?? {}, 'player_count') ?? players.length,
    status,
    createdAt: readString(room ?? {}, 'created_at') || activeRoom.value?.createdAt || '',
    updatedAt: readString(room ?? {}, 'updated_at') || activeRoom.value?.updatedAt || '',
    players: players.map((item) => item.roomPlayer),
    myHand: privateHand.map((tile) => tileToText(tile)),
    game: {
      state: {
        wall: Array.from({length: wallCount ?? 0}, (_, index) => `wall-${index}`),
        scores: gameStore.scores,
        dealerIndex: typeof dealerIndex === 'number' ? dealerIndex : activeRoom.value?.game?.state?.dealerIndex ?? -1,
        currentTurn: typeof currentTurn === 'number' ? currentTurn : activeRoom.value?.game?.state?.currentTurn ?? -1,
        phase,
      },
    },
  })
}

const networkLabel = computed(() => {
  const map: Record<WsStatus, string> = {
    connected: '已连接',
    connecting: '连接中',
    error: '连接异常',
    idle: '未连接',
    closed: '未连接',
  }

  return map[wsStatus.value] ?? '未连接'
})

const formattedClock = computed(() => {
  return new Date(now.value).toLocaleTimeString('zh-CN', {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  })
})

function buildWallTileImage(
    seat: SeatKey,
    tile: Tile | undefined,
    imageType: TableTileImageType,
): string {
  switch (seat) {
    case 'top':
      return getTopTileImage(tile, imageType, 'top')
    case 'right':
      return getRightTileImage(tile, imageType, 'right')
    case 'left':
      return getLeftTileImage(tile, imageType, 'left')
    case 'bottom':
    default:
      if (!tile) {
        return ''
      }
      return getBottomTileImage(tile, imageType, 'bottom')
  }
}

function emptyWallSeat(): WallSeatState {
  return {
    tiles: [],
  }
}

function emptyDeskSeat(): DeskSeatState {
  return {
    tiles: [],
    hasHu: false,
  }
}

const wallSeats = computed<Record<SeatKey, WallSeatState>>(() => {
  const emptyState: Record<SeatKey, WallSeatState> = {
    top: emptyWallSeat(),
    right: emptyWallSeat(),
    bottom: emptyWallSeat(),
    left: emptyWallSeat(),
  }

  if (gameStore.phase === 'waiting' && myHandTiles.value.length === 0) {
    return emptyState
  }

  for (const seat of seatViews.value) {
    if (!seat.player) {
      continue
    }

    const seatTiles: WallTileItem[] = []
    const targetSeat = seat.key

    if (seat.isSelf) {
      const missingSuit = seat.player.missingSuit as Tile['suit'] | null | undefined
      sortedVisibleHandTiles.value.forEach((tile, index) => {
        const src = buildWallTileImage(targetSeat, tile, 'hand')
        if (!src) {
          return
        }
        const previousTile = index > 0 ? sortedVisibleHandTiles.value[index - 1] : undefined
        const isMissingSuitGroupStart = Boolean(
            missingSuit &&
            tile.suit === missingSuit &&
            (!previousTile || previousTile.suit !== tile.suit),
        )

        seatTiles.push({
          key: `hand-${tile.id}-${index}`,
          src,
          alt: formatTile(tile),
          imageType: 'hand',
          showLackTag: isMissingSuitGroupStart,
          suit: tile.suit,
          tile,
        })
      })
    } else {
      for (let index = 0; index < seat.player.handCount; index += 1) {
        const src = buildWallTileImage(targetSeat, undefined, 'hand')
        if (!src) {
          continue
        }

        seatTiles.push({
          key: `concealed-${index}`,
          src,
          alt: '手牌背面',
          imageType: 'hand',
        })
      }
    }

    emptyState[targetSeat] = {
      tiles: seatTiles,
    }
  }

  return emptyState
})

const deskSeats = computed<Record<SeatKey, DeskSeatState>>(() => {
  const emptyState: Record<SeatKey, DeskSeatState> = {
    top: emptyDeskSeat(),
    right: emptyDeskSeat(),
    bottom: emptyDeskSeat(),
    left: emptyDeskSeat(),
  }

  if (gameStore.phase === 'waiting' && myHandTiles.value.length === 0) {
    return emptyState
  }

  for (const seat of seatViews.value) {
    if (!seat.player) {
      continue
    }

    const seatTiles: WallTileItem[] = []
    const targetSeat = seat.key

    seat.player.discardTiles.forEach((tile, index) => {
      const src = buildWallTileImage(targetSeat, tile, 'exposed')
      if (!src) {
        return
      }

      seatTiles.push({
        key: `discard-${tile.id}-${index}`,
        src,
        alt: formatTile(tile),
        imageType: 'exposed',
        suit: tile.suit,
      })
    })

    seat.player.melds.forEach((meld, meldIndex) => {
      meld.tiles.forEach((tile, tileIndex) => {
        const imageType: TableTileImageType = meld.type === 'an_gang' ? 'covered' : 'exposed'
        const src = buildWallTileImage(targetSeat, tile, imageType)
        if (!src) {
          return
        }

        seatTiles.push({
          key: `desk-${meld.type}-${meldIndex}-${tile.id}-${tileIndex}`,
          src,
          alt: formatTile(tile),
          imageType,
          isGroupStart: tileIndex === 0,
          suit: tile.suit,
        })
      })
    })

    emptyState[targetSeat] = {
      tiles: seatTiles,
      hasHu: seat.player.hasHu,
    }
  }

  return emptyState
})

const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
  const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1
  const defaultMissingSuitLabel = missingSuitLabel(null)

  const emptySeat = (): SeatPlayerCardModel => ({
    avatarUrl: '',
    name: '空位',
    dealer: false,
    isTurn: false,
    isReady: false,
    isTrustee: false,
    missingSuitLabel: defaultMissingSuitLabel,
  })

  const result: Record<SeatKey, SeatPlayerCardModel> = {
    top: emptySeat(),
    right: emptySeat(),
    bottom: emptySeat(),
    left: emptySeat(),
  }

  for (const seat of seatViews.value) {
    if (!seat.player) {
      continue
    }

    const displayName = seat.player.displayName || `玩家${seat.player.seatIndex + 1}`
    const avatarUrl = seat.isSelf
        ? (localCachedAvatarUrl.value || seat.player.avatarURL || '')
        : (seat.player.avatarURL || '')
    const selfDisplayName = seat.player.displayName || loggedInUserName.value || '你自己'

    result[seat.key] = {
      avatarUrl,
      name: Array.from(seat.isSelf ? selfDisplayName : displayName).slice(0, 4).join(''),
      dealer: seat.player.seatIndex === dealerIndex,
      isTurn: seat.isTurn,
      isReady: Boolean(seat.player.isReady),
      isTrustee: Boolean(seat.player.isTrustee),
      missingSuitLabel: missingSuitLabel(seat.player.missingSuit),
    }
  }

  return result
})

function missingSuitLabel(value: string | null | undefined): string {
  const suitMap: Record<string, string> = {
    w: '万',
    t: '筒',
    b: '条',
    wan: '万',
    tong: '筒',
    tiao: '条',
  }

  if (!value) {
    return ''
  }

  const normalized = value.trim().toLowerCase()
  return suitMap[normalized] ?? value
}

function toggleMenu(): void {
  menuTriggerActive.value = true
  if (menuTriggerTimer !== null) {
    window.clearTimeout(menuTriggerTimer)
  }
  menuTriggerTimer = window.setTimeout(() => {
    menuTriggerActive.value = false
    menuTriggerTimer = null
  }, 180)

  if (menuOpen.value) {
    menuOpen.value = false
    return
  }

  if (menuOpenTimer !== null) {
    window.clearTimeout(menuOpenTimer)
  }
  menuOpenTimer = window.setTimeout(() => {
    menuOpen.value = true
    menuOpenTimer = null
  }, 85)
}

function toggleTrustMode(): void {
  isTrustMode.value = !isTrustMode.value
  menuOpen.value = false
}

function formatTile(tile: Tile): string {
  return `${tile.suit}${tile.value}`
}

function handlePlayerHandResponse(message: unknown): void {
  const source = asRecord(message)
  if (!source || typeof source.type !== 'string') {
    return
  }

  if (normalizeWsType(source.type) !== 'PLAYER_HAND') {
    return
  }

  const payload = asRecord(source.payload)
  if (!payload) {
    return
  }

  syncCurrentUserID(readString(source, 'target'))

  const roomId =
      readString(payload, 'room_id', 'roomId') ||
      readString(source, 'roomId')
  if (roomId && gameStore.roomId && roomId !== gameStore.roomId) {
    return
  }

  const handTiles = normalizeTiles(payload.hand)
  if (!loggedInUserId.value || handTiles.length === 0) {
    return
  }

  clearTurnActionPending()
  const existingPlayer = gameStore.players[loggedInUserId.value]
  if (existingPlayer) {
    existingPlayer.handTiles = handTiles
    existingPlayer.handCount = handTiles.length
  }
  dingQuePending.value = false

  const room = activeRoom.value
  if (room && room.roomId === (roomId || gameStore.roomId)) {
    room.myHand = handTiles.map((tile) => tileToText(tile))
    const roomPlayer = room.players.find((item) => item.playerId === loggedInUserId.value)
    if (roomPlayer) {
      roomPlayer.hand = room.myHand
    }
  }

  markDiscardCompleted()
  if (gameStore.phase !== 'waiting') {
    startGamePending.value = false
  }
}

function handleDingQueCountdown(message: unknown): void {
  const source = asRecord(message)
  if (!source || typeof source.type !== 'string') {
    return
  }

  if (normalizeWsType(source.type) !== 'DING_QUE_COUNTDOWN') {
    return
  }

  const payload = asRecord(source.payload) ?? source
  const roomId =
      readString(payload, 'room_id', 'roomId') ||
      readString(source, 'roomId')
  if (roomId && gameStore.roomId && roomId !== gameStore.roomId) {
    return
  }

  const playerIds = readStringArray(payload, 'player_ids', 'playerIds', 'PlayerIDs')
  const fallbackPlayerId = readString(payload, 'player_id', 'playerId', 'PlayerID')
  const normalizedPlayerIds = playerIds.length > 0 ? playerIds : (fallbackPlayerId ? [fallbackPlayerId] : [])
  if (normalizedPlayerIds.length === 0) {
    roomCountdown.value = null
    return
  }

  const countdownSeconds = readNumber(payload, 'countdown_seconds', 'CountdownSeconds') ?? 0
  const duration = readNumber(payload, 'duration', 'Duration') ?? countdownSeconds
  const remaining = readNumber(payload, 'remaining', 'Remaining') ?? countdownSeconds
  const actionDeadlineAt = readString(payload, 'action_deadline_at', 'ActionDeadlineAt') || null

  if (countdownSeconds <= 0 && remaining <= 0 && !actionDeadlineAt) {
    roomCountdown.value = null
    return
  }

  roomCountdown.value = {
    playerIds: normalizedPlayerIds,
    actionDeadlineAt,
    countdownSeconds,
    duration,
    remaining,
  }
}

function applyPlayerTurnCountdown(payload: PlayerTurnPayload): void {
  const playerId = readPlayerTurnPlayerId(payload)
  const timeout =
      (typeof payload.timeout === 'number' && Number.isFinite(payload.timeout) ? payload.timeout : null) ??
      (typeof payload.Timeout === 'number' && Number.isFinite(payload.Timeout) ? payload.Timeout : null) ??
      0
  const startAtRaw =
      (typeof payload.start_at === 'number' && Number.isFinite(payload.start_at) ? payload.start_at : null) ??
      (typeof payload.startAt === 'number' && Number.isFinite(payload.startAt) ? payload.startAt : null) ??
      (typeof payload.StartAt === 'number' && Number.isFinite(payload.StartAt) ? payload.StartAt : null)

  if (!playerId || timeout <= 0) {
    roomCountdown.value = null
    return
  }

  const startAtMs = normalizeTimestampMs(startAtRaw)
  const deadlineAtMs = startAtMs !== null ? startAtMs + timeout * 1000 : null
  const remaining = deadlineAtMs !== null
      ? Math.max(0, Math.ceil((deadlineAtMs - now.value) / 1000))
      : timeout

  roomCountdown.value = {
    playerIds: [playerId],
    actionDeadlineAt: deadlineAtMs !== null ? new Date(deadlineAtMs).toISOString() : null,
    countdownSeconds: timeout,
    duration: timeout,
    remaining,
  }
}

function resetRoundStateForNextTurn(payload: Record<string, unknown>): void {
  const nextRound = readNumber(payload, 'current_round', 'currentRound')
  const totalRounds = readNumber(payload, 'total_rounds', 'totalRounds')
  if (typeof nextRound !== 'number' && typeof totalRounds !== 'number') {
    return
  }

  if (typeof nextRound === 'number') {
    gameStore.currentRound = nextRound
  }
  if (typeof totalRounds === 'number') {
    gameStore.totalRounds = totalRounds
  }

  nextRoundPending.value = false
  settlementOverlayDismissed.value = false
  settlementDeadlineMs.value = null
  dingQuePending.value = false
  roomCountdown.value = null
  claimActionPending.value = false
  selfTurnAllowActions.value = []
  gameStore.pendingClaim = undefined
  gameStore.winners = []

  markDiscardCompleted()
  clearTurnActionPending()

  Object.values(gameStore.players).forEach((player) => {
    player.missingSuit = null
    player.hasHu = false
  })

  const room = activeRoom.value
  if (room && room.roomId === gameStore.roomId) {
    room.players.forEach((player) => {
      player.missingSuit = null
      player.hasHu = false
    })
  }
}

function handlePlayerTurn(message: unknown): void {
  const source = asRecord(message)
  if (!source || typeof source.type !== 'string') {
    return
  }

  const normalizedType = normalizeWsType(source.type)
  if (normalizedType !== 'PLAYER_TURN' && normalizedType !== 'NEXT_TURN') {
    return
  }

  const payload = asRecord(source.payload) ?? source
  const roomId =
      readString(payload, 'room_id', 'roomId') ||
      readString(source, 'roomId')
  if (roomId && gameStore.roomId && roomId !== gameStore.roomId) {
    return
  }

  resetRoundStateForNextTurn(payload)
  const turnPayload = payload as PlayerTurnPayload
  const turnPlayerID = readPlayerTurnPlayerId(turnPayload)
  if (turnPlayerID && turnPlayerID === loggedInUserId.value) {
    selfTurnAllowActions.value = readPlayerTurnAllowActions(turnPayload)
  } else {
    selfTurnAllowActions.value = []
  }
  if (normalizedType === 'PLAYER_TURN') {
    applyPlayerTurnCountdown(turnPayload)
  }
}

function handleActionError(message: unknown): void {
  const source = asRecord(message)
  if (!source || typeof source.type !== 'string') {
    return
  }

  if (normalizeWsType(source.type) !== 'ACTION_ERROR') {
    return
  }

  const payload = asRecord(source.payload)
  const roomId =
      readString(payload ?? {}, 'room_id', 'roomId') ||
      readString(source, 'roomId')
  if (roomId && gameStore.roomId && roomId !== gameStore.roomId) {
    return
  }

  const action = readString(payload ?? {}, 'action') || 'unknown'
  const messageText = readString(payload ?? {}, 'message') || '操作失败'
  claimActionPending.value = false
  clearTurnActionPending()
  wsError.value = messageText
  wsMessages.value.push(`[action-error] ${action}: ${messageText}`)
}

function toGameAction(message: unknown): GameAction | null {
  if (!message || typeof message !== 'object') {
    return null
  }

  const source = message as Record<string, unknown>
  if (typeof source.type !== 'string') {
    return null
  }

  const type = normalizeWsType(source.type)
  const payload = source.payload

  switch (type) {
    case 'GAME_INIT':
      if (payload && typeof payload === 'object') {
        return {type: 'GAME_INIT', payload: payload as GameActionPayload<'GAME_INIT'>}
      }
      return null
    case 'GAME_START':
      if (payload && typeof payload === 'object') {
        return {type: 'GAME_START', payload: payload as GameActionPayload<'GAME_START'>}
      }
      return null
    case 'DRAW_TILE':
      if (payload && typeof payload === 'object') {
        return {type: 'DRAW_TILE', payload: payload as GameActionPayload<'DRAW_TILE'>}
      }
      return null
    case 'PLAY_TILE':
      if (payload && typeof payload === 'object') {
        return {type: 'PLAY_TILE', payload: payload as GameActionPayload<'PLAY_TILE'>}
      }
      return null
    case 'PENDING_CLAIM':
      if (payload && typeof payload === 'object') {
        return {type: 'PENDING_CLAIM', payload: payload as GameActionPayload<'PENDING_CLAIM'>}
      }
      return null
    case 'CLAIM_RESOLVED':
      if (payload && typeof payload === 'object') {
        return {type: 'CLAIM_RESOLVED', payload: payload as GameActionPayload<'CLAIM_RESOLVED'>}
      }
      return null
    case 'PENG':
    case 'GANG':
    case 'HU':
    case 'PASS': {
      const resolvedPayload = asRecord(payload)
      const playerId =
          readString(resolvedPayload ?? {}, 'player_id', 'playerId', 'PlayerID') ||
          readString(source, 'target')
      const action = type.toLowerCase()
      if (!playerId || (action !== 'peng' && action !== 'gang' && action !== 'hu' && action !== 'pass')) {
        return null
      }
      return {
        type: 'CLAIM_RESOLVED',
        payload: {
          playerId,
          action,
        },
      }
    }
    case 'ROOM_PLAYER_UPDATE':
      if (payload && typeof payload === 'object') {
        return {type: 'ROOM_PLAYER_UPDATE', payload: payload as GameActionPayload<'ROOM_PLAYER_UPDATE'>}
      }
      return null
    case 'ROOM_MEMBER_JOINED':
      if (payload && typeof payload === 'object') {
        return {type: 'ROOM_PLAYER_UPDATE', payload: payload as GameActionPayload<'ROOM_PLAYER_UPDATE'>}
      }
      return null
    case 'ROOM_TRUSTEE':
      if (payload && typeof payload === 'object') {
        return {type: 'ROOM_TRUSTEE', payload: payload as GameActionPayload<'ROOM_TRUSTEE'>}
      }
      return {
        type: 'ROOM_TRUSTEE',
        payload: source as unknown as GameActionPayload<'ROOM_TRUSTEE'>,
      }
    case 'PLAYER_TURN':
    case 'NEXT_TURN':
      if (payload && typeof payload === 'object') {
        return {type: 'PLAYER_TURN', payload: payload as GameActionPayload<'PLAYER_TURN'>}
      }
      return {
        type: 'PLAYER_TURN',
        payload: source as unknown as GameActionPayload<'PLAYER_TURN'>,
      }
    default:
      return null
  }
}

function syncTrusteeState(payload: RoomTrusteePayload): void {
  const playerId =
      (typeof payload.player_id === 'string' && payload.player_id) ||
      (typeof payload.playerId === 'string' && payload.playerId) ||
      ''
  if (!playerId) {
    return
  }

  const trustee = typeof payload.trustee === 'boolean' ? payload.trustee : true
  if (playerId === loggedInUserId.value) {
    isTrustMode.value = trustee
  }

  const room = activeRoom.value
  if (!room || room.roomId !== gameStore.roomId) {
    return
  }

  const roomPlayer = room.players.find((item) => item.playerId === playerId)
  if (roomPlayer) {
    roomPlayer.trustee = trustee
  }
}

function handleReadyStateResponse(message: unknown): void {
  if (!message || typeof message !== 'object') {
    return
  }

  const source = message as Record<string, unknown>
  if (typeof source.type !== 'string') {
    return
  }

  const type = normalizeWsType(source.type)
  if (type !== 'SET_READY') {
    return
  }

  const payload = source.payload
  if (!payload || typeof payload !== 'object') {
    return
  }

  const readyPayload = payload as Record<string, unknown>
  const roomId =
      typeof readyPayload.room_id === 'string'
          ? readyPayload.room_id
          : typeof source.roomId === 'string'
              ? source.roomId
              : ''
  const userId =
      typeof readyPayload.user_id === 'string'
          ? readyPayload.user_id
          : typeof source.target === 'string'
              ? source.target
              : ''
  const ready = readBoolean(readyPayload, 'ready', 'Ready')

  if (roomId && roomId !== gameStore.roomId) {
    return
  }

  if (ready !== null && userId) {
    applyPlayerReadyState(userId, ready)
  }

  if (userId && userId === loggedInUserId.value) {
    readyTogglePending.value = false
  }
}

function handlePlayerDingQueResponse(message: unknown): void {
  if (!message || typeof message !== 'object') {
    return
  }

  const source = message as Record<string, unknown>
  if (typeof source.type !== 'string') {
    return
  }

  if (normalizeWsType(source.type) !== 'PLAYER_DING_QUE') {
    return
  }

  const payload = asRecord(source.payload)
  if (!payload) {
    return
  }

  const roomId =
      readString(payload, 'room_id', 'roomId') ||
      readString(source, 'roomId')
  if (roomId && roomId !== gameStore.roomId) {
    return
  }

  const userId =
      readString(payload, 'user_id', 'userId', 'player_id', 'playerId') ||
      readString(source, 'target')
  const suit = readString(payload, 'suit', 'Suit')

  if (!userId || !suit) {
    return
  }

  const player = gameStore.players[userId]
  if (player) {
    player.missingSuit = suit
  }

  const room = activeRoom.value
  if (room && room.roomId === (roomId || gameStore.roomId)) {
    const roomPlayer = room.players.find((item) => item.playerId === userId)
    if (roomPlayer) {
      roomPlayer.missingSuit = suit
    }
  }

  if (userId === loggedInUserId.value) {
    dingQuePending.value = false
  }
}

function logoutToLogin(): void {
  clearAuth()
  auth.value = null
  wsClient.close()
  void router.replace('/login')
}

function currentSession(): AuthSession | null {
  const current = auth.value
  if (!current?.token) {
    return null
  }

  return {
    token: current.token,
    tokenType: current.tokenType,
    refreshToken: current.refreshToken,
    expiresIn: current.expiresIn,
  }
}

function syncAuthSession(next: AuthSession): void {
  if (!auth.value) {
    return
  }

  auth.value = {
    ...auth.value,
    token: next.token,
    tokenType: next.tokenType ?? auth.value.tokenType,
    refreshToken: next.refreshToken ?? auth.value.refreshToken,
    expiresIn: next.expiresIn,
  }
  writeStoredAuth(auth.value)
}

function syncCurrentUserID(userID: string): void {
  if (!userID || loggedInUserId.value) {
    return
  }

  const currentAuth = auth.value
  if (!currentAuth) {
    return
  }

  auth.value = {
    ...currentAuth,
    user: {
      ...(currentAuth.user ?? {}),
      id: userID,
    },
  }
  writeStoredAuth(auth.value)
}

async function ensureCurrentUserLoaded(): Promise<void> {
  if (loggedInUserId.value) {
    return
  }

  const currentAuth = auth.value
  const session = currentSession()
  if (!session) {
    return
  }

  try {
    const userInfo = await getUserInfo(session, syncAuthSession)
    const resolvedId = userInfo.userID ?? userInfo.user_id ?? userInfo.id ?? currentAuth?.user?.id
    const nextUser = {
      ...(currentAuth?.user ?? {}),
      ...userInfo,
      id:
          typeof resolvedId === 'string' || typeof resolvedId === 'number'
              ? resolvedId
              : undefined,
    }

    if (!currentAuth) {
      return
    }

    auth.value = {
      ...currentAuth,
      user: nextUser,
    }
    writeStoredAuth(auth.value)
  } catch (error) {
    if (error instanceof AuthExpiredError) {
      logoutToLogin()
    }
  }
}

function decodeJwtExpMs(token: string): number | null {
  const parts = token.split('.')
  const payloadPart = parts[1]
  if (!payloadPart) {
    return null
  }

  try {
    const normalized = payloadPart.replace(/-/g, '+').replace(/_/g, '/')
    const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4)
    const payload = JSON.parse(window.atob(padded)) as { exp?: number }
    return typeof payload.exp === 'number' ? payload.exp * 1000 : null
  } catch {
    return null
  }
}

function shouldRefreshWsToken(token: string): boolean {
  const expMs = decodeJwtExpMs(token)
  if (!expMs) {
    return false
  }

  return expMs <= Date.now() + 30_000
}

async function resolveWsToken(forceRefresh = false, logoutOnRefreshFail = false): Promise<string | null> {
  const current = auth.value
  if (!current?.token) {
    return null
  }

  if (!forceRefresh && !shouldRefreshWsToken(current.token)) {
    return current.token
  }

  if (!current.refreshToken || refreshingWsToken) {
    return current.token
  }

  refreshingWsToken = true
  try {
    const refreshed = await refreshAccessToken({
      token: current.token,
      tokenType: current.tokenType,
      refreshToken: current.refreshToken,
    })

    const nextAuth = {
      ...current,
      token: refreshed.token,
      tokenType: refreshed.tokenType ?? current.tokenType,
      refreshToken: refreshed.refreshToken ?? current.refreshToken,
      expiresIn: refreshed.expiresIn,
    }
    auth.value = nextAuth
    writeStoredAuth(nextAuth)
    return nextAuth.token
  } catch {
    if (logoutOnRefreshFail) {
      logoutToLogin()
    }
    return null
  } finally {
    refreshingWsToken = false
  }
}

async function ensureWsConnected(forceRefresh = false): Promise<void> {
  const token = await resolveWsToken(forceRefresh, false)
  if (!token) {
    wsError.value = '未找到登录凭证，无法建立连接'
    return
  }

  wsError.value = ''
  wsClient.connect(buildWsUrl(), token)
}

function backHall(): void {
  leaveRoomPending.value = true
  const roomId = gameStore.roomId
  sendWsMessage({
    type: 'leave_room',
    roomId,
    payload: {
      room_id: roomId,
    },
  })
  wsClient.close()
  void router.push('/hall').finally(() => {
    leaveRoomPending.value = false
  })
}

function toggleReadyState(): void {
  if (readyTogglePending.value) {
    return
  }

  const nextReady = !myReadyState.value
  readyTogglePending.value = true
  console.log('[ready-toggle]', {
    loggedInUserId: loggedInUserId.value,
    myReadyState: myReadyState.value,
    nextReady,
  })
  sendWsMessage({
    type: 'set_ready',
    roomId: gameStore.roomId,
    payload: {
      ready: nextReady,
      isReady: nextReady,
    },
  })
}

function startGame(): void {
  if (!canStartGame.value) {
    return
  }

  startGamePending.value = true
  sendWsMessage({
    type: 'start_game',
    roomId: gameStore.roomId,
    payload: {
      room_id: gameStore.roomId,
    },
  })
}

function nextRound(): void {
  if (nextRoundPending.value || !gameStore.roomId || gameStore.phase !== 'settlement') {
    return
  }

  settlementOverlayDismissed.value = true
  nextRoundPending.value = true
  sendWsMessage({
    type: 'next_round',
    roomId: gameStore.roomId,
    payload: {},
  })
}

function chooseDingQue(suit: Tile['suit']): void {
  if (dingQuePending.value || !showDingQueChooser.value) {
    return
  }

  dingQuePending.value = true
  sendWsMessage({
    type: 'ding_que',
    roomId: gameStore.roomId,
    payload: {
      suit,
    },
  })
}

function clearDiscardPendingTimer(): void {
  if (discardPendingTimer !== null) {
    window.clearTimeout(discardPendingTimer)
    discardPendingTimer = null
  }
}

function markDiscardCompleted(): void {
  clearDiscardPendingTimer()
  discardPending.value = false
  selectedDiscardTileId.value = null
}

function markDiscardPendingWithFallback(): void {
  clearDiscardPendingTimer()
  discardPending.value = true
  discardPendingTimer = window.setTimeout(() => {
    discardPending.value = false
    selectedDiscardTileId.value = null
    discardPendingTimer = null
  }, 2000)
}

function selectDiscardTile(tile: Tile): void {
  const blockedReason = discardTileBlockedReason(tile)
  if (blockedReason) {
    wsError.value = blockedReason
    wsMessages.value.push(`[client-blocked] select ${formatTile(tile)}: ${blockedReason}`)
    selectedDiscardTileId.value = null
    return
  }

  wsError.value = ''
  selectedDiscardTileId.value = selectedDiscardTileId.value === tile.id ? null : tile.id
}

function confirmDiscard(): void {
  const tile = selectedDiscardTile.value
  if (!tile) {
    return
  }

  const blockedReason = discardTileBlockedReason(tile)
  if (blockedReason || !gameStore.roomId) {
    if (blockedReason) {
      wsError.value = blockedReason
      wsMessages.value.push(`[client-blocked] discard ${formatTile(tile)}: ${blockedReason}`)
    }
    return
  }

  wsError.value = ''
  markDiscardPendingWithFallback()
  sendWsMessage({
    type: 'discard',
    roomId: gameStore.roomId,
    payload: {
      tile: {
        id: tile.id,
        suit: tile.suit,
        value: tile.value,
      },
    },
  })
}

function drawTile(): void {
  if (!canDrawTile.value) {
    return
  }

  sendWsMessage({
    type: 'draw',
    roomId: gameStore.roomId,
    payload: {
      room_id: gameStore.roomId,
    },
  })
}

function submitSelfGang(): void {
  if (!gameStore.roomId || !canSelfGang.value || turnActionPending.value) {
    return
  }

  markTurnActionPending('gang')
  sendWsMessage({
    type: 'gang',
    roomId: gameStore.roomId,
    payload: {
      room_id: gameStore.roomId,
    },
  })
}

function submitSelfHu(): void {
  if (!gameStore.roomId || !canSelfHu.value || turnActionPending.value) {
    return
  }

  markTurnActionPending('hu')
  sendWsMessage({
    type: 'hu',
    roomId: gameStore.roomId,
    payload: {
      room_id: gameStore.roomId,
    },
  })
}

function submitClaim(action: ClaimOptionState): void {
  if (claimActionPending.value || !gameStore.roomId || !visibleClaimOptions.value.includes(action)) {
    return
  }

  const claimTile = gameStore.pendingClaim?.tile
  claimActionPending.value = true
  sendWsMessage({
    type: action,
    roomId: gameStore.roomId,
    payload: {
      room_id: gameStore.roomId,
      ...(action !== 'pass' && claimTile
          ? {
            tile: {
              id: claimTile.id,
              suit: claimTile.suit,
              value: claimTile.value,
            },
          }
          : {}),
    },
  })
}

function handleLeaveRoom(): void {
  menuOpen.value = false
  backHall()
}

function handleGlobalClick(event: MouseEvent): void {
  const target = event.target as HTMLElement | null
  if (!target) {
    return
  }

  if (target.closest('.menu-trigger-wrap')) {
    return
  }

  menuOpen.value = false
}

function handleGlobalEsc(event: KeyboardEvent): void {
  if (event.key === 'Escape') {
    menuOpen.value = false
  }
}

function hydrateFromActiveRoom(routeRoomId: string): void {
  const room = activeRoom.value
  if (!room) {
    return
  }

  const targetRoomId = routeRoomId || room.roomId
  if (!targetRoomId || room.roomId !== targetRoomId) {
    return
  }

  gameStore.roomId = room.roomId

  const phaseMap: Record<string, typeof gameStore.phase> = {
    waiting: 'waiting',
    playing: 'playing',
    finished: 'settlement',
  }
  gameStore.phase = phaseMap[room.status] ?? gameStore.phase

  const nextPlayers: Record<string, PlayerState> = {}
  for (const player of room.players) {
    if (!player.playerId) {
      continue
    }

    const previous = gameStore.players[player.playerId]
    nextPlayers[player.playerId] = {
      playerId: player.playerId,
      seatIndex: player.index,
      displayName: player.displayName || player.playerId,
      avatarURL: previous?.avatarURL,
      missingSuit: typeof player.missingSuit === 'undefined' ? (previous?.missingSuit ?? null) : player.missingSuit,
      isTrustee: player.trustee ?? previous?.isTrustee ?? false,
      isReady: player.ready,
      handTiles: previous?.handTiles ?? [],
      handCount: previous?.handCount ?? 0,
      melds: previous?.melds ?? [],
      discardTiles: previous?.discardTiles ?? [],
      hasHu: previous?.hasHu ?? false,
      score: previous?.score ?? 0,
    }
  }
  gameStore.players = nextPlayers
}


onMounted(() => {
  const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
  needsInitialRoomInfo = true
  void ensureCurrentUserLoaded().finally(() => {
    hydrateFromActiveRoom(routeRoomId)
    if (routeRoomId) {
      gameStore.roomId = routeRoomId
    }
    if (wsStatus.value === 'connected' && needsInitialRoomInfo) {
      requestRoomInfo()
    }
  })

  const handler = (status: WsStatus) => {
    wsStatus.value = status
    if (status === 'connected' && needsInitialRoomInfo) {
      requestRoomInfo()
    }
  }

  wsClient.onMessage((msg: unknown) => {
    const text = typeof msg === 'string' ? msg : JSON.stringify(msg)
    wsMessages.value.push(`[server] ${text}`)
    handleRoomInfoResponse(msg)
    handleRoomStateResponse(msg)
    handlePlayerHandResponse(msg)
    handlePlayerTurn(msg)
    handleActionError(msg)
    handleDingQueCountdown(msg)
    handleReadyStateResponse(msg)
    handlePlayerDingQueResponse(msg)
    const gameAction = toGameAction(msg)
    if (gameAction) {
      dispatchGameAction(gameAction)
      if (gameAction.type === 'GAME_START') {
        startGamePending.value = false
        roomCountdown.value = null
      }
      if (gameAction.type === 'PLAY_TILE' && gameAction.payload.playerId === loggedInUserId.value) {
        markDiscardCompleted()
      }
      if (gameAction.type === 'PLAY_TILE' || gameAction.type === 'PENDING_CLAIM' || gameAction.type === 'CLAIM_RESOLVED') {
        roomCountdown.value = null
      }
      if (gameAction.type === 'ROOM_PLAYER_UPDATE') {
        syncReadyStatesFromRoomUpdate(gameAction.payload)
        readyTogglePending.value = false
      }
      if (gameAction.type === 'CLAIM_RESOLVED') {
        claimActionPending.value = false
        clearTurnActionPending()
      }
      if (gameAction.type === 'ROOM_TRUSTEE') {
        syncTrusteeState(gameAction.payload)
      }
      if (gameAction.type === 'PLAYER_TURN' && readPlayerTurnPlayerId(gameAction.payload) !== loggedInUserId.value) {
        selectedDiscardTileId.value = null
      }
    }
  })
  wsClient.onError((message: string) => {
    markDiscardCompleted()
    clearTurnActionPending()
    wsError.value = message
    wsMessages.value.push(`[error] ${message}`)

    // WebSocket 握手失败时浏览器拿不到 401 状态码，统一按需强制刷新 token 后重连一次
    const nowMs = Date.now()
    if (nowMs - lastForcedRefreshAt > 5000) {
      lastForcedRefreshAt = nowMs
      void resolveWsToken(true, true).then((refreshedToken) => {
        if (!refreshedToken) {
          return
        }
        wsError.value = ''
        wsClient.reconnect(buildWsUrl(), refreshedToken)
      }).catch(() => {
        logoutToLogin()
      })
    }
  })

  unsubscribe = wsClient.onStatusChange(handler)
  void ensureWsConnected()

  clockTimer = window.setInterval(() => {
    now.value = Date.now()
  }, 1000)

  window.addEventListener('click', handleGlobalClick)
  window.addEventListener('keydown', handleGlobalEsc)
})

onBeforeUnmount(() => {
  if (unsubscribe) {
    unsubscribe()
    unsubscribe = null
  }

  if (clockTimer !== null) {
    window.clearInterval(clockTimer)
    clockTimer = null
  }
  clearDiscardPendingTimer()
  clearTurnActionPending()

  window.removeEventListener('click', handleGlobalClick)
  window.removeEventListener('keydown', handleGlobalEsc)

  if (menuTriggerTimer !== null) {
    window.clearTimeout(menuTriggerTimer)
    menuTriggerTimer = null
  }

  if (menuOpenTimer !== null) {
    window.clearTimeout(menuOpenTimer)
    menuOpenTimer = null
  }
})
</script>

<template>
  <section class="picture-scene">
    <div class="picture-layout">
      <section class="table-stage">
        <img class="table-desk" :src="deskImage" alt=""/>

        <div class="table-felt">
          <div class="table-surface"></div>
          <div class="inner-outline outer"></div>
          <div class="inner-outline mid"></div>

          <div class="top-left-tools">
            <div class="menu-trigger-wrap">
              <button
                  class="metal-circle menu-trigger"
                  :class="{ 'is-feedback': menuTriggerActive }"
                  type="button"
                  :disabled="leaveRoomPending"
                  @click.stop="toggleMenu"
              >
                <span class="menu-trigger-icon">☰</span>
              </button>
              <transition name="menu-pop">
                <div v-if="menuOpen" class="menu-popover" @click.stop>
                  <div class="menu-list">
                    <button class="menu-item menu-item-delay-1" type="button" @click="toggleTrustMode">
                      <span class="menu-item-icon">
                        <img :src="robotIcon" alt=""/>
                      </span>
                      <span>{{ isTrustMode ? '取消托管' : '托管' }}</span>
                    </button>
                    <button
                        class="menu-item menu-item-danger menu-item-delay-2"
                        type="button"
                        :disabled="leaveRoomPending"
                        @click="handleLeaveRoom"
                    >
                      <span class="menu-item-icon">
                        <img :src="exitIcon" alt=""/>
                      </span>
                      <span>{{ leaveRoomPending ? '退出中...' : '退出' }}</span>
                    </button>
                  </div>
                </div>
              </transition>
            </div>
            <div class="left-counter">
              <span class="counter-light"></span>
              <strong>{{ roomState.game?.state?.wall?.length ?? 48 }}</strong>
            </div>
            <span v-if="isTrustMode" class="trust-chip">托管中</span>
          </div>

          <div class="top-right-clock">
            <div class="signal-chip">
              <span class="wifi-dot" :class="`is-${wsStatus}`"></span>
              <strong>{{ networkLabel }}</strong>
            </div>
            <span>{{ formattedClock }}</span>
          </div>

          <div class="room-status-panel">
            <div class="room-status-grid">
              <div class="room-status-item">
                <span>房间</span>
                <strong>{{ roomState.name || roomName || '未命名' }}</strong>
              </div>
              <div class="room-status-item">
                <span>阶段</span>
                <strong>{{ currentPhaseText }}</strong>
              </div>
              <div class="room-status-item">
                <span>人数</span>
                <strong>{{ roomState.playerCount }}/{{ roomState.maxPlayers }}</strong>
              </div>
              <div v-if="roundText" class="room-status-item">
                <span>局数</span>
                <strong>{{ roundText }}</strong>
              </div>
              <div class="room-status-item">
                <span>状态</span>
                <strong>{{ roomStatusText }}</strong>
              </div>
            </div>
            <p v-if="wsError" class="room-status-error">{{ wsError }}</p>
          </div>

          <div v-if="actionCountdown" class="action-countdown" :class="{ 'is-self': actionCountdown.isSelf }">
            <div class="action-countdown-head">
              <span>{{ actionCountdown.playerLabel }}操作倒计时</span>
              <strong>{{ actionCountdown.remaining }}s</strong>
            </div>
            <div class="action-countdown-track">
              <span class="action-countdown-fill" :style="{ width: `${actionCountdown.progress}%` }"></span>
            </div>
          </div>


          <TopPlayerCard :player="seatDecor.top"/>
          <RightPlayerCard :player="seatDecor.right"/>
          <BottomPlayerCard :player="seatDecor.bottom"/>
          <LeftPlayerCard :player="seatDecor.left"/>

          <div v-if="deskSeats.top.tiles.length > 0 || deskSeats.top.hasHu" class="desk-zone desk-zone-top">
            <img
                v-for="tile in deskSeats.top.tiles"
                :key="tile.key"
                class="desk-tile"
                :class="{
                  'is-group-start': tile.isGroupStart,
                  'is-covered': tile.imageType === 'covered',
                }"
                :src="tile.src"
                :alt="tile.alt"
            />
            <span v-if="deskSeats.top.hasHu" class="desk-hu-flag">胡</span>
          </div>
          <div v-if="deskSeats.right.tiles.length > 0 || deskSeats.right.hasHu" class="desk-zone desk-zone-right">
            <img
                v-for="tile in deskSeats.right.tiles"
                :key="tile.key"
                class="desk-tile"
                :class="{
                  'is-group-start': tile.isGroupStart,
                  'is-covered': tile.imageType === 'covered',
                }"
                :src="tile.src"
                :alt="tile.alt"
            />
            <span v-if="deskSeats.right.hasHu" class="desk-hu-flag">胡</span>
          </div>
          <div v-if="deskSeats.bottom.tiles.length > 0 || deskSeats.bottom.hasHu" class="desk-zone desk-zone-bottom">
            <img
                v-for="tile in deskSeats.bottom.tiles"
                :key="tile.key"
                class="desk-tile"
                :class="{
                  'is-group-start': tile.isGroupStart,
                  'is-covered': tile.imageType === 'covered',
                }"
                :src="tile.src"
                :alt="tile.alt"
            />
            <span v-if="deskSeats.bottom.hasHu" class="desk-hu-flag">胡</span>
          </div>
          <div v-if="deskSeats.left.tiles.length > 0 || deskSeats.left.hasHu" class="desk-zone desk-zone-left">
            <img
                v-for="tile in deskSeats.left.tiles"
                :key="tile.key"
                class="desk-tile"
                :class="{
                  'is-group-start': tile.isGroupStart,
                  'is-covered': tile.imageType === 'covered',
                }"
                :src="tile.src"
                :alt="tile.alt"
            />
            <span v-if="deskSeats.left.hasHu" class="desk-hu-flag">胡</span>
          </div>

          <div v-if="wallSeats.top.tiles.length > 0" class="wall wall-top wall-live">
            <img
                v-for="(tile, index) in wallSeats.top.tiles"
                :key="tile.key"
                class="wall-live-tile"
                :class="{
                  'is-group-start': index > 0 && tile.suit && wallSeats.top.tiles[index - 1]?.suit !== tile.suit,
                  'is-exposed': tile.imageType !== 'hand',
                }"
                :src="tile.src"
                :alt="tile.alt"
            />
          </div>
          <div v-if="wallSeats.right.tiles.length > 0" class="wall wall-right wall-live">
            <img
                v-for="(tile, index) in wallSeats.right.tiles"
                :key="tile.key"
                class="wall-live-tile"
                :class="{
                  'is-group-start': index > 0 && tile.suit && wallSeats.right.tiles[index - 1]?.suit !== tile.suit,
                  'is-exposed': tile.imageType !== 'hand',
                }"
                :src="tile.src"
                :alt="tile.alt"
            />
          </div>
          <div v-if="wallSeats.bottom.tiles.length > 0" class="wall wall-bottom wall-live">
            <template v-for="(tile, index) in wallSeats.bottom.tiles" :key="tile.key">
              <button
                  v-if="tile.tile && tile.imageType === 'hand'"
                  class="wall-live-tile-button"
                  :class="{
                    'is-group-start': index > 0 && tile.suit && wallSeats.bottom.tiles[index - 1]?.suit !== tile.suit,
                    'is-lack-tagged': tile.showLackTag,
                    'is-selected': selectedDiscardTileId === tile.tile.id,
                  }"
                  :data-testid="`hand-tile-${tile.tile.id}`"
                  type="button"
                  :disabled="Boolean(discardBlockedReason)"
                  :title="discardTileBlockedReason(tile.tile) || formatTile(tile.tile)"
                  @click="selectDiscardTile(tile.tile)"
              >
                <span v-if="tile.showLackTag" class="wall-live-tile-lack-tag">缺</span>
                <img
                    class="wall-live-tile"
                    :src="tile.src"
                    :alt="tile.alt"
                />
              </button>
              <img
                  v-else
                  class="wall-live-tile"
                  :class="{
                    'is-group-start': index > 0 && tile.suit && wallSeats.bottom.tiles[index - 1]?.suit !== tile.suit,
                    'is-exposed': tile.imageType !== 'hand',
                  }"
                  :src="tile.src"
                  :alt="tile.alt"
              />
            </template>
          </div>
          <div v-if="wallSeats.left.tiles.length > 0" class="wall wall-left wall-live">
            <img
                v-for="(tile, index) in wallSeats.left.tiles"
                :key="tile.key"
                class="wall-live-tile"
                :class="{
                  'is-group-start': index > 0 && tile.suit && wallSeats.left.tiles[index - 1]?.suit !== tile.suit,
                  'is-exposed': tile.imageType !== 'hand',
                }"
                :src="tile.src"
                :alt="tile.alt"
            />
          </div>

<!--          <div class="floating-status top">-->
<!--            <img v-if="floatingMissingSuit.top" :src="floatingMissingSuit.top" alt=""/>-->
<!--            <span>{{ seatDecor.top.missingSuitLabel }}</span>-->
<!--          </div>-->
<!--          <div class="floating-status left">-->
<!--            <img v-if="floatingMissingSuit.left" :src="floatingMissingSuit.left" alt=""/>-->
<!--            <span>{{ seatDecor.left.missingSuitLabel }}</span>-->
<!--          </div>-->
<!--          <div class="floating-status right">-->
<!--            <img v-if="floatingMissingSuit.right" :src="floatingMissingSuit.right" alt=""/>-->
<!--            <span>{{ seatDecor.right.missingSuitLabel }}</span>-->
<!--          </div>-->

          <WindSquare class="center-wind-square" :seat-winds="seatWinds" :active-position="currentTurnSeat"/>

          <div v-if="showWaitingOwnerTip" class="waiting-owner-tip">
            <span>等待房主开始游戏</span>
          </div>

          <div v-if="showSettlementOverlay" class="settlement-overlay">
            <div class="settlement-panel">
              <h2 class="settlement-title">
                {{ isLastRound ? '最终结算' : `第 ${gameStore.currentRound} 局结算` }}
              </h2>
              <p v-if="gameStore.totalRounds > 0" class="settlement-round-info">
                {{ gameStore.currentRound }} / {{ gameStore.totalRounds }} 局
              </p>
              <div class="settlement-list">
                <div
                    v-for="(item, index) in settlementPlayers"
                    :key="item.playerId"
                    class="settlement-row"
                    :class="{ 'is-winner': item.isWinner, 'is-self': item.playerId === loggedInUserId }"
                >
                  <span class="settlement-rank">{{ index + 1 }}</span>
                  <span class="settlement-name">
                    {{ item.displayName }}
                    <span v-if="item.isWinner" class="settlement-winner-badge">胡</span>
                  </span>
                  <span class="settlement-score" :class="{ 'is-positive': item.score > 0, 'is-negative': item.score < 0 }">
                    {{ item.score > 0 ? '+' : '' }}{{ item.score }}
                  </span>
                </div>
              </div>
              <div class="settlement-actions">
                <button
                    v-if="!isLastRound"
                    class="ready-toggle ready-toggle-inline settlement-btn"
                    type="button"
                    :disabled="nextRoundPending"
                    @click="nextRound"
                >
                  <span class="ready-toggle-label">
                    {{ nextRoundPending ? '准备中...' : settlementCountdown != null && settlementCountdown > 0 ? `下一局 (${settlementCountdown}s)` : '下一局' }}
                  </span>
                </button>
                <button
                    v-else
                    class="ready-toggle ready-toggle-inline settlement-btn"
                    type="button"
                    @click="backHall"
                >
                  <span class="ready-toggle-label">返回大厅</span>
                </button>
              </div>
            </div>
          </div>

          <div class="bottom-control-panel">
            <div v-if="showDingQueChooser || showReadyToggle || showStartGameButton || selectedDiscardTile" class="bottom-action-bar">
              <div v-if="showDingQueChooser" class="ding-que-bar">
                <button
                    class="ding-que-button"
                    data-testid="ding-que-w"
                    type="button"
                    :disabled="dingQuePending"
                    @click="chooseDingQue('W')"
                >
                  万
                </button>
                <button
                    class="ding-que-button"
                    data-testid="ding-que-t"
                    type="button"
                    :disabled="dingQuePending"
                    @click="chooseDingQue('T')"
                >
                  筒
                </button>
                <button
                    class="ding-que-button"
                    data-testid="ding-que-b"
                    type="button"
                    :disabled="dingQuePending"
                    @click="chooseDingQue('B')"
                >
                  条
                </button>
              </div>

              <button
                  v-if="selectedDiscardTile"
                  class="ready-toggle ready-toggle-inline discard-confirm-button"
                  data-testid="confirm-discard"
                  type="button"
                  :disabled="!canConfirmDiscard || discardPending"
                  @click="confirmDiscard"
              >
                <span class="ready-toggle-label">{{ confirmDiscardLabel }}</span>
              </button>

              <button
                  v-if="showReadyToggle"
                  class="ready-toggle ready-toggle-inline"
                  data-testid="ready-toggle"
                  type="button"
                  :disabled="readyTogglePending"
                  @click="toggleReadyState"
              >
                <span class="ready-toggle-label">{{ myReadyState ? '取 消' : '准 备' }}</span>
              </button>

              <button
                  v-if="canDrawTile"
                  class="ready-toggle ready-toggle-inline"
                  data-testid="draw-tile"
                  type="button"
                  @click="drawTile"
              >
                <span class="ready-toggle-label">摸牌</span>
              </button>

              <button
                  v-if="showStartGameButton && isRoomOwner"
                  class="ready-toggle ready-toggle-inline"
                  data-testid="start-game"
                  type="button"
                  :disabled="!canStartGame"
                  @click="startGame"
              >
                <span class="ready-toggle-label">开始游戏</span>
              </button>
            </div>

            <div v-if="canSelfGang" class="hand-action-bar">
              <button
                  class="hand-action-tile"
                  data-testid="hand-gang"
                  type="button"
                  :disabled="turnActionPending"
                  @click="submitSelfGang"
              >
                杠
              </button>
            </div>

            <div v-if="canSelfHu || showClaimActions" class="claim-action-bar" data-testid="claim-action-bar">
              <button
                  v-if="canSelfHu"
                  class="ready-toggle ready-toggle-inline"
                  data-testid="claim-self-hu"
                  type="button"
                  :disabled="turnActionPending"
                  @click="submitSelfHu"
              >
                <span class="ready-toggle-label">胡</span>
              </button>
              <button
                  v-for="option in visibleClaimOptions"
                  :key="option"
                  class="ready-toggle ready-toggle-inline"
                  :data-testid="`claim-${option}`"
                  type="button"
                  :disabled="claimActionPending"
                  @click="submitClaim(option)"
              >
                <span class="ready-toggle-label">{{ option === 'peng' ? '碰' : option === 'gang' ? '杠' : option === 'hu' ? '胡' : '过' }}</span>
              </button>
            </div>
          </div>
        </div>
      </section>

    </div>
  </section>
</template>
