<script setup lang="ts">
import { Client, type IFrame, type IMessage } from '@stomp/stompjs'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import AppShell from './components/AppShell.vue'
import GamePageContainer from './pages/GamePageContainer.vue'
import ReviewPageContainer from './pages/ReviewPageContainer.vue'
import RoomPageContainer from './pages/RoomPageContainer.vue'
import type {
  ApiResponse,
  DiagnosticItem,
  GameStateResponse,
  PrivateActionMessage,
  PrivateTeachingMessage,
  PublicGameMessage,
  RoomSummaryResponse,
  ScoreChangeCardView,
  SettlementCardView,
  ViewUserOption
} from './types/game'
import type { ReviewSummaryResponse } from './types/review'
import {
  actionScopeLabelMap,
  formatSettlementType,
  readNumber,
  readString,
  phaseLabelMap,
  toActionLabel,
  toSettlementDetailView,
  triggerEventTypeLabelMap
} from './utils/gameUi'

const busy = ref(false)
const error = ref('')
const info = ref('H5 对局原型已接入公共事件、私有动作与私有教学消息，当前开始补正式动作面板。')

const ownerId = ref('host-1')
const ownerName = ref('房主')
const joinUserId = ref('player-2')
const joinUserName = ref('玩家二')
const roomIdInput = ref('')
const lackSuit = ref('WAN')

const room = ref<RoomSummaryResponse | null>(null)
const game = ref<GameStateResponse | null>(null)
const reviewSummary = ref<ReviewSummaryResponse | null>(null)
const currentUserId = ref('host-1')

const wsStatus = ref<'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'>('idle')
const reviewLoading = ref(false)
const publicEvents = ref<PublicGameMessage[]>([])
const privateAction = ref<PrivateActionMessage | null>(null)
const privateTeaching = ref<PrivateTeachingMessage | null>(null)
let stompClient: Client | null = null

const canSelectLack = computed(() => game.value?.phase === 'LACK_SELECTION')
const canDiscard = computed(
  () =>
    game.value?.phase === 'PLAYING' &&
    !game.value.selfSeat.won &&
    game.value.selfSeat.playerId === currentUserId.value &&
    game.value.currentSeatNo === game.value.selfSeat.seatNo
)

const publicSeats = computed(() => game.value?.seats ?? [])

const privateActionSummary = computed(() => {
  if (!privateAction.value) {
    return ''
  }
  const scopeLabel = actionScopeLabelMap[privateAction.value.actionScope] ?? privateAction.value.actionScope
  const readableActions = privateAction.value.availableActions.map(toActionLabel)
  return `${scopeLabel}：${readableActions.join(' / ')}`
})

const turnActionCandidates = computed(() => {
  if (privateAction.value?.actionScope !== 'TURN') {
    return []
  }
  // 当前回合的出牌依然直接通过点击手牌触发，动作面板只承载额外动作，避免一个动作出现两套入口。
  return privateAction.value.candidates.filter((candidate) => candidate.actionType !== 'DISCARD')
})

const responseActionCandidates = computed(() => {
  if (privateAction.value?.actionScope !== 'RESPONSE') {
    return []
  }
  return privateAction.value.candidates
})

const responseContextSummary = computed(() => {
  if (!privateAction.value || privateAction.value.actionScope !== 'RESPONSE') {
    return ''
  }
  const triggerLabel = privateAction.value.triggerEventType
    ? triggerEventTypeLabelMap[privateAction.value.triggerEventType] ?? privateAction.value.triggerEventType
    : '响应窗口'
  const sourceSeatLabel =
    privateAction.value.sourceSeatNo !== null ? `来源座位 ${privateAction.value.sourceSeatNo}` : '来源座位未知'
  const triggerTileLabel = privateAction.value.triggerTile ? `目标牌 ${privateAction.value.triggerTile}` : '未携带目标牌'
  return `${triggerLabel}，${sourceSeatLabel}，${triggerTileLabel}`
})

