import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
import type { MovementVector } from '../../../input'
import { useGameAction, useInput, useMovementVectorRef } from '../../../input'
import {
  useDualScreen,
  useDualScreenPublisher,
  type DualScreenCombatState,
} from '../../../dualScreen'
import type { PartyMember, Role, Spell } from '../../../game'
import {
  createInitialIwt2ArenaState,
  tickIwt2Arena,
  type Iwt2ArenaState,
} from '../sim/arenaState'
import type { Iwt2ArenaBounds, Iwt2EntityId } from '../sim'
import { castIwt2HealerAbility } from '../sim'
import { PhaserArena } from '../render/PhaserArena'
import {
  recordIwt2BossKillReward,
  type Iwt2BossDropAward,
  type Iwt2BossPetAward,
  type Iwt2Save,
} from '../save/iwt2Repository'
import { AbilityBar } from '../components/AbilityBar'
import { BossHud } from '../components/BossHud'
import { PartyFrames } from '../components/PartyFrames'
import { IWT2_CLASS_METADATA } from '../content/classes'
import { IWT2_ABILITY_ACTIONS, IWT2_TARGET_ACTIONS } from '../content/controls'
import type { Iwt2Difficulty } from '../content/difficulties'
import { abilitiesForHealer, type Iwt2HealerAbility, type Iwt2TriggeredAbilityEffect } from '../content/healerAbilities'
import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
import type {
  Iwt2RoguelikeContentType,
  Iwt2RoguelikeOpponentDebuffId,
  Iwt2RoguelikeSelfBuffId,
  Iwt2RoguelikeVariant,
} from '../content/roguelike'
import {
  createRoguelikePressureState,
  roguelikeIncomingDamageScale,
} from '../sim/roguelikePressure'
import { applyIwt2PveGearStats, applyIwt2PveGearToHealerAbilities } from '../sim/equipmentStats'
import {
  IWT2_AEGIS_SCRIPT_DAMAGE_REDUCTION_BUFF_ID,
  IWT2_BARKSKIN_HOT_BONUS_BUFF_ID,
  IWT2_SUN_WARD_DAMAGE_REDUCTION_BUFF_ID,
} from '../content/roguelike'

type ArenaStatus = 'playing' | 'paused' | 'victory' | 'defeat'
type OverlayAction = 'primary' | 'requeue' | 'menu'
type OverlayNavEntry = {
  action: OverlayAction
  row: number
}

const DEFAULT_OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [
  { action: 'primary', row: 0 },
  { action: 'menu', row: 1 },
]
const PVP_RESULT_OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [
  { action: 'primary', row: 0 },
  { action: 'requeue', row: 1 },
  { action: 'menu', row: 2 },
]
const EMPTY_ROGUELIKE_BUFFS: Iwt2RoguelikeSelfBuffId[] = []
const IWT2_PVP_BOSS_HEALTH_MULTIPLIER = 0.7
const IWT2_TOP_PARTY_RAIL_WIDTH = 172
const IWT2_THOR_TOP_PARTY_RAIL_WIDTH = 154
const IWT2_THOR_TOP_BREAKPOINT_WIDTH = 1000
const IWT2_THOR_TOP_BREAKPOINT_HEIGHT = 620

type BossArenaScreenProps = {
  bossId: Iwt2BossId
  bossIds?: Iwt2BossId[]
  difficulty?: Iwt2Difficulty
  modeLabel?: string
  save: Iwt2Save
  onBack: () => void
  onMainMenu?: () => void
  onPvpRequeue?: () => void
  onSaveUpdated: (save: Iwt2Save) => void
  roguelikeRun?: {
    buffs: Iwt2RoguelikeSelfBuffId[]
    contentType: Iwt2RoguelikeContentType
    debuffs: Iwt2RoguelikeOpponentDebuffId[]
    onVictory: () => void
    stage: number
    variant: Iwt2RoguelikeVariant
  }
}

