<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
import { onHide, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ShootProgress from "./components/ShootProgress.vue";
import BowTarget from "./components/BowTarget.vue";
import ScorePanel2 from "./components/ScorePanel2.vue";
import ScoreResult from "./components/ScoreResult.vue";
import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue";
import TestDistance from "./components/TestDistance.vue";
import BubbleTip from "./components/BubbleTip.vue";
import audioManager from "@/audioManager";

import {
  createPractiseV2API,
  getCurrentPractiseAPI,
  startPractiseAPI,
  endPractiseAPI,
  getPractiseAPI,
  getTrainingDifficultyListAPI,
} from "@/apis";
import {
  connectMatchWebSocket,
  closeMatchWebSocket,
  setMatchAppHideResumable,
  MATCH_WS_PRACTICE_SYNC_EVENT,
  MATCH_WS_STATE_EVENT,
} from "@/matchWebsocket";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce, getDirectionText } from "@/util";
import { MESSAGETYPESV2, roundsName } from "@/constants";

import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);

const sound = ref(true);
const start = ref(false);
const pageStages = Object.freeze({
  DISTANCE: "distance",
  SHOOTING: "shooting",
  RESULT: "result",
  LOADING: "loading",
});
const pageStage = ref(pageStages.LOADING);
const scores = ref([]);
// 只在实时 ShootResult 新增一箭时递增，避免同步快照重播飞箭特效。
const shotEffectToken = ref(0);
const defaultTotal = 12;
const defaultTargetType = 1;
const total = ref(defaultTotal);
const practiseResult = ref({});
const practiceEndSnapshot = ref({});
const hasNextDifficulty = ref(false);
const practiseId = ref("");
const showGuide = ref(false);
const tips = ref("");
const targetType = ref(defaultTargetType);
const trainingParams = ref({});
const practiceInfo = ref({});
// 服务端状态立即落到 practiceInfo，精准训练的目标区域单独延迟展示。
const visiblePrecisionTarget = ref({
  randomBlock: 0,
  randomRingArea: 0,
});
const trainingDifficultyStorageKey = "training-selection";
const useHighlightTest = ref(false);
const highlightTestState = ref({
  blocks: 8,
  randomBlock: 1,
  randomRingArea: 0,
});
const highlightTestTimer = ref(null);
const serverAddr = ref("");
const practiceEnded = ref(false);
const stopCompleted = ref(false);
const stopInFlight = ref(false);
const exiting = ref(false);
const hiddenWhileActive = ref(false);
const resumeInFlight = ref(false);
const foregroundResumeSyncPending = ref(false);
const pageVisible = ref(true);
const appHideResumable = ref(false);
const connectionClosed = ref(true);
let stopPracticeTask = null;
let practiceSyncTimer = null;
let waitingPracticeSync = false;
let shotPresentationGeneration = 0;
let nextDifficultyRequest = null;
let nextDifficultyRequestGeneration = 0;
const audioWaiters = new Set();
const shotEffectWaiters = new Map();
const PRACTICE_SYNC_TIMEOUT_MS = 5000;
const SHOT_EFFECT_WAIT_TIMEOUT_MS = 1200;
const AUDIO_TIMEOUT_BASE = 3500;
const AUDIO_TIMEOUT_PER_KEY = 2600;
const AUDIO_TIMEOUT_MAX = 12000;

const env = computed(() => {
  try {
    return uni.getAccountInfoSync().miniProgram.envVersion;
  } catch (error) {
    return "release";
  }
});

const isDistanceStage = computed(() => pageStage.value === pageStages.DISTANCE);
const isShootingStage = computed(() => pageStage.value === pageStages.SHOOTING);
const hasPractiseResult = computed(() => !!practiseResult.value?.details);
const showResult = computed(
  () => pageStage.value === pageStages.RESULT && hasPractiseResult.value
);
const isSvip = computed(() => practiceInfo.value.sVip === true);
const isVip = computed(
  () => practiceInfo.value.vip === true && !isSvip.value
);

const setPracticeAppHideResumable = (enabled) => {
  const nextValue = enabled === true;
  appHideResumable.value = nextValue;
  setMatchAppHideResumable(nextValue);
};

const trainingType = computed(
  () => practiceInfo.value.trainingType || trainingParams.value.type || ""
);

const getPracticeNumber = (value, fallback = 0) => {
  if (value === undefined || value === null || value === "") return fallback;
  const numberValue = Number(value);
  return Number.isFinite(numberValue) ? numberValue : fallback;
};

const getPositiveInteger = (value) => {
  const numberValue = Number(value);
  return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
};

const getPrecisionTargetSnapshot = (source = {}) => ({
  randomBlock: getPositiveInteger(source.randomBlock),
  randomRingArea: getPositiveInteger(source.randomRingArea),
});

const applyVisiblePrecisionTarget = (source = {}) => {
  visiblePrecisionTarget.value = getPrecisionTargetSnapshot(source);
};

const currentDifficultyLevel = computed(
  () =>
    getPositiveInteger(practiseResult.value.difficultyLevel) ||
    getPositiveInteger(practiseResult.value.difficulty_level) ||
    getPositiveInteger(practiceInfo.value.difficultyLevel) ||
    getPositiveInteger(trainingParams.value.difficulty)
);

const resetNextDifficultyState = () => {
  nextDifficultyRequestGeneration += 1;
  nextDifficultyRequest = null;
  hasNextDifficulty.value = false;
};

