<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from "vue";
import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
import Container from "./components/Container.vue";
import BattleHeader from "./components/BattleHeader.vue";
import BowTarget from "./components/BowTarget.vue";
import BattleFooter from "./components/BattleFooter.vue";
import ScreenHint from "./components/ScreenHint.vue";
import SButton from "./components/SButton.vue";
import RoundEndTip from "./components/RoundEndTip.vue";
import TestDistance from "./components/TestDistance.vue";
import TeamAvatars from "./components/TeamAvatars.vue";
import ShootProgress2 from "./components/ShootProgress2.vue";
import SModal from "./components/SModal.vue";
import { laserCloseAPI, getBattleAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText } from "@/util";
import audioManager, {
  AUDIO_INTERRUPTION_BEGIN_EVENT,
  AUDIO_INTERRUPTION_END_EVENT,
} from "@/audioManager";
import useStore from "@/store";
import { storeToRefs } from "pinia";

const store = useStore();
const { user, online } = storeToRefs(store);

const DEFAULT_SHOOT_TIME = 15;
const READY_SECONDS = 15;
const READY_TIMER_EMIT_DELAY = 200;
const RESTORE_DELAY = 300;
const RESTORE_EMPTY_ID_RETRY_DELAY = 50;
const RESTORE_LOADING_TIMEOUT = 8000;
const ROUND_TIP_DELAY = 800;
const ROUND_TIP_WAIT_GRACE = 300;
const PROGRESS_START_DELAY = 100;
const PROGRESS_MIN_TICK_MS = 1;
const PROGRESS_ZERO_WAIT_GRACE = 120;
const AUDIO_TIMEOUT_BASE = 3500;
const AUDIO_TIMEOUT_PER_KEY = 2600;
const AUDIO_TIMEOUT_MAX = 12000;
const BATTLE_CANCEL_RETURN_DELAY = 2000;
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
const X_RING_STREAKS_KEY = "team-battle-x-ring-streaks";
const PROGRESS_ZERO_EVENT = "team-battle-progress-zero";
const COUNTDOWN_READY_EVENT = "team-battle-countdown-ready";

// 页面级状态：负责承载比赛快照、当前轮次、倒计时、结算弹窗等展示信息。
const start = ref(null);
const tips = ref("");
const battleId = ref("");
const currentRound = ref(0);
const roundTipRound = ref(0);
const goldenRound = ref(0);
const currentRedPoint = ref(0);
const currentBluePoint = ref(0);
const scores = ref([]);
const blueScores = ref([]);
const redTeam = ref([]);
const blueTeam = ref([]);
const currentShooterId = ref(0);
const roundResults = ref([]);
const redPoints = ref(0);
const bluePoints = ref(0);
const latestShotFlash = ref(null);
const showRoundTip = ref(false);
const isFinalShoot = ref(false);
const matchStatus = ref(undefined);
const shootTimeTotal = ref(DEFAULT_SHOOT_TIME);
const showOfflineModal = ref(false);
const restoreLoading = ref(false);
const xRingStreaks = ref({});

// 消息队列：只保存“待执行”的战况消息，页面展示统一由队列串行驱动。
const battleQueue = ref([]);
const queueRunning = ref(false);
// latestSnapshotServerTime：回前台拉到的最新快照时间。
const latestSnapshotServerTime = ref(0);
// latestAppliedServerTime：已经真正应用到页面上的最新消息时间。
const latestAppliedServerTime = ref(0);

// 队列代际：页面重置、离开或恢复快照时递增，让旧任务自动失效。
let queueGeneration = 0;
let queueOrder = 0;
let restoreGeneration = 0;
let pendingRestoreTimer = null;
let restoreLoadingTimer = null;
// 轮次提示音和普通射击音分开处理，避免新回合提示被旧状态打断。
let pendingRoundAudio = false;
// 一旦收到 BattleEnd，后续普通消息就不再进入队列。
let battleEnded = false;
const handledMessageKeys = new Set();
const handledMessageKeyOrder = [];
const queuedMessageKeys = new Set();
// 每个语音任务都挂一个等待器，既等 audioEnded，也等超时兜底。
const audioWaiters = new Set();
// 记录倒计时归零的时刻，用来卡住下一位射手的切换时机。
let progressDeadlineAt = 0;
const progressZeroWaiters = new Set();
// 回合结算弹窗也纳入队列节奏，确保上一轮看完再进下一轮。
const roundTipWaiters = new Set();

// 监听设备在线状态：比赛进行中掉线时展示提示弹窗。
watch(online, (newVal, oldVal) => {
  if (!newVal && oldVal && start.value === true) {
    showOfflineModal.value = true;
  }
});

// 统一把秒级或毫秒级时间戳转成毫秒，方便和本机时间比较。
function loadXRingStreaks() {
  const cached = uni.getStorageSync(X_RING_STREAKS_KEY);
  xRingStreaks.value =
    cached && typeof cached === "object" && !Array.isArray(cached) ? cached : {};
}

function saveXRingStreaks() {
  uni.setStorageSync(X_RING_STREAKS_KEY, xRingStreaks.value);
}

function clearXRingStreaks() {
  xRingStreaks.value = {};
  uni.removeStorageSync(X_RING_STREAKS_KEY);
}

function normalizeTimestamp(value) {
  const numberValue = Number(value || 0);
  if (!numberValue) return 0;
  return numberValue < 1000000000000 ? numberValue * 1000 : numberValue;
}

// 从不同消息结构中提取服务端时间，作为恢复和去重的时间基准。
function getServerTime(message) {
  return normalizeTimestamp(
    message?.serverTime ?? message?.current?.serverTime ?? message?.shootData?.serverTime
  );
}

