import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import type {
  BarkBattleDerivedMetrics,
  BarkBattlePublishedConfig,
  BarkBattleRuntimeConfig,
  BarkBattleRunStartResponse,
  BarkBattleServerResult,
} from '../../../../packages/shared/src/contracts/barkBattle';
import {
  finishBarkBattleRun,
  startBarkBattleRun,
} from '../../../services/bark-battle-runtime';
import {
  type BarkBattleConfig,
  buildBarkBattleDefaultOnomatopoeia,
  DEFAULT_BARK_BATTLE_CONFIG,
} from '../application/BarkBattleConfig';
import { BarkBattleController } from '../application/BarkBattleController';
import type { MicrophoneFailureReason } from '../domain/BarkBattleTypes';
import {
  type BrowserMicrophoneSampler,
  startBrowserMicrophoneSampler,
} from '../infrastructure/BrowserMicrophoneInput';
import { BarkBattleHud } from './BarkBattleHud';

type BarkBattleRuntimeMode = 'draft' | 'published';

type BarkBattleRuntimeShellProps = {
  title?: string;
  workId?: string;
  publishedConfig?: BarkBattlePublishedConfig | null;
  runtimeMode?: BarkBattleRuntimeMode;
  onExit?: () => void;
};

type DebugEvent = {
  id: number;
  text: string;
};

type BarkBattleActiveRun = Pick<
  BarkBattleRunStartResponse,
  | 'runId'
  | 'runToken'
  | 'workId'
  | 'configVersion'
  | 'rulesetVersion'
  | 'difficultyPreset'
  | 'serverStartedAt'
>;

type BarkBattleMetricAccumulator = {
  sampleCount: number;
  volumeSum: number;
  maxVolume: number;
  comboMax: number;
  currentCombo: number;
};

const BARK_BATTLE_CLIENT_RUNTIME_VERSION = 'bark-battle-web-v1';

function createMetricAccumulator(): BarkBattleMetricAccumulator {
  return {
    sampleCount: 0,
    volumeSum: 0,
    maxVolume: 0,
    comboMax: 0,
    currentCombo: 0,
  };
}

function normalizeMetricVolume(volume: number) {
  if (!Number.isFinite(volume)) {
    return 0;
  }
  return Math.max(0, Math.min(1, volume));
}

function resolveClientResult(
  winner: 'player' | 'opponent' | 'draw' | null,
): BarkBattleServerResult {
  if (winner === 'player') {
    return 'player_win';
  }
  if (winner === 'opponent') {
    return 'opponent_win';
  }
  return 'draw';
}

function resolveResultTitle(winner: 'player' | 'opponent' | 'draw' | null) {
  if (winner === 'player') {
    return '汪力压制成功';
  }
  if (winner === 'opponent') {
    return '对手声浪更强';
  }
  return '势均力敌';
}

const DEBUG_CONFIG_FIELDS: Array<{
  key: keyof Pick<
    BarkBattleConfig,
    | 'roundDurationMs'
    | 'countdownMs'
    | 'drawThreshold'
    | 'barkThreshold'
    | 'minBarkGapMs'
    | 'balanceFactor'
    | 'opponentBasePower'
  >;
  label: string;
  min: number;
  max: number;
  step: number;
}> = [
  {
    key: 'roundDurationMs',
    label: '局长(ms)',
    min: 1000,
    max: 60000,
    step: 1000,
  },
  { key: 'countdownMs', label: '倒计时(ms)', min: 0, max: 5000, step: 500 },
  { key: 'drawThreshold', label: '平局阈值', min: 0, max: 40, step: 1 },
  { key: 'barkThreshold', label: '叫声阈值', min: 0.1, max: 1, step: 0.05 },
  { key: 'minBarkGapMs', label: '叫声间隔(ms)', min: 100, max: 1200, step: 50 },
  { key: 'balanceFactor', label: '拉锯速度', min: 5, max: 80, step: 1 },
  { key: 'opponentBasePower', label: '对手基础力', min: 0, max: 1, step: 0.05 },
];

const MICROPHONE_FAILURE_REASONS = new Set<MicrophoneFailureReason>([
  'unsupported',
  'permission-denied',
  'non-secure-context',
  'not-found',
  'not-readable',
  'audio-context-blocked',
  'calibration-timeout',
  'calibration-sample-unreadable',
  'unknown',
]);