export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, onBack, onMainMenu, onPvpRequeue, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
  const bossMetadata = IWT2_BOSS_METADATA[bossId]
  const pvpRoguelike = roguelikeRun?.variant === 'pvp'
  const pveGearActive = roguelikeRun?.variant !== 'pvp'
  const bossHealthScale = roguelikeRun ? roguelikeBossHealthScale(roguelikeRun.stage) : 1
  const difficultyHealthScale = difficulty?.healthMultiplier ?? 1
  const difficultyDamageScale = difficulty?.damageMultiplier ?? 1
  const roguelikeStage = roguelikeRun?.stage
  const roguelikeContentType = roguelikeRun?.contentType
  const roguelikeBuffs = roguelikeRun?.buffs ?? EMPTY_ROGUELIKE_BUFFS
  const roguelikeDamageScale = roguelikeRun
    ? roguelikeIncomingDamageScale(roguelikeRun.stage, roguelikeRun.contentType)
    : 1
  const experienceMultiplier = difficulty?.experienceMultiplier ?? 1
  const difficultySlug = difficulty?.slug ?? 'initiate'
  const pvpBossHealthScale = pvpRoguelike ? IWT2_PVP_BOSS_HEALTH_MULTIPLIER : 1
  const combinedBossHealthScale = bossHealthScale * difficultyHealthScale * pvpBossHealthScale
  const combinedDamageScale = difficultyDamageScale * roguelikeDamageScale
  const arenaBounds = useMemo(() => createTopScreenArenaBounds(), [])
  const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createArenaState(
    bossId,
    bossIds,
    combinedBossHealthScale,
    combinedDamageScale,
    roguelikeBuffs,
    createPressureState(roguelikeStage, roguelikeContentType),
    pveGearActive ? save.gearProgress : undefined,
    arenaBounds,
  ))
  const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
    pvpRoguelike
      ? createInitialIwt2ArenaState(
        bossId,
        bossIds,
        combinedBossHealthScale,
        combinedDamageScale,
        createPressureState(roguelikeStage, roguelikeContentType),
        arenaBounds,
      )
      : null
  ))
  const [abilityCooldowns, setAbilityCooldowns] = useState<Record<string, number>>({})
  const [status, setStatus] = useState<ArenaStatus>('playing')
  const [selectedOverlayAction, setSelectedOverlayAction] = useState<OverlayAction>('primary')
  const [selectedPartyId, setSelectedPartyId] = useState<Iwt2EntityId>('player-healer')
  const [dropAwards, setDropAwards] = useState<Iwt2BossDropAward[]>([])
  const [petAwards, setPetAwards] = useState<Iwt2BossPetAward[]>([])
  const stateRef = useRef(arenaState)
  const opponentStateRef = useRef<Iwt2ArenaState | null>(opponentArenaState)
  const statusRef = useRef(status)
  const selectedOverlayActionRef = useRef<OverlayAction>(selectedOverlayAction)
  const selectedPartyIdRef = useRef<Iwt2EntityId>(selectedPartyId)
  const saveRef = useRef(save)
  const recordedKillIdsRef = useRef<Set<Iwt2BossId>>(new Set())
  const lastPublishTimeRef = useRef(0)
  const lastHudSignatureRef = useRef('')
  const abilityCooldownsRef = useRef<Record<string, number>>({})
  const movementRef = useMovementVectorRef(status === 'playing')
  const {
    bindings,
    controllerIconStyle,
    directPartyTargeting,
    lastDevice,
  } = useInput()
  const { enabled: dualScreenEnabled } = useDualScreen()
  const activeBindings = bindings[lastDevice]

  useEffect(() => {
    stateRef.current = arenaState
  }, [arenaState])

  useEffect(() => {
    opponentStateRef.current = opponentArenaState
  }, [opponentArenaState])

  useEffect(() => {
    statusRef.current = status
  }, [status])

  useEffect(() => {
    selectedOverlayActionRef.current = selectedOverlayAction
  }, [selectedOverlayAction])

  useEffect(() => {
    selectedPartyIdRef.current = selectedPartyId
  }, [selectedPartyId])

  useEffect(() => {
    saveRef.current = save
  }, [save])

  const resetArena = useCallback(() => {
    const pressureState = createPressureState(roguelikeStage, roguelikeContentType)
    const next = createArenaState(
      bossId,
      bossIds,
      combinedBossHealthScale,
      combinedDamageScale,
      roguelikeBuffs,
      pressureState,
      pveGearActive ? save.gearProgress : undefined,
      arenaBounds,
    )
    const nextOpponentState = pvpRoguelike
      ? createInitialIwt2ArenaState(
        bossId,
        bossIds,
        combinedBossHealthScale,
        combinedDamageScale,
        pressureState,
        arenaBounds,
      )
      : null
    recordedKillIdsRef.current = new Set()
    abilityCooldownsRef.current = {}
    lastHudSignatureRef.current = arenaHudSignature(next)
    stateRef.current = next
    opponentStateRef.current = nextOpponentState
    setArenaState(next)
    setOpponentArenaState(nextOpponentState)
    setAbilityCooldowns({})
    setDropAwards([])
    setPetAwards([])
    setSelectedOverlayAction('primary')
    setStatus('playing')
  }, [arenaBounds, bossId, bossIds, combinedBossHealthScale, combinedDamageScale, pveGearActive, pvpRoguelike, roguelikeBuffs, roguelikeContentType, roguelikeStage, save.gearProgress])

  const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
    setSelectedOverlayAction('primary')
    statusRef.current = nextStatus
    setStatus(nextStatus)
  }, [])

  const resumeArena = useCallback(() => {
    statusRef.current = 'playing'
    setStatus('playing')
  }, [])

  const abilities = useMemo(
    () => {
      const baseAbilities = abilitiesForHealer(
        save.character.healerStyle,
        pveGearActive ? save.gearProgress.healer.infusionAbilityId : null,
      )
      const gearAbilities = pveGearActive
        ? applyIwt2PveGearToHealerAbilities(baseAbilities, save.gearProgress)
        : baseAbilities
      return applyRoguelikeModifiers(
        gearAbilities,
        roguelikeRun?.buffs ?? [],
        roguelikeRun?.debuffs ?? [],
      )
    },
    [pveGearActive, roguelikeRun?.buffs, roguelikeRun?.debuffs, save.character.healerStyle, save.gearProgress],
  )

  const castAbility = useCallback((ability: Iwt2HealerAbility) => {
    if (statusRef.current !== 'playing') return
    if ((abilityCooldownsRef.current[ability.id] ?? 0) > 0) return
    const result = castIwt2HealerAbility(stateRef.current, ability, selectedPartyIdRef.current)
    if (!result.cast) return

    const nextCooldowns = {
      ...abilityCooldownsRef.current,
      [ability.id]: ability.cooldownSeconds,
    }
    abilityCooldownsRef.current = nextCooldowns
    stateRef.current = result.state
    lastHudSignatureRef.current = arenaHudSignature(result.state)
    setAbilityCooldowns(nextCooldowns)
    setArenaState(result.state)
  }, [])

  const moveOverlaySelection = useCallback((action: string) => {
    setSelectedOverlayAction((current) => {
      const entries = overlayNavEntriesFor(statusRef.current, pvpRoguelike)
      const active = entries.find((entry) => entry.action === current) ?? entries[0]
      const candidates = entries.filter((entry) => {
        if (entry.action === current) return false
        if (action === 'navigateUp') return entry.row < active.row
        if (action === 'navigateDown') return entry.row > active.row
        return false
      })
      if (candidates.length === 0) return current
      candidates.sort((a, b) => Math.abs(a.row - active.row) - Math.abs(b.row - active.row))
      return candidates[0]?.action ?? current
    })
  }, [pvpRoguelike])

  const activateOverlayAction = useCallback((overlayAction = selectedOverlayActionRef.current) => {
    if (overlayAction === 'requeue') {
      onPvpRequeue?.()
      return
    }
    if (overlayAction === 'menu') {
      if (statusRef.current === 'paused') {
        const returnToMainMenu = onMainMenu ?? onBack
        returnToMainMenu()
        return
      }
      onBack()
      return
    }
    if (statusRef.current === 'paused') {
      resumeArena()
      return
    }
    if (statusRef.current === 'victory' && roguelikeRun) {
      roguelikeRun.onVictory()
      return
    }
    resetArena()
  }, [onBack, onMainMenu, onPvpRequeue, resetArena, resumeArena, roguelikeRun])

  useGameAction((action, device) => {
    if (statusRef.current !== 'playing' && (device === 'controller' || device === 'pc')) {
      if (action.startsWith('navigate')) {
        moveOverlaySelection(action)
        return
      }
      if (action === 'confirm') {
        activateOverlayAction()
        return
      }
      if (action === 'back') {
        if (statusRef.current === 'paused') resumeArena()
        return
      }
    }
    if (action === 'pause' || action === 'back') {
      if (statusRef.current === 'playing') showOverlay('paused')
      else if (statusRef.current === 'paused') resumeArena()
      return
    }
    if (statusRef.current === 'playing' && action.startsWith('ability')) {
      const ability = abilities[IWT2_ABILITY_ACTIONS.indexOf(action)]
      if (ability) castAbility(ability)
      return
    }
    if (
      device === 'controller'
      && statusRef.current === 'playing'
      && !directPartyTargeting
      && (
        action === 'navigateUp'
        || action === 'navigateLeft'
        || action === 'previousTarget'
        || action === 'navigateDown'
        || action === 'navigateRight'
        || action === 'nextTarget'
      )
    ) {
      const previous = action === 'navigateUp' || action === 'navigateLeft' || action === 'previousTarget'
      setSelectedPartyId((current) => nextTargetId(stateRef.current, current, previous ? -1 : 1))
      return
    }
    if (action.startsWith('targetParty')) {
      if (statusRef.current !== 'playing') return
      const index = Number(action.replace('targetParty', '')) - 1
      const target = stateRef.current.party[index]
      if (target) setSelectedPartyId(target.id)
      return
    }
  })

  const onStep = useCallback((movement: MovementVector, dtSeconds: number) => {
    if (statusRef.current !== 'playing') return stateRef.current
    abilityCooldownsRef.current = tickCooldowns(abilityCooldownsRef.current, dtSeconds)
    const next = tickIwt2Arena(stateRef.current, {
      moveX: movement.x,
      moveY: movement.y,
    }, dtSeconds)
    stateRef.current = next
    let nextOpponentState = opponentStateRef.current
    if (pvpRoguelike && nextOpponentState) {
      const opponentMovement = iwt2OpponentMovement(nextOpponentState)
      nextOpponentState = tickIwt2Arena(
        nextOpponentState,
        {
          moveX: opponentMovement.x,
          moveY: opponentMovement.y,
        },
        dtSeconds,
      )
      opponentStateRef.current = nextOpponentState
    }

    const newlyDefeatedBosses = next.bosses.filter((boss) => boss.health <= 0 && !recordedKillIdsRef.current.has(boss.bossId))
    if (newlyDefeatedBosses.length > 0) {
      const nextRecordedIds = new Set(recordedKillIdsRef.current)
      let updatedSave = saveRef.current
      const newDropAwards: Iwt2BossDropAward[] = []
      const newPetAwards: Iwt2BossPetAward[] = []
      for (const defeatedBoss of newlyDefeatedBosses) {
        nextRecordedIds.add(defeatedBoss.bossId)
        const reward = recordIwt2BossKillReward(updatedSave, defeatedBoss.bossId, {
          difficultySlug,
          experienceMultiplier,
        })
        updatedSave = reward.save
        newDropAwards.push(reward.dropAwarded)
        if (reward.petAwarded) newPetAwards.push(reward.petAwarded)
      }
      recordedKillIdsRef.current = nextRecordedIds
      saveRef.current = updatedSave
      if (newDropAwards.length > 0) setDropAwards((current) => [...current, ...newDropAwards])
      if (newPetAwards.length > 0) setPetAwards((current) => [...current, ...newPetAwards])
      onSaveUpdated(updatedSave)
    }

    if (next.bosses.every((boss) => boss.health <= 0)) {
      if (pvpRoguelike && roguelikeRun) {
        statusRef.current = 'victory'
        setStatus('victory')
        roguelikeRun.onVictory()
      } else {
        showOverlay('victory')
      }
    } else if (next.party.every((member) => member.health <= 0)) {
      showOverlay('defeat')
    }

    const hudSignature = arenaHudSignature(next)
    if (
      hudSignature !== lastHudSignatureRef.current
      || next.time - lastPublishTimeRef.current >= 0.08
      || next.bosses.some((boss) => boss.health <= 0)
    ) {
      lastHudSignatureRef.current = hudSignature
      lastPublishTimeRef.current = next.time
      setArenaState(next)
      if (pvpRoguelike) setOpponentArenaState(nextOpponentState)
      setAbilityCooldowns(abilityCooldownsRef.current)
    }
    return next
  }, [difficultySlug, experienceMultiplier, onSaveUpdated, pvpRoguelike, roguelikeRun, showOverlay])

  const targetBindings = directPartyTargeting
    ? IWT2_TARGET_ACTIONS.map((action) => activeBindings[action] ?? null)
    : undefined
  const playerMana = arenaState.party.find((member) => member.id === 'player-healer')?.mana ?? 0
  const alivePartyCount = arenaState.party.filter((member) => member.health > 0).length
  const totalPartyDamage = arenaState.party.reduce((total, member) => total + member.damageDone, 0)
  const defeatedBossCount = arenaState.bosses.filter((boss) => boss.health <= 0).length
  const victoryExperience = Math.round(arenaState.bosses.length * 125 * experienceMultiplier)
  const bossTitle = formatBossEncounterTitle(arenaState.bosses)
  const overlayPrimaryLabel = status === 'paused'
    ? 'Resume'
    : status === 'victory' && roguelikeRun
      ? 'Choose Upgrade'
    : pvpRoguelike && status !== 'playing'
      ? 'Rematch'
      : 'Restart'
  const overlayTitle = status === 'victory'
    ? `${bossTitle} Down`
    : status === 'defeat'
      ? 'Party Defeated'
      : 'Arena Paused'
  const overlayEyebrow = status === 'victory'
    ? 'Hunt Complete'
    : status === 'defeat'
      ? 'Recovery'
      : 'Paused'
  const overlayTone = status === 'victory'
    ? 'is-victory'
    : status === 'defeat'
      ? 'is-defeat'
      : 'is-paused'
  const pauseTitle = arenaPauseTitle(roguelikeRun, modeLabel ?? bossMetadata.name)
  const pauseCopy = roguelikeRun?.variant === 'pvp'
    ? undefined
    : 'Combat is stopped. Continue the fight or return to the main menu.'
  const dualScreenState = useMemo(
    () => buildIwt2DualScreenCombatState({
      abilities,
      arenaState,
      bindings: activeBindings,
      cooldowns: abilityCooldowns,
      controllerIconStyle,
      directPartyTargeting,
      modeLabel: modeLabel ?? 'Arena',
      opponentArenaState,
      playerMana,
      roguelikeRun,
      selectedPartyId,
      status,
    }),
    [
      abilities,
      activeBindings,
      arenaState,
      abilityCooldowns,
      controllerIconStyle,
      directPartyTargeting,
      modeLabel,
      opponentArenaState,
      playerMana,
      roguelikeRun,
      selectedPartyId,
      status,
    ],
  )

  useDualScreenPublisher(dualScreenState, dualScreenEnabled)

  return (
    <main
      className="game-shell iwt2-arena-shell"
      data-combat-active={status === 'playing' ? 'true' : 'false'}
      data-continuous-movement-active={status === 'playing' ? 'true' : undefined}
    >
      <section className="iwt2-arena-layout">
        <div className="iwt2-arena-stage">
          <BossHud bosses={arenaState.bosses} />
          <PartyFrames
            controllerIconStyle={controllerIconStyle}
            onTarget={setSelectedPartyId}
            party={arenaState.party}
            selectedPartyId={selectedPartyId}
            targetBindings={targetBindings}
          />

          <PhaserArena
            movementRef={movementRef}
            onStep={onStep}
            selectedPartyIdRef={selectedPartyIdRef}
            stateRef={stateRef}
          />
          {status === 'paused' && (
            <div className="pause-screen iwt2-arena-overlay is-paused" data-game-nav-active="true" role="dialog" aria-modal="true">
              <div>
                <p className="eyebrow">Paused</p>
                <h2>{pauseTitle}</h2>
                {pauseCopy && <p>{pauseCopy}</p>}
                <button
                  className={selectedOverlayAction === 'primary' ? 'game-selected' : ''}
                  data-game-selected={selectedOverlayAction === 'primary' ? 'true' : undefined}
                  onClick={() => activateOverlayAction('primary')}
                  onPointerDown={() => setSelectedOverlayAction('primary')}
                  type="button"
                >
                  Continue
                </button>
                <button
                  className={`secondary-result-button ${selectedOverlayAction === 'menu' ? 'game-selected' : ''}`}
                  data-game-selected={selectedOverlayAction === 'menu' ? 'true' : undefined}
                  onClick={() => activateOverlayAction('menu')}
                  onPointerDown={() => setSelectedOverlayAction('menu')}
                  type="button"
                >
                  Main Menu
                </button>
              </div>
            </div>
          )}

          {status !== 'playing' && status !== 'paused' && (
            <div className={`pause-screen iwt2-arena-overlay ${overlayTone}`} data-game-nav-active="true">
              <div className="iwt2-result-panel">
                <div className="iwt2-result-crest" style={{ '--boss-color': bossMetadata.color, '--boss-accent': bossMetadata.accentColor } as CSSProperties}>
                  <span>{arenaState.bosses.map((boss) => IWT2_BOSS_METADATA[boss.bossId].icon).join('')}</span>
                </div>
                <p className="eyebrow">{overlayEyebrow}</p>
                <h1>{overlayTitle}</h1>
                {roguelikeRun && (
                  <p className="iwt2-result-hint">
                    {roguelikeRun.variant.toUpperCase()} {formatRoguelikeContentType(roguelikeRun.contentType)} Stage {roguelikeRun.stage}
                  </p>
                )}
                <div className="iwt2-result-summary" aria-label="Arena result summary">
                  <span>
                    <strong>{formatArenaTime(arenaState.time)}</strong>
                    Clear
                  </span>
                  <span>
                    <strong>{alivePartyCount}/{arenaState.party.length}</strong>
                    Standing
                  </span>
                  <span>
                    <strong>{defeatedBossCount}/{arenaState.bosses.length}</strong>
                    Bosses
                  </span>
                  <span>
                    <strong>{Math.round(totalPartyDamage)}</strong>
                    Damage
                  </span>
                </div>
                {status === 'victory' && (
                  <div className="iwt2-result-reward">
                    <span>+{victoryExperience} XP</span>
                    <span>Log +{arenaState.bosses.length}</span>
                    {dropAwards.map((award) => (
                      <span key={`${award.dropId}:${award.quantityAfter}`}>
                        {award.dropName}{award.quantity > 1 ? ` x${award.quantity}` : ''}
                      </span>
                    ))}
                    {petAwards.map((award) => (
                      <span key={`${award.petId}:${award.quantityAfter}`}>
                        {award.petName}{award.duplicate ? ` x${award.quantityAfter}` : ''}
                      </span>
                    ))}
                  </div>
                )}
                <div className="iwt2-overlay-actions">
                  <button
                    className={`iwt2-result-button is-primary ${selectedOverlayAction === 'primary' ? 'game-selected' : ''}`}
                    data-game-selected={selectedOverlayAction === 'primary' ? 'true' : undefined}
                    onClick={() => activateOverlayAction('primary')}
                    onPointerDown={() => setSelectedOverlayAction('primary')}
                    type="button"
                  >
                    {overlayPrimaryLabel}
                  </button>
                  {pvpRoguelike && (
                    <button
                      className={`iwt2-result-button is-secondary ${selectedOverlayAction === 'requeue' ? 'game-selected' : ''}`}
                      data-game-selected={selectedOverlayAction === 'requeue' ? 'true' : undefined}
                      onClick={() => activateOverlayAction('requeue')}
                      onPointerDown={() => setSelectedOverlayAction('requeue')}
                      type="button"
                    >
                      Requeue
                    </button>
                  )}
                  <button
                    className={`iwt2-result-button is-secondary ${selectedOverlayAction === 'menu' ? 'game-selected' : ''}`}
                    data-game-selected={selectedOverlayAction === 'menu' ? 'true' : undefined}
                    onClick={() => activateOverlayAction('menu')}
                    onPointerDown={() => setSelectedOverlayAction('menu')}
                    type="button"
                  >
                    Menu
                  </button>
                </div>
                {lastDevice === 'controller' && (
                  <small className="iwt2-result-hint">D-pad selects, A confirms</small>
                )}
              </div>
            </div>
          )}

          <AbilityBar
            abilities={abilities}
            cooldowns={abilityCooldowns}
            mana={playerMana}
            onCast={castAbility}
          />
        </div>
      </section>
    </main>
  )
}