function getMessageKey(message) {
  // 优先使用 serverTime 作为稳定去重主键；没有 serverTime 时再用业务字段拼降级 key。
  const serverTime = getServerTime(message);
  if (serverTime) return `${message.type}:${serverTime}`;

  const current = message?.current || {};
  const shootData = message?.shootData || {};
  return [
    message?.type ?? "",
    current.round ?? "",
    current.playerId ?? shootData.playerId ?? "",
    normalizeTimestamp(current.startTime),
    message?.status ?? "",
    shootData.id ?? shootData.shootId ?? shootData.arrowId ?? "",
    shootData.ring ?? "",
    shootData.ringX ? 1 : 0,
    shootData.x ?? "",
    shootData.y ?? "",
    shootData.angle ?? "",
  ].join(":");
}

function rememberHandledMessageKey(key) {
  if (!key || handledMessageKeys.has(key)) return;
  handledMessageKeys.add(key);
  handledMessageKeyOrder.push(key);
  if (handledMessageKeyOrder.length > 500) {
    const oldKey = handledMessageKeyOrder.shift();
    handledMessageKeys.delete(oldKey);
  }
}

function getMessagePriority(type) {
  if (type === MESSAGETYPESV2.BattleEnd) return 100;
  if (type === MESSAGETYPESV2.BattleStart) return 10;
  if (type === MESSAGETYPESV2.ShootResult) return 20;
  if (type === MESSAGETYPESV2.NewRound) return 30;
  if (type === MESSAGETYPESV2.ToSomeoneShoot) return 40;
  if (type === MESSAGETYPESV2.InvalidShot) return 50;
  return 60;
}

// 队列排序：终局优先，其次按服务端时间，最后按消息类型优先级和入队顺序兜底。
function sortBattleQueue() {
  battleQueue.value.sort((a, b) => {
    if (a.type === MESSAGETYPESV2.BattleEnd && b.type !== MESSAGETYPESV2.BattleEnd) return -1;
    if (b.type === MESSAGETYPESV2.BattleEnd && a.type !== MESSAGETYPESV2.BattleEnd) return 1;
    if (a.serverTime && b.serverTime && a.serverTime !== b.serverTime) {
      return a.serverTime - b.serverTime;
    }
    if (a.serverTime && !b.serverTime) return -1;
    if (!a.serverTime && b.serverTime) return 1;
    const priorityDiff = getMessagePriority(a.type) - getMessagePriority(b.type);
    return priorityDiff || a.order - b.order;
  });
}

function isQueueAlive(runId) {
  return runId === queueGeneration;
}

function clearAudioWaiters() {
  Array.from(audioWaiters).forEach((waiter) => waiter.done());
}

function clearProgressZeroWaiters() {
  Array.from(progressZeroWaiters).forEach((waiter) => waiter.done());
}

function clearRoundTipWaiters() {
  Array.from(roundTipWaiters).forEach((waiter) => waiter.done());
}

function waitForRoundTipClosed(isFinal) {
  const autoCloseSeconds = isFinal ? 11 : 4;
  const waitMs = autoCloseSeconds * 1000 + ROUND_TIP_WAIT_GRACE;

  return new Promise((resolve) => {
    let settled = false;
    const waiter = {
      done: () => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        roundTipWaiters.delete(waiter);
        resolve();
      },
    };
    const timer = setTimeout(waiter.done, waitMs);
    roundTipWaiters.add(waiter);
  });
}

function closeRoundTip() {
  showRoundTip.value = false;
  clearRoundTipWaiters();
}

function cancelRoundTipDisplay() {
  closeRoundTip();
}

function handleRoundTipAutoClose() {
  closeRoundTip();
}

function markProgressDeadline(countdown, delayMs = 0) {
  if (!countdown?.value || !countdown?.durationMs) {
    progressDeadlineAt = 0;
    clearProgressZeroWaiters();
    return;
  }
  progressDeadlineAt = Date.now() + delayMs + countdown.durationMs;
}

function waitForProgressZero() {
  const waitMs = Math.ceil(progressDeadlineAt - Date.now());
  if (waitMs <= 0) return Promise.resolve();

  return new Promise((resolve) => {
    let settled = false;
    const waiter = {
      done: () => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        progressZeroWaiters.delete(waiter);
        resolve();
      },
    };
    const timer = setTimeout(waiter.done, waitMs + PROGRESS_ZERO_WAIT_GRACE);
    progressZeroWaiters.add(waiter);
  });
}

function onProgressZero() {
  progressDeadlineAt = 0;
  clearProgressZeroWaiters();
}

function clearRestoreLoadingTimer() {
  if (restoreLoadingTimer) {
    clearTimeout(restoreLoadingTimer);
    restoreLoadingTimer = null;
  }
}

function hideRestoreLoading() {
  restoreLoading.value = false;
  clearRestoreLoadingTimer();
}

function showRestoreLoading() {
  clearRestoreLoadingTimer();
  restoreLoading.value = true;
  restoreLoadingTimer = setTimeout(() => {
    restoreLoadingTimer = null;
    restoreLoading.value = false;
  }, RESTORE_LOADING_TIMEOUT);
}

// 页面重置、离开或恢复快照时调用，统一清理队列、等待器、音频和进度条。
function invalidateBattleQueue({ stopAudio = false, stopProgress = false } = {}) {
  // 提升队列代际并清空待执行消息，确保恢复快照时不会继续跑旧战况。
  queueGeneration += 1;
  battleQueue.value = [];
  queueRunning.value = false;
  queuedMessageKeys.clear();
  clearAudioWaiters();
  progressDeadlineAt = 0;
  clearProgressZeroWaiters();
  cancelRoundTipDisplay();
  if (stopAudio) audioManager.stopAll();
  if (stopProgress) uni.$emit("update-remain", { stop: true });
}