const loadNextDifficultyState = (result = {}) => {
  const requestGeneration = ++nextDifficultyRequestGeneration;
  const currentLevel =
    getPositiveInteger(result.difficultyLevel) ||
    getPositiveInteger(result.difficulty_level) ||
    currentDifficultyLevel.value;
  const currentTrainingType = result.trainingType || trainingType.value;

  hasNextDifficulty.value = false;
  if (!currentTrainingType || !currentLevel) {
    nextDifficultyRequest = Promise.resolve(false);
    return nextDifficultyRequest;
  }

  nextDifficultyRequest = getTrainingDifficultyListAPI(currentTrainingType)
    .then((difficultyResult) => {
      const levels = Array.isArray(difficultyResult?.list)
        ? difficultyResult.list
            .filter(
              (item) => !item?.type || item.type === currentTrainingType
            )
            .map((item) => getPositiveInteger(item?.difficulty))
            .filter(Boolean)
        : [];
      const maxLevel = Math.max(0, ...levels);
      const highestCompletedLevel = getPositiveInteger(
        difficultyResult?.user_levels?.[currentTrainingType]
      );
      const latestUnlockedLevel = Math.min(
        highestCompletedLevel + 1,
        maxLevel
      );
      const canAdvance =
        currentLevel < maxLevel && latestUnlockedLevel > currentLevel;

      if (requestGeneration === nextDifficultyRequestGeneration) {
        hasNextDifficulty.value = canAdvance;
      }
      return canAdvance;
    })
    .catch((error) => {
      console.log("training next difficulty load failed", error);
      if (requestGeneration === nextDifficultyRequestGeneration) {
        hasNextDifficulty.value = false;
      }
      return false;
    });

  return nextDifficultyRequest;
};

// time_limit 缺失或非正数都表示整局不限时。
const timeLimit = computed(() => getPositiveInteger(practiceInfo.value.timeLimit));
const hasTimeLimit = computed(() => timeLimit.value > 0);

const precisionBlocks = computed(() => {
  if (useHighlightTest.value) {
    return getPositiveInteger(highlightTestState.value.blocks);
  }
  if (trainingType.value !== "precision") return 0;

  return (
    getPositiveInteger(practiceInfo.value.blocks) ||
    getPositiveInteger(trainingParams.value.blocks)
  );
});

const precisionRandomBlock = computed(() => {
  const block = getPositiveInteger(
    useHighlightTest.value
      ? highlightTestState.value.randomBlock
      : visiblePrecisionTarget.value.randomBlock
  );
  return block <= precisionBlocks.value ? block : 0;
});

const precisionRandomRingArea = computed(() => {
  const ring = getPositiveInteger(
    useHighlightTest.value
      ? highlightTestState.value.randomRingArea
      : visiblePrecisionTarget.value.randomRingArea
  );
  return ring >= 1 && ring <= 10 ? ring : 0;
});

// 只展示后端进度，不在前端重复判断训练是否完成。
const trainingCopy = computed(() => {
  if (trainingType.value === "base") {
    const hitReq = getPracticeNumber(
      practiceInfo.value.hitReq,
      trainingParams.value.hitReq
    );
    const arrowsLeft = getPracticeNumber(
      practiceInfo.value.arrowsLeft,
      total.value
    );

    return {
      title: `每箭命中${hitReq}环之上`,
      details: [
        { text: "剩余" },
        { text: arrowsLeft, highlight: true },
        { text: "箭达到条件" },
      ],
    };
  }

  if (trainingType.value === "endurance") {
    const targetArrows = getPracticeNumber(
      practiceInfo.value.targetArrows,
      total.value
    );
    const targetRings = getPracticeNumber(
      practiceInfo.value.targetRings,
      trainingParams.value.totalReq
    );
    const currentArrows = getPracticeNumber(practiceInfo.value.currentArrows);
    const currentRings = getPracticeNumber(practiceInfo.value.currentRings);

    return {
      title: `完成${targetArrows}箭并累计${targetRings}环`,
      details: [
        { text: "已完成" },
        { text: currentArrows, highlight: true },
        { text: "箭，累计" },
        { text: currentRings, highlight: true },
        { text: "环" },
      ],
    };
  }

  if (trainingType.value === "precision") {
    const block = precisionRandomBlock.value;
    const ring = precisionRandomRingArea.value;
    const arrowsLeft = getPracticeNumber(
      practiceInfo.value.arrowsLeft,
      total.value
    );
    const title = block
      ? ring
        ? `请命中区域${block}的${ring}环`
        : `请命中区域${block}`
      : "等待目标区域";

    return {
      title,
      details: [
        { text: "剩余" },
        { text: arrowsLeft, highlight: true },
        { text: "箭" },
      ],
    };
  }

  return null;
});

const toRouteNumber = (value, fallback = 0) => {
  const numberValue = Number(value);
  return Number.isFinite(numberValue) ? numberValue : fallback;
};

const toPositiveRouteNumber = (value, fallback) => {
  const numberValue = toRouteNumber(value, fallback);
  return numberValue > 0 ? numberValue : fallback;
};

const practiceInfoFields = [
  "id",
  "userId",
  "status",
  "statusText",
  "startTime",
  "targetType",
  "vip",
  "sVip",
  "trainingType",
  "difficultyLevel",
  "hitReq",
  "arrowsLeft",
  "targetArrows",
  "targetRings",
  "currentArrows",
  "currentRings",
  "blocks",
  "randomBlock",
  "randomRingArea",
  "timeLimit",
  "completed",
  "totalArrows",
  "duration",
  "averageRing",
  "stability",
  "maxCombo",
  "totalHits",
  "deltaTotalHits",
  "deltaDuration",
  "deltaMaxCombo",
  "deltaTotalRings",
  "deltaTotalArrows",
  "deltaAverageRing",
  "deltaStability",
  "beforeExp",
  "beforeLevel",
  "currentExp",
  "level",
  "upgradeExp",
  "calories",
  "shootData",
  "details",
];

