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 { Iwt2EntityId } from '../sim'
import { castIwt2HealerAbility } from '../sim'
import { PhaserArena } from '../render/PhaserArena'
import {
  recordIwt2BossKill,
  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 { abilitiesForHealer, type Iwt2HealerAbility } from '../content/healerAbilities'
import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
import type {
  Iwt2RoguelikeContentType,
  Iwt2RoguelikeOpponentDebuffId,
  Iwt2RoguelikeSelfBuffId,
  Iwt2RoguelikeVariant,
} from '../content/roguelike'

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

const OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [
  { action: 'primary', row: 0 },
  { action: 'menu', row: 1 },
]

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

export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
  const bossMetadata = IWT2_BOSS_METADATA[bossId]
  const pvpRoguelike = roguelikeRun?.variant === 'pvp'
  const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createInitialIwt2ArenaState(bossId, bossIds))
  const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
    pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds) : 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 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 next = createInitialIwt2ArenaState(bossId, bossIds)
    const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds) : null
    recordedKillIdsRef.current = new Set()
    abilityCooldownsRef.current = {}
    lastHudSignatureRef.current = arenaHudSignature(next)
    stateRef.current = next
    opponentStateRef.current = nextOpponentState
    setArenaState(next)
    setOpponentArenaState(nextOpponentState)
    setAbilityCooldowns({})
    setSelectedOverlayAction('primary')
    setStatus('playing')
  }, [bossId, bossIds, pvpRoguelike])

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

  const abilities = useMemo(
    () => applyRoguelikeModifiers(
      abilitiesForHealer(save.character.healerStyle),
      roguelikeRun?.buffs ?? [],
      roguelikeRun?.debuffs ?? [],
    ),
    [roguelikeRun?.buffs, roguelikeRun?.debuffs, save.character.healerStyle],
  )

  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 active = OVERLAY_NAV_ENTRIES.find((entry) => entry.action === current) ?? OVERLAY_NAV_ENTRIES[0]
      const candidates = OVERLAY_NAV_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
    })
  }, [])

  const activateOverlayAction = useCallback((overlayAction = selectedOverlayActionRef.current) => {
    if (overlayAction === 'menu') {
      onBack()
      return
    }
    if (statusRef.current === 'paused') {
      setStatus('playing')
      return
    }
    if (statusRef.current === 'victory' && roguelikeRun) {
      roguelikeRun.onVictory()
      return
    }
    resetArena()
  }, [onBack, resetArena, roguelikeRun])

  useGameAction((action, device) => {
    if (device === 'controller' && statusRef.current !== 'playing') {
      if (action.startsWith('navigate')) {
        moveOverlaySelection(action)
        return
      }
      if (action === 'confirm') {
        activateOverlayAction()
        return
      }
      if (action === 'back') {
        if (statusRef.current === 'paused') setStatus('playing')
        else onBack()
        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')) {
      const index = Number(action.replace('targetParty', '')) - 1
      const target = stateRef.current.party[index]
      if (target) setSelectedPartyId(target.id)
      return
    }
    if (device !== 'controller') return
    if (action === 'pause' || action === 'back') {
      if (statusRef.current === 'playing') showOverlay('paused')
      else if (statusRef.current === 'paused') setStatus('playing')
    }
  })

  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
      for (const defeatedBoss of newlyDefeatedBosses) {
        nextRecordedIds.add(defeatedBoss.bossId)
        updatedSave = recordIwt2BossKill(updatedSave, defeatedBoss.bossId)
      }
      recordedKillIdsRef.current = nextRecordedIds
      saveRef.current = updatedSave
      onSaveUpdated(updatedSave)
    }

    if (next.bosses.every((boss) => boss.health <= 0)) {
      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
  }, [onSaveUpdated, pvpRoguelike, 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 bossTitle = formatBossEncounterTitle(arenaState.bosses)
  const overlayPrimaryLabel = status === 'paused'
    ? 'Resume'
    : status === 'victory' && roguelikeRun
      ? 'Choose Upgrade'
      : '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. Resume the fight or leave the current run.'
  const pauseLeaveLabel = roguelikeRun?.variant === 'pvp'
    ? 'Leave'
    : roguelikeRun
      ? 'Leave Roguelike'
      : `Leave ${modeLabel ?? 'Arena'}`
  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-controller-nav="skip"
                  data-game-selected={selectedOverlayAction === 'primary' ? 'true' : undefined}
                  onClick={() => activateOverlayAction('primary')}
                  onPointerDown={() => setSelectedOverlayAction('primary')}
                  type="button"
                >
                  Resume
                </button>
                <button
                  className={`secondary-result-button ${selectedOverlayAction === 'menu' ? 'game-selected' : ''}`}
                  data-controller-nav="skip"
                  data-game-selected={selectedOverlayAction === 'menu' ? 'true' : undefined}
                  onClick={() => activateOverlayAction('menu')}
                  onPointerDown={() => setSelectedOverlayAction('menu')}
                  type="button"
                >
                  {pauseLeaveLabel}
                </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>+{arenaState.bosses.length * 125} XP</span>
                    <span>{arenaState.bosses.length} carves</span>
                    <span>Log +{arenaState.bosses.length}</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')}
                    type="button"
                  >
                    {overlayPrimaryLabel}
                  </button>
                  <button
                    className={`iwt2-result-button is-secondary ${selectedOverlayAction === 'menu' ? 'game-selected' : ''}`}
                    data-game-selected={selectedOverlayAction === 'menu' ? 'true' : undefined}
                    onClick={() => activateOverlayAction('menu')}
                    type="button"
                  >
                    Menu
                  </button>
                </div>
                {lastDevice === 'controller' && (
                  <small className="iwt2-result-hint">D-pad selects, A confirms, B exits</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 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 applyRoguelikeModifiers(
  abilities: Iwt2HealerAbility[],
  buffs: Iwt2RoguelikeSelfBuffId[],
  debuffs: Iwt2RoguelikeOpponentDebuffId[],
): Iwt2HealerAbility[] {
  if (buffs.length === 0 && debuffs.length === 0) return abilities
  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`)
    if (costDown === 0 && costUp === 0 && cooldownDown === 0 && cooldownUp === 0 && extraTargets === 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)),
    }
  })
}

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(', ')
}