// socket 消息入口：先做去重和时间边界判断，再放入队列等待串行执行。
function enqueueBattleMessage(message) {
  if (Array.isArray(message) || !message?.type) return;
  if (battleEnded && message.type !== MESSAGETYPESV2.BattleEnd) return;
  if (message.type === MESSAGETYPESV2.BattleEnd) battleEnded = true;

  // 入队阶段只做排序、去重和时间边界判断，不直接改 UI。
  const serverTime = getServerTime(message);
  const key = getMessageKey(message);
  if (!key || handledMessageKeys.has(key) || queuedMessageKeys.has(key)) return;
  if (serverTime && latestSnapshotServerTime.value && serverTime <= latestSnapshotServerTime.value) return;
  if (serverTime && latestAppliedServerTime.value && serverTime < latestAppliedServerTime.value) return;
  queuedMessageKeys.add(key);

  if (message.type === MESSAGETYPESV2.BattleEnd) {
    battleQueue.value = battleQueue.value.filter(
      (task) => task.type === MESSAGETYPESV2.BattleEnd
    );
  }

  battleQueue.value.push({
    message,
    type: message.type,
    key,
    serverTime,
    receivedAt: Date.now(),
    order: ++queueOrder,
  });
  sortBattleQueue();
  runBattleQueue();
}

// 队列消费入口：同一时间只允许一个消费者执行，保证 UI 和语音顺序稳定。
async function runBattleQueue() {
  if (queueRunning.value) return;
  queueRunning.value = true;
  const runId = queueGeneration;

  // 串行消费队列，保证“播音 -> 展示 -> 倒计时”顺序稳定。
  while (battleQueue.value.length && isQueueAlive(runId)) {
    const task = battleQueue.value.shift();
    try {
      if (task?.serverTime) {
        latestAppliedServerTime.value = Math.max(latestAppliedServerTime.value, task.serverTime);
      }
      await executeBattleTask(task, runId);
      queuedMessageKeys.delete(task?.key);
      rememberHandledMessageKey(task?.key);
    } catch (err) {
      queuedMessageKeys.delete(task?.key);
      rememberHandledMessageKey(task?.key);
      console.log("team battle queue task error:", err);
    }
  }

  if (isQueueAlive(runId)) queueRunning.value = false;
}

// 根据消息类型分发到具体任务处理器，所有任务都要尊重当前队列代际。
async function executeBattleTask(task, runId) {
  if (!task || !isQueueAlive(runId)) return;
  const type = task.type;

  if (type === MESSAGETYPESV2.BattleStart) {
    await runBattleStartTask(task, runId);
  } else if (type === MESSAGETYPESV2.ToSomeoneShoot) {
    await runToSomeoneShootTask(task, runId);
  } else if (type === MESSAGETYPESV2.ShootResult) {
    await runShootResultTask(task, runId);
  } else if (type === MESSAGETYPESV2.NewRound) {
    await runNewRoundTask(task, runId);
  } else if (type === MESSAGETYPESV2.BattleEnd) {
    await runBattleEndTask(task, runId);
  } else if (type === MESSAGETYPESV2.InvalidShot) {
    await runInvalidShotTask(runId);
  }
}

// 播放一组语音并等待完成；带超时兜底，避免 audioEnded 丢失导致队列卡死。
function playAudioKeys(keys, { interrupt = false, timeout } = {}) {
  const audioKeys = (Array.isArray(keys) ? keys : [keys]).filter(Boolean);
  if (!audioKeys.length) return Promise.resolve();

  // 语音任务必须有超时兜底，避免 audioEnded 丢失导致队列卡死。
  const expectedKey = audioKeys[audioKeys.length - 1];
  const waitTime =
    timeout ??
    Math.min(
      AUDIO_TIMEOUT_MAX,
      Math.max(AUDIO_TIMEOUT_BASE, audioKeys.length * AUDIO_TIMEOUT_PER_KEY)
    );

  return new Promise((resolve) => {
    let settled = false;
    const waiter = {
      expectedKey,
      done: () => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        audioWaiters.delete(waiter);
        resolve();
      },
    };
    const timer = setTimeout(waiter.done, waitTime);
    audioWaiters.add(waiter);
    audioManager.play(audioKeys, interrupt);
  });
}

function onAudioEnded(key) {
  // 某个语音真正播完后，唤醒对应等待器，继续执行后续队列。
  Array.from(audioWaiters).forEach((waiter) => {
    if (waiter.expectedKey === key) waiter.done();
  });
}

function handleBattleCovered() {
  if (pendingRestoreTimer) {
    clearTimeout(pendingRestoreTimer);
    pendingRestoreTimer = null;
  }
  hideRestoreLoading();
  pendingRoundAudio = false;
  invalidateBattleQueue({ stopAudio: true, stopProgress: true });
}

function handleBattleRecovered() {
  scheduleRestoreLatestBattle();
}

// 队伍信息优先用接口返回值；接口缺失时使用本地缓存，避免重进页面时头像为空。
function loadTeamPlayers(teamInfo, storageKey) {
  if (Array.isArray(teamInfo?.players)) return [...teamInfo.players];
  const cached = uni.getStorageSync(storageKey);
  return Array.isArray(cached) ? [...cached] : [];
}

function updateTeams(battleInfo) {
  const teams = battleInfo?.teams || {};
  blueTeam.value = loadTeamPlayers(teams[1], "blue-team");
  redTeam.value = loadTeamPlayers(teams[2], "red-team");
}

function updateGoldenRound(battleInfo) {
  if (!battleInfo?.current?.goldRound) {
    goldenRound.value = 0;
    return;
  }
  const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
  const finishedGoldCount = rounds.filter((round) => !!round?.ifGold).length;
  goldenRound.value = Math.max(1, finishedGoldCount + (battleInfo.current?.playerId ? 1 : 0));
}