const actionPanelHint = computed(() => {
  if (!privateAction.value) {
    return '等待下一条私有动作消息。收到回合动作或响应动作后，这里会自动切换到对应面板。'
  }
  if (privateAction.value.actionScope === 'TURN') {
    return '出牌通过上方手牌直接执行；若当前可自摸胡或可杠，则在这里统一提交。'
  }
  return `${responseContextSummary.value}。请在当前响应窗口关闭前完成选择。`
})

const recommendedDiscardTile = computed(() => {
  // 当前教学服务只对出牌建议生效，因此 recommendedAction 在现阶段可直接视为“建议打出的牌名”。
  if (!privateTeaching.value?.recommendedAction) {
    return null
  }
  if (!game.value?.selfSeat.handTiles.includes(privateTeaching.value.recommendedAction)) {
    return null
  }
  return privateTeaching.value.recommendedAction
})

const teachingHint = computed(() => {
  if (!privateTeaching.value) {
    return '尚未收到当前视角的教学建议。'
  }
  if (recommendedDiscardTile.value) {
    return `教学建议优先打出 ${recommendedDiscardTile.value}。`
  }
  return privateTeaching.value.explanation
})

const teachingCandidates = computed(() => privateTeaching.value?.candidates ?? [])

const currentViewLabel = computed(() => {
  if (!game.value) {
    return currentUserId.value
  }
  return `${game.value.selfSeat.nickname} · 座位 ${game.value.selfSeat.seatNo}`
})

const viewUserOptions = computed<ViewUserOption[]>(() => {
  if (game.value) {
    const options = new Map<string, ViewUserOption>()
    options.set(game.value.selfSeat.playerId, {
      userId: game.value.selfSeat.playerId,
      label: `${game.value.selfSeat.nickname} · 座位 ${game.value.selfSeat.seatNo}`,
      seatNo: game.value.selfSeat.seatNo
    })
    for (const seat of game.value.seats) {
      options.set(seat.playerId, {
        userId: seat.playerId,
        label: `${seat.nickname} · 座位 ${seat.seatNo}`,
        seatNo: seat.seatNo
      })
    }
    return [...options.values()].sort((left, right) => left.seatNo - right.seatNo)
  }

  return [
    { userId: ownerId.value, label: `${ownerName.value} · 房主`, seatNo: 0 },
    { userId: joinUserId.value, label: `${joinUserName.value} · 玩家二`, seatNo: 1 }
  ]
})

const actionDiagnosticItems = computed<DiagnosticItem[]>(() => {
  if (!privateAction.value) {
    return []
  }
  return [
    {
      key: 'scope',
      label: '作用域',
      value: actionScopeLabelMap[privateAction.value.actionScope] ?? privateAction.value.actionScope
    },
    {
      key: 'window',
      label: '窗口 ID',
      value: privateAction.value.windowId ? privateAction.value.windowId.slice(0, 8) : '当前回合'
    },
    {
      key: 'event',
      label: '触发来源',
      value: privateAction.value.triggerEventType
        ? triggerEventTypeLabelMap[privateAction.value.triggerEventType] ?? privateAction.value.triggerEventType
        : '当前回合'
    },
    {
      key: 'source',
      label: '来源座位',
      value: privateAction.value.sourceSeatNo !== null ? `座位 ${privateAction.value.sourceSeatNo}` : '无'
    }
  ]
})

const latestSettlementCard = computed<SettlementCardView | null>(() => {
  const settlementEvent = publicEvents.value.find((event) => event.eventType === 'SETTLEMENT_APPLIED')
  if (!settlementEvent) {
    return null
  }

  return {
    settlementType: formatSettlementType(readString(settlementEvent.payload.settlementType)),
    actionType: toActionLabel(readString(settlementEvent.payload.actionType) ?? '-'),
    actorSeatNo: readNumber(settlementEvent.payload.actorSeatNo),
    sourceSeatNo: readNumber(settlementEvent.payload.sourceSeatNo),
    triggerTile: readString(settlementEvent.payload.triggerTile),
    detail: toSettlementDetailView(settlementEvent.payload.settlementDetail)
  }
})

