import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from 'react'
import {
  DEFAULT_GROUP_HEAL_TARGETS,
  INITIAL_PARTY,
  tankPressureTargets,
  type CombatLogEntry,
  type PartyMember,
  type Spell,
} from '../game'
import { completeRoguelike, recordPvpMatch } from '../profile'
import type { CharacterProfile } from '../profile'
import type { GameMode } from '../gameRepository'
import { PartyMemberFrame } from './PartyFrames'
import { SpellBar } from './SpellBars'
import { barFillStyle } from './barStyles'
import { focusFirstControl, useGameAction, useInput, type InputAction } from '../input'
import { useDeadlineTimer, useRoundCountdown } from '../hooks/useCountdownTimer'
import { useSpellSlots } from '../hooks/useSpellSlots'
import { useSidedFloatingCombatText } from '../hooks/useFloatingCombatText'
import { usePartyTargeting } from '../hooks/usePartyTargeting'
import { useDualScreen, useDualScreenPublisher } from '../dualScreen'
import {
  clamp,
  createLogEntry,
  effectiveMaxHealth,
  toCombatSpell,
} from '../combat/rules'
import { createStackCounts, summarizeStackCounts, type StackCounts } from '../combat/stackCounts'
import {
  hasSpellStack,
  spellCooldownMultiplier,
  spellExtraTargets,
  spellPowerMultiplier,
  spellResourceCost as modifiedSpellResourceCost,
} from '../combat/spellModifiers'
import { regenerateResource } from '../combat/combatTick'
import { advanceMemberTick } from '../combat/combatEngine'
import { buildStadiumDualScreenState } from '../combat/dualScreenPayloads'
import { applyPvpSpellCast } from '../combat/pvpSpellCasting'
import {
  pruneExpiredCooldowns,
} from '../combat/spellCasting'
import {
  appendCombatLog,
  STADIUM_COMBAT_LOG_LIMIT,
} from '../combat/combatPresentation'
import {
  buildSpellTargetPlan,
  type SpellEffectProfile,
} from '../combat/spellEffects'
import { runCpuHealTurn, type CpuTurnBehavior } from '../combat/cpuAi'
import {
  chooseStadiumCpuPurchases,
  resolveStadiumRound,
  stadiumShopPointsForOutcome,
  type StadiumExperienceMode,
  type StadiumRoundOutcome,
} from '../combat/stadiumLifecycle'
import {
  createStadiumLiveMatchStart,
  createStadiumMatchStart,
  createStadiumStarterSide,
} from '../combat/stadiumMatchSetup'
import {
  createEmptyRewardSummary,
  mergeDungeonRewardSummary,
  type RewardSummaryBase,
} from '../combat/rewardSummaries'
import { RewardXpSummary } from './RewardPanels'
import { ResultScreen } from './ResultScreen'
import {
  publishPvpMatchState,
  submitPvpUpgradeChoice,
  type CpuDifficulty,
  type PvpMatchSide,
  type PvpMatchSnapshot,
} from '../pvpRoguelike'
import { startPvpQueueWithCpuFallback } from '../pvpQueueLifecycle'
import { usePvpLiveMatchSync } from '../pvpLiveLifecycle'

const TICK_MS = 700
const ROUND_START_SECONDS = 3
const SHOP_SECONDS = 60
const MAX_RESOURCE = 100
const RESOURCE_REGEN_PER_TICK = 0.8

type SlotKey = '1' | '2' | '3' | '4' | '5'
type StadiumBuffId =
  | `slot${SlotKey}-extra-target`
  | `slot${SlotKey}-cost-down`
  | `slot${SlotKey}-cooldown-down`
  | 'slot1-applies-renew'
  | 'slot1-applies-shield'
  | 'slot2-applies-shield'
  | 'slot2-double-duration'
  | 'slot3-applies-shield'
  | 'slot3-applies-renew'
  | 'slot4-applies-renew'
  | 'slot5-applies-renew'
  | 'slot5-applies-shield'
  | 'fifth-cast-free'
  | 'group-heal-boost'
  | 'shield-boost'

type StadiumBuff = {
  id: StadiumBuffId
  name: string
  description: string
  category: SlotKey | 'misc'
  cost: 1 | 2
}

type StadiumSideState = {
  party: PartyMember[]
  resource: number
  cooldowns: Record<string, number>
  buffs: StadiumBuffId[]
  castsTowardFree: number
  freeCastReady: boolean
  survivalSeconds: number
  dampeningPercent: number
  roundIndex: number
  roundWins: number
  roundStatus: 'playing' | 'shop' | 'won' | 'lost'
  lastRoundOutcome?: StadiumRoundOutcome
  shopReady: boolean
}

type LivePvpMatch = {
  id: string
  side: PvpMatchSide
  opponentSide: PvpMatchSide
  opponentName: string
  opponentClassName: string
}

const CPU_BEHAVIOR: Record<CpuDifficulty, CpuTurnBehavior> = {
  1: { actionEveryTicks: 4, mistakeChance: 0.35, directHealThreshold: 0.54, groupHealThreshold: 0.5, hotThreshold: 0.6, shieldThreshold: 0.48 },
  2: { actionEveryTicks: 3, mistakeChance: 0.24, directHealThreshold: 0.6, groupHealThreshold: 0.56, hotThreshold: 0.66, shieldThreshold: 0.54 },
  3: { actionEveryTicks: 3, mistakeChance: 0.16, directHealThreshold: 0.66, groupHealThreshold: 0.62, hotThreshold: 0.72, shieldThreshold: 0.6 },
  4: { actionEveryTicks: 2, mistakeChance: 0.08, directHealThreshold: 0.72, groupHealThreshold: 0.68, hotThreshold: 0.78, shieldThreshold: 0.66 },
  5: { actionEveryTicks: 2, mistakeChance: 0.03, directHealThreshold: 0.78, groupHealThreshold: 0.74, hotThreshold: 0.84, shieldThreshold: 0.72 },
}

function formatTime(seconds: number) {
  const total = Math.max(0, Math.floor(seconds))
  const minutes = Math.floor(total / 60)
  const remaining = total % 60
  return `${minutes}:${String(remaining).padStart(2, '0')}`
}

function slotLabel(slot: SlotKey, spells: Spell[]) {
  const spell = spells.find((candidate) => candidate.key === slot)
  return spell ? `${spell.name} (Slot ${slot})` : `Slot ${slot}`
}

function slotSpellName(slot: SlotKey, spells: Spell[], fallback: string) {
  return spells.find((candidate) => candidate.key === slot)?.name ?? fallback
}