// Restore an info snapshot whose eventType points at the NewRound phase.
function getRestorePrevRound(battleInfo) {
  const currentRoundValue = Number(battleInfo?.current?.round || 0);
  if (currentRoundValue > 1) return currentRoundValue - 1;
  const rounds = Array.isArray(battleInfo?.rounds) ? battleInfo.rounds : [];
  return Math.max(1, rounds.length || 1);
}

function applyRestoreNewRoundSnapshot(battleInfo) {
  const prevRound = getRestorePrevRound(battleInfo);
  start.value = true;
  showRoundTip.value = false;
  scores.value = [];
  blueScores.value = [];
  currentRound.value = prevRound;

  if (battleInfo.current?.goldRound) {
    store.updateShotInfo(0, 0);
    currentBluePoint.value = battleInfo.teams?.[1]?.score ?? 0;
    currentRedPoint.value = battleInfo.teams?.[2]?.score ?? 0;
  } else {
    const latestRound = battleInfo.rounds?.[prevRound - 1];
    if (battleInfo?.shootNumber) {
      store.updateShotInfo(0, battleInfo.shootNumber);
    }

    if (latestRound) {
      currentBluePoint.value = latestRound.scores?.[1]?.score ?? 0;
      currentRedPoint.value = latestRound.scores?.[2]?.score ?? 0;
    } else {
      currentBluePoint.value = 0;
      currentRedPoint.value = 0;
    }
  }

  return prevRound;
}

// 回填比赛基础信息：队伍、比分、轮次、金箭状态等公共字段都在这里统一处理。
function applyBattleBase(battleInfo) {
  if (!battleInfo) return;
  if (battleInfo.matchId) battleId.value = battleInfo.matchId;
  if (battleInfo.status !== undefined) {
    start.value = battleInfo.status !== 0;
    matchStatus.value = battleInfo.status;
    if (battleInfo.status !== 2 && battleInfo.status !== 4) {
      battleEnded = false;
    }
  }
  updateTeams(battleInfo);
  shootTimeTotal.value = getShootTimeSeconds(battleInfo);
  roundResults.value = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
  bluePoints.value = battleInfo.teams?.[1]?.score ?? bluePoints.value;
  redPoints.value = battleInfo.teams?.[2]?.score ?? redPoints.value;
  isFinalShoot.value = !!battleInfo.current?.goldRound;
  updateGoldenRound(battleInfo);
}

// 同步顶部射箭进度到全局 store，供 HeaderProgress 在重进页面时直接恢复显示。
function updateShotInfo(battleInfo) {
  if (battleInfo?.status === 0 || battleInfo?.current?.goldRound) {
    store.updateShotInfo(0, 0);
    return;
  }
  const totalShot = Number(battleInfo?.shootNumber || 0);
  if (!totalShot) return;

  const indexMap = battleInfo?.current?.indexMap || {};
  const userId = user.value.id;
  const myShot = indexMap[userId] ?? indexMap[String(userId)];
  store.updateShotInfo(myShot ?? 0, totalShot);
}

// 从当前轮数据中拆出红蓝两队箭点，驱动靶面重新渲染。
function updateCurrentRoundScores(battleInfo) {
  const round = battleInfo?.current?.round || currentRound.value;
  const latestRound = battleInfo?.rounds?.[round - 1];
  if (!latestRound) {
    scores.value = [];
    blueScores.value = [];
    return;
  }
  blueScores.value = latestRound.shoots?.[1] || [];
  scores.value = latestRound.shoots?.[2] || [];
}

function resolveTargetTeam(battleInfo) {
  const playerId = battleInfo?.current?.playerId;
  const redPlayers = battleInfo?.teams?.[2]?.players || redTeam.value;
  const isRed = redPlayers.some((item) => String(item.id) === String(playerId));
  return isRed ? "red" : "blue";
}

function resolveShotTeam(battleInfo) {
  const playerId =
    battleInfo?.shootData?.playerId ?? battleInfo?.current?.playerId ?? currentShooterId.value;
  const redPlayers = battleInfo?.teams?.[2]?.players || redTeam.value;
  const isRed = redPlayers.some((item) => String(item.id) === String(playerId));
  return isRed ? "red" : "blue";
}

// 生成页面和顶部进度条展示的射手提示文案。
function buildShooterTips(battleInfo, team) {
  let nextTips = team === "red" ? "请红队射箭" : "请蓝队射箭";
  if (String(battleInfo?.current?.playerId) === String(user.value.id)) {
    nextTips += "你";
  }
  return nextTips;
}

function updateTips(nextTips) {
  tips.value = nextTips;
  store.updateTips(nextTips);
  uni.$emit("update-tips", nextTips);
}

function getShooterAudioKey(battleInfo, team) {
  if (String(battleInfo?.current?.playerId) === String(user.value.id)) return "轮到你了";
  return team === "red" ? "请红方射箭" : "请蓝方射箭";
}

function getRoundAudioKey(battleInfo) {
  if (battleInfo?.current?.goldRound) return "决金箭轮";
  const round = battleInfo?.current?.round || currentRound.value || 1;
  return `第${ROUND_AUDIO_NAMES[Math.max(0, round - 1)] || round}轮`;
}

function getShootTimeSeconds(battleInfo) {
  const total = Number(battleInfo?.shootTime ?? shootTimeTotal.value ?? DEFAULT_SHOOT_TIME);
  if (!Number.isFinite(total) || total <= 0) return DEFAULT_SHOOT_TIME;
  return total;
}

function normalizeRemainSeconds(value, total) {
  const numberValue = Number(value);
  if (!Number.isFinite(numberValue)) return null;
  if (numberValue <= 0) return 0;
  // 兼容后端返回毫秒的剩余时间字段，比如 8000 表示 8 秒。
  const seconds = numberValue > total + 1 ? numberValue / 1000 : numberValue;
  return Math.max(0, Math.min(total, seconds));
}