const practiceResultFields = [
  "trainingType",
  "difficultyLevel",
  "completed",
  "totalArrows",
  "duration",
  "averageRing",
  "stability",
  "maxCombo",
  "totalHits",
  "currentRings",
  "deltaTotalHits",
  "deltaDuration",
  "deltaMaxCombo",
  "deltaTotalRings",
  "deltaTotalArrows",
  "deltaAverageRing",
  "deltaStability",
  "beforeExp",
  "beforeLevel",
  "currentExp",
  "level",
  "upgradeExp",
  "calories",
  "details",
];

const syncPracticeInfo = (message = {}) => {
  const nextInfo = practiceInfoFields.reduce((result, field) => {
    if (Object.prototype.hasOwnProperty.call(message, field)) {
      result[field] = message[field];
    }
    return result;
  }, {});

  const isPrecisionSnapshot =
    (message.type === MESSAGETYPESV2.BattleStart ||
      message.type === MESSAGETYPESV2.ShootResult) &&
    (message.trainingType === "precision" || trainingType.value === "precision");
  if (isPrecisionSnapshot) {
    // proto3 会省略数值 0；新快照未携带时必须清除上一箭的随机目标。
    if (!Object.prototype.hasOwnProperty.call(message, "randomBlock")) {
      nextInfo.randomBlock = 0;
    }
    if (!Object.prototype.hasOwnProperty.call(message, "randomRingArea")) {
      nextInfo.randomRingArea = 0;
    }
  }

  if (Object.keys(nextInfo).length === 0) return;
  practiceInfo.value = {
    ...practiceInfo.value,
    ...nextInfo,
  };
};

const buildShootResultAudioKeys = (message = {}) => {
  const latestDetail =
    Array.isArray(message.details) && message.details.length > 0
      ? message.details[message.details.length - 1]
      : null;
  // 与原 ShootProgress 保持一致：优先使用当前箭，details 仅作兼容兜底。
  const arrow = message.shootData || latestDetail;
  if (!arrow) return [];
  if (
    arrow.playerId !== undefined &&
    arrow.playerId !== null &&
    String(arrow.playerId) !== String(user.value?.id)
  ) {
    return [];
  }

  const keys = [
    arrow.ring ? `${arrow.ringX ? "X" : arrow.ring}环` : "未上靶",
  ];
  if (arrow.angle !== null && arrow.angle !== undefined) {
    keys.push(`向${getDirectionText(arrow.angle)}调整`);
  }
  if (arrow.threeConsecutive10Rings === true) {
    keys.push("tententen");
  }
  return keys;
};

const playAudioKeysAndWait = (keys) => {
  const audioKeys = (Array.isArray(keys) ? keys : [keys]).filter(Boolean);
  if (audioKeys.length === 0) return Promise.resolve();

  const expectedKey = audioKeys[audioKeys.length - 1];
  const waitTime = Math.min(
    AUDIO_TIMEOUT_MAX,
    Math.max(AUDIO_TIMEOUT_BASE, audioKeys.length * AUDIO_TIMEOUT_PER_KEY)
  );

  return new Promise((resolve) => {
    let settled = false;
    let timer = null;
    const waiter = {
      expectedKey,
      done: () => {
        if (settled) return;
        settled = true;
        if (timer) clearTimeout(timer);
        audioWaiters.delete(waiter);
        resolve();
      },
    };

    timer = setTimeout(() => {
      if (typeof audioManager.recoverIfStale === "function") {
        audioManager.recoverIfStale(expectedKey);
      }
      waiter.done();
    }, waitTime);
    audioWaiters.add(waiter);

    try {
      audioManager.play(audioKeys, false);
    } catch (error) {
      console.error("training shoot result audio failed", error);
      waiter.done();
    }
  });
};

const shouldWaitForShotEffect = (shot) => {
  const x = Number(shot?.x);
  const y = Number(shot?.y);
  return (
    isSvip.value &&
    Number(shot?.ring) > 0 &&
    Number.isFinite(x) &&
    Number.isFinite(y)
  );
};

const waitForShotEffect = (token, shouldWait) => {
  if (!shouldWait) return Promise.resolve();

  return new Promise((resolve) => {
    let settled = false;
    let timer = null;
    const waiter = {
      done: () => {
        if (settled) return;
        settled = true;
        if (timer) clearTimeout(timer);
        if (shotEffectWaiters.get(token) === waiter) {
          shotEffectWaiters.delete(token);
        }
        resolve();
      },
    };

    timer = setTimeout(waiter.done, SHOT_EFFECT_WAIT_TIMEOUT_MS);
    shotEffectWaiters.set(token, waiter);
  });
};

const invalidateShotPresentations = ({ resetVisible = false } = {}) => {
  shotPresentationGeneration += 1;
  Array.from(audioWaiters).forEach((waiter) => waiter.done());
  Array.from(shotEffectWaiters.values()).forEach((waiter) => waiter.done());
  if (resetVisible) {
    applyVisiblePrecisionTarget();
  }
  return shotPresentationGeneration;
};

const onShotEffectComplete = (payload = {}) => {
  const token = Number(payload?.token ?? payload);
  if (!Number.isFinite(token)) return;
  shotEffectWaiters.get(token)?.done();
};