function formatArenaTime(seconds: number): string {
  const safeSeconds = Math.max(0, Math.floor(seconds))
  const minutes = Math.floor(safeSeconds / 60)
  const remainder = safeSeconds % 60
  return `${minutes}:${remainder.toString().padStart(2, '0')}`
}

function roguelikeBossHealthScale(stage: number): number {
  return 1 + Math.max(0, stage - 1) * 0.1
}

function overlayNavEntriesFor(status: ArenaStatus, pvpRoguelike: boolean): OverlayNavEntry[] {
  if (pvpRoguelike && (status === 'victory' || status === 'defeat')) return PVP_RESULT_OVERLAY_NAV_ENTRIES
  return DEFAULT_OVERLAY_NAV_ENTRIES
}

function arenaHudSignature(state: Iwt2ArenaState): string {
  return [
    ...state.bosses.map((boss) => [
      boss.id,
      Math.ceil(boss.health),
      boss.attackPhase,
    ].join(':')),
    state.hostileAdds.length,
    state.hazards.length,
    ...state.party.map((member) => [
      member.id,
      Math.ceil(member.health),
      Math.ceil(member.shield),
      Math.ceil(member.mana),
      Math.ceil(Math.max(member.status.stunnedSeconds, member.status.knockedDownSeconds) * 10),
      member.hotEffects.map((effect) => `${effect.id}:${Math.ceil(effect.remainingSeconds)}:${Math.ceil(effect.nextTickInSeconds * 10)}`).join(','),
    ].join(':')),
  ].join('|')
}