// 读取后端可能返回的各种剩余时间字段，兼容秒和毫秒两种格式。
function getBackendRemainingSeconds(battleInfo, total) {
  const current = battleInfo?.current || {};
  const candidates = [
    current.remainingSeconds,
    current.remainSeconds,
    current.remainingSecond,
    current.remainSecond,
    current.remainingTime,
    current.remainTime,
    current.leftSeconds,
    current.leftTime,
    current.timeLeft,
    current.countdown,
    current.countDown,
    battleInfo?.remainingSeconds,
    battleInfo?.remainSeconds,
    battleInfo?.remainingTime,
    battleInfo?.remainTime,
  ];

  for (const item of candidates) {
    const remain = normalizeRemainSeconds(item, total);
    if (remain !== null) return remain;
  }
  return null;
}

// 计算当前倒计时剩余秒数：后端字段优先，其次按开始时间和当前时间补算。
function getRemainingSeconds(battleInfo, task, options = {}) {
  const total = getShootTimeSeconds(battleInfo);
  const backendRemain = getBackendRemainingSeconds(battleInfo, total);
  if (backendRemain !== null) return backendRemain;
  // 新轮首箭由后端 shootTime 定完整时长；没有明确剩余时间时，不扣前端轮次弹窗/语音耗时。
  if (options.fullDurationIfNoBackendRemain) return total;

  const startTime = normalizeTimestamp(battleInfo?.current?.startTime);
  if (!startTime) return total;

  // 后端时间是准的，前端只根据语音耗时和接收延迟做近似补偿。
  // 但回前台的 API 快照不一定带“当前后端时间”，恢复场景优先用本机当前时间计算最新剩余秒数。
  const anchorTime = task?.preferClientNow
    ? Date.now()
    : task?.serverTime || getServerTime(battleInfo) || Date.now();
  const waitAfterReceive = task?.serverTime ? Date.now() - task.receivedAt : 0;
  const effectiveNow = anchorTime + waitAfterReceive;
  const elapsed = Math.max(0, (effectiveNow - startTime) / 1000);
  return Math.max(0, Math.min(total, total - elapsed));
}

// 将真实剩余时间转换成进度条组件需要的显示秒数、tick 间隔和总时长。
function getCountdownPayload(remainingSeconds, displaySeconds = remainingSeconds) {
  const remain = Math.max(0, Number(remainingSeconds) || 0);
  if (remain <= 0) return { value: 0, tickMs: 1000, durationMs: 0 };

  // displaySeconds 控制展示出来的秒数，remainingSeconds 控制真实要跑多久。
  // 新一轮射箭时按 shootTime 展示，再把真实剩余时间平均摊到每一秒上。
  const showSeconds = Math.max(1, Math.ceil(Number(displaySeconds) || remain));
  const tickMs = Math.max(PROGRESS_MIN_TICK_MS, Math.round((remain * 1000) / showSeconds));
  return { value: showSeconds, tickMs, durationMs: tickMs * showSeconds };
}

// 重进比赛或切换射手时，先重置进度条，再启动新的倒计时。
function resetAndStartProgress(battleInfo, task, team) {
  const remainingSeconds = getRemainingSeconds(battleInfo, task);
  const countdown = getCountdownPayload(remainingSeconds);
  markProgressDeadline(countdown, PROGRESS_START_DELAY);

  nextTick(() => {
    uni.$emit("update-remain", { reset: true, value: countdown.value, team });
    setTimeout(() => {
      uni.$emit("update-remain", {
        stop: false,
        value: countdown.value,
        tickMs: countdown.tickMs,
        team,
      });
    }, PROGRESS_START_DELAY);
  });
}

// 没有当前射手时，停止进度条，避免页面继续跑旧的倒计时。
function stopProgressAfterMount() {
  progressDeadlineAt = 0;
  clearProgressZeroWaiters();
  nextTick(() => {
    setTimeout(() => {
      uni.$emit("update-remain", { stop: true });
    }, PROGRESS_START_DELAY);
  });
}

// 待开局状态：清理比赛态展示，并恢复准备倒计时。
function applyReadyState(battleInfo) {
  hideRestoreLoading();
  start.value = false;
  showRoundTip.value = false;
  currentShooterId.value = 0;
  scores.value = [];
  blueScores.value = [];
  store.updateShotInfo(0, 0);
  updateTips("");
  progressDeadlineAt = 0;
  clearProgressZeroWaiters();
  cancelRoundTipDisplay();

  const createTime = normalizeTimestamp(battleInfo?.createTime || Date.now());
  const readyElapsed = (Date.now() - createTime) / 1000;
  if (readyElapsed > 0 && readyElapsed < READY_SECONDS) {
    setTimeout(() => {
      uni.$emit("update-timer", READY_SECONDS - readyElapsed - 0.2);
    }, READY_TIMER_EMIT_DELAY);
  }
}

// 快照恢复入口：只把页面拉到服务端最新状态，不重放已经发生过的语音。
function applyBattleSnapshot(battleInfo, { restore = false, restoreEventType = 0 } = {}) {
  // 快照恢复只负责“把页面拉回最新状态”，不重放历史语音。
  applyBattleBase(battleInfo);
  if (battleInfo.status === 0) {
    applyReadyState(battleInfo);
    return;
  }

  start.value = true;
  showRoundTip.value = false;

  if (restore && restoreEventType === MESSAGETYPESV2.NewRound) {
    applyRestoreNewRoundSnapshot(battleInfo);
    return;
  }

  updateShotInfo(battleInfo);
  updateCurrentRoundScores(battleInfo);

  const current = battleInfo.current || {};
  currentRound.value = current.round || currentRound.value || 1;
  currentShooterId.value = current.playerId || 0;

  if (!current.playerId) {
    stopProgressAfterMount();
    return;
  }

  const targetTeam = resolveTargetTeam(battleInfo);
  updateTips(buildShooterTips(battleInfo, targetTeam));

  if (restore) {
    resetAndStartProgress(
      battleInfo,
      { preferClientNow: true, receivedAt: Date.now() },
      targetTeam
    );
  }
}