const commitPrecisionTargetAfterPresentation = async ({
  generation,
  target,
  audioPromise,
  effectPromise,
}) => {
  await Promise.all([audioPromise, effectPromise]);
  if (
    generation !== shotPresentationGeneration ||
    practiceEnded.value ||
    !isShootingStage.value ||
    trainingType.value !== "precision"
  ) {
    return;
  }
  visiblePrecisionTarget.value = target;
};

const createPracticeEndSnapshot = (message = {}) => {
  const source = {
    ...practiceInfo.value,
    ...message,
  };
  const snapshot = practiceResultFields.reduce((result, field) => {
    if (Object.prototype.hasOwnProperty.call(source, field)) {
      result[field] = source[field];
    }
    return result;
  }, {});

  // proto3 会省略 false，PRACTICE_END 未携带 completed 时按未达标处理。
  snapshot.completed = message.completed === true;
  snapshot.trainingType = source.trainingType || trainingType.value;
  if (Array.isArray(message.details)) {
    snapshot.details = message.details;
  } else if (scores.value.length > 0) {
    snapshot.details = [...scores.value];
  } else if (Array.isArray(practiceInfo.value.details)) {
    snapshot.details = practiceInfo.value.details;
  } else {
    delete snapshot.details;
  }
  return snapshot;
};

const mergePracticeResult = (apiResult = {}) => {
  const snapshot = practiceEndSnapshot.value;
  const snapshotDetails = Array.isArray(snapshot.details)
    ? snapshot.details
    : null;
  const apiDetails = Array.isArray(apiResult.details) ? apiResult.details : null;
  const details = snapshotDetails?.length
    ? snapshotDetails
    : apiDetails || snapshotDetails || [...scores.value];

  return {
    ...apiResult,
    ...snapshot,
    details,
  };
};

const clearPracticeSyncTimer = () => {
  if (!practiceSyncTimer) return;
  clearTimeout(practiceSyncTimer);
  practiceSyncTimer = null;
};

const cancelPracticeSyncWait = () => {
  waitingPracticeSync = false;
  clearPracticeSyncTimer();
};

const preparePracticeSyncWait = () => {
  cancelPracticeSyncWait();
  waitingPracticeSync = true;
};

const startPracticeSyncTimer = () => {
  clearPracticeSyncTimer();
  waitingPracticeSync = true;
  practiceSyncTimer = setTimeout(() => {
    practiceSyncTimer = null;
    if (!waitingPracticeSync) return;
    waitingPracticeSync = false;

    if (foregroundResumeSyncPending.value) {
      foregroundResumeSyncPending.value = false;
      closePracticeConnection("training-practice-resume-timeout", {
        sendLeave: false,
      });
      uni.showToast({
        title: "训练重连失败，请重试",
        icon: "none",
      });
      return;
    }

    if (pageStage.value !== pageStages.LOADING) return;

    uni.showToast({
      title: "练习信息获取失败，请重试",
      icon: "none",
    });
    setTimeout(() => {
      void exitPractice();
    }, 500);
  }, PRACTICE_SYNC_TIMEOUT_MS);
};

const onMatchSocketState = (event = {}) => {
  if (event.state !== "open") return;
  if (
    event.matchId &&
    String(event.matchId) !== String(practiseId.value)
  ) {
    return;
  }

  // 管理器会在 open 事件后立即发送 5，从这里开始计算响应超时。
  startPracticeSyncTimer();
};

const onPracticeInfoSync = (payload = {}) => {
  const responseMatchId = String(
    payload.matchId || payload.practiceInfo?.id || ""
  );
  if (!responseMatchId || responseMatchId !== String(practiseId.value)) return;

  const snapshot = payload.practiceInfo;
  if (!snapshot || typeof snapshot !== "object") return;

  const shouldShowDistance =
    waitingPracticeSync && pageStage.value === pageStages.LOADING;
  const resumedFromForeground = foregroundResumeSyncPending.value;
  cancelPracticeSyncWait();
  foregroundResumeSyncPending.value = false;
  invalidateShotPresentations();

  // 14 是完整快照，先清空旧值，避免 proto3 省略的 0 沿用上一份状态。
  practiceInfo.value = {};
  practiceEndSnapshot.value = {};
  syncPracticeInfo(snapshot);
  applyVisiblePrecisionTarget(practiceInfo.value);
  scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];

  if (resumedFromForeground) {
    hiddenWhileActive.value = false;
  }

  if (shouldShowDistance) {
    start.value = false;
    pageStage.value = pageStages.DISTANCE;
  }
};

// 训练在难度页创建，目标页只消费连接上下文，避免进入页面后重复创建。
const getTrainingContext = () => {
  const context = uni.getStorageSync(trainingDifficultyStorageKey);
  return context && typeof context === "object" ? context : {};
};

const updateTrainingContext = (practice = {}) => {
  const context = getTrainingContext();
  uni.setStorageSync(trainingDifficultyStorageKey, {
    ...context,
    trainingType: trainingParams.value.type,
    difficultyLevel: trainingParams.value.difficulty,
    practiceId: practice.id || "",
    serverAddr: practice.serverAddr || "",
    createdAt: practice.id ? Date.now() : 0,
  });
};

const clearPracticeRuntimeContext = () => {
  const context = getTrainingContext();
  if (
    context.practiceId &&
    practiseId.value &&
    String(context.practiceId) !== String(practiseId.value)
  ) {
    return;
  }

  const {
    practiceId: _practiceId,
    serverAddr: _serverAddr,
    createdAt: _createdAt,
    ...selectionContext
  } = context;
  uni.setStorageSync(trainingDifficultyStorageKey, selectionContext);
};