function buildStadiumBuffs(spells: Spell[]): StadiumBuff[] {
  const directName = slotSpellName('1', spells, 'Mend')
  const sustainName = slotSpellName('2', spells, 'Renew')
  const groupName = slotSpellName('3', spells, 'Radiance')
  const shieldName = slotSpellName('4', spells, 'Sun Ward')
  const cleanseName = slotSpellName('5', spells, 'Purify')
  const slotBuffs = (['1', '2', '3', '4', '5'] as SlotKey[]).flatMap((slot) => {
    const label = slotLabel(slot, spells)
    const baseBuffs: StadiumBuff[] = [
      {
        id: `slot${slot}-extra-target` as StadiumBuffId,
        name: '+1 target',
        description: `${label} affects 1 additional ally when possible.`,
        category: slot,
        cost: 2,
      },
      {
        id: `slot${slot}-cost-down` as StadiumBuffId,
        name: '-25% cost',
        description: `${label} costs 25% less mana.`,
        category: slot,
        cost: 1,
      },
      {
        id: `slot${slot}-cooldown-down` as StadiumBuffId,
        name: '-25% cooldown',
        description: `${label} recharges 25% faster.`,
        category: slot,
        cost: 1,
      },
    ]
    const specialBuffs: Partial<Record<SlotKey, StadiumBuff[]>> = {
      1: [
        {
          id: 'slot1-applies-renew',
          name: `Applies ${sustainName}`,
          description: `${directName} also applies ${sustainName} to the target.`,
          category: slot,
          cost: 2,
        },
        {
          id: 'slot1-applies-shield',
          name: `Applies ${shieldName}`,
          description: `${directName} also applies a ${shieldName} barrier to the target.`,
          category: slot,
          cost: 2,
        },
      ],
      2: [
        {
          id: 'slot2-applies-shield',
          name: `Applies ${shieldName}`,
          description: `${sustainName} also applies a ${shieldName} barrier to the target.`,
          category: slot,
          cost: 2,
        },
        {
          id: 'slot2-double-duration',
          name: 'Double Duration',
          description: `${sustainName} lasts twice as long or gains extra charges.`,
          category: slot,
          cost: 2,
        },
      ],
      3: [
        {
          id: 'slot3-applies-shield',
          name: `Applies 50% ${shieldName}`,
          description: `${groupName} applies a ${shieldName} barrier at 50% strength to affected targets.`,
          category: slot,
          cost: 2,
        },
        {
          id: 'slot3-applies-renew',
          name: `Applies ${sustainName}`,
          description: `${groupName} applies ${sustainName} to affected targets.`,
          category: slot,
          cost: 2,
        },
      ],
      4: [
        {
          id: 'slot4-applies-renew',
          name: `Applies ${sustainName}`,
          description: `${shieldName} also applies ${sustainName} to the target.`,
          category: slot,
          cost: 2,
        },
      ],
      5: [
        {
          id: 'slot5-applies-renew',
          name: `Applies ${sustainName}`,
          description: `${cleanseName} also applies ${sustainName} to the target.`,
          category: slot,
          cost: 2,
        },
        {
          id: 'slot5-applies-shield',
          name: `Applies ${shieldName}`,
          description: `${cleanseName} also applies a ${shieldName} barrier to the target.`,
          category: slot,
          cost: 2,
        },
      ],
    }
    return [...baseBuffs, ...(specialBuffs[slot] ?? [])]
  })
  return [
    ...slotBuffs,
    {
      id: 'fifth-cast-free',
      name: 'Stored Momentum',
      description: 'After 5 spell casts, your next cast is free.',
      category: 'misc',
      cost: 1,
    },
    {
      id: 'group-heal-boost',
      name: 'Wide Radiance',
      description: 'Party healing is 25% stronger.',
      category: 'misc',
      cost: 1,
    },
    {
      id: 'shield-boost',
      name: 'Dense Shields',
      description: 'Shield absorbs are 25% stronger.',
      category: 'misc',
      cost: 1,
    },
  ]
}

function summarizeStacks(items: StadiumBuffId[], catalog: StadiumBuff[]) {
  return summarizeStackCounts(createStackCounts(items), catalog, 'none')
}

function cooldownMultiplier(spell: Spell, buffs: StackCounts<StadiumBuffId>) {
  return spellCooldownMultiplier(spell, {
    stacks: buffs,
    id: (slot) => `slot${slot as SlotKey}-cooldown-down` as StadiumBuffId,
  })
}

function spellResourceCost(spell: Spell, buffs: StackCounts<StadiumBuffId>, freeCastReady: boolean) {
  return modifiedSpellResourceCost({
    spell,
    costDown: {
      stacks: buffs,
      id: (slot) => `slot${slot as SlotKey}-cost-down` as StadiumBuffId,
    },
    freeCastReady,
    freeCast: {
      stacks: buffs,
      id: 'fifth-cast-free',
    },
  })
}