// 开局任务：切换到正式比赛态，并播报“比赛开始”。
async function runBattleStartTask(task, runId) {
  // 开赛任务只负责切换正式态并播“比赛开始”，后续进入队列顺序。
  clearXRingStreaks();
  applyBattleBase(task.message);
  start.value = true;
  pendingRoundAudio = true;
  updateShotInfo(task.message);
  await playAudioKeys("比赛开始", { interrupt: false });
  if (!isQueueAlive(runId)) return;
}

// 新射手任务：切换当前射手、播报提示，并启动这一箭的倒计时。
async function runToSomeoneShootTask(task, runId) {
  // 新射手任务：先等上一轮倒计时归零，再更新展示、播轮次/射手语音、启动倒计时。
  const battleInfo = task.message;
  const shouldEnterImmediately = showRoundTip.value;
  if (shouldEnterImmediately) {
    cancelRoundTipDisplay();
    progressDeadlineAt = 0;
    clearProgressZeroWaiters();
  } else {
    await waitForProgressZero();
  }
  if (!isQueueAlive(runId)) return;

  applyBattleBase(battleInfo);
  start.value = true;
  hideRestoreLoading();
  cancelRoundTipDisplay();
  updateShotInfo(battleInfo);

  const current = battleInfo.current || {};
  currentRound.value = current.round || currentRound.value || 1;
  currentShooterId.value = current.playerId || 0;
  if (!current.playerId) {
    stopProgressAfterMount();
    return;
  }

  const targetTeam = resolveTargetTeam(battleInfo);
  const nextTips = buildShooterTips(battleInfo, targetTeam);
  updateTips(nextTips);

  const audioKeys = [];
  const isRoundFirstShoot = pendingRoundAudio;
  if (isRoundFirstShoot) {
    audioKeys.push(getRoundAudioKey(battleInfo));
    pendingRoundAudio = false;
  }
  audioKeys.push(getShooterAudioKey(battleInfo, targetTeam));

  const audioPromise = playAudioKeys(audioKeys, { interrupt: false });
  nextTick(() => {
    uni.$emit("update-remain", {
      zeroThenReset: true,
      value: shootTimeTotal.value,
      team: targetTeam,
    });
  });

  await audioPromise;
  if (!isQueueAlive(runId)) return;

  const remainingSeconds = getRemainingSeconds(battleInfo, task, {
    fullDurationIfNoBackendRemain: isRoundFirstShoot,
  });
  const countdown = getCountdownPayload(remainingSeconds, shootTimeTotal.value);
  markProgressDeadline(countdown);
  uni.$emit("update-remain", {
    stop: false,
    value: countdown.value,
    tickMs: countdown.tickMs,
    team: targetTeam,
  });
}

function updateXRingStreak(shooterId, isXRing) {
  if (!shooterId) return false;
  const id = String(shooterId);
  if (!isXRing) {
    xRingStreaks.value[id] = 0;
    saveXRingStreaks();
    return false;
  }

  xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
  if (xRingStreaks.value[id] < 3) {
    saveXRingStreaks();
    return false;
  }
  xRingStreaks.value[id] = 0;
  saveXRingStreaks();
  return true;
}

function buildShootResultAudioKeys(shootData) {
  if (!shootData) return [];
  const audioKeys = [
    shootData.ring ? `${shootData.ringX ? "X" : shootData.ring}环` : "未上靶",
  ];
  if (shootData.angle !== null && shootData.angle !== undefined) {
    audioKeys.push(`向${getDirectionText(shootData.angle)}调整`);
  }
  return audioKeys;
}

// 报靶任务：更新箭点、比分和报靶语音，不主动切换轮次。
async function runShootResultTask(task) {
  // 报靶任务只更新箭靶、分数和语音，不抢轮次提示的时序。
  const battleInfo = task.message;
  showRoundTip.value = false;
  applyBattleBase(battleInfo);
  updateShotInfo(battleInfo);
  updateCurrentRoundScores(battleInfo);
  if (battleInfo?.shootData) {
    latestShotFlash.value = {
      key: task?.key || `${battleInfo.shootData.playerId || ""}-${task?.serverTime || Date.now()}`,
      team: resolveShotTeam(battleInfo),
      shootData: battleInfo.shootData,
    };
  }

  const isTententen = updateXRingStreak(
    currentShooterId.value,
    !!(battleInfo.shootData?.ringX && battleInfo.shootData?.ring)
  );
  const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
  if (isTententen) audioKeys.push("tententen");
  await playAudioKeys(audioKeys, { interrupt: false });
}

// 新回合任务：展示上一回合结算弹窗，弹窗关闭后再允许下一轮继续。
async function runNewRoundTask(task, runId) {
  // 新回合提示要故意延后一点，避免和上一箭结果展示抢先后顺序。
  const battleInfo = task.message;
  const prevRound = task.restorePrevRound || currentRound.value;
  await new Promise((resolve) => setTimeout(resolve, ROUND_TIP_DELAY));
  if (!isQueueAlive(runId)) return;

  hideRestoreLoading();
  showRoundTip.value = true;
  isFinalShoot.value = !!battleInfo.current?.goldRound;
  scores.value = [];
  blueScores.value = [];
  roundTipRound.value = prevRound;
  roundResults.value = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : roundResults.value;
  bluePoints.value = battleInfo.teams?.[1]?.score ?? bluePoints.value;
  redPoints.value = battleInfo.teams?.[2]?.score ?? redPoints.value;
  updateGoldenRound(battleInfo);

  if (battleInfo.current?.goldRound) {
    store.updateShotInfo(0, 0);
  } else if (battleInfo?.shootNumber) {
    // 私有 HeaderProgress 不再直接消费 socket，普通新回合由页面主动归零箭数。
    store.updateShotInfo(0, battleInfo.shootNumber);
  }

  const latestRound = battleInfo.rounds?.[prevRound - 1];
  if (latestRound) {
    if (battleInfo.current?.goldRound) {
      currentBluePoint.value = battleInfo.teams?.[1]?.score ?? 0;
      currentRedPoint.value = battleInfo.teams?.[2]?.score ?? 0;
    } else {
      currentBluePoint.value = latestRound.scores?.[1]?.score ?? 0;
      currentRedPoint.value = latestRound.scores?.[2]?.score ?? 0;
    }
  } else {
    currentBluePoint.value = 0;
    currentRedPoint.value = 0;
  }
  pendingRoundAudio = true;
}