function isMicrophoneFailureReason(
  reason: unknown,
): reason is MicrophoneFailureReason {
  return (
    typeof reason === 'string' &&
    MICROPHONE_FAILURE_REASONS.has(reason as MicrophoneFailureReason)
  );
}

function buildRuntimeConfigFromPublishedConfig(
  publishedConfig?: BarkBattlePublishedConfig | null,
): BarkBattleConfig {
  if (!publishedConfig) {
    return DEFAULT_BARK_BATTLE_CONFIG;
  }

  const difficultyOverrides: Record<
    BarkBattlePublishedConfig['difficultyPreset'],
    Partial<BarkBattleConfig>
  > = {
    easy: { opponentBasePower: 0.16 },
    normal: { opponentBasePower: 0.22 },
    hard: { opponentBasePower: 0.3 },
  };

  return {
    ...DEFAULT_BARK_BATTLE_CONFIG,
    ...difficultyOverrides[publishedConfig.difficultyPreset],
  };
}

function buildRuntimeConfigFromServerConfig(
  runtimeConfig: BarkBattleRuntimeConfig,
): BarkBattleConfig {
  const baseConfig = buildRuntimeConfigFromPublishedConfig({
    workId: runtimeConfig.workId,
    draftId: null,
    configVersion: runtimeConfig.configVersion,
    rulesetVersion: runtimeConfig.rulesetVersion,
    playTypeId: runtimeConfig.playTypeId,
    title: '',
    description: '',
    themeDescription: runtimeConfig.themeDescription,
    playerImageDescription: runtimeConfig.playerImageDescription,
    opponentImageDescription: runtimeConfig.opponentImageDescription,
    onomatopoeia: runtimeConfig.onomatopoeia,
    playerCharacterImageSrc: runtimeConfig.playerCharacterImageSrc,
    opponentCharacterImageSrc: runtimeConfig.opponentCharacterImageSrc,
    uiBackgroundImageSrc: runtimeConfig.uiBackgroundImageSrc,
    difficultyPreset: runtimeConfig.difficultyPreset,
    updatedAt: runtimeConfig.updatedAt,
    publishedAt: runtimeConfig.updatedAt,
  });
  return {
    ...baseConfig,
    roundDurationMs: runtimeConfig.durationMs,
    drawThreshold: runtimeConfig.drawThreshold,
    minBarkGapMs: runtimeConfig.minBarkGapMs,
  };
}

function normalizeOnomatopoeiaPool(
  publishedConfig?: BarkBattlePublishedConfig | null,
) {
  const custom = publishedConfig?.onomatopoeia
    ?.map((word) => word.trim())
    .filter(Boolean)
    .slice(0, 24);
  if (custom?.length) {
    return custom;
  }
  return buildBarkBattleDefaultOnomatopoeia({
    themeDescription: publishedConfig?.themeDescription,
    playerImageDescription: publishedConfig?.playerImageDescription,
    opponentImageDescription: publishedConfig?.opponentImageDescription,
  });
}

function buildPublishedConfigFromServerRuntimeConfig(
  current: BarkBattlePublishedConfig,
  runtimeConfig: BarkBattleRuntimeConfig,
): BarkBattlePublishedConfig {
  return {
    ...current,
    workId: runtimeConfig.workId,
    configVersion: runtimeConfig.configVersion,
    rulesetVersion: runtimeConfig.rulesetVersion,
    playTypeId: runtimeConfig.playTypeId,
    themeDescription: runtimeConfig.themeDescription,
    playerImageDescription: runtimeConfig.playerImageDescription,
    opponentImageDescription: runtimeConfig.opponentImageDescription,
    onomatopoeia: runtimeConfig.onomatopoeia,
    playerCharacterImageSrc: runtimeConfig.playerCharacterImageSrc,
    opponentCharacterImageSrc: runtimeConfig.opponentCharacterImageSrc,
    uiBackgroundImageSrc: runtimeConfig.uiBackgroundImageSrc,
    difficultyPreset: runtimeConfig.difficultyPreset,
    updatedAt: runtimeConfig.updatedAt,
  };
}

function pickRandomOnomatopoeia(
  pool: readonly string[],
  previous: string,
) {
  if (!pool.length) {
    return '炸场！';
  }
  if (pool.length === 1) {
    return pool[0] ?? '炸场！';
  }
  const candidates = pool.filter((word) => word !== previous);
  const activePool = candidates.length ? candidates : pool;
  const index = Math.min(
    activePool.length - 1,
    Math.floor(Math.random() * activePool.length),
  );
  return activePool[index] ?? activePool[0] ?? '炸场！';
}