const closePracticeConnection = (reason, { sendLeave = true } = {}) => {
  cancelPracticeSyncWait();
  if (connectionClosed.value) return;
  connectionClosed.value = true;
  closeMatchWebSocket({ reason, sendLeave });
};

const connectPracticeServer = ({
  resumableOnAppHide = appHideResumable.value,
} = {}) => {
  if (!practiseId.value || !String(serverAddr.value || "").trim()) {
    return false;
  }

  appHideResumable.value = resumableOnAppHide === true;
  cancelPracticeSyncWait();
  closeMatchWebSocket({ reason: "training-practice-switch" });
  preparePracticeSyncWait();
  connectMatchWebSocket({
    serverAddr: serverAddr.value,
    matchId: practiseId.value,
    userId: user.value.id,
    requestPracticeInfoOnOpen: true,
    appHideResumable: appHideResumable.value,
    practiceEndAudioKey: "练习结束",
  });
  connectionClosed.value = false;
  return true;
};

// 返回、切后台和页面销毁可能连续触发，复用同一个 stop 任务避免重复请求。
const stopCurrentPractice = () => {
  if (
    !practiseId.value ||
    practiceEnded.value ||
    stopCompleted.value
  ) {
    return Promise.resolve();
  }
  if (stopPracticeTask) return stopPracticeTask;

  stopInFlight.value = true;
  stopPracticeTask = endPractiseAPI(practiseId.value)
    .then(() => {
      stopCompleted.value = true;
      clearPracticeRuntimeContext();
    })
    .catch((error) => {
      console.error("training practice stop failed", error);
    })
    .finally(() => {
      stopInFlight.value = false;
      stopPracticeTask = null;
    });

  return stopPracticeTask;
};

// 当前训练不可恢复时，始终使用页面本地的练习 ID 停止。
const stopEndedCurrentPracticeAndExit = async () => {
  hiddenWhileActive.value = false;
  foregroundResumeSyncPending.value = false;
  setPracticeAppHideResumable(false);
  exiting.value = true;

  try {
    await stopCurrentPractice();
  } finally {
    closePracticeConnection("training-practice-current-missing");
    clearPracticeRuntimeContext();
    uni.showToast({
      title: "训练已结束，请重新进入",
      icon: "none",
    });
    uni.navigateBack();
  }
};

// 回到前台后先获取最新比赛服地址，再通过 type 5/type 14 恢复完整快照。
const resumeCurrentPractice = async () => {
  if (
    resumeInFlight.value ||
    !hiddenWhileActive.value ||
    !appHideResumable.value ||
    exiting.value
  ) {
    return;
  }

  resumeInFlight.value = true;
  try {
    let currentPractice;
    try {
      currentPractice = await getCurrentPractiseAPI();
    } catch (error) {
      if (!pageVisible.value || exiting.value) return;
      console.error("get current practice failed", error);
      await stopEndedCurrentPracticeAndExit();
      return;
    }

    console.log(1111111111111111111111, currentPractice)
    if (
      !pageVisible.value ||
      !hiddenWhileActive.value ||
      exiting.value
    ) {
      return;
    }

    const latestPracticeId = currentPractice?.id;
    const latestServerAddr = String(
      currentPractice?.serverAddr || ""
    ).trim();
    if (
      currentPractice === null ||
      !latestPracticeId ||
      !latestServerAddr
    ) {
      await stopEndedCurrentPracticeAndExit();
      return;
    }

    practiseId.value = latestPracticeId;
    serverAddr.value = latestServerAddr;
    updateTrainingContext({
      id: latestPracticeId,
      serverAddr: latestServerAddr,
    });

    foregroundResumeSyncPending.value = true;
    setPracticeAppHideResumable(true);
    if (!connectPracticeServer({ resumableOnAppHide: true })) {
      foregroundResumeSyncPending.value = false;
      await stopEndedCurrentPracticeAndExit();
    }
  } catch (error) {
    foregroundResumeSyncPending.value = false;
    if (!pageVisible.value || exiting.value) return;
    console.error("training practice resume failed", error);
    uni.showToast({
      title: "训练重连失败，请重试",
      icon: "none",
    });
  } finally {
    resumeInFlight.value = false;
  }
};

const createPractice = async () => {
  const trainingType = trainingParams.value.type;
  const difficultyLevel = trainingParams.value.difficulty;

  if (!trainingType || difficultyLevel <= 0) {
    uni.showToast({
      title: "训练参数异常，请重新选择难度",
      icon: "none",
    });
    return null;
  }

  closePracticeConnection("training-practice-recreate");
  const result = await createPractiseV2API(trainingType, difficultyLevel);

  if (!result?.id || !String(result?.serverAddr || "").trim()) {
    if (result?.id) {
      try {
        await endPractiseAPI(result.id);
      } catch (error) {
        console.error("training practice cleanup failed", error);
      }
    }
    uni.showToast({
      title: "练习连接信息异常，请重试",
      icon: "none",
    });
    return null;
  }

  practiseId.value = result.id;
  serverAddr.value = result.serverAddr;
  practiceEnded.value = false;
  stopCompleted.value = false;
  stopInFlight.value = false;
  stopPracticeTask = null;
  updateTrainingContext(result);
  connectPracticeServer({ resumableOnAppHide: true });
  return result;
};

const clearHighlightTestTimer = () => {
  if (highlightTestTimer.value) {
    clearInterval(highlightTestTimer.value);
    highlightTestTimer.value = null;
  }
};