// 终局任务：播放结束语音后，根据状态跳结果页或返回上一页。
async function runBattleEndTask(task, runId) {
  const battleInfo = task.message;
  applyBattleBase(battleInfo);
  battleEnded = true;
  clearXRingStreaks();
  matchStatus.value = battleInfo.status;
  if (battleInfo.status === 4) {
    showRoundTip.value = true;
    currentBluePoint.value = 0;
    currentRedPoint.value = 0;
  }

  // 终局语音必须完整播完，再决定跳转或返回。
  await playAudioKeys("比赛结束", { interrupt: false, timeout: AUDIO_TIMEOUT_MAX });
  if (!isQueueAlive(runId)) return;

  if (matchStatus.value === 2) {
    uni.redirectTo({
      url: `/pages/friend-battle-result?battleId=${battleId.value}`,
    });
  } else if (matchStatus.value === 4) {
    setTimeout(() => {
      uni.navigateBack();
    }, BATTLE_CANCEL_RETURN_DELAY);
  }
}

// 无效射击任务：弹 toast 并播报无效射击，不修改比赛主状态。
async function runInvalidShotTask(runId) {
  if (!isQueueAlive(runId)) return;
  uni.showToast({
    title: "距离不足，无效",
    icon: "none",
  });
  await playAudioKeys("射击无效", { interrupt: false });
}

// 回前台恢复入口：拉取服务端快照，处理结束态，然后继续消费增量队列。
async function restoreLatestBattle() {
  if (!battleId.value) return;
  const currentRestoreId = ++restoreGeneration;
  showRestoreLoading();

  await new Promise((resolve) => setTimeout(resolve, RESTORE_DELAY));
  if (currentRestoreId !== restoreGeneration) return;

  let result = null;
  try {
    result = await getBattleAPI(battleId.value);
  } catch (err) {
    console.log("restore latest battle failed:", err);
  }
  if (currentRestoreId !== restoreGeneration) return;
  if (!result) {
    hideRestoreLoading();
    runBattleQueue();
    return;
  }

  const snapshotTime = getServerTime(result);
  if (snapshotTime) {
    latestSnapshotServerTime.value = Math.max(latestSnapshotServerTime.value, snapshotTime);
    latestAppliedServerTime.value = Math.max(latestAppliedServerTime.value, snapshotTime);
  }

  const restoreEventType = Number(result?.eventType || 0);

  if (result.status === 2) {
    clearXRingStreaks();
    hideRestoreLoading();
    uni.redirectTo({
      url: `/pages/friend-battle-result?battleId=${result.matchId}`,
    });
    return;
  }
  if (result.status === 4) {
    clearXRingStreaks();
  }

  if (restoreEventType === MESSAGETYPESV2.NewRound) {
    const prevRound = getRestorePrevRound(result);
    const restoreMessage = { ...result, type: MESSAGETYPESV2.NewRound };
    const restoreKey = getMessageKey(restoreMessage);
    applyBattleSnapshot(result, { restore: true, restoreEventType });

    if (!handledMessageKeys.has(restoreKey) && !queuedMessageKeys.has(restoreKey)) {
      queuedMessageKeys.add(restoreKey);
      battleQueue.value.unshift({
        message: result,
        type: MESSAGETYPESV2.NewRound,
        key: restoreKey,
        serverTime: snapshotTime,
        receivedAt: Date.now(),
        order: ++queueOrder,
        restorePrevRound: prevRound,
      });
    }

    runBattleQueue();
    return;
  }

  applyBattleSnapshot(result, { restore: true, restoreEventType });
  runBattleQueue();
}

function scheduleRestoreLatestBattle() {
  if (pendingRestoreTimer) {
    clearTimeout(pendingRestoreTimer);
    pendingRestoreTimer = null;
  }

  if (battleId.value) {
    restoreLatestBattle();
    return;
  }

  pendingRestoreTimer = setTimeout(() => {
    pendingRestoreTimer = null;
    if (battleId.value) restoreLatestBattle();
  }, RESTORE_EMPTY_ID_RETRY_DELAY);
}

// 页面统一的 socket 回调，只负责把消息送进战况队列。
function onReceiveMessage(message) {
  enqueueBattleMessage(message);
}

// 新对局入口：彻底清空上一局残留的状态、队列和缓存。
onLoad((options) => {
  console.log('重新进入了')
  // 新对局入口：把所有会串场的状态、队列、时间戳和缓存一次性清空。
  start.value = null;
  tips.value = "";
  battleId.value = options.battleId || "";
  currentRound.value = 0;
  roundTipRound.value = 0;
  goldenRound.value = 0;
  currentRedPoint.value = 0;
  currentBluePoint.value = 0;
  scores.value = [];
  blueScores.value = [];
  redTeam.value = [];
  blueTeam.value = [];
  currentShooterId.value = 0;
  roundResults.value = [];
  redPoints.value = 0;
  bluePoints.value = 0;
  showRoundTip.value = false;
  isFinalShoot.value = false;
  matchStatus.value = undefined;
  shootTimeTotal.value = DEFAULT_SHOOT_TIME;
  showOfflineModal.value = false;
  hideRestoreLoading();
  loadXRingStreaks();
  queueGeneration += 1;
  battleQueue.value = [];
  queueRunning.value = false;
  latestSnapshotServerTime.value = 0;
  latestAppliedServerTime.value = 0;
  restoreGeneration += 1;
  pendingRoundAudio = false;
  battleEnded = false;
  handledMessageKeys.clear();
  handledMessageKeyOrder.length = 0;
  queuedMessageKeys.clear();
  progressDeadlineAt = 0;
  clearProgressZeroWaiters();
  cancelRoundTipDisplay();
  store.updateShotInfo(0, 0);
  store.updateTips("");
  latestShotFlash.value = null;
  scheduleRestoreLatestBattle();
});