function formatBossEncounterTitle(bosses: Iwt2ArenaState['bosses']): string {
  return bosses
    .map((boss) => IWT2_BOSS_METADATA[boss.bossId].name)
    .join(' + ')
}

function nextTargetId(state: Iwt2ArenaState, currentId: Iwt2EntityId, direction: -1 | 1): Iwt2EntityId {
  const living = state.party.filter((member) => member.health > 0)
  const targets = living.length > 0 ? living : state.party
  const currentIndex = Math.max(0, targets.findIndex((member) => member.id === currentId))
  const nextIndex = (currentIndex + direction + targets.length) % targets.length
  return targets[nextIndex]?.id ?? currentId
}

function tickCooldowns(cooldowns: Record<string, number>, dt: number) {
  let changed = false
  const next: Record<string, number> = {}
  for (const [id, remaining] of Object.entries(cooldowns)) {
    const value = Math.max(0, remaining - dt)
    if (value !== remaining) changed = true
    if (value > 0) next[id] = value
  }
  return changed ? next : cooldowns
}

function createArenaState(
  bossId: Iwt2BossId,
  bossIds: Iwt2BossId[] | undefined,
  bossHealthScale: number,
  partyDamageTakenScale: number,
  buffs: Iwt2RoguelikeSelfBuffId[],
  roguelikePressure: ReturnType<typeof createPressureState>,
  gearProgress?: Iwt2Save['gearProgress'],
  bounds?: Iwt2ArenaBounds,
): Iwt2ArenaState {
  const baseState = createInitialIwt2ArenaState(
    bossId,
    bossIds,
    bossHealthScale,
    partyDamageTakenScale,
    roguelikePressure,
    bounds,
  )
  const state = gearProgress ? applyIwt2PveGearStats(baseState, gearProgress) : baseState
  const shieldedDamageTakenMultiplier = shieldedDamageTakenMultiplierForBuffs(buffs)
  const shieldedHotHealingMultiplier = buffs.includes(IWT2_BARKSKIN_HOT_BONUS_BUFF_ID) ? 1.25 : undefined
  if (shieldedDamageTakenMultiplier === undefined && shieldedHotHealingMultiplier === undefined) return state
  return {
    ...state,
    party: state.party.map((member) => ({
      ...member,
      shieldedDamageTakenMultiplier,
      shieldedHotHealingMultiplier,
    })),
  }
}