const latestSettlementScoreChanges = computed<ScoreChangeCardView[]>(() => {
  const settlementEvent = publicEvents.value.find((event) => event.eventType === 'SETTLEMENT_APPLIED')
  if (!settlementEvent) {
    return []
  }

  const latestSettlementType = readString(settlementEvent.payload.settlementType)
  return publicEvents.value
    .filter((event) => event.eventType === 'SCORE_CHANGED')
    .filter((event) => readString(event.payload.settlementType) === latestSettlementType)
    .slice(0, 4)
    .map((event) => ({
      seatNo: event.seatNo ?? -1,
      delta: readNumber(event.payload.delta) ?? 0,
      score: readNumber(event.payload.score),
      settlementType: readString(event.payload.settlementType)
    }))
})

async function requestJson<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, {
    headers: {
      'Content-Type': 'application/json'
    },
    ...options
  })

  if (!response.ok) {
    throw new Error(`请求失败：${url}`)
  }

  const payload = (await response.json()) as ApiResponse<T>
  if (!payload.success) {
    throw new Error(payload.message || '接口处理失败')
  }

  return payload.data
}

async function runTask(task: () => Promise<void>) {
  busy.value = true
  error.value = ''
  try {
    await task()
  } catch (err) {
    error.value = err instanceof Error ? err.message : '操作失败'
  } finally {
    busy.value = false
  }
}

async function loadCurrentReview() {
  if (!game.value) {
    error.value = '请先进入一局真实对局，再加载当前视角复盘'
    return
  }
  reviewLoading.value = true
  error.value = ''
  try {
    reviewSummary.value = await requestJson<ReviewSummaryResponse>(
      `/api/games/${game.value.gameId}/review?userId=${encodeURIComponent(currentUserId.value)}`
    )
    info.value = '已加载当前视角的真实局后复盘摘要。'
  } catch (err) {
    error.value = err instanceof Error ? err.message : '复盘加载失败'
  } finally {
    reviewLoading.value = false
  }
}

function buildBrokerUrl() {
  const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
  const host = window.location.hostname
  const port = window.location.port === '5173' ? '8080' : window.location.port
  return `${protocol}://${host}${port ? `:${port}` : ''}/ws`
}

function disconnectWs() {
  if (stompClient) {
    stompClient.deactivate()
    stompClient = null
  }
  wsStatus.value = 'disconnected'
}

function connectWs(gameId: string, userId: string) {
  disconnectWs()
  wsStatus.value = 'connecting'
  publicEvents.value = []
  privateAction.value = null
  privateTeaching.value = null

  const client = new Client({
    brokerURL: buildBrokerUrl(),
    reconnectDelay: 3000,
    onConnect() {
      wsStatus.value = 'connected'
      client.subscribe(`/topic/games/${gameId}/events`, (message: IMessage) => {
        const payload = JSON.parse(message.body) as PublicGameMessage
        handlePublicEvent(payload)
      })
      client.subscribe(`/topic/users/${userId}/actions`, (message: IMessage) => {
        // 私有动作消息是前端动作面板的唯一事实来源，后续所有按钮启用状态都以这里为准。
        privateAction.value = JSON.parse(message.body) as PrivateActionMessage
      })
      client.subscribe(`/topic/users/${userId}/teaching`, (message: IMessage) => {
        privateTeaching.value = JSON.parse(message.body) as PrivateTeachingMessage
      })
    },
    onStompError(frame: IFrame) {
      wsStatus.value = 'error'
      error.value = frame.headers.message ?? 'WebSocket 订阅失败'
    },
    onWebSocketClose() {
      wsStatus.value = 'disconnected'
    },
    onWebSocketError() {
      wsStatus.value = 'error'
    }
  })

  client.activate()
  stompClient = client
}

watch(
  () => [game.value?.gameId, currentUserId.value] as const,
  ([gameId, userId]) => {
    reviewSummary.value = null
    // 视角一旦切换，必须重新订阅对应用户的私有主题，避免沿用上一个玩家的动作/教学消息。
    if (gameId) {
      connectWs(gameId, userId)
    } else {
      disconnectWs()
    }
  }
)