// 挂载后开启常亮、监听 socket 和语音事件，并关闭激光瞄准。
onMounted(async () => {
  uni.setKeepScreenOn({
    keepScreenOn: true,
  });
  uni.$on("socket-inbox", onReceiveMessage);
  uni.$on("audioEnded", onAudioEnded);
  uni.$on(AUDIO_INTERRUPTION_BEGIN_EVENT, handleBattleCovered);
  uni.$on(AUDIO_INTERRUPTION_END_EVENT, handleBattleRecovered);
  uni.$on(PROGRESS_ZERO_EVENT, onProgressZero);
  uni.$on(COUNTDOWN_READY_EVENT, hideRestoreLoading);
  await laserCloseAPI();
});

// 离开页面时清理监听、停止音频，并让当前队列整体失效。
onBeforeUnmount(() => {
  uni.setKeepScreenOn({
    keepScreenOn: false,
  });
  uni.$off("socket-inbox", onReceiveMessage);
  uni.$off("audioEnded", onAudioEnded);
  uni.$off(PROGRESS_ZERO_EVENT, onProgressZero);
  uni.$off(COUNTDOWN_READY_EVENT, hideRestoreLoading);
  if (pendingRestoreTimer) {
    clearTimeout(pendingRestoreTimer);
    pendingRestoreTimer = null;
  }
  hideRestoreLoading();
  invalidateBattleQueue({ stopAudio: true, stopProgress: true });
  console.log('onBeforeUnmount', '页面卸载前')
  audioManager.stopAll();
  uni.$off(AUDIO_INTERRUPTION_BEGIN_EVENT, handleBattleCovered);
  uni.$off(AUDIO_INTERRUPTION_END_EVENT, handleBattleRecovered);
});

onHide(()=>{
  console.log('onHide', '页面大退')
  handleBattleCovered();
})

// 每次回到前台都重新拉最新比赛快照，确保画面与后端一致。
onShow(() => {
  console.log('onshow')
  scheduleRestoreLatestBattle();
});
</script>

<template>
  <Container
      :bgType="start ? 3 : 1"
      :loading="restoreLoading"
      loadingText="正在恢复比赛..."
  >
    <view class="container">
      <!-- 未正式开局时显示等待态：头图、队伍信息和测试距离。 -->
      <BattleHeader
          v-if="start === false"
          :redTeam="redTeam"
          :blueTeam="blueTeam"
          :winner="0"
      />
      <TestDistance v-if="start === false" :guide="false" :isBattle="true"/>
      <!-- 比赛进行中显示：左右队伍、进度条、靶面和底部比分。 -->
      <view v-if="start" class="players-row">
        <TeamAvatars
            :team="blueTeam"
            :isRed="false"
            :currentShooterId="currentShooterId"
        />
        <ShootProgress2
            :tips="tips"
            :total="shootTimeTotal"
            :currentRound="
            goldenRound > 0 ? 'gold' + goldenRound : 'round' + currentRound
          "
        />
        <TeamAvatars :team="redTeam" :currentShooterId="currentShooterId"/>
      </view>
      <BowTarget
          v-if="start"
          mode="team"
          :scores="scores"
          :blueScores="blueScores"
          :latestShotFlash="latestShotFlash"
      />
      <BattleFooter
          v-if="start"
          :roundResults="roundResults"
          :redPoints="redPoints"
          :bluePoints="bluePoints"
          :goldenRound="goldenRound"
      />
      <!-- 回合结束提示层：展示上一回合结果，并在自动关闭后进入下一轮。 -->
      <ScreenHint
          :show="showRoundTip"
          :mode="isFinalShoot ? 'tall' : 'normal'"
      >
        <RoundEndTip
            v-if="showRoundTip"
            :isFinal="isFinalShoot"
            :round="roundTipRound"
            :bluePoint="currentBluePoint"
            :redPoint="currentRedPoint"
            :roundData="
            roundResults[roundTipRound - 1] ? roundResults[roundTipRound - 1] : []
          "
            :onAutoClose="handleRoundTipAutoClose"
        />
      </ScreenHint>
      <!-- 设备离线提示弹窗：前台检测到在线状态变化时展示。 -->
      <SModal
          :show="showOfflineModal"
          :noBg="true"
          height="360rpx"
          :onClose="() => (showOfflineModal = false)"
      >
        <view class="offline-modal">
          <text class="offline-title">设备已离线</text>
          <text class="offline-desc">检测到设备已断开连接，请检查设备后继续比赛</text>
          <SButton @click="showOfflineModal = false">我知道了</SButton>
        </view>
      </SModal>
    </view>
  </Container>
</template>

<style scoped>
.container {
  width: 100%;
  height: 100%;
}

.players-row {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: -2%;
  margin-bottom: 6%;
}

.offline-modal {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 60rpx 40rpx 40rpx;
  gap: 24rpx;
}

.offline-title {
  font-size: 36rpx;
  font-weight: bold;
  color: #FED847;
}

.offline-desc {
  font-size: 28rpx;
  color: #CCCCCC;
  text-align: center;
  line-height: 1.6;
}
</style>