function createTopScreenArenaBounds(): Iwt2ArenaBounds {
  if (typeof window === 'undefined') return { width: 960, height: 540 }
  const thorTopLayout = window.innerWidth <= IWT2_THOR_TOP_BREAKPOINT_WIDTH
    && window.innerHeight <= IWT2_THOR_TOP_BREAKPOINT_HEIGHT
  const railWidth = thorTopLayout ? IWT2_THOR_TOP_PARTY_RAIL_WIDTH : IWT2_TOP_PARTY_RAIL_WIDTH
  return {
    width: Math.max(320, Math.round(window.innerWidth - railWidth)),
    height: Math.max(240, Math.round(window.innerHeight)),
  }
}

function createPressureState(
  stage: number | undefined,
  contentType: Iwt2RoguelikeContentType | undefined,
) {
  return stage && contentType ? createRoguelikePressureState(stage, contentType) : undefined
}

function shieldedDamageTakenMultiplierForBuffs(buffs: Iwt2RoguelikeSelfBuffId[]) {
  const multipliers: number[] = []
  if (buffs.includes(IWT2_SUN_WARD_DAMAGE_REDUCTION_BUFF_ID)) multipliers.push(0.5)
  if (buffs.includes(IWT2_AEGIS_SCRIPT_DAMAGE_REDUCTION_BUFF_ID)) multipliers.push(0.7)
  return multipliers.length > 0 ? Math.min(...multipliers) : undefined
}