onBeforeUnmount(() => {
  disconnectWs()
})

async function createRoom() {
  await runTask(async () => {
    const result = await requestJson<RoomSummaryResponse>('/api/rooms', {
      method: 'POST',
      body: JSON.stringify({
        ownerUserId: ownerId.value,
        allowBotFill: true
      })
    })
    room.value = result
    roomIdInput.value = result.roomId
    currentUserId.value = ownerId.value
    info.value = `房间已创建，邀请码 ${result.inviteCode}。`
  })
}

async function loadRoom() {
  if (!roomIdInput.value) {
    error.value = '请先输入房间 ID'
    return
  }
  await runTask(async () => {
    room.value = await requestJson<RoomSummaryResponse>(`/api/rooms/${roomIdInput.value}`)
    info.value = '已刷新房间状态。'
  })
}

async function joinRoom() {
  if (!roomIdInput.value) {
    error.value = '请先输入房间 ID'
    return
  }
  await runTask(async () => {
    room.value = await requestJson<RoomSummaryResponse>(`/api/rooms/${roomIdInput.value}/join`, {
      method: 'POST',
      body: JSON.stringify({
        userId: joinUserId.value,
        displayName: joinUserName.value
      })
    })
    info.value = `${joinUserName.value} 已加入房间。`
  })
}

async function toggleReady(userId: string, ready: boolean) {
  if (!room.value) {
    return
  }
  const targetRoomId = room.value.roomId
  await runTask(async () => {
    room.value = await requestJson<RoomSummaryResponse>(`/api/rooms/${targetRoomId}/ready`, {
      method: 'POST',
      body: JSON.stringify({
        userId,
        ready
      })
    })
    info.value = ready ? `${userId} 已准备。` : `${userId} 已取消准备。`
  })
}

async function startGame() {
  if (!room.value) {
    error.value = '请先创建或加载房间'
    return
  }
  const targetRoomId = room.value.roomId
  await runTask(async () => {
    currentUserId.value = ownerId.value
    game.value = await requestJson<GameStateResponse>(`/api/rooms/${targetRoomId}/start`, {
      method: 'POST',
      body: JSON.stringify({
        operatorUserId: ownerId.value
      })
    })
    info.value = '对局已开始，进入定缺阶段。'
  })
}

async function refreshGameState() {
  if (!game.value) {
    return
  }
  const targetGameId = game.value.gameId
  await runTask(async () => {
    game.value = await requestJson<GameStateResponse>(
      `/api/games/${targetGameId}/state?userId=${encodeURIComponent(currentUserId.value)}`
    )
    info.value = '已刷新对局状态。'
  })
}

async function switchUserView(userId: string) {
  if (currentUserId.value === userId) {
    return
  }

  currentUserId.value = userId

  if (!game.value) {
    info.value = `已切换到 ${userId} 视角。`
    return
  }

  const targetGameId = game.value.gameId
  await runTask(async () => {
    game.value = await requestJson<GameStateResponse>(
      `/api/games/${targetGameId}/state?userId=${encodeURIComponent(userId)}`
    )
    info.value = `已切换到 ${userId} 视角，并同步最新对局状态。`
  })
}

async function submitAction(actionType: string, tile?: string, sourceSeatNo?: number | null) {
  if (!game.value) {
    return
  }
  const targetGameId = game.value.gameId
  await runTask(async () => {
    game.value = await requestJson<GameStateResponse>(`/api/games/${targetGameId}/actions`, {
      method: 'POST',
      body: JSON.stringify({
        userId: currentUserId.value,
        actionType,
        tile: tile ?? null,
        sourceSeatNo: sourceSeatNo ?? null
      })
    })

    const readableAction = toActionLabel(actionType)
    if (actionType === 'DISCARD' && tile) {
      info.value = `已打出 ${tile}。`
      return
    }
    if (tile) {
      info.value = `已提交 ${readableAction} ${tile}。`
      return
    }
    info.value = `已提交动作 ${readableAction}。`
  })
}