export function BarkBattleRuntimeShell({
  title = '汪汪声浪大作战',
  workId,
  publishedConfig,
  runtimeMode = 'draft',
  onExit,
}: BarkBattleRuntimeShellProps) {
  const initialConfig = useMemo(
    () => buildRuntimeConfigFromPublishedConfig(publishedConfig),
    [publishedConfig],
  );
  const [config, setConfig] = useState(initialConfig);
  const runtimeConfigRef = useRef(initialConfig);
  const controllerRef = useRef<BarkBattleController | null>(null);
  if (!controllerRef.current) {
    controllerRef.current = new BarkBattleController(config);
  }
  const controller = controllerRef.current;
  const [snapshot, setSnapshot] = useState(() => controller.getSnapshot());
  const [particleText, setParticleText] = useState('');
  const replacementConfig = publishedConfig ?? null;
  const [activePublishedConfig, setActivePublishedConfig] = useState(
    replacementConfig,
  );
  const onomatopoeiaPool = useMemo(
    () => normalizeOnomatopoeiaPool(activePublishedConfig),
    [activePublishedConfig],
  );
  const [playerBurstText, setPlayerBurstText] = useState(
    () => onomatopoeiaPool[0] ?? '炸场！',
  );
  const isPublishedRuntime =
    runtimeMode === 'published' && Boolean(replacementConfig?.workId);
  const [inputMode, setInputMode] = useState<'mock' | 'microphone'>(
    isPublishedRuntime ? 'microphone' : 'mock',
  );
  useEffect(() => {
    setInputMode(isPublishedRuntime ? 'microphone' : 'mock');
  }, [isPublishedRuntime]);
  const [liveInputVolume, setLiveInputVolume] = useState(0);
  const [isDebugExpanded, setIsDebugExpanded] = useState(false);
  const [playerPulseKey, setPlayerPulseKey] = useState(0);
  const [opponentPulseKey, setOpponentPulseKey] = useState(0);
  const [debugEvents, setDebugEvents] = useState<DebugEvent[]>([]);
  const [runtimeError, setRuntimeError] = useState<string | null>(null);
  const heldRef = useRef(false);
  const lastPlayerBarkCountRef = useRef(0);
  const lastOpponentPowerRef = useRef(0);
  const debugEventIdRef = useRef(0);
  const microphoneSamplerRef = useRef<BrowserMicrophoneSampler | null>(null);
  const activeRunRef = useRef<BarkBattleActiveRun | null>(null);
  const pendingRunStartRef = useRef<Promise<boolean> | null>(null);
  const runStartedAtRef = useRef<string | null>(null);
  const submittedRunIdsRef = useRef<Set<string>>(new Set());
  const autoStartMicrophoneAttemptedRef = useRef(false);
  const metricAccumulatorRef = useRef<BarkBattleMetricAccumulator>(
    createMetricAccumulator(),
  );
  const lastOnomatopoeiaRef = useRef('');
  // 中文注释：正式公开 runtime 面向玩家，只保留真实麦克风入口；mock 与调参面板只服务草稿试玩。
  const shouldShowDebugPanel = !isPublishedRuntime;

  useEffect(() => {
    lastOnomatopoeiaRef.current = '';
    setPlayerBurstText(onomatopoeiaPool[0] ?? '炸场！');
  }, [onomatopoeiaPool]);

  const appendDebugEvent = useCallback((text: string) => {
    debugEventIdRef.current += 1;
    const event = { id: debugEventIdRef.current, text };
    setDebugEvents((current) => [event, ...current].slice(0, 5));
  }, []);

  const flashOnomatopoeia = useCallback(() => {
    const nextWord = pickRandomOnomatopoeia(
      onomatopoeiaPool,
      lastOnomatopoeiaRef.current,
    );
    lastOnomatopoeiaRef.current = nextWord;
    setPlayerBurstText(nextWord);
    setParticleText(nextWord);
    window.setTimeout(() => setParticleText(''), 520);
  }, [onomatopoeiaPool]);

  const resetRuntimeRunState = useCallback(() => {
    activeRunRef.current = null;
    pendingRunStartRef.current = null;
    runStartedAtRef.current = null;
    submittedRunIdsRef.current = new Set();
    metricAccumulatorRef.current = createMetricAccumulator();
    setRuntimeError(null);
  }, []);

  const recordRuntimeSample = useCallback((volume: number) => {
    const normalized = normalizeMetricVolume(volume);
    const metrics = metricAccumulatorRef.current;
    metrics.sampleCount += 1;
    metrics.volumeSum += normalized;
    metrics.maxVolume = Math.max(metrics.maxVolume, normalized);
  }, []);

  const recordRuntimeTrigger = useCallback((volume: number) => {
    const normalized = normalizeMetricVolume(volume);
    const metrics = metricAccumulatorRef.current;
    metrics.currentCombo += 1;
    metrics.comboMax = Math.max(metrics.comboMax, metrics.currentCombo);
    metrics.maxVolume = Math.max(metrics.maxVolume, normalized);
  }, []);

  const buildDerivedMetrics = useCallback((): BarkBattleDerivedMetrics => {
    const metrics = metricAccumulatorRef.current;
    const nextSnapshot = controller.getSnapshot();
    return {
      triggerCount: nextSnapshot.player.barkCount,
      maxVolume: Number(metrics.maxVolume.toFixed(3)),
      averageVolume: Number(
        (metrics.sampleCount
          ? metrics.volumeSum / metrics.sampleCount
          : 0
        ).toFixed(3),
      ),
      finalEnergy: Number(nextSnapshot.energy.toFixed(2)),
      comboMax: metrics.comboMax,
    };
  }, [controller]);

  const submitFinishedRunIfNeeded = useCallback(
    (nextSnapshot = controller.getSnapshot()) => {
      if (!isPublishedRuntime || nextSnapshot.phase !== 'finished') {
        return;
      }
      const activeRun = activeRunRef.current;
      if (!activeRun || submittedRunIdsRef.current.has(activeRun.runId)) {
        return;
      }
      submittedRunIdsRef.current.add(activeRun.runId);
      const finishedAt = new Date().toISOString();
      const startedAt =
        runStartedAtRef.current ?? activeRun.serverStartedAt ?? finishedAt;
      const durationMs = Math.max(
        0,
        runtimeConfigRef.current.roundDurationMs - nextSnapshot.remainingMs,
      );
      void finishBarkBattleRun(activeRun.runId, {
        runId: activeRun.runId,
        runToken: activeRun.runToken,
        workId: activeRun.workId,
        configVersion: activeRun.configVersion,
        rulesetVersion: activeRun.rulesetVersion,
        difficultyPreset: activeRun.difficultyPreset,
        clientStartedAt: startedAt,
        clientFinishedAt: finishedAt,
        durationMs,
        derivedMetrics: buildDerivedMetrics(),
        clientResult: resolveClientResult(nextSnapshot.winner),
        clientRuntimeVersion: BARK_BATTLE_CLIENT_RUNTIME_VERSION,
      })
        .then(() => {
          appendDebugEvent('正式成绩已提交');
        })
        .catch((error) => {
          setRuntimeError(
            error instanceof Error ? error.message : '提交正式成绩失败',
          );
          appendDebugEvent('正式成绩提交失败');
        });
    },
    [
      appendDebugEvent,
      buildDerivedMetrics,
      controller,
      isPublishedRuntime,
    ],
  );

  const startFormalRunIfNeeded = useCallback(async (): Promise<boolean> => {
    if (!isPublishedRuntime || !replacementConfig?.workId) {
      return true;
    }
    if (activeRunRef.current) {
      return true;
    }
    if (!pendingRunStartRef.current) {
      pendingRunStartRef.current = (async () => {
        try {
          setRuntimeError(null);
          const started = await startBarkBattleRun(replacementConfig.workId, {
            // 中文注释：公开广场摘要可能滞后于已发布运行配置；正式开局以服务端当前发布快照为准。
            sourceRoute:
              typeof window === 'undefined'
                ? 'bark-battle-runtime'
                : window.location.pathname,
            clientRuntimeVersion: BARK_BATTLE_CLIENT_RUNTIME_VERSION,
          });
          const serverRuntimeConfig = buildRuntimeConfigFromServerConfig(
            started.runtimeConfig,
          );
          // 中文注释：公开卡片可能只带摘要；正式开局后用服务端 runtimeConfig 刷新拟声词和素材。
          setActivePublishedConfig((current) =>
            buildPublishedConfigFromServerRuntimeConfig(
              current ?? replacementConfig,
              started.runtimeConfig,
            ),
          );
          runtimeConfigRef.current = serverRuntimeConfig;
          controller.updateConfigForActiveRound(serverRuntimeConfig);
          activeRunRef.current = {
            runId: started.runId,
            runToken: started.runToken,
            workId: started.workId,
            configVersion: started.configVersion,
            rulesetVersion: started.rulesetVersion,
            difficultyPreset: started.difficultyPreset,
            serverStartedAt: started.serverStartedAt,
          };
          runStartedAtRef.current = new Date().toISOString();
          appendDebugEvent(`正式对局已登记：${started.runId}`);
          return true;
        } catch (error) {
          const message =
            error instanceof Error ? error.message : '启动正式对局失败';
          setRuntimeError(message);
          appendDebugEvent(message);
          return false;
        } finally {
          pendingRunStartRef.current = null;
        }
      })();
    }
    return pendingRunStartRef.current ?? Promise.resolve(true);
    }, [appendDebugEvent, controller, isPublishedRuntime, replacementConfig]);

  const syncSnapshot = useCallback(() => {
    const nextSnapshot = controller.getSnapshot();
    if (nextSnapshot.player.barkCount > lastPlayerBarkCountRef.current) {
      setPlayerPulseKey((current) => current + 1);
      recordRuntimeTrigger(nextSnapshot.player.power);
      flashOnomatopoeia();
      appendDebugEvent(
        `玩家叫声触发 #${nextSnapshot.player.barkCount} · 能量 ${Math.round(nextSnapshot.energy)}`,
      );
    }
    if (
      nextSnapshot.phase === 'playing' &&
      Math.abs(nextSnapshot.opponent.power - lastOpponentPowerRef.current) >=
        0.08
    ) {
      setOpponentPulseKey((current) => current + 1);
      appendDebugEvent(
        `对手反击强度 ${(nextSnapshot.opponent.power * 100).toFixed(0)}%`,
      );
    }
    lastPlayerBarkCountRef.current = nextSnapshot.player.barkCount;
    lastOpponentPowerRef.current = nextSnapshot.opponent.power;
    setSnapshot(nextSnapshot);
    submitFinishedRunIfNeeded(nextSnapshot);
  }, [
    appendDebugEvent,
    controller,
    flashOnomatopoeia,
    recordRuntimeTrigger,
    submitFinishedRunIfNeeded,
  ]);

  const stopMicrophone = useCallback(() => {
    microphoneSamplerRef.current?.stop();
    microphoneSamplerRef.current = null;
  }, []);

  // 中文注释：领域层沿用 startMockRound 表示“进入对局倒计时”，正式/草稿输入差异由外层 sampler 控制。
  const startRuntimeRound = useCallback(() => {
    controller.startWithMockInput();
  }, [controller]);

  useEffect(() => {
    setConfig(initialConfig);
    runtimeConfigRef.current = initialConfig;
    controller.updateConfig(initialConfig);
    setActivePublishedConfig(replacementConfig);
  }, [controller, initialConfig, replacementConfig]);

  const startMicrophone = useCallback(async () => {
    stopMicrophone();
    let shouldAcceptMicrophoneSamples = false;
    try {
      const sampler = await startBrowserMicrophoneSampler((volume, atMs) => {
        if (!shouldAcceptMicrophoneSamples) {
          return;
        }
        setLiveInputVolume(volume);
        recordRuntimeSample(volume);
        if (volume >= config.barkThreshold) {
          appendDebugEvent(`麦克风输入 ${(volume * 100).toFixed(0)}%`);
        }
        controller.submitInputSample(
          volume,
          controller.getSampleClockMs() + atMs,
        );
      });
      if (!(await startFormalRunIfNeeded())) {
        sampler.stop();
        return;
      }
      startRuntimeRound();
      microphoneSamplerRef.current = sampler;
      setInputMode('microphone');
      shouldAcceptMicrophoneSamples = true;
      appendDebugEvent('真实麦克风已开启');
      syncSnapshot();
    } catch (error) {
      const reason =
        error && typeof error === 'object' && 'reason' in error
          ? error.reason
          : 'unknown';
      const failureReason = isMicrophoneFailureReason(reason)
        ? reason
        : 'unknown';
      controller.failMicrophone(failureReason);
      appendDebugEvent(`麦克风不可用：${failureReason}`);
      syncSnapshot();
    }
  }, [
    appendDebugEvent,
    config.barkThreshold,
    controller,
    recordRuntimeSample,
    startFormalRunIfNeeded,
    startRuntimeRound,
    stopMicrophone,
    syncSnapshot,
  ]);

  useEffect(() => {
    if (
      !isPublishedRuntime ||
      snapshot.phase !== 'permission' ||
      autoStartMicrophoneAttemptedRef.current
    ) {
      return;
    }

    // 中文注释：公开作品从详情页“启动”进入运行态后立即申请麦克风，授权成功后直接进入倒计时。
    autoStartMicrophoneAttemptedRef.current = true;
    void startMicrophone();
  }, [isPublishedRuntime, snapshot.phase, startMicrophone]);

  useEffect(() => stopMicrophone, [stopMicrophone]);

  useEffect(() => {
    runtimeConfigRef.current = config;
    controller.updateConfig(config);
    setSnapshot(controller.getSnapshot());
  }, [config, controller]);

  useEffect(() => {
    const timer = window.setInterval(() => {
      controller.tick(100);
      if (inputMode === 'mock' && !isPublishedRuntime) {
        if (heldRef.current) {
          recordRuntimeSample(0.88);
          controller.submitMockSample(0.88);
        } else {
          recordRuntimeSample(0.12);
          controller.submitMockSample(0.12);
          setLiveInputVolume(0);
        }
      }
      syncSnapshot();
    }, 100);
    return () => window.clearInterval(timer);
  }, [
    controller,
    inputMode,
    isPublishedRuntime,
    recordRuntimeSample,
    syncSnapshot,
  ]);

  const restart = () => {
    heldRef.current = false;
    stopMicrophone();
    setInputMode(isPublishedRuntime ? 'microphone' : 'mock');
    setLiveInputVolume(0);
    controller.restart();
    setParticleText('');
    setPlayerBurstText(onomatopoeiaPool[0] ?? '炸场！');
    setDebugEvents([]);
    resetRuntimeRunState();
    autoStartMicrophoneAttemptedRef.current = false;
    lastPlayerBarkCountRef.current = 0;
    lastOpponentPowerRef.current = 0;
    syncSnapshot();
  };

  const startMock = async () => {
    if (isPublishedRuntime) {
      const message = '正式对局需要使用真实麦克风';
      setRuntimeError(message);
      appendDebugEvent(message);
      return;
    }
    stopMicrophone();
    setInputMode('mock');
    setLiveInputVolume(0);
    startRuntimeRound();
    appendDebugEvent('开始 mock 对局（不会请求浏览器麦克风权限）');
    syncSnapshot();
  };

  const finishNow = () => {
    if (isPublishedRuntime && !activeRunRef.current) {
      const message = '正式对局需要使用真实麦克风';
      setRuntimeError(message);
      appendDebugEvent(message);
      return;
    }
    heldRef.current = false;
    stopMicrophone();
    controller.finishNow();
    appendDebugEvent('人工结束对局');
    syncSnapshot();
  };

  const bark = async () => {
    if (isPublishedRuntime) {
      const message = '正式对局需要使用真实麦克风';
      setRuntimeError(message);
      appendDebugEvent(message);
      return;
    }
    recordRuntimeSample(0.9);
    controller.forcePlayerBark(0.9);
    syncSnapshot();
  };

  const exitRuntime = () => {
    heldRef.current = false;
    stopMicrophone();
    onExit?.();
  };

  return (
    <main className="bark-battle-runtime" aria-label={title}>
      {onExit ? (
        <button
          className="bark-battle-runtime__back-button"
          type="button"
          onClick={exitRuntime}
        >
          返回
        </button>
      ) : null}
      <BarkBattleHud
        snapshot={snapshot}
        playerPulseKey={playerPulseKey}
        opponentPulseKey={opponentPulseKey}
        playerCharacterImageSrc={activePublishedConfig?.playerCharacterImageSrc}
        opponentCharacterImageSrc={activePublishedConfig?.opponentCharacterImageSrc}
        uiBackgroundImageSrc={activePublishedConfig?.uiBackgroundImageSrc}
        onStartMicrophone={startMicrophone}
        onMockBark={bark}
        onMockQuiet={() => {
          heldRef.current = false;
        }}
        onRestart={restart}
        enableMockControls={!isPublishedRuntime}
        runtimeError={shouldShowDebugPanel ? null : runtimeError}
        playerBurstText={playerBurstText}
        opponentBurstText="反击"
      />
      {shouldShowDebugPanel ? (
        <aside
          className={`bark-battle-debug-panel${isDebugExpanded ? ' bark-battle-debug-panel--expanded' : ''}`}
          aria-label="调试面板"
        >
          <header>
            <strong>调试面板</strong>
            <button
              type="button"
              className="bark-battle-debug-panel__toggle"
              aria-expanded={isDebugExpanded}
              onClick={() => setIsDebugExpanded((current) => !current)}
            >
              {isDebugExpanded ? '收起' : '展开'}
            </button>
            <span>{snapshot.phase}</span>
          </header>
          <div className="bark-battle-debug-panel__body">
            <div className="bark-battle-debug-panel__controls">
              <button type="button" onClick={startMock}>
                开始
              </button>
              <button type="button" onClick={finishNow}>
                结束
              </button>
              <button type="button" onClick={restart}>
                重置
              </button>
              {onExit ? (
                <button type="button" onClick={onExit}>
                  返回配置
                </button>
              ) : null}
            </div>
            {workId ? (
              <p className="bark-battle-debug-panel__work-id">作品：{workId}</p>
            ) : null}
            {runtimeError ? (
              <p className="bark-battle-debug-panel__work-id" role="alert">
                {runtimeError}
              </p>
            ) : null}
            <div className="bark-battle-debug-metrics" aria-label="触发反馈">
              <span className="bark-battle-debug-metrics__wide">
                输入模式：
                {inputMode === 'microphone' ? '真实麦克风' : 'Mock 输入'}
              </span>
              <span>实时音量：{(liveInputVolume * 100).toFixed(0)}%</span>
              <span>采样时钟：{controller.getSampleClockMs()}ms</span>
              <span>玩家触发：{snapshot.player.barkCount}</span>
              <span>玩家强度：{(snapshot.player.power * 100).toFixed(0)}%</span>
              <span>对手强度：{(snapshot.opponent.power * 100).toFixed(0)}%</span>
              <span>能量：{Math.round(snapshot.energy)}</span>
            </div>
            <ol className="bark-battle-debug-events" aria-label="触发日志">
              {debugEvents.length ? (
                debugEvents.map((event) => <li key={event.id}>{event.text}</li>)
              ) : (
                <li>等待输入触发</li>
              )}
            </ol>
            {DEBUG_CONFIG_FIELDS.map((field) => (
              <label key={field.key}>
                <span>{field.label}</span>
                <input
                  aria-label={field.label}
                  type="range"
                  min={field.min}
                  max={field.max}
                  step={field.step}
                  value={config[field.key]}
                  onChange={(event) => {
                    const value = Number(event.currentTarget.value);
                    setConfig((current) => ({ ...current, [field.key]: value }));
                  }}
                />
                <output>{config[field.key]}</output>
              </label>
            ))}
          </div>
        </aside>
      ) : null}
      {particleText ? (
        <div className="bark-battle-particles">{particleText}</div>
      ) : null}
      {snapshot.result ? (
        <div className="bark-battle-result-modal" role="presentation">
          <section
            className="bark-battle-result bark-battle-result--modal"
            role="dialog"
            aria-modal="true"
            aria-label="对战结算"
          >
            <p className="bark-battle-result__eyebrow">本局结束</p>
            <h2>{resolveResultTitle(snapshot.result.winner)}</h2>
            <div className="bark-battle-result__stats">
              <span>
                <strong>{snapshot.result.playerBarkCount}</strong>
                玩家叫声
              </span>
              <span>
                <strong>{snapshot.result.opponentBarkCount}</strong>
                对手压制
              </span>
              <span>
                <strong>{snapshot.result.score}</strong>
                声浪分
              </span>
            </div>
            <div className="bark-battle-result__actions">
              {onExit ? (
                <button type="button" onClick={exitRuntime}>
                  返回
                </button>
              ) : null}
              <button
                className="bark-battle-primary-button"
                type="button"
                onClick={restart}
              >
                再来一局
              </button>
            </div>
          </section>
        </div>
      ) : null}
    </main>
  );
}