function applyRoguelikeModifiers(
  abilities: Iwt2HealerAbility[],
  buffs: Iwt2RoguelikeSelfBuffId[],
  debuffs: Iwt2RoguelikeOpponentDebuffId[],
): Iwt2HealerAbility[] {
  if (buffs.length === 0 && debuffs.length === 0) return abilities
  const abilityById = new Map(abilities.map((ability) => [ability.id, ability]))
  const renewEffect = triggeredEffectFromAbility(abilityById.get('dawnweaver-renew'))
  const sunWardEffect = triggeredEffectFromAbility(abilityById.get('dawnweaver-sun-ward'))
  const halfSunWardEffect = triggeredEffectFromAbility(abilityById.get('dawnweaver-sun-ward'), 0.5)
  const seedOfLifeEffect = triggeredEffectFromAbility(abilityById.get('lifebinder-seed-of-life'))
  const barkskinEffect = triggeredEffectFromAbility(abilityById.get('lifebinder-barkskin'))
  const halfBarkskinEffect = triggeredEffectFromAbility(abilityById.get('lifebinder-barkskin'), 0.5)
  const mendingRuneEffect = triggeredEffectFromAbility(abilityById.get('runesage-mending-rune'))
  const aegisScriptEffect = triggeredEffectFromAbility(abilityById.get('runesage-aegis-script'))
  const halfAegisScriptEffect = triggeredEffectFromAbility(abilityById.get('runesage-aegis-script'), 0.5)
  return abilities.map((ability) => {
    const slot = String(ability.slot)
    const costDown = countStacks(buffs, `slot${slot}-cost-down`)
    const costUp = countStacks(debuffs, `opp-slot${slot}-cost-up`)
    const cooldownDown = countStacks(buffs, `slot${slot}-cooldown-down`)
    const cooldownUp = countStacks(debuffs, `opp-slot${slot}-cooldown-up`)
    const extraTargets = countStacks(buffs, `slot${slot}-extra-target`)
    const triggeredEffects = iwt2TriggeredEffectsForAbility({
      ability,
      aegisScriptEffect,
      barkskinEffect,
      buffs,
      halfAegisScriptEffect,
      halfBarkskinEffect,
      halfSunWardEffect,
      mendingRuneEffect,
      renewEffect,
      seedOfLifeEffect,
      sunWardEffect,
    })
    if (costDown === 0 && costUp === 0 && cooldownDown === 0 && cooldownUp === 0 && extraTargets === 0 && triggeredEffects.length === 0) return ability
    return {
      ...ability,
      cooldownSeconds: roundModifier(ability.cooldownSeconds * 0.75 ** cooldownDown * 1.25 ** cooldownUp),
      extraTargets: (ability.extraTargets ?? 0) + extraTargets,
      manaCost: Math.max(1, Math.ceil(ability.manaCost * 0.75 ** costDown * 1.25 ** costUp)),
      triggeredEffects: triggeredEffects.length > 0 ? triggeredEffects : ability.triggeredEffects,
    }
  })
}