export function PvpStadiumScreen({
  profile,
  gameMode,
  onExit,
  onProfileUpdated,
}: {
  profile: CharacterProfile
  gameMode: GameMode
  onExit: () => void
  onProfileUpdated: (profile: CharacterProfile) => void
}) {
  const gameClass = profile.classes.find((candidate) => candidate.id === profile.character.classId)!
  const starterSpells = useMemo(() => gameClass.spells
    .filter((spell) => spell.unlockLevel === 1)
    .slice(0, 5)
    .map((spell, index) => toCombatSpell(spell, String(index + 1))), [gameClass.spells])
  const buffCatalog = useMemo(() => buildStadiumBuffs(starterSpells), [starterSpells])
  const partyTemplate = useMemo(
    () => INITIAL_PARTY.map((member) => ({
      ...member,
      name: member.id === 'mira' ? profile.character.name : member.name,
    })),
    [profile.character.name],
  )
  const cpuPartyTemplate = useMemo(
    () => INITIAL_PARTY.map((member) => ({
      ...member,
      name: member.id === 'mira' ? 'CPU Healer' : member.name,
    })),
    [],
  )
  const rewardDungeon = profile.dungeons.find((candidate) => candidate.contentType === 'dungeon') ?? profile.dungeons[0]
  const rewardDifficulty = rewardDungeon.difficulties[0]
  const [status, setStatus] = useState<'queueing' | 'round-countdown' | 'playing' | 'shop' | 'won' | 'lost'>('queueing')
  const [playerSide, setPlayerSide] = useState<StadiumSideState>(() => createStadiumStarterSide<StadiumBuffId>({
    partyTemplate,
    maxResource: MAX_RESOURCE,
    roundIndex: 1,
  }))
  const [cpuSide, setCpuSide] = useState<StadiumSideState>(() => createStadiumStarterSide<StadiumBuffId>({
    partyTemplate: cpuPartyTemplate,
    maxResource: MAX_RESOURCE,
    roundIndex: 1,
  }))
  const [selectedId, setSelectedId] = useState(partyTemplate[0].id)
  const [roundIndex, setRoundIndex] = useState(1)
  const [roundWins, setRoundWins] = useState({ player: 0, opponent: 0 })
  const [shopPoints, setShopPoints] = useState(0)
  const [shopReady, setShopReady] = useState(false)
  const [shopCategory, setShopCategory] = useState<SlotKey | 'misc'>('1')
  const [elapsedTicks, setElapsedTicks] = useState(0)
  const [cpuDifficulty, setCpuDifficulty] = useState<CpuDifficulty | null>(null)
  const [liveMatch, setLiveMatch] = useState<LivePvpMatch | null>(null)
  const [queueMessage, setQueueMessage] = useState('Searching Stadium queue...')
  const [paused, setPaused] = useState(false)
  const [log, setLog] = useState<CombatLogEntry[]>([{ id: 1, text: 'Queueing Stadium opponent...', tone: 'system' }])
  const {
    playerFloatingTextsByMember,
    cpuFloatingTextsByMember,
    dualScreenFloatingTexts,
    addFloatingText,
    clearFloatingTexts,
  } = useSidedFloatingCombatText()
  const [rewardSummary, setRewardSummary] = useState<RewardSummaryBase>(() => createEmptyRewardSummary())
  const [rewardError, setRewardError] = useState('')
  const [showEndLog, setShowEndLog] = useState(false)
  const selectedIdRef = useRef(partyTemplate[0].id)
  const playerRef = useRef(playerSide)
  const cpuRef = useRef(cpuSide)
  const liveMatchRef = useRef<LivePvpMatch | null>(null)
  const nextLogId = useRef(2)
  const submittedShopRef = useRef(false)
  const awardedXpRef = useRef(new Set<string>())
  const matchStatsRecordedRef = useRef(false)
  const queuedMatchRef = useRef(false)
  const roundResolvedRef = useRef(false)
  const loggedOpponentRoundRef = useRef('')
  const {
    bindings,
    controllerIconStyle,
    directPartyTargeting,
    lastDevice,
  } = useInput()
  const { enabled: dualScreenEnabled } = useDualScreen()
  const activeBindings = bindings[lastDevice]
  const opponentLabel = liveMatch ? liveMatch.opponentName : `CPU ${cpuDifficulty ?? 1}`
  const playerAlive = playerSide.party.some((member) => member.health > 0)
  const playerBuffCounts = useMemo(() => createStackCounts(playerSide.buffs), [playerSide.buffs])
  const playerSpellSlotCost = useCallback(
    (spell: Spell) => spellResourceCost(spell, playerBuffCounts, playerSide.freeCastReady),
    [playerBuffCounts, playerSide.freeCastReady],
  )
  const playerSpellSlots = useSpellSlots({
    spells: starterSpells,
    cooldowns: playerSide.cooldowns,
    active: status === 'playing' && !paused,
    cost: playerSpellSlotCost,
  })
  const opponentBuffSummary = useMemo(
    () => summarizeStacks(cpuSide.buffs, buffCatalog),
    [buffCatalog, cpuSide.buffs],
  )
  const playerBuffSummary = useMemo(
    () => summarizeStacks(playerSide.buffs, buffCatalog),
    [buffCatalog, playerSide.buffs],
  )
  const opponentDebuffSummary = useMemo(
    () => `Dampening ${playerSide.dampeningPercent}%`,
    [playerSide.dampeningPercent],
  )
  const partyColumns = 3

  const setSelectedTargetId = useCallback((id: string) => {
    selectedIdRef.current = id
    setSelectedId(id)
  }, [])

  const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => {
    setLog((current) => appendCombatLog(current, createLogEntry(nextLogId, text, tone), STADIUM_COMBAT_LOG_LIMIT))
  }, [])

  const addFloatingHeal = useCallback((side: 'player' | 'cpu', memberId: string, value: number) => {
    addFloatingText({ side, memberId, value })
  }, [addFloatingText])

  const {
    timeLeft: roundCountdown,
    start: startRoundCountdown,
    clear: clearRoundCountdown,
  } = useRoundCountdown({
    seconds: ROUND_START_SECONDS,
    active: status === 'round-countdown',
    onComplete: () => {
      setStatus((current) => current === 'round-countdown' ? 'playing' : current)
    },
  })

  const {
    timeLeft: shopTimeLeft,
    start: startShopTimer,
    reset: resetShopTimer,
  } = useDeadlineTimer({
    initialSeconds: SHOP_SECONDS,
    active: status === 'shop' && !shopReady,
    intervalMs: 200,
    onExpire: () => finishShop(),
  })

  const beginRoundCountdown = useCallback(() => {
    clearRoundCountdown()
    roundResolvedRef.current = false
    startRoundCountdown()
    setStatus('round-countdown')
  }, [clearRoundCountdown, startRoundCountdown])

  const awardXp = useCallback((key: string, mode: StadiumExperienceMode) => {
    if (awardedXpRef.current.has(key)) return
    awardedXpRef.current.add(key)
    completeRoguelike(rewardDungeon.id, rewardDifficulty.id, 0, 0, Math.max(1, Math.floor(playerRef.current.survivalSeconds || 1)), {
      bossesCleared: 0,
      fightsCleared: 1,
      experienceMode: mode,
    })
      .then((result) => {
        setRewardSummary((current) => mergeDungeonRewardSummary(current, result))
        onProfileUpdated(result.profile)
        if (result.experienceGained > 0) addLog(`+${result.experienceGained} XP awarded.`, 'loot')
      })
      .catch((reason: unknown) => {
        setRewardError(reason instanceof Error ? reason.message : 'Unable to award Stadium XP.')
      })
  }, [addLog, onProfileUpdated, rewardDifficulty.id, rewardDungeon.id])

  const startLiveMatch = useCallback((match: PvpMatchSnapshot<StadiumSideState>, side: PvpMatchSide, message?: string) => {
    const setup = createStadiumLiveMatchStart<StadiumBuffId>({
      match,
      side,
      partyTemplate,
      opponentPartyTemplate: cpuPartyTemplate,
      maxResource: MAX_RESOURCE,
      message,
    })
    playerRef.current = setup.playerSide
    cpuRef.current = setup.opponentSide
    liveMatchRef.current = setup.liveMatch
    queuedMatchRef.current = true
    nextLogId.current = 2
    awardedXpRef.current = new Set()
    matchStatsRecordedRef.current = false
    roundResolvedRef.current = false
    setPlayerSide(setup.playerSide)
    setCpuSide(setup.opponentSide)
    setRoundIndex(setup.defaults.roundIndex)
    setRoundWins(setup.defaults.roundWins)
    setSelectedTargetId(partyTemplate[0].id)
    setElapsedTicks(setup.defaults.elapsedTicks)
    setShopPoints(setup.defaults.shopPoints)
    setShopReady(setup.defaults.shopReady)
    resetShopTimer(SHOP_SECONDS)
    setCpuDifficulty(null)
    setLiveMatch(setup.liveMatch)
    setPaused(setup.defaults.paused)
    setRewardSummary(createEmptyRewardSummary())
    setRewardError(setup.defaults.rewardError)
    setShowEndLog(setup.defaults.showEndLog)
    clearFloatingTexts()
    loggedOpponentRoundRef.current = ''
    setQueueMessage(setup.logText)
    setLog([{ id: 1, text: setup.logText, tone: 'system' }])
    beginRoundCountdown()
  }, [beginRoundCountdown, clearFloatingTexts, cpuPartyTemplate, partyTemplate, resetShopTimer, setSelectedTargetId])

  const startMatch = useCallback(() => {
    clearRoundCountdown()
    const setup = createStadiumMatchStart<StadiumBuffId>({
      partyTemplate,
      opponentPartyTemplate: cpuPartyTemplate,
      maxResource: MAX_RESOURCE,
    })
    playerRef.current = setup.playerSide
    cpuRef.current = setup.opponentSide
    liveMatchRef.current = null
    queuedMatchRef.current = true
    nextLogId.current = 2
    awardedXpRef.current = new Set()
    matchStatsRecordedRef.current = false
    roundResolvedRef.current = false
    setPlayerSide(setup.playerSide)
    setCpuSide(setup.opponentSide)
    setRoundIndex(setup.defaults.roundIndex)
    setRoundWins(setup.defaults.roundWins)
    setSelectedTargetId(partyTemplate[0].id)
    setElapsedTicks(setup.defaults.elapsedTicks)
    setStatus('queueing')
    setShopPoints(setup.defaults.shopPoints)
    setShopReady(setup.defaults.shopReady)
    resetShopTimer(SHOP_SECONDS)
    setCpuDifficulty(null)
    setLiveMatch(null)
    setPaused(setup.defaults.paused)
    setRewardSummary(createEmptyRewardSummary())
    setRewardError(setup.defaults.rewardError)
    setShowEndLog(setup.defaults.showEndLog)
    clearFloatingTexts()
    loggedOpponentRoundRef.current = ''
    const beginCpuMatch = (randomCpu: CpuDifficulty, message: string) => {
      setCpuDifficulty(randomCpu)
      setQueueMessage(message)
      setLog([{ id: 1, text: message, tone: 'system' }])
      beginRoundCountdown()
    }
    return startPvpQueueWithCpuFallback<StadiumSideState>({
      contentType: 'stadium',
      startStage: 1,
      gameMode,
      liveMatchActive: () => Boolean(liveMatchRef.current),
      onSearching: (message) => {
        setQueueMessage(message)
        setLog([{ id: 1, text: message, tone: 'system' }])
      },
      onCpuMatch: beginCpuMatch,
      onLiveMatch: (match, side, message) => startLiveMatch(match, side, message),
      messages: {
        offline: (difficulty) => `Offline mode. CPU ${difficulty} enters Stadium.`,
        searching: 'Searching Stadium queue for 5s.',
        notFound: (difficulty) => `No Stadium player found after 5s. CPU ${difficulty} steps in.`,
        unavailable: (difficulty) => `PvP server unavailable. CPU ${difficulty} steps in.`,
        liveFound: (match, side) => {
          const opponentSide: PvpMatchSide = side === 'a' ? 'b' : 'a'
          return `${match.players[opponentSide].characterName} found. Stadium begins.`
        },
      },
    })
  }, [beginRoundCountdown, clearFloatingTexts, clearRoundCountdown, cpuPartyTemplate, gameMode, partyTemplate, resetShopTimer, setSelectedTargetId, startLiveMatch])

  useEffect(() => {
    const frame = window.requestAnimationFrame(() => startMatch())
    return () => window.cancelAnimationFrame(frame)
  }, [startMatch])

  const applySpell = useCallback((
    current: StadiumSideState,
    setCurrent: Dispatch<SetStateAction<StadiumSideState>>,
    sideName: 'player' | 'cpu',
    spell: Spell,
    targetId: string,
  ) => {
    const buffCounts = createStackCounts(current.buffs)
    const effectiveCost = spellResourceCost(spell, buffCounts, current.freeCastReady)
    const dampenMultiplier = Math.max(0, 1 - current.dampeningPercent / 100)
    const hasBuff = (id: StadiumBuffId) => hasSpellStack({ stacks: buffCounts, id })
    const extraTargets = spellExtraTargets(spell, {
      stacks: buffCounts,
      id: (slot) => `slot${slot as SlotKey}-extra-target` as StadiumBuffId,
    })
    const renewDuration = hasBuff('slot2-double-duration') && spell.key === '2' ? 10 : 5
    const shieldEffect = starterSpells.find((candidate) => candidate.kind === 'shield')
    const shieldPower = (sourcePower: number, strength = 1) => Math.round(
      sourcePower
      * strength
      * spellPowerMultiplier({ stacks: buffCounts, id: 'shield-boost' })
      * dampenMultiplier,
    )
    const appliesRenew = spell.effectType === 'direct_hot'
      || (spell.key === '1' && hasBuff('slot1-applies-renew'))
      || (spell.key === '4' && hasBuff('slot4-applies-renew'))
      || (spell.key === '5' && hasBuff('slot5-applies-renew'))
    const appliesShield = (spell.key === '1' && hasBuff('slot1-applies-shield'))
      || (spell.key === '2' && hasBuff('slot2-applies-shield'))
      || (spell.key === '5' && hasBuff('slot5-applies-shield'))
    const {
      directTargets,
      hotTargets,
      shieldTargets,
      damageReductionTargets,
      groupTargets,
    } = buildSpellTargetPlan({
      party: current.party,
      spell,
      targetId,
      extraTargets,
      directTarget: spell.kind === 'direct' || spell.kind === 'cleanse',
      hotTarget: spell.kind === 'hot' || spell.kind === 'bounce_heal' || appliesRenew,
      shieldTarget: spell.kind === 'shield' || appliesShield,
      damageReductionTarget: spell.kind === 'damage_reduction',
      groupTargetCount: DEFAULT_GROUP_HEAL_TARGETS + extraTargets,
      extraTargetMode: {
        hot: 'hot',
        bounce_heal: 'hot',
        shield: 'shield',
        damage_reduction: 'damageReduction',
      },
    })
    if (spell.effectType === 'direct_hot') directTargets.forEach((id) => hotTargets.add(id))
    const groupHealBoost = spellPowerMultiplier({ stacks: buffCounts, id: 'group-heal-boost' })
    const spellEffectProfile: SpellEffectProfile = {
      modeName: 'stadium',
      heal: (member, power) => clamp(member.health + power, 0, effectiveMaxHealth(member)),
      healingMultiplier: () => 1,
      power: {
        direct: (source) => Math.round(source.power * dampenMultiplier),
        cleanse: (source) => Math.round(source.power * dampenMultiplier),
        groupHeal: (source) => Math.round(source.power * groupHealBoost * dampenMultiplier),
        groupAbsorb: (source) => shieldPower(source.power),
        shield: shieldPower,
      },
      hot: {
        mode: 'ticks',
        defaultTicks: renewDuration,
        groupTicks: 5,
        radianceTicks: 5,
        merge: 'max',
        groupMerge: 'max',
      },
      effects: {
        renewSpell: undefined,
        shieldSpell: shieldEffect,
        groupAbsorbOnly: (source) => source.effectType === 'party_absorb',
        groupHotOnly: (source) => source.effectType === 'party_hot',
        groupAppliesShield: (source) => source.effectType === 'party_absorb'
          || hasBuff('slot3-applies-shield'),
        groupAppliesHot: (source) => source.effectType === 'party_hot'
          || hasBuff('slot3-applies-renew'),
        shieldAppliesHot: () => false,
        hotSpellForDirect: (source) => source,
      },
      ratios: {
        groupShield: 0.5,
        directShield: 1,
      },
      damageReductionTicks: 12,
      floatingHeals: {
        group: true,
        direct: true,
        cleanse: true,
      },
      bounceHeals: false,
    }
    return applyPvpSpellCast({
      current,
      spell,
      targetId,
      resourceCost: effectiveCost,
      targetPlan: {
        directTargets,
        hotTargets,
        shieldTargets,
        damageReductionTargets,
        groupTargets,
      },
      profile: spellEffectProfile,
      setCurrent,
      emitFloatingHeal: (memberId, value) => addFloatingHeal(sideName, memberId, value),
      cooldownMultiplier: cooldownMultiplier(spell, buffCounts),
      freeCast: {
        enabled: hasBuff('fifth-cast-free'),
        wasReady: effectiveCost === 0 && current.freeCastReady,
      },
    })
  }, [addFloatingHeal, starterSpells])

  const castPlayerSpell = useCallback((spell: Spell) => {
    if (status !== 'playing' || !playerAlive) return
    const targetId = selectedIdRef.current
    const succeeded = applySpell(playerRef.current, (value) => {
      const next = typeof value === 'function' ? value(playerRef.current) : value
      playerRef.current = next
      setPlayerSide(next)
    }, 'player', spell, targetId)
    if (succeeded) addLog(`${spell.name} cast on ${playerRef.current.party.find((member) => member.id === targetId)?.name ?? 'target'}.`, 'heal')
  }, [addLog, applySpell, playerAlive, status])

  const getTargetParty = useCallback(() => playerRef.current.party, [])
  const {
    selectRelativeTarget,
    selectDirectionalTarget,
    selectDirectTarget,
  } = usePartyTargeting({
    getParty: getTargetParty,
    selectedIdRef,
    setSelectedTargetId,
    columns: partyColumns,
    livingOnly: true,
  })

  const cpuTakeTurn = useCallback(() => {
    if (!cpuDifficulty || status !== 'playing') return
    const behavior = CPU_BEHAVIOR[cpuDifficulty]
    runCpuHealTurn({
      side: cpuRef.current,
      elapsedTicks,
      spells: starterSpells,
      behavior,
      preferSlots: true,
      applySpell: (side, spell, targetId) => {
        applySpell(side, (value) => {
          const next = typeof value === 'function' ? value(cpuRef.current) : value
          cpuRef.current = next
          setCpuSide(next)
        }, 'cpu', spell, targetId)
      },
    })
  }, [applySpell, cpuDifficulty, elapsedTicks, starterSpells, status])

  const advanceBoss = useCallback((side: StadiumSideState) => {
    if (side.roundStatus !== 'playing') return side
    const party = side.party
    const nextSurvival = side.survivalSeconds + TICK_MS / 1000
    const dampeningPercent = Math.floor(nextSurvival / 5)
    const dampenMultiplier = Math.max(0, 1 - dampeningPercent / 100)
    const hotHealing = Math.round(6 * dampenMultiplier)
    const living = party.filter((member) => member.health > 0)
    if (living.length === 0) return side
    const spikeTarget = living[Math.floor(Math.random() * living.length)]
    const tankIds = new Set(tankPressureTargets(party).targets.map((member) => member.id))
    const pulse = elapsedTicks > 0 && elapsedTicks % 5 === 0
    const spike = elapsedTicks > 0 && elapsedTicks % 8 === 0
    const nextParty = party.map((member) => {
      if (member.health <= 0) return member
      let damage = tankIds.has(member.id) ? 8 : 0
      if (pulse) damage += 9
      if (spike && member.id === spikeTarget.id) damage += 22
      const result = advanceMemberTick({
        member,
        party,
        damage,
        hotHealing,
        hotTicks: 'ticks',
        applyDebuff: spike && member.id === spikeTarget.id
          ? { label: 'Marked', ticks: 4 }
          : undefined,
        decrementDamageReduction: true,
        damageReductionRounding: 'ceil',
      })
      return {
        ...result.member,
        damageReductionTicks: result.member.damageReductionTicks || undefined,
        debuffTicks: result.member.debuffTicks || undefined,
      }
    })
    return {
      ...side,
      party: nextParty,
      resource: regenerateResource(side.resource, RESOURCE_REGEN_PER_TICK, MAX_RESOURCE),
      cooldowns: pruneExpiredCooldowns(side.cooldowns),
      survivalSeconds: nextSurvival,
      dampeningPercent,
    }
  }, [elapsedTicks])

  const beginShop = useCallback((outcome: StadiumRoundOutcome, nextWins: { player: number; opponent: number }) => {
    const points = stadiumShopPointsForOutcome(outcome, 'player')
    submittedShopRef.current = false
    setShopPoints(points)
    setShopReady(false)
    startShopTimer()
    setRoundWins(nextWins)
    setStatus('shop')
    setPlayerSide((current) => {
      const next = { ...current, roundStatus: 'shop' as const, lastRoundOutcome: outcome, shopReady: false, roundWins: nextWins.player }
      playerRef.current = next
      return next
    })
    if (!liveMatchRef.current) {
      const purchases = chooseStadiumCpuPurchases<StadiumBuffId, StadiumBuff>(
        buffCatalog,
        stadiumShopPointsForOutcome(outcome, 'opponent'),
      )
      setCpuSide((current) => {
        const next = { ...current, buffs: [...current.buffs, ...purchases], roundStatus: 'shop' as const, roundWins: nextWins.opponent }
        cpuRef.current = next
        return next
      })
    }
  }, [buffCatalog, startShopTimer])

  const finishRound = useCallback((outcome: StadiumRoundOutcome) => {
    if (status !== 'playing') return
    if (roundResolvedRef.current) return
    roundResolvedRef.current = true
    const result = resolveStadiumRound({ outcome, roundIndex, wins: roundWins })
    awardXp(result.roundExperience.key, result.roundExperience.mode)
    addLog(result.log.text, result.log.tone)
    if (result.status === 'won') {
      setRoundWins(result.nextWins)
      setStatus(result.status)
      setPlayerSide((current) => {
        const next = { ...current, roundStatus: result.playerRoundStatus, lastRoundOutcome: outcome, roundWins: result.nextWins.player }
        playerRef.current = next
        return next
      })
      if (result.matchExperience) awardXp(result.matchExperience.key, result.matchExperience.mode)
      return
    }
    if (result.status === 'lost') {
      setRoundWins(result.nextWins)
      setStatus(result.status)
      setPlayerSide((current) => {
        const next = { ...current, roundStatus: result.playerRoundStatus, lastRoundOutcome: outcome, roundWins: result.nextWins.player }
        playerRef.current = next
        return next
      })
      return
    }
    beginShop(outcome, result.nextWins)
  }, [addLog, awardXp, beginShop, roundIndex, roundWins, status])

  useEffect(() => {
    if (status !== 'playing' || paused) return
    const timer = window.setInterval(() => {
      setElapsedTicks((value) => value + 1)
      if (!liveMatchRef.current) cpuTakeTurn()
      const nextPlayer = advanceBoss(playerRef.current)
      const nextCpu = liveMatchRef.current ? cpuRef.current : advanceBoss(cpuRef.current)
      playerRef.current = nextPlayer
      cpuRef.current = nextCpu
      setPlayerSide(nextPlayer)
      setCpuSide(nextCpu)
      const nextPlayerAlive = nextPlayer.party.some((member) => member.health > 0)
      const nextCpuAlive = nextCpu.party.some((member) => member.health > 0)
      if (!nextPlayerAlive && (!liveMatchRef.current && !nextCpuAlive)) finishRound('tie')
      else if (!nextPlayerAlive) finishRound('loss')
      else if (!liveMatchRef.current && !nextCpuAlive) finishRound('win')
    }, TICK_MS)
    return () => window.clearInterval(timer)
  }, [advanceBoss, cpuTakeTurn, finishRound, paused, status])

  useEffect(() => {
    if (status !== 'won' && status !== 'lost') return
    if (matchStatsRecordedRef.current) return
    matchStatsRecordedRef.current = true
    recordPvpMatch(status === 'won')
      .then(onProfileUpdated)
      .catch((reason: unknown) => {
        addLog(
          reason instanceof Error ? reason.message : 'Unable to record PvP match.',
          'danger',
        )
      })
  }, [addLog, onProfileUpdated, status])

  const startNextRound = useCallback(() => {
    const nextRound = roundIndex + 1
    const nextPlayer = createStadiumStarterSide<StadiumBuffId>({
      partyTemplate,
      maxResource: MAX_RESOURCE,
      roundIndex: nextRound,
      buffs: playerRef.current.buffs,
      roundWins: roundWins.player,
    })
    const nextCpu = createStadiumStarterSide<StadiumBuffId>({
      partyTemplate: cpuPartyTemplate,
      maxResource: MAX_RESOURCE,
      roundIndex: nextRound,
      buffs: cpuRef.current.buffs,
      roundWins: roundWins.opponent,
    })
    playerRef.current = nextPlayer
    cpuRef.current = nextCpu
    roundResolvedRef.current = false
    loggedOpponentRoundRef.current = ''
    setRoundIndex(nextRound)
    setPlayerSide(nextPlayer)
    setCpuSide(nextCpu)
    setSelectedTargetId(partyTemplate[0].id)
    setElapsedTicks(0)
    setShopReady(false)
    setShopPoints(0)
    addLog(`Round ${nextRound} starts. HP and mana restored.`, 'system')
    if (liveMatchRef.current) {
      publishPvpMatchState<StadiumSideState>(liveMatchRef.current.id, {
        state: nextPlayer,
        status: 'playing',
        stage: nextRound,
        encounterIndex: nextRound,
        encountersCleared: roundWins.player,
        enemyHealth: 0,
        alive: true,
        elapsedTicks: 0,
      }).catch(() => undefined)
    }
    beginRoundCountdown()
  }, [addLog, beginRoundCountdown, cpuPartyTemplate, partyTemplate, roundIndex, roundWins.opponent, roundWins.player, setSelectedTargetId])

  const finishShop = useCallback(() => {
    if (shopReady || status !== 'shop') return
    const liveMatch = liveMatchRef.current
    if (!liveMatch) {
      startNextRound()
      return
    }
    setShopReady(true)
    submittedShopRef.current = true
    const readyPlayer = { ...playerRef.current, shopReady: true }
    playerRef.current = readyPlayer
    setPlayerSide(readyPlayer)
    submitPvpUpgradeChoice(liveMatch.id, {
      encounterIndex: roundIndex,
      buffId: 'stadium-shop',
      debuffId: '',
      purchases: playerRef.current.buffs,
      shopReady: true,
    }).catch(() => undefined)
  }, [roundIndex, shopReady, startNextRound, status])

  const { rematchRequested, rematchMessage, handleRematch } = usePvpLiveMatchSync<StadiumSideState, LivePvpMatch>({
    liveMatch,
    syncEnabled: status !== 'queueing',
    startLiveMatch,
    getPayload: () => ({
      state: playerRef.current,
      status: status === 'shop' || status === 'won' || status === 'lost' ? status : 'playing',
      stage: roundIndex,
      encounterIndex: roundIndex,
      encountersCleared: roundWins.player,
      enemyHealth: 0,
      alive: playerRef.current.party.some((member) => member.health > 0),
      elapsedTicks,
    }),
    onSnapshot: (snapshot, currentLiveMatch) => {
      const opponentState = snapshot.states[currentLiveMatch.opponentSide]
      if (opponentState && opponentState.roundIndex >= roundIndex) {
        cpuRef.current = opponentState
        setCpuSide(opponentState)
      }
      const opponentStatus = snapshot.statuses[currentLiveMatch.opponentSide]
      if (opponentStatus === 'won' && status !== 'won' && status !== 'lost') setStatus('lost')
      if (opponentStatus === 'lost' && status !== 'won' && status !== 'lost') setStatus('won')
      if (!opponentState) return
      if (opponentState.roundIndex < roundIndex) return
      if (status === 'playing' && opponentState.roundIndex === roundIndex && opponentState.roundStatus === 'shop' && opponentState.lastRoundOutcome) {
        const key = `${roundIndex}-${opponentState.lastRoundOutcome}`
        if (loggedOpponentRoundRef.current === key) return
        loggedOpponentRoundRef.current = key
        if (opponentState.lastRoundOutcome === 'loss') finishRound('win')
        else if (opponentState.lastRoundOutcome === 'win') finishRound('loss')
        else finishRound('tie')
      }
      if (
        status === 'shop'
        && shopReady
        && (
          (opponentState.roundIndex === roundIndex && opponentState.shopReady)
          || opponentState.roundIndex > roundIndex
        )
      ) {
        startNextRound()
      }
    },
  })

  const buyBuff = useCallback((buff: StadiumBuff) => {
    if (status !== 'shop' || shopReady || shopPoints < buff.cost) return
    let purchased = false
    setShopPoints((points) => {
      if (points < buff.cost) return points
      purchased = true
      return points - buff.cost
    })
    if (!purchased) return
    setPlayerSide((current) => {
      const next = { ...current, buffs: [...current.buffs, buff.id] }
      playerRef.current = next
      return next
    })
    addLog(`${buff.name} purchased.`, 'loot')
  }, [addLog, shopPoints, shopReady, status])

  useEffect(() => {
    if (status !== 'shop') return
    window.requestAnimationFrame(() => focusFirstControl())
  }, [status])

  useEffect(() => {
    if (!paused) return
    window.requestAnimationFrame(() => focusFirstControl())
  }, [paused])

  useGameAction((action) => {
    if (action === 'pause' || action === 'back') {
      if (status === 'playing') setPaused((value) => !value)
      return
    }
    if (paused || status !== 'playing') return
    if (action.startsWith('navigate')) {
      selectDirectionalTarget(action)
      return
    }
    if (action === 'previousTarget') {
      selectRelativeTarget(-1)
      return
    }
    if (action === 'nextTarget') {
      selectRelativeTarget(1)
      return
    }
    if (action.startsWith('targetParty')) {
      selectDirectTarget(Number(action.slice('targetParty'.length)) - 1)
      return
    }
    if (action.startsWith('ability')) {
      const spell = starterSpells.find((candidate) => candidate.key === action.slice('ability'.length))
      if (spell) castPlayerSpell(spell)
    }
  })

  const dualScreenState = useMemo(() => buildStadiumDualScreenState({
    difficultyName: 'Equalized iLvl 10',
    dungeonName: 'Stadium',
    contentName: 'Stadium',
    encounterName: 'Iron Arbiter',
    encounterDescription: 'Survive escalating arena pressure.',
    encounterHealth: 0,
    encounterMaxHealth: 1,
    encounterIsBoss: true,
    encounterIndex: roundIndex - 1,
    encounterCount: 5,
    party: playerSide.party,
    opponentName: opponentLabel,
    opponentClassName: liveMatch?.opponentClassName ?? (cpuDifficulty ? `CPU ${cpuDifficulty}` : 'CPU'),
    opponentParty: cpuSide.party,
    opponentEnemyHealth: 0,
    opponentResource: cpuSide.resource,
    opponentMaxResource: MAX_RESOURCE,
    opponentResourceName: gameClass.resourceName,
    opponentBuffSummary,
    opponentDebuffSummary,
    floatingTexts: dualScreenFloatingTexts,
    partySize: playerSide.party.length,
    selectedId,
    status: status === 'queueing' || status === 'round-countdown' ? 'playing' : status === 'shop' ? 'upgrade-choice' : status,
    resource: playerSide.resource,
    maxResource: MAX_RESOURCE,
    resourceName: gameClass.resourceName,
    playerIsAlive: playerAlive,
    spells: playerSpellSlots,
    bindings: activeBindings,
    controllerIconStyle,
    directPartyTargeting,
    paused,
    stadium: {
      dampeningPercent: playerSide.dampeningPercent,
      roundIndex,
      playerWins: roundWins.player,
      opponentWins: roundWins.opponent,
      survivalSeconds: playerSide.survivalSeconds,
      opponentSurvivalSeconds: cpuSide.survivalSeconds,
    },
  }), [
    activeBindings,
    controllerIconStyle,
    cpuDifficulty,
    cpuSide.party,
    cpuSide.resource,
    cpuSide.survivalSeconds,
    directPartyTargeting,
    dualScreenFloatingTexts,
    gameClass.resourceName,
    liveMatch?.opponentClassName,
    opponentBuffSummary,
    opponentDebuffSummary,
    opponentLabel,
    paused,
    playerAlive,
    playerSide.dampeningPercent,
    playerSide.party,
    playerSide.resource,
    playerSide.survivalSeconds,
    roundIndex,
    roundWins.opponent,
    roundWins.player,
    selectedId,
    playerSpellSlots,
    status,
  ])
  useDualScreenPublisher(dualScreenState, dualScreenEnabled)

  const visibleBuffs = useMemo(
    () => buffCatalog.filter((buff) => buff.category === shopCategory),
    [buffCatalog, shopCategory],
  )
  const categoryLabel = shopCategory === 'misc' ? 'Miscellaneous' : slotLabel(shopCategory, starterSpells)

  return (
    <main className={`game-shell ${dualScreenEnabled ? 'dual-top-game-shell' : ''}`} data-combat-active={status === 'playing' && !paused ? 'true' : 'false'}>
      <section className="content-screen stadium-screen">
        {status === 'queueing' && (
          <div className="placeholder-panel">
            <div className="placeholder-runes">P V P</div>
            <p>{queueMessage}</p>
          </div>
        )}

        {dualScreenEnabled && status !== 'queueing' && (
          <div className="dual-top-main stadium-dual-top">
            <header className="stadium-header">
              <div>
                <p className="eyebrow">Stadium</p>
                <h2>Round {roundIndex} / Best of 5</h2>
              </div>
              <strong>{roundWins.player} - {roundWins.opponent}</strong>
              <div>
                <p>Dampening {playerSide.dampeningPercent}%</p>
                <small>Survival {formatTime(playerSide.survivalSeconds)} | Equalized iLvl 10</small>
              </div>
            </header>

            <section className="stadium-pressure-panel">
              <strong>Iron Arbiter</strong>
              <span>No boss health | Next pulse in {Math.max(0, 5 - (elapsedTicks % 5) * (TICK_MS / 1000)).toFixed(1)}s</span>
            </section>

            <section className="dual-top-party">
              <div className="dual-top-party-grid">
                {playerSide.party.map((member, index) => {
                  const action = `targetParty${index + 1}` as InputAction
                  const targetBinding = directPartyTargeting ? activeBindings[action] : null
                  return (
                    <PartyMemberFrame
                      baseClassName="dual-top-member"
                      controllerIconStyle={controllerIconStyle}
                      effectMode="compact"
                      floatingTexts={playerFloatingTextsByMember.get(member.id) ?? []}
                      key={member.id}
                      member={member}
                      onSelect={setSelectedTargetId}
                      selected={selectedId === member.id}
                      targetBinding={targetBinding}
                    />
                  )
                })}
              </div>
            </section>

            <section className="dual-top-spell-strip">
              {playerSpellSlots.map((spell, slotIndex) => {
                if (!spell) return null
                const remaining = spell.remaining
                const percent = remaining > 0
                  ? Math.min(100, (remaining / Math.max(1, spell.cooldown)) * 100)
                  : 0
                return (
                  <button
                    className="dual-top-spell"
                    disabled={status !== 'playing' || !playerAlive || remaining > 0 || playerSide.resource < spell.cost || paused}
                    key={spell.id}
                    onClick={() => castPlayerSpell(spell)}
                    type="button"
                  >
                    <span className={`spell-icon spell-${spell.kind}`}>{spell.glyph}</span>
                    {remaining > 0 && <i style={{ height: `${percent}%` }} />}
                    {remaining > 0 && <small>{remaining.toFixed(0)}</small>}
                    <span className="sr-only">{slotIndex + 1}. {spell.name}</span>
                  </button>
                )
              })}
              <div className="dual-top-resource">
                <strong>{gameClass.resourceName} {Math.floor(playerSide.resource)} / {MAX_RESOURCE}</strong>
                <div className="bar mana-bar">
                  <span style={barFillStyle((playerSide.resource / MAX_RESOURCE) * 100)} />
                </div>
              </div>
            </section>
          </div>
        )}

        {!dualScreenEnabled && status !== 'queueing' && (
          <div className="stadium-board">
            <header className="stadium-header">
              <div>
                <p className="eyebrow">Stadium</p>
                <h2>Round {roundIndex} / Best of 5</h2>
              </div>
              <strong>{roundWins.player} - {roundWins.opponent}</strong>
              <div>
                <p>Dampening {playerSide.dampeningPercent}%</p>
                <small>Survival {formatTime(playerSide.survivalSeconds)} | Equalized iLvl 10</small>
              </div>
            </header>

            <section className="stadium-pressure-panel">
              <strong>Iron Arbiter</strong>
              <span>No boss health | Next pulse in {Math.max(0, 5 - (elapsedTicks % 5) * (TICK_MS / 1000)).toFixed(1)}s</span>
            </section>

            <section className="combat-panel stadium-side">
              <div className="encounter-header">
                <div>
                  <p className="eyebrow">You</p>
                  <h2>{profile.character.name}</h2>
                </div>
                <div className="pvp-resource-wrap">
                  <span>{gameClass.resourceName} {Math.floor(playerSide.resource)} / {MAX_RESOURCE}</span>
                  <div className="bar mana-bar"><span style={barFillStyle((playerSide.resource / MAX_RESOURCE) * 100)} /></div>
                </div>
              </div>
              <div className="party-grid pvp-party-grid">
                {playerSide.party.map((member) => (
                  <PartyMemberFrame
                    deadClassName="down"
                    effectMode="compact"
                    floatingTexts={playerFloatingTextsByMember.get(member.id) ?? []}
                    key={member.id}
                    member={member}
                    onSelect={setSelectedTargetId}
                    selected={selectedId === member.id}
                    showHeaderHealth={false}
                    showHealthText
                  />
                ))}
              </div>
              <p className="roguelike-upgrade-list">Buffs: {playerBuffSummary}</p>
            </section>

            <section className="combat-panel stadium-side">
              <div className="encounter-header">
                <div>
                  <p className="eyebrow">Opponent</p>
                  <h2>{opponentLabel}</h2>
                  <small>Survival {formatTime(cpuSide.survivalSeconds)}</small>
                </div>
                <div className="pvp-resource-wrap">
                  <span>{gameClass.resourceName} {Math.floor(cpuSide.resource)} / {MAX_RESOURCE}</span>
                  <div className="bar mana-bar"><span style={barFillStyle((cpuSide.resource / MAX_RESOURCE) * 100)} /></div>
                </div>
              </div>
              <div className="party-grid pvp-party-grid">
                {cpuSide.party.map((member) => (
                  <PartyMemberFrame
                    as="div"
                    deadClassName="down"
                    effectMode="compact"
                    floatingTexts={cpuFloatingTextsByMember.get(member.id) ?? []}
                    key={member.id}
                    member={member}
                    showHeaderHealth={false}
                    showHealthText
                  />
                ))}
              </div>
              <p className="roguelike-upgrade-list">Buffs: {opponentBuffSummary}</p>
            </section>

            <SpellBar
              bindings={activeBindings}
              canCast={status === 'playing' && playerAlive}
              className="pvp-bottom-spell-bar"
              focusable={false}
              iconStyle={controllerIconStyle}
              onCast={castPlayerSpell}
              resource={playerSide.resource}
              resourceName={gameClass.resourceName}
              spells={playerSpellSlots}
            />
          </div>
        )}

        {status === 'round-countdown' && (
          <div className="pvp-round-countdown">
            <div>
              <p className="eyebrow">Round Starts</p>
              <h2>{Math.max(1, Math.ceil(roundCountdown))}</h2>
            </div>
          </div>
        )}

        {status === 'shop' && (
          <div className="result-screen">
            <div className="stadium-shop-dialog">
              <div className="pvp-upgrade-header">
                <div>
                  <p className="eyebrow">Stadium Buy Round</p>
                  <h2>Round {roundIndex} Complete</h2>
                </div>
                <strong>{shopTimeLeft.toFixed(0)}s</strong>
              </div>
              <div className="stadium-shop-summary">
                <span>Points: {shopPoints}</span>
                <span>Score: {roundWins.player} - {roundWins.opponent}</span>
                {shopReady && <span>Waiting for opponent...</span>}
              </div>
              <div className="stadium-shop-layout">
                <nav className="stadium-shop-tabs">
                  {(['1', '2', '3', '4', '5'] as SlotKey[]).map((slot) => (
                    <button className={shopCategory === slot ? 'active' : ''} key={slot} onClick={() => setShopCategory(slot)} type="button">
                      {slotLabel(slot, starterSpells)}
                    </button>
                  ))}
                  <button className={shopCategory === 'misc' ? 'active' : ''} onClick={() => setShopCategory('misc')} type="button">
                    Miscellaneous
                  </button>
                </nav>
                <section>
                  <h3>{categoryLabel} Buffs</h3>
                  <div className="stadium-shop-grid">
                    {visibleBuffs.map((buff) => (
                      <button disabled={shopReady || shopPoints < buff.cost} key={buff.id} onClick={() => buyBuff(buff)} type="button">
                        <strong>[{buff.cost}] {buff.name}</strong>
                        <small>{buff.description}</small>
                      </button>
                    ))}
                  </div>
                  <p>Active: {playerBuffSummary}</p>
                </section>
              </div>
              <button className="secondary-result-button" disabled={shopReady} onClick={finishShop} type="button">
                {shopReady ? 'Finished' : 'Finished Buying'}
              </button>
            </div>
          </div>
        )}

        {paused && (
          <div className="pause-screen">
            <div>
              <p className="eyebrow">Paused</p>
              <h2>Stadium</h2>
              <button onClick={() => setPaused(false)} type="button">Resume</button>
              <button className="secondary-result-button" onClick={onExit} type="button">Leave</button>
            </div>
          </div>
        )}

        {(status === 'won' || status === 'lost') && (
          <ResultScreen
            actions={[
              { label: 'Queue Next Match', onClick: () => startMatch() },
              { label: 'Back to Roguelike', onClick: onExit, className: 'secondary-result-button' },
            ]}
            eyebrow={status === 'won' ? 'Victory' : 'Defeat'}
            log={log}
            onToggleLog={() => setShowEndLog((value) => !value)}
            rematch={{
              visible: Boolean(liveMatch),
              requested: rematchRequested,
              message: rematchMessage,
              onRematch: handleRematch,
            }}
            showLog={showEndLog}
            title={status === 'won' ? 'Stadium Won' : `${opponentLabel} Wins`}
          >
              <p>Final score {roundWins.player} - {roundWins.opponent}</p>
              <div className="reward-summary">
                <RewardXpSummary reward={rewardSummary} />
                {rewardError && <p className="reward-error">{rewardError}</p>}
              </div>
          </ResultScreen>
        )}
      </section>
    </main>
  )
}