// 开发环境测试入口：依次切换 8 个顺时针区域，偶数区域只高亮指定环。
const runHighlightTest = () => {
  clearHighlightTestTimer();
  useHighlightTest.value = true;
  practiseResult.value = {};
  pageStage.value = pageStages.SHOOTING;
  start.value = true;

  let block = 1;
  highlightTestState.value = {
    blocks: 8,
    randomBlock: block,
    randomRingArea: 0,
  };

  highlightTestTimer.value = setInterval(() => {
    if (block >= highlightTestState.value.blocks) {
      clearHighlightTestTimer();
      return;
    }

    block += 1;
    highlightTestState.value = {
      blocks: 8,
      randomBlock: block,
      randomRingArea: block % 2 === 0 ? Math.min(block, 10) : 0,
    };
  }, 1000);
};

const resetHighlightTest = () => {
  clearHighlightTestTimer();
  useHighlightTest.value = false;
  highlightTestState.value = {
    blocks: 8,
    randomBlock: 1,
    randomRingArea: 0,
  };
  scores.value = [];
};

onLoad((options = {}) => {
  const trainingContext = getTrainingContext();
  targetType.value = toPositiveRouteNumber(options.target, defaultTargetType);
  total.value = toPositiveRouteNumber(options.arrows, defaultTotal);
  trainingParams.value = {
    type: options.type || trainingContext.trainingType || "",
    difficultyId: options.difficultyId || "",
    difficulty: toRouteNumber(
      options.difficulty,
      toRouteNumber(trainingContext.difficultyLevel)
    ),
    recordId: options.recordId || "",
    hitReq: toRouteNumber(options.hitReq),
    totalReq: toRouteNumber(options.totalReq),
    blocks: toRouteNumber(options.blocks),
    mode: toRouteNumber(options.mode),
  };
  practiseId.value = trainingContext.practiceId || "";
  serverAddr.value = trainingContext.serverAddr || "";
});

const onReady = async () => {
  if (
    !practiseId.value ||
    practiceEnded.value ||
    stopCompleted.value ||
    stopInFlight.value
  ) {
    uni.showToast({
      title: "训练已结束，请重新进入",
      icon: "none",
    });
    return;
  }

  pageStage.value = pageStages.LOADING;
  clearHighlightTestTimer();
  useHighlightTest.value = false;
  practiceEndSnapshot.value = {};
  invalidateShotPresentations();
  try {
    await startPractiseAPI(practiseId.value);
    practiseResult.value = {};
    scores.value = [];
    shotEffectToken.value = 0;
    start.value = true;
    pageStage.value = pageStages.SHOOTING;
    setPracticeAppHideResumable(true);
    audioManager.play("练习开始");
  } catch (error) {
    start.value = false;
    pageStage.value = pageStages.DISTANCE;
    setPracticeAppHideResumable(true);
    throw error;
  }
};

const onTimeLimitReached = () => {
  if (!hasTimeLimit.value || !isShootingStage.value) return;
  // 本地倒计时只负责停止射击展示，最终结算以 PRACTICE_END 为准。
  start.value = false;
  hiddenWhileActive.value = false;
  setPracticeAppHideResumable(false);
};

const enterPracticeResult = (result = {}) => {
  practiseResult.value = result;
  if (!hasPractiseResult.value) return false;

  // 正常结算不调用 stop，只清理上下文并断开比赛服连接。
  practiceEnded.value = true;
  setPracticeAppHideResumable(false);
  invalidateShotPresentations();
  clearPracticeRuntimeContext();
  closePracticeConnection("training-practice-result");
  pageStage.value = pageStages.RESULT;
  return true;
};

const onOver = async () => {
  if (!isShootingStage.value) return;

  clearHighlightTestTimer();
  pageStage.value = pageStages.LOADING;
  start.value = false;
  setPracticeAppHideResumable(false);

  try {
    const apiResult = (await getPractiseAPI(practiseId.value)) || {};
    await (nextDifficultyRequest || loadNextDifficultyState(apiResult));
    if (!enterPracticeResult(mergePracticeResult(apiResult))) {
      pageStage.value = pageStages.DISTANCE;
    }
  } catch (error) {
    if (Object.keys(practiceEndSnapshot.value).length > 0) {
      await (nextDifficultyRequest || loadNextDifficultyState(practiceEndSnapshot.value));
      enterPracticeResult(mergePracticeResult());
      return;
    }
    start.value = true;
    pageStage.value = pageStages.SHOOTING;
    setPracticeAppHideResumable(true);
    throw error;
  }
};

async function onReceiveMessage(msg) {
  const previousScoreLength = scores.value.length;
  syncPracticeInfo(msg);

  if (msg.type === MESSAGETYPESV2.BattleStart) {
    invalidateShotPresentations();
    applyVisiblePrecisionTarget(practiceInfo.value);
  } else if (
    msg.type === MESSAGETYPESV2.ShootResult &&
    isShootingStage.value
  ) {
    let hasNewShot = false;
    let latestShot = null;
    if (Array.isArray(msg.details)) {
      scores.value = msg.details;
      hasNewShot = msg.details.length === previousScoreLength + 1;
      latestShot = hasNewShot ? msg.details[msg.details.length - 1] : null;
    }

    if (trainingType.value === "precision") {
      const generation = invalidateShotPresentations();
      const target = getPrecisionTargetSnapshot(practiceInfo.value);
      const nextEffectToken = hasNewShot ? shotEffectToken.value + 1 : 0;
      const effectPromise = waitForShotEffect(
        nextEffectToken,
        hasNewShot && shouldWaitForShotEffect(latestShot)
      );
      const audioPromise = playAudioKeysAndWait(
        buildShootResultAudioKeys(msg)
      );

      if (hasNewShot) {
        shotEffectToken.value = nextEffectToken;
      }
      void commitPrecisionTargetAfterPresentation({
        generation,
        target,
        audioPromise,
        effectPromise,
      });
    } else if (hasNewShot) {
      shotEffectToken.value += 1;
    }
  } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
    practiceEndSnapshot.value = createPracticeEndSnapshot(msg);
    void loadNextDifficultyState(practiceEndSnapshot.value);
    if (
      trainingType.value === "base" &&
      Number(msg.status) === 3 &&
      !Object.prototype.hasOwnProperty.call(msg, "arrowsLeft")
    ) {
      practiceInfo.value = {
        ...practiceInfo.value,
        arrowsLeft: 0,
      };
    }
    practiceEnded.value = true;
    setPracticeAppHideResumable(false);
    invalidateShotPresentations();
    clearPracticeRuntimeContext();
    // setTimeout(onOver, 1500);
  }
}