function iwt2TriggeredEffectsForAbility({
  ability,
  aegisScriptEffect,
  barkskinEffect,
  buffs,
  halfAegisScriptEffect,
  halfBarkskinEffect,
  halfSunWardEffect,
  mendingRuneEffect,
  renewEffect,
  seedOfLifeEffect,
  sunWardEffect,
}: {
  ability: Iwt2HealerAbility
  aegisScriptEffect: Iwt2TriggeredAbilityEffect | undefined
  barkskinEffect: Iwt2TriggeredAbilityEffect | undefined
  buffs: Iwt2RoguelikeSelfBuffId[]
  halfAegisScriptEffect: Iwt2TriggeredAbilityEffect | undefined
  halfBarkskinEffect: Iwt2TriggeredAbilityEffect | undefined
  halfSunWardEffect: Iwt2TriggeredAbilityEffect | undefined
  mendingRuneEffect: Iwt2TriggeredAbilityEffect | undefined
  renewEffect: Iwt2TriggeredAbilityEffect | undefined
  seedOfLifeEffect: Iwt2TriggeredAbilityEffect | undefined
  sunWardEffect: Iwt2TriggeredAbilityEffect | undefined
}): Iwt2TriggeredAbilityEffect[] {
  const effects = [...ability.triggeredEffects ?? []]
  const addEffect = (effect: Iwt2TriggeredAbilityEffect | undefined) => {
    if (effect && !effects.some((existing) => existing.id === effect.id && existing.power === effect.power)) {
      effects.push(effect)
    }
  }
  if (ability.id === 'dawnweaver-mend') {
    if (buffs.includes('dawnweaver-mend-applies-renew')) addEffect(renewEffect)
    if (buffs.includes('dawnweaver-mend-applies-sun-ward')) addEffect(sunWardEffect)
  } else if (ability.id === 'dawnweaver-renew') {
    if (buffs.includes('dawnweaver-renew-applies-sun-ward')) addEffect(sunWardEffect)
  } else if (ability.id === 'dawnweaver-radiance') {
    if (buffs.includes('dawnweaver-radiance-applies-renew')) addEffect(renewEffect)
    if (buffs.includes('dawnweaver-radiance-applies-sun-ward')) addEffect(halfSunWardEffect)
  } else if (ability.id === 'dawnweaver-sun-ward') {
    if (buffs.includes('dawnweaver-sun-ward-applies-renew')) addEffect(renewEffect)
  } else if (ability.id === 'dawnweaver-purify') {
    if (buffs.includes('dawnweaver-purify-applies-renew')) addEffect(renewEffect)
    if (buffs.includes('dawnweaver-purify-applies-sun-ward')) addEffect(sunWardEffect)
  } else if (ability.id === 'lifebinder-verdant-touch') {
    if (buffs.includes('lifebinder-verdant-touch-applies-seed-of-life')) addEffect(seedOfLifeEffect)
    if (buffs.includes('lifebinder-verdant-touch-applies-barkskin')) addEffect(halfBarkskinEffect)
  } else if (ability.id === 'lifebinder-seed-of-life') {
    if (buffs.includes('lifebinder-seed-of-life-applies-barkskin')) addEffect(barkskinEffect)
  } else if (ability.id === 'lifebinder-wild-growth') {
    if (buffs.includes('lifebinder-wild-growth-applies-seed-of-life')) addEffect(seedOfLifeEffect)
    if (buffs.includes('lifebinder-wild-growth-applies-barkskin')) addEffect(halfBarkskinEffect)
  } else if (ability.id === 'lifebinder-barkskin') {
    if (buffs.includes('lifebinder-barkskin-applies-seed-of-life')) addEffect(seedOfLifeEffect)
  } else if (ability.id === 'lifebinder-purging-sap') {
    if (buffs.includes('lifebinder-purging-sap-applies-seed-of-life')) addEffect(seedOfLifeEffect)
    if (buffs.includes('lifebinder-purging-sap-applies-barkskin')) addEffect(barkskinEffect)
  } else if (ability.id === 'runesage-etched-mend') {
    if (buffs.includes('runesage-etched-mend-applies-mending-rune')) addEffect(mendingRuneEffect)
    if (buffs.includes('runesage-etched-mend-applies-aegis-script')) addEffect(halfAegisScriptEffect)
  } else if (ability.id === 'runesage-mending-rune') {
    if (buffs.includes('runesage-mending-rune-applies-aegis-script')) addEffect(aegisScriptEffect)
  } else if (ability.id === 'runesage-concordance') {
    if (buffs.includes('runesage-concordance-applies-mending-rune')) addEffect(mendingRuneEffect)
    if (buffs.includes('runesage-concordance-applies-aegis-script')) addEffect(halfAegisScriptEffect)
  } else if (ability.id === 'runesage-aegis-script') {
    if (buffs.includes('runesage-aegis-script-applies-mending-rune')) addEffect(mendingRuneEffect)
  } else if (ability.id === 'runesage-unravel') {
    if (buffs.includes('runesage-unravel-applies-mending-rune')) addEffect(mendingRuneEffect)
    if (buffs.includes('runesage-unravel-applies-aegis-script')) addEffect(aegisScriptEffect)
  }
  return effects
}

function triggeredEffectFromAbility(
  ability: Iwt2HealerAbility | undefined,
  strength = 1,
): Iwt2TriggeredAbilityEffect | undefined {
  if (!ability) return undefined
  return {
    effectType: ability.effectType,
    id: ability.id,
    kind: ability.kind,
    name: ability.name,
    power: Math.max(1, Math.round(ability.power * strength)),
  }
}

function countStacks(items: readonly string[], id: string) {
  return items.filter((item) => item === id).length
}

function roundModifier(value: number) {
  return Math.max(0.1, Math.round(value * 100) / 100)
}

function formatRoguelikeContentType(contentType: Iwt2RoguelikeContentType) {
  if (contentType === 'raid') return 'Raid'
  if (contentType === 'stadium') return 'Stadium'
  return 'Dungeon'
}

function arenaPauseTitle(
  roguelikeRun: BossArenaScreenProps['roguelikeRun'] | undefined,
  fallbackTitle: string,
) {
  if (!roguelikeRun) return fallbackTitle
  if (roguelikeRun.contentType === 'stadium') return 'Stadium'
  if (roguelikeRun.variant === 'pvp') {
    return roguelikeRun.contentType === 'raid' ? 'Raid Clash' : 'Dungeon Clash'
  }
  return roguelikeRun.contentType === 'raid' ? 'Raid Roguelike' : 'Dungeon Roguelike'
}