function selectLack() {
  return submitAction('SELECT_LACK_SUIT', lackSuit.value)
}

function discard(tile: string) {
  return submitAction('DISCARD', tile)
}

function submitCandidateAction(actionType: string, tile: string | null) {
  const sourceSeatNo = privateAction.value?.sourceSeatNo ?? null
  return submitAction(actionType, tile ?? undefined, sourceSeatNo)
}

function handlePublicEvent(event: PublicGameMessage) {
  // 公共事件除了进入时间线，也负责驱动前端把已经失效的私有动作面板收起来。
  if (shouldClearPrivateActionByEvent(event)) {
    privateAction.value = null
  }
  publicEvents.value = [event, ...publicEvents.value].slice(0, 16)
}

function shouldClearPrivateActionByEvent(event: PublicGameMessage) {
  if (!privateAction.value) {
    return false
  }

  const currentAction = privateAction.value
  const selfSeatNo = game.value?.selfSeat.seatNo ?? null

  if (event.eventType === 'RESPONSE_WINDOW_CLOSED') {
    const sourceSeatNo = readNumber(event.payload.sourceSeatNo)
    return currentAction.actionScope === 'RESPONSE' && sourceSeatNo === currentAction.sourceSeatNo
  }

  if (event.eventType === 'GAME_PHASE_CHANGED') {
    return readString(event.payload.phase) !== 'PLAYING'
  }

  if (event.eventType === 'TILE_DISCARDED') {
    return currentAction.actionScope === 'TURN' && selfSeatNo !== null && event.seatNo === selfSeatNo
  }

  if (event.eventType === 'TURN_SWITCHED') {
    const nextSeatNo = readNumber(event.payload.currentSeatNo)
    return currentAction.actionScope === 'TURN' && selfSeatNo !== null && nextSeatNo !== selfSeatNo
  }

  return false
}
</script>

<template>
  <AppShell :info="info" :error="error">
    <RoomPageContainer
      :busy="busy"
      :owner-id="ownerId"
      :owner-name="ownerName"
      :join-user-id="joinUserId"
      :join-user-name="joinUserName"
      :room-id-input="roomIdInput"
      :room="room"
      :current-user-id="currentUserId"
      @update:owner-id="ownerId = $event"
      @update:owner-name="ownerName = $event"
      @update:join-user-id="joinUserId = $event"
      @update:join-user-name="joinUserName = $event"
      @update:room-id-input="roomIdInput = $event"
      @create-room="createRoom"
      @load-room="loadRoom"
      @switch-user-view="switchUserView"
      @join-room="joinRoom"
      @toggle-ready="toggleReady"
      @start-game="startGame"
    />

    <GamePageContainer
      :game="game"
      :current-user-id="currentUserId"
      :current-view-label="currentViewLabel"
      :view-user-options="viewUserOptions"
      :lack-suit="lackSuit"
      :can-select-lack="canSelectLack"
      :can-discard="canDiscard"
      :recommended-discard-tile="recommendedDiscardTile"
      :private-teaching="privateTeaching"
      :private-teaching-hint="teachingHint"
      :private-teaching-candidates="teachingCandidates"
      :private-action="privateAction"
      :private-action-summary="privateActionSummary"
      :action-panel-hint="actionPanelHint"
      :turn-action-candidates="turnActionCandidates"
      :response-action-candidates="responseActionCandidates"
      :response-context-summary="responseContextSummary"
      :action-diagnostic-items="actionDiagnosticItems"
      :public-seats="publicSeats"
      :public-events="publicEvents"
      :latest-settlement-card="latestSettlementCard"
      :latest-settlement-score-changes="latestSettlementScoreChanges"
      :ws-status="wsStatus"
      @update:lack-suit="lackSuit = $event"
      @refresh-game-state="refreshGameState"
      @select-lack="selectLack"
      @switch-user-view="switchUserView"
      @discard="discard"
      @submit-candidate-action="submitCandidateAction"
    />

    <ReviewPageContainer :review="reviewSummary" :loading="reviewLoading" @load-review="loadCurrentReview" />
  </AppShell>
</template>