function onComplete() {
  pageStage.value = pageStages.LOADING;
  start.value = false;
  practiceEnded.value = true;
  setPracticeAppHideResumable(false);
  invalidateShotPresentations();
  clearPracticeRuntimeContext();
  closePracticeConnection("training-practice-complete");
  uni.navigateBack();
}

async function onRetry() {
  pageStage.value = pageStages.LOADING;
  setPracticeAppHideResumable(false);
  clearHighlightTestTimer();
  useHighlightTest.value = false;
  resetNextDifficultyState();
  practiseId.value = "";
  serverAddr.value = "";
  practiseResult.value = {};
  practiceEndSnapshot.value = {};
  practiceInfo.value = {};
  invalidateShotPresentations({ resetVisible: true });
  start.value = false;
  scores.value = [];
  shotEffectToken.value = 0;
  try {
    const practice = await createPractice();
    if (!practice) pageStage.value = pageStages.DISTANCE;
  } catch (error) {
    console.error("training practice retry failed", error);
    pageStage.value = pageStages.DISTANCE;
  }
}

const onClickShare = debounce(async () => {
  await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
  await wxShare("shareCanvas");
});

function onAudioEnded(key) {
  Array.from(audioWaiters).forEach((waiter) => {
    if (waiter.expectedKey === key) waiter.done();
  });
  if (["比赛结束", "练习结束"].includes(String(key || ""))) {
    void onOver();
  }
}

const updateSound = () => {
  sound.value = !sound.value;
  audioManager.setMuted(!sound.value);
};

const exitPractice = async () => {
  if (exiting.value) return;
  exiting.value = true;
  setPracticeAppHideResumable(false);
  invalidateShotPresentations();

  try {
    await stopCurrentPractice();
  } finally {
    closePracticeConnection("training-practice-exit");
    clearPracticeRuntimeContext();
    uni.navigateBack();
  }
};

onHide(() => {
  pageVisible.value = false;
  if (
    !appHideResumable.value ||
    exiting.value ||
    !practiseId.value ||
    practiceEnded.value ||
    stopCompleted.value
  ) {
    return;
  }

  hiddenWhileActive.value = true;
  invalidateShotPresentations();
});

onShow(async () => {
  pageVisible.value = true;
  await resumeCurrentPractice();
});

onUnload(() => {
  setPracticeAppHideResumable(false);
  invalidateShotPresentations();
  clearPracticeRuntimeContext();
  void stopCurrentPractice();
  closePracticeConnection("training-practice-unload");
});

onMounted(() => {
  // audioManager.play("第一轮");
  uni.setKeepScreenOn({
    keepScreenOn: true,
  });
  uni.$on("socket-inbox", onReceiveMessage);
  uni.$on(MATCH_WS_PRACTICE_SYNC_EVENT, onPracticeInfoSync);
  uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
  uni.$on("share-image", onClickShare);
  uni.$on("audioEnded", onAudioEnded);
  if (!connectPracticeServer({ resumableOnAppHide: true })) {
    uni.showToast({
      title: "练习连接信息异常，请重试",
      icon: "none",
    });
    setTimeout(() => {
      void exitPractice();
    }, 500);
  }
});

onBeforeUnmount(() => {
  setPracticeAppHideResumable(false);
  invalidateShotPresentations();
  clearPracticeRuntimeContext();
  void stopCurrentPractice();
  uni.setKeepScreenOn({
    keepScreenOn: false,
  });
  uni.$off("socket-inbox", onReceiveMessage);
  uni.$off(MATCH_WS_PRACTICE_SYNC_EVENT, onPracticeInfoSync);
  uni.$off(MATCH_WS_STATE_EVENT, onMatchSocketState);
  uni.$off("share-image", onClickShare);
  uni.$off("audioEnded", onAudioEnded);
  audioManager.stopAll();
  clearHighlightTestTimer();
  closePracticeConnection("training-practice-unmount");
});
</script>