function buildIwt2DualScreenCombatState({
  abilities,
  arenaState,
  bindings,
  cooldowns,
  controllerIconStyle,
  directPartyTargeting,
  modeLabel,
  opponentArenaState,
  playerMana,
  roguelikeRun,
  selectedPartyId,
  status,
}: {
  abilities: Iwt2HealerAbility[]
  arenaState: Iwt2ArenaState
  bindings: DualScreenCombatState['bindings']
  cooldowns: Record<string, number>
  controllerIconStyle: DualScreenCombatState['controllerIconStyle']
  directPartyTargeting: boolean
  modeLabel: string
  opponentArenaState: Iwt2ArenaState | null
  playerMana: number
  roguelikeRun: BossArenaScreenProps['roguelikeRun'] | undefined
  selectedPartyId: Iwt2EntityId
  status: ArenaStatus
}): DualScreenCombatState {
  const opponentHealer = opponentArenaState?.party.find((member) => member.id === 'player-healer')
  const arenaBossHealth = totalBossHealth(arenaState)
  const arenaBossMaxHealth = totalBossMaxHealth(arenaState)
  const pvpOpponentState = roguelikeRun?.variant === 'pvp' && opponentArenaState
    ? {
        opponentBuffSummary: `Stage ${roguelikeRun.stage}`,
        opponentClassName: `CPU Healer | ${formatRoguelikeContentType(roguelikeRun.contentType)}`,
        opponentDebuffSummary: formatIwt2DebuffSummary(roguelikeRun.debuffs),
        opponentArena: toDualScreenOpponentArena(opponentArenaState),
        opponentEnemyHealth: totalBossHealth(opponentArenaState),
        opponentMaxResource: opponentHealer?.maxMana ?? 100,
        opponentName: 'CPU Rival',
        opponentParty: opponentArenaState.party.map((member) => toDualScreenPartyMember(member, true)),
        opponentResource: opponentHealer?.mana ?? 100,
        opponentResourceName: 'Mana',
      }
    : {}
  return {
    ...pvpOpponentState,
    bindings,
    contentName: modeLabel,
    controllerIconStyle,
    difficultyName: 'IWT2',
    directPartyTargeting,
    dungeonName: `${formatBossEncounterTitle(arenaState.bosses)} Arena`,
    encounterCount: 1,
    encounterDescription: arenaState.boss.attackPhase,
    encounterHealth: arenaBossHealth,
    encounterIndex: 0,
    encounterIsBoss: true,
    encounterMaxHealth: arenaBossMaxHealth,
    encounterName: formatBossEncounterTitle(arenaState.bosses),
    floatingTexts: [],
    maxResource: 100,
    party: arenaState.party.map((member) => toDualScreenPartyMember(member)),
    partySize: arenaState.party.length,
    paused: status === 'paused',
    playerIsAlive: (arenaState.party.find((member) => member.id === 'player-healer')?.health ?? 0) > 0,
    resource: playerMana,
    resourceName: 'Mana',
    selectedId: selectedPartyId,
    speedMultiplier: 1,
    spells: abilities.map((ability, slotIndex) => toDualScreenSpell(ability, slotIndex, cooldowns[ability.id] ?? 0)),
    status: status === 'victory' ? 'won' : status === 'defeat' ? 'lost' : 'playing',
    targetGroup: 0,
  }
}

function totalBossHealth(state: Iwt2ArenaState): number {
  return state.bosses.reduce((total, boss) => total + Math.max(0, boss.health), 0)
}

function totalBossMaxHealth(state: Iwt2ArenaState): number {
  return state.bosses.reduce((total, boss) => total + boss.maxHealth, 0)
}

function toDualScreenOpponentArena(state: Iwt2ArenaState): NonNullable<DualScreenCombatState['opponentArena']> {
  return {
    bounds: state.bounds,
    bosses: state.bosses.map((boss) => {
      const metadata = IWT2_BOSS_METADATA[boss.bossId]
      return {
        id: boss.id,
        name: metadata.name,
        icon: metadata.icon,
        color: metadata.color,
        x: boss.position.x,
        y: boss.position.y,
        radius: boss.radius,
        health: boss.health,
        maxHealth: boss.maxHealth,
      }
    }),
    party: state.party.map((member) => {
      const metadata = IWT2_CLASS_METADATA[member.classId]
      return {
        id: member.id,
        icon: metadata.icon,
        color: metadata.color,
        x: member.position.x,
        y: member.position.y,
        radius: member.radius,
        health: member.health,
        maxHealth: member.maxHealth,
        isHealer: member.id === 'player-healer',
      }
    }),
  }
}

function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number], opponent = false): PartyMember {
  const metadata = IWT2_CLASS_METADATA[member.classId]
  return {
    bounceHeals: [],
    health: member.health,
    hotEffects: member.hotEffects.map((effect) => ({
      id: effect.id,
      label: effect.label,
      power: effect.power,
      spellId: effect.id,
      ticks: Math.ceil(effect.remainingSeconds),
    })),
    hotTicks: member.hotEffects.length,
    id: opponent ? `opponent-${member.id}` : member.id,
    maxHealth: member.maxHealth,
    name: opponent ? opponentPartyName(metadata.name) : metadata.name,
    role: toDualScreenRole(metadata.role),
    shield: member.shield,
  }
}

function opponentPartyName(name: string): string {
  if (name === 'Player Healer') return 'CPU Healer'
  return `CPU ${name.replace(' Tank', '')}`
}

function toDualScreenRole(role: (typeof IWT2_CLASS_METADATA)[keyof typeof IWT2_CLASS_METADATA]['role']): Role {
  if (role === 'tank') return 'Tank'
  if (role === 'healer') return 'Healer'
  return 'Damage'
}

function toDualScreenSpell(
  ability: Iwt2HealerAbility,
  slotIndex: number,
  remaining: number,
): Spell & { slotIndex: number; remaining: number } {
  return {
    cooldown: ability.cooldownSeconds,
    cost: ability.manaCost,
    description: 'IWT2 arena ability',
    effectType: ability.effectType,
    glyph: ability.icon,
    id: ability.id,
    key: String(ability.slot),
    kind: ability.kind,
    name: ability.name,
    power: ability.power,
    remaining,
    slotIndex,
  }
}

function iwt2OpponentMovement(state: Iwt2ArenaState): MovementVector {
  const healer = state.party.find((member) => member.id === 'player-healer')
  if (!healer || healer.health <= 0) return { x: 0, y: 0 }
  const orbitSeconds = Math.max(1, state.time)
  return normalizedArenaMovement({
    x: Math.cos(orbitSeconds * 0.75),
    y: Math.sin(orbitSeconds * 0.75) * 0.55,
  })
}

function normalizedArenaMovement(vector: MovementVector): MovementVector {
  const magnitude = Math.hypot(vector.x, vector.y)
  if (magnitude <= 1) return vector
  return {
    x: vector.x / magnitude,
    y: vector.y / magnitude,
  }
}

function formatIwt2DebuffSummary(debuffs: readonly string[]): string {
  if (debuffs.length === 0) return 'none'
  return debuffs
    .map((debuff) => debuff
      .replace('opp-', '')
      .replaceAll('-', ' '))
    .join(', ')
}