<template>
  <Container
    :bgType="isDistanceStage ? 9 : 11"
    :showBottom="isDistanceStage"
    :scroll="!isShootingStage"
    :onBack="exitPractice"
  >
    <view class="practise-content">
      <TestDistance
        v-if="isDistanceStage"
        :targetType="practiceInfo.targetType"
      />
      <view v-else-if="isShootingStage" class="shooting-layout">
        <view class="shooting-fixed">
          <ShootProgress
            :start="start"
            :total="timeLimit"
            :countdownEnabled="hasTimeLimit"
            :trainingType="trainingType"
            :isVip="isVip"
            :isSvip="isSvip"
            :externalShootResultAudio="trainingType === 'precision'"
            :onStop="onTimeLimitReached"
          />
          <view class="user-row">
            <!-- <Avatar :src="user.avatar" :size="35" /> -->
            <BubbleTip v-if="showGuide" type="normal2">
              <text>还有两场，坚持</text>
              <text>就是胜利！💪</text>
            </BubbleTip>
            <!-- <BowPower /> -->
          </view>
          <BowTarget
            :totalRound="start ? total / 4 : 0"
            :currentRound="scores.length % 3"
            :scores="scores"
            :isSvip="isSvip"
            :shotEffectToken="shotEffectToken"
            :showCrosshair="false"
            :sectorCount="precisionBlocks"
            :activeSector="precisionRandomBlock"
            :activeRing="precisionRandomRingArea"
            :showSectorLabels="precisionBlocks > 0"
            stable-shot-effect
            @shot-effect-complete="onShotEffectComplete"
          />
          <!-- <view v-if="env !== 'release'" class="highlight-test-actions">
            <button
              class="highlight-test-btn"
              hover-class="none"
              @click="runHighlightTest"
            >
              扇区测试
            </button>
            <button
              class="highlight-test-btn"
              hover-class="none"
              @click="resetHighlightTest"
            >
              重置高亮
            </button>
          </view> -->
          <view class="sound-text-box">
            <button class="sound-btn" hover-class="none" @click="updateSound">
              <image
                class="sound-icon"
                :src="`/static/sound${sound ? '' : '-off'}-yellow.png`"
                mode="aspectFit"
              />
            </button>
            <view class="bat-text-big-box">
              <image
                class="dao-icon"
                src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/dao-icon.png"
                mode="widthFix"
              />
              <view v-if="trainingCopy" class="bat-text-box">
                <view class="bat-text-small-box">
                  <view class="text-round-box">
                    <view class="text1">{{ trainingCopy.title }}</view>
                    <view class="text2">
                      <text
                        v-for="(part, index) in trainingCopy.details"
                        :key="index"
                        :class="{ 'text2-yellow': part.highlight }"
                      >{{ part.text }}</text>
                    </view>
                  </view>
                </view>
              </view>
            </view>
          </view>
        </view>
        <scroll-view
          class="score-scroll"
          scroll-y
          :enhanced="true"
          :show-scrollbar="false"
        >
          <ScorePanel2 :arrows="scores" :total="total" />
        </scroll-view>
      </view>
      <ScoreResult
        v-else-if="showResult"
        :rowCount="6"
        :total="total"
        :onClose="onComplete"
        :onRetry="onRetry"
        :trainingType="trainingType"
        :difficultyLevel="currentDifficultyLevel"
        :hasNextDifficulty="hasNextDifficulty"
        :result="practiseResult"
      />
      <canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
    </view>
    <template #bottom>
      <view class="btn-box">
        <image
          class="btn-box-bg"
          src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/par-star.png"
          mode="widthFix"
        />
        <button class="btn" @click="onReady">准备好了，开始练习</button>
      </view>
    </template>
  </Container>
</template>

<style scoped>
.practise-content {
  height: 100%;
  min-height: 0;
}

.shooting-layout {
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.shooting-fixed {
  flex-shrink: 0;
}

.score-scroll {
  flex: 1;
  height: 0;
  min-height: 0;
  overflow: hidden;
}

.btn-box{
  width: 488rpx;
  height: 234rpx;
  position: fixed;
  bottom: 130rpx;
  left: 50%;
  transform: translateX(-50%);
}
.btn-box-bg{
  width: 100%;
}
.btn{
  width: 330rpx;
  height: 70rpx;
  line-height: 70rpx;
  background: #FED847;
  border-radius: 44rpx;
  text-align: center;
  color: #000000;
  font-size: 28rpx;
  font-weight: 500;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: -36rpx;
}

.highlight-test-actions {
  display: flex;
  justify-content: center;
  margin-top: -24rpx;
  position: relative;
  z-index: 10;
}

.highlight-test-btn {
  width: 150rpx;
  height: 48rpx;
  line-height: 48rpx;
  padding: 0;
  border-radius: 24rpx;
  background: rgba(0, 0, 0, 0.48);
  color: #fed847;
  font-size: 22rpx;
}

.highlight-test-btn::after {
  border: none;
}

.highlight-test-btn + .highlight-test-btn {
  margin-left: 16rpx;
}

.sound-text-box{
  height: 125rpx;
  padding: 0 56rpx;
  display: flex;
  align-items: flex-end;
}
.sound-btn {
  width: 76rpx;
  height: 70rpx;
  border: none;
}

.sound-btn::after {
  border: none;
}

.sound-icon {
  width: 76rpx;
  height: 70rpx;
}
.bat-text-big-box{
  flex: 1;
  position: relative;
}
.dao-icon{
  width: 160rpx;
  height: 125rpx;
  position: absolute;
  left: 0;
  bottom: 0;
}
.text-round-box{
  width: 100%;
}
.bat-text-box{
  display: flex;
}
.bat-text-small-box{
  background: rgba(0, 0, 0, 0.5);
  width: auto;
  min-width: 100rpx;
  border-radius: 16rpx 60rpx 60rpx 16rpx;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-left: 176rpx;
  height: 112rpx;
  padding-right: 30rpx;
  font-size: 30rpx;
  color: #E7BA80;
}
.text1{
  font-size: 30rpx;
  font-weight: 400;
  color: #E7BA80;
  line-height: 42rpx;
}
.text2{
  color: #FFFFFF;
  font-size: 26rpx;
  font-weight: 400;
  line-height: 36rpx;
}
.text2-yellow{
  font-size: 30rpx;
  color: #FFD947;
  font-weight: 500;
  margin: 0 4rpx;
}
</style>
