BattleSession.kt

package io.github.lishangbu.avalon.game.battle.engine.core.session

import io.github.lishangbu.avalon.game.battle.engine.core.model.BattleLifecycle
import io.github.lishangbu.avalon.game.battle.engine.core.model.BattleState
import io.github.lishangbu.avalon.game.battle.engine.core.model.BattleType
import io.github.lishangbu.avalon.game.battle.engine.core.model.FieldState
import io.github.lishangbu.avalon.game.battle.engine.core.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplier
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleFlowEngine
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleFlowPhaseProcessor
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleMutationInterceptorChain
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.MoveResolutionResult
import io.github.lishangbu.avalon.game.battle.engine.core.session.specification.BattleSessionCaptureChoiceSpecification
import io.github.lishangbu.avalon.game.battle.engine.core.session.specification.BattleSessionItemChoiceSpecification
import io.github.lishangbu.avalon.game.battle.engine.core.session.specification.BattleSessionMoveChoiceSpecification
import io.github.lishangbu.avalon.game.battle.engine.core.session.specification.BattleSessionRunChoiceSpecification
import io.github.lishangbu.avalon.game.battle.engine.core.session.specification.BattleSessionTargetChoiceSpecification
import io.github.lishangbu.avalon.game.battle.engine.core.session.specification.BattleSessionTurnReadySpecification
import io.github.lishangbu.avalon.game.battle.engine.core.session.specification.BattleSessionUnitChoiceSpecification
import io.github.lishangbu.avalon.game.battle.engine.core.session.target.BattleSessionTargetQuery
import io.github.lishangbu.avalon.game.battle.engine.core.session.target.BattleSessionTargetQueryService
import io.github.lishangbu.avalon.game.battle.engine.spi.capture.CaptureActionResolver
import io.github.lishangbu.avalon.game.battle.engine.spi.effect.EffectDefinitionRepository

/**
 * 最小可跑的 battle session。
 *
 * 设计意图:
 * - 把 BattleRuntimeSnapshot 和 BattleFlowEngine 包装成可持续推进的最小会话对象。
 * - 提供“开始、注册单位、收集动作、执行回合、结束回合、自动替补、最小胜负判定”能力。
 *
 * 当前阶段不负责:
 * - 玩家决策收集
 * - 完整行动优先级系统
 * - 玩家可选替补决策
 * - 复杂胜负规则
 *
 * @property effectRepository effect 定义查询入口。
 * @property battleFlowEngine battle runtime 主流程入口。
 * @property battleFlowPhaseProcessor battle attached-effect 生命周期处理入口。
 * @property mutationApplier 结构化 mutation 写回组件。
 * @property replacementStrategy 自动替补选择策略。
 * @property captureActionResolver battle 内捕捉动作解析器。
 * @property actionExecutionSupport session action 共用执行辅助组件。
 * @property choiceHandlerRegistry choice 提交处理器注册中心。
 * @property actionHandlerRegistry action 执行处理器注册中心。
 * @property turnPipeline 回合推进 pipeline。
 * @property turnReadySpecification 回合结算前置规格。
 * @property unitChoiceSpecification active 单位行动提交规格。
 * @property runChoiceSpecification side 逃跑提交规格。
 * @property targetChoiceSpecification effect 目标合法性规格。
 * @property captureChoiceSpecification 捕捉动作合法性规格。
 * @property moveChoiceSpecification 招式选择限制规格。
 * @property itemChoiceSpecification 物品选择限制规格。
 * @property targetQueryService battle session 目标查询服务。
 * @property actionSortingStrategy battle session action 排序策略。
 * @property eventPublisher battle session 内部事件发布器。
 * @property commandFactory battle session 命令工厂。
 */
class BattleSession(
    internal val effectRepository: EffectDefinitionRepository,
    internal val battleFlowEngine: BattleFlowEngine,
    internal val battleFlowPhaseProcessor: BattleFlowPhaseProcessor,
    internal val mutationInterceptorChain: BattleMutationInterceptorChain,
    internal val mutationApplier: MutationApplier,
    internal val replacementStrategy: ReplacementStrategy,
    internal val captureActionResolver: CaptureActionResolver,
    internal val actionExecutionSupport: BattleSessionActionExecutionSupport,
    private val choiceHandlerRegistry: BattleSessionChoiceHandlerRegistry,
    private val actionHandlerRegistry: BattleSessionActionHandlerRegistry,
    private val turnPipeline: BattleSessionTurnPipeline,
    private val turnReadySpecification: BattleSessionTurnReadySpecification,
    private val unitChoiceSpecification: BattleSessionUnitChoiceSpecification,
    private val runChoiceSpecification: BattleSessionRunChoiceSpecification,
    private val targetChoiceSpecification: BattleSessionTargetChoiceSpecification,
    private val captureChoiceSpecification: BattleSessionCaptureChoiceSpecification,
    private val moveChoiceSpecification: BattleSessionMoveChoiceSpecification,
    private val itemChoiceSpecification: BattleSessionItemChoiceSpecification,
    private val targetQueryService: BattleSessionTargetQueryService,
    private val actionSortingStrategy: BattleSessionActionSortingStrategy,
    private val eventPublisher: BattleSessionEventPublisher,
    private val commandFactory: BattleSessionCommandFactory,
    battleId: String,
    formatId: String,
) {
    /**
     * 当前回合待执行动作队列。
     */
    internal val actionQueue: BattleSessionActionQueue = BattleSessionActionQueue(actionSortingStrategy)

    /**
     * 面向玩家可读的 battle log 缓存。
     */
    internal val battleLogs: MutableList<String> = mutableListOf()

    /**
     * 结构化事件日志缓存。
     */
    internal val eventLogs: MutableList<BattleSessionEvent> = mutableListOf()

    /**
     * 当前待处理的替补请求集合。
     */
    internal val replacementRequests: MutableList<BattleSessionReplacementRequest> = mutableListOf()

    /**
     * battle 内资源消耗账本。
     */
    internal val resourceLedger: MutableList<BattleSessionResourceUsage> = mutableListOf()

    /**
     * 当前会话持有的最新运行时快照。
     */
    internal var currentSnapshot: BattleRuntimeSnapshot =
        BattleRuntimeSnapshot(
            battle = BattleState(id = battleId, formatId = formatId),
            field = FieldState(),
            units = emptyMap(),
            sides = emptyMap(),
        )

    /**
     * choice 合法性校验器。
     */
    private val choiceValidator: BattleSessionChoiceValidator =
        BattleSessionChoiceValidator(
            session = this,
            turnReadySpecification = turnReadySpecification,
            unitChoiceSpecification = unitChoiceSpecification,
            runChoiceSpecification = runChoiceSpecification,
            targetChoiceSpecification = targetChoiceSpecification,
            captureChoiceSpecification = captureChoiceSpecification,
            moveChoiceSpecification = moveChoiceSpecification,
            itemChoiceSpecification = itemChoiceSpecification,
        )

    /**
     * 已入队 action 的执行协调器。
     */
    private val actionExecutor: BattleSessionActionExecutor = BattleSessionActionExecutor(this, actionHandlerRegistry)

    /**
     * 濒死、替补与胜负更新协调器。
     */
    private val replacementResolver: BattleSessionReplacementResolver = BattleSessionReplacementResolver(this)

    /**
     * `before_turn / switch / faint` 等生命周期 hook 的统一调度器。
     */
    private val hookLifecycleProcessor: BattleSessionHookLifecycleProcessor =
        BattleSessionHookLifecycleProcessor(
            session = this,
            battleFlowPhaseProcessor = battleFlowPhaseProcessor,
        )

    /**
     * 回合末推进与替补提交流程协调器。
     */
    private val lifecycleCoordinator: BattleSessionLifecycleCoordinator = BattleSessionLifecycleCoordinator(this)

    /**
     * 建局、恢复与会话外壳状态协调器。
     */
    private val setupCoordinator: BattleSessionSetupCoordinator = BattleSessionSetupCoordinator(this)

    /**
     * session 内部的统一 mutation 提交器。
     */
    private val mutationProcessor: BattleSessionMutationProcessor = BattleSessionMutationProcessor(this)

    /**
     * publication 发布与读模型写入协调器。
     */
    private val publicationRecorder: BattleSessionPublicationRecorder =
        BattleSessionPublicationRecorder(
            session = this,
            eventPublisher = eventPublisher,
        )

    /**
     * 返回当前会话快照。
     */
    fun snapshot(): BattleRuntimeSnapshot = currentSnapshot

    /**
     * 返回当前会话的统一查询结果。
     */
    fun query(): BattleSessionQuery =
        BattleSessionQuery(
            snapshot = snapshot(),
            pendingActions = pendingActions(),
            choiceStatuses = choiceStatuses(),
            replacementRequests = pendingReplacementRequests(),
            resourceLedger = resourceLedger(),
            battleLogs = battleLogs(),
            eventLogs = eventLogs(),
        )

    /**
     * 查询某个 effect 在当前会话下的目标模式和可选目标。
     *
     * @param effectId 被查询的 effect 标识
     * @param actorUnitId 当前出手单位标识
     * @return 当前快照下的目标查询结果
     */
    fun queryTargets(
        effectId: String,
        actorUnitId: String,
    ): BattleSessionTargetQuery {
        ensureRunning()
        return targetQueryService.resolve(
            snapshot = currentSnapshot,
            effectId = effectId,
            actorUnitId = actorUnitId,
        )
    }

    /**
     * 返回当前待执行动作队列快照。
     */
    fun pendingActions(): List<BattleSessionAction> = actionQueue.snapshot()

    /**
     * 返回每个 side 的当前回合输入状态。
     */
    fun choiceStatuses(): List<BattleSessionChoiceStatus> =
        currentSnapshot.sides.values.map { side ->
            val activeUnitIds = side.activeUnitIds
            val submittedUnitIds = submittedUnitIdsForSide(side)
            val missingUnitIds = activeUnitIds.filterNot { unitId -> unitId in submittedUnitIds }
            BattleSessionChoiceStatus(
                sideId = side.id,
                activeUnitIds = activeUnitIds,
                submittedUnitIds = submittedUnitIds,
                missingUnitIds = missingUnitIds,
                requiredActionCount = activeUnitIds.size,
                submittedActionCount = submittedUnitIds.size,
                ready = missingUnitIds.isEmpty(),
            )
        }

    /**
     * 返回当前回合尚未提交行动的 side 标识列表。
     *
     * 约定:
     * - 仅统计当前仍有 active 单位的 side。
     * - side 下只要还有任一 active 单位未提交行动,就视为该 side 仍缺少输入。
     */
    fun missingChoiceSideIds(): List<String> =
        choiceStatuses()
            .filterNot(BattleSessionChoiceStatus::ready)
            .map(BattleSessionChoiceStatus::sideId)

    /**
     * 返回本回合尚未提交行动的 active 单位列表。
     */
    fun pendingChoiceUnitIds(): List<String> =
        choiceStatuses()
            .flatMap(BattleSessionChoiceStatus::missingUnitIds)

    /**
     * 当前回合是否已经满足最小提交条件。
     */
    fun isTurnReady(): Boolean = missingChoiceSideIds().isEmpty()

    /**
     * 当前回合是否允许进入结算。
     *
     * 额外约束:
     * - 不能有未完成的 replacement request。
     */
    fun canResolveTurn(): Boolean = turnStatus() == BattleSessionTurnStatus.READY_TO_RESOLVE

    /**
     * 返回人类可读 battle log 快照。
     */
    fun battleLogs(): List<String> = battleLogs.toList()

    /**
     * 返回结构化 event log 快照。
     */
    fun eventLogs(): List<BattleSessionEvent> = eventLogs.toList()

    /**
     * 返回当前待处理的替补请求。
     */
    fun pendingReplacementRequests(): List<BattleSessionReplacementRequest> = replacementRequests.toList()

    /**
     * 返回当前 battle 内部资源账本。
     */
    fun resourceLedger(): List<BattleSessionResourceUsage> = resourceLedger.toList()

    /**
     * 导出当前可持久化状态。
     */
    fun exportState(): BattleSessionState = setupCoordinator.exportState()

    /**
     * 用已持久化状态恢复当前 session。
     */
    fun restoreState(state: BattleSessionState): BattleRuntimeSnapshot = setupCoordinator.restoreState(state)

    /**
     * 启动 battle session。
     *
     * 约定:
     * - 启动后 turn 初始化为 1。
     */
    fun start(): BattleRuntimeSnapshot = setupCoordinator.start()

    /**
     * 注册一个 side。
     *
     * @param sideId 待注册的 side 标识
     * @return 更新后的快照
     */
    fun registerSide(sideId: String): BattleRuntimeSnapshot = setupCoordinator.registerSide(sideId)

    /**
     * 配置当前 battle 的业务语义,例如 wild/trainer 和可捕捉 side。
     */
    fun configureBattle(
        battleKind: BattleType,
        capturableSideId: String? = null,
    ): BattleRuntimeSnapshot = setupCoordinator.configureBattle(battleKind, capturableSideId)

    /**
     * 注册一个单位并挂到指定 side。
     *
     * @param sideId 目标 side 标识
     * @param unit 要注册的单位状态
     * @param active 当前是否直接加入 activeUnitIds
     * @return 更新后的快照
     */
    fun registerUnit(
        sideId: String,
        unit: UnitState,
        active: Boolean = true,
    ): BattleRuntimeSnapshot = setupCoordinator.registerUnit(sideId, unit, active)

    /**
     * 执行一次出招。
     *
     * @param moveId 招式或 effect 标识
     * @param attackerId 出手单位标识
     * @param targetId 目标单位标识
     * @param accuracy 命中率输入;为空时交由底层默认规则处理
     * @param evasion 回避率输入;为空时交由底层默认规则处理
     * @param basePower 基础威力输入
     * @param damage 预期直接伤害
     * @param attributes 附加属性
     * @return 最终结算结果
     */
    fun useMove(
        moveId: String,
        attackerId: String,
        targetId: String,
        accuracy: Int? = null,
        evasion: Int? = null,
        basePower: Int,
        damage: Int,
        attributes: Map<String, Any?> = emptyMap(),
    ): MoveResolutionResult {
        ensureRunning()
        return executeResolvedEffect(
            effectId = moveId,
            actorUnitId = attackerId,
            targetUnitId = targetId,
            accuracy = accuracy,
            evasion = evasion,
            basePower = basePower,
            damage = damage,
            attributes = attributes,
        )
    }

    /**
     * 将一个 move action 加入当前回合待执行队列。
     *
     * @param moveId 招式或 effect 标识
     * @param attackerId 出手单位标识
     * @param targetId 目标单位标识
     * @param priority 行动优先级
     * @param speed 排序速度
     * @param accuracy 命中率输入
     * @param evasion 回避率输入
     * @param basePower 基础威力
     * @param damage 预期直接伤害
     * @param attributes 附加属性
     * @return 当前动作队列快照
     */
    fun queueMove(
        moveId: String,
        attackerId: String,
        targetId: String,
        priority: Int = 0,
        speed: Int = 0,
        accuracy: Int? = null,
        evasion: Int? = null,
        basePower: Int,
        damage: Int,
        attributes: Map<String, Any?> = emptyMap(),
    ): List<BattleSessionAction> {
        ensureRunning()
        ensureUnitCanSubmitChoice(attackerId)
        ensureMoveChoiceIsLegal(moveId = moveId, actorUnitId = attackerId)
        ensureTargetIsLegalForAction(effectId = moveId, actorUnitId = attackerId, targetUnitId = targetId)
        actionQueue.enqueue(
            commandFactory.createMoveAction(
                moveId = moveId,
                attackerId = attackerId,
                targetId = targetId,
                priority = priority,
                speed = speed,
                accuracy = accuracy,
                evasion = evasion,
                basePower = basePower,
                damage = damage,
                attributes = attributes,
            ),
        )
        recordLog("Queued move $moveId from $attackerId to $targetId.")
        recordEvent(
            BattleSessionMoveQueuedPayload(
                moveId = moveId,
                attackerId = attackerId,
                targetId = targetId,
                priority = priority,
                speed = speed,
            ),
        )
        return actionQueue.snapshot()
    }

    /**
     * 提交一个玩家出招选择。
     *
     * @param moveId 招式或 effect 标识
     * @param attackerId 出手单位标识
     * @param targetId 目标单位标识
     * @param priority 行动优先级
     * @param speed 排序速度
     * @param accuracy 命中率输入
     * @param evasion 回避率输入
     * @param basePower 基础威力
     * @param damage 预期直接伤害
     * @param attributes 附加属性
     * @return 当前动作队列快照
     */
    fun submitMoveChoice(
        moveId: String,
        attackerId: String,
        targetId: String,
        priority: Int = 0,
        speed: Int = 0,
        accuracy: Int? = null,
        evasion: Int? = null,
        basePower: Int,
        damage: Int,
        attributes: Map<String, Any?> = emptyMap(),
    ): List<BattleSessionAction> =
        submitChoice(
            commandFactory.createMoveChoice(
                moveId = moveId,
                attackerId = attackerId,
                targetId = targetId,
                priority = priority,
                speed = speed,
                accuracy = accuracy,
                evasion = evasion,
                basePower = basePower,
                damage = damage,
                attributes = attributes,
            ),
        )

    /**
     * 提交一个统一 choice 输入。
     *
     * 设计意图:
     * - 为调用方提供统一的 session 输入入口。
     * - 保留具体 `submitMoveChoice/submitSwitchChoice/...` 作为薄包装。
     *
     * @param choice 本次要提交的统一选择对象
     * @return 当前动作队列快照
     */
    fun submitChoice(choice: BattleSessionChoice): List<BattleSessionAction> {
        ensureAwaitingChoices()
        return choiceHandlerRegistry.get(choice).submit(choice, this)
    }

    /**
     * 批量提交多个统一 choice 输入。
     *
     * @param choices 本次要提交的选择列表
     * @return 当前动作队列快照
     */
    fun submitChoices(choices: List<BattleSessionChoice>): List<BattleSessionAction> {
        choices.forEach(::submitChoice)
        return actionQueue.snapshot()
    }

    /**
     * 提交一个捕捉选择。
     */
    fun submitCaptureChoice(
        playerId: String,
        ballItemId: String,
        sourceUnitId: String,
        targetId: String,
        priority: Int = 0,
        speed: Int = 0,
    ): List<BattleSessionAction> =
        submitChoice(
            commandFactory.createCaptureChoice(
                playerId = playerId,
                ballItemId = ballItemId,
                sourceUnitId = sourceUnitId,
                targetId = targetId,
                priority = priority,
                speed = speed,
            ),
        )

    /**
     * 提交一个等待动作。
     */
    fun submitWaitChoice(
        unitId: String,
        priority: Int = 0,
        speed: Int = 0,
    ): List<BattleSessionAction> =
        submitChoice(
            commandFactory.createWaitChoice(
                unitId = unitId,
                priority = priority,
                speed = speed,
            ),
        )

    /**
     * 提交一个玩家替换选择。
     *
     * @param sideId 发起替换的 side 标识
     * @param outgoingUnitId 当前下场单位
     * @param incomingUnitId 即将上场单位
     * @param priority 行动优先级
     * @param speed 排序速度
     * @return 当前动作队列快照
     */
    fun submitSwitchChoice(
        sideId: String,
        outgoingUnitId: String,
        incomingUnitId: String,
        priority: Int = 0,
        speed: Int = 0,
    ): List<BattleSessionAction> =
        submitChoice(
            commandFactory.createSwitchChoice(
                sideId = sideId,
                outgoingUnitId = outgoingUnitId,
                incomingUnitId = incomingUnitId,
                priority = priority,
                speed = speed,
            ),
        )

    /**
     * 提交一个玩家物品使用选择。
     *
     * @param itemId 物品或 effect 标识
     * @param actorUnitId 使用者单位标识
     * @param targetId 目标单位标识
     * @param priority 行动优先级
     * @param speed 排序速度
     * @param attributes 附加属性
     * @return 当前动作队列快照
     */
    fun submitItemChoice(
        itemId: String,
        actorUnitId: String,
        targetId: String,
        priority: Int = 0,
        speed: Int = 0,
        attributes: Map<String, Any?> = emptyMap(),
    ): List<BattleSessionAction> =
        submitChoice(
            commandFactory.createItemChoice(
                itemId = itemId,
                actorUnitId = actorUnitId,
                targetId = targetId,
                priority = priority,
                speed = speed,
                attributes = attributes,
            ),
        )

    /**
     * 提交一个玩家逃跑选择。
     *
     * @param sideId 发起逃跑的 side 标识
     * @param priority 行动优先级
     * @param speed 排序速度
     * @return 当前动作队列快照
     */
    fun submitRunChoice(
        sideId: String,
        priority: Int = 0,
        speed: Int = 0,
    ): List<BattleSessionAction> =
        submitChoice(
            commandFactory.createRunChoice(
                sideId = sideId,
                priority = priority,
                speed = speed,
            ),
        )

    /**
     * 按当前队列排序规则执行本回合已收集的全部 move action。
     *
     * @return 每个动作对应的执行结果列表
     */
    fun executeQueuedActions(): List<BattleSessionActionExecutionResult> = actionExecutor.executeQueuedActions()

    /**
     * 执行当前回合动作队列,然后推进到回合结束。
     *
     * @return 本回合汇总结果
     */
    fun resolveTurn(): BattleSessionTurnResult {
        ensureReadyToResolve()
        return turnPipeline.resolve(this)
    }

    /**
     * 对一个待处理替补请求提交具体选择。
     *
     * @param sideId 需要替补的 side 标识
     * @param incomingUnitId 被选中的替补单位
     * @return 更新后的快照
     */
    fun submitReplacementChoice(
        sideId: String,
        incomingUnitId: String,
    ): BattleRuntimeSnapshot = lifecycleCoordinator.submitReplacementChoice(sideId, incomingUnitId)

    /**
     * 推进到回合结束并执行 residual。
     *
     * @return 回合结束后的快照
     */
    fun endTurn(): BattleRuntimeSnapshot = lifecycleCoordinator.endTurn()

    /**
     * 记录一次捕捉失败,不结束 battle。
     */
    internal fun recordCaptureFailure(
        ballItemId: String,
        targetUnitId: String,
        shakes: Int,
        reason: String,
        finalRate: Double,
    ): BattleRuntimeSnapshot {
        ensureRunning()
        recordLog(
            "Capture failed: item=$ballItemId target=$targetUnitId shakes=$shakes reason=$reason finalRate=$finalRate.",
        )
        recordEvent(
            BattleSessionCaptureFailedPayload(
                ballItemId = ballItemId,
                targetUnitId = targetUnitId,
                shakes = shakes,
                reason = reason,
                finalRate = finalRate,
            ),
        )
        return currentSnapshot
    }

    /**
     * 以捕捉成功结束当前 battle。
     */
    internal fun finishByCapture(targetUnitId: String): BattleRuntimeSnapshot {
        ensureRunning()
        // 捕捉成功会立即终止 battle,但资源扣减与落库存仍要留到统一 settlement 再做。
        currentSnapshot =
            currentSnapshot.copy(
                battle =
                    currentSnapshot.battle.copy(
                        lifecycle = BattleLifecycle.ENDED_UNSETTLED,
                        winner = null,
                        endedReason = "capture",
                        capturedUnitId = targetUnitId,
                    ),
            )
        recordLog("Battle ended by capture. target=$targetUnitId.")
        recordEvent(
            BattleSessionCaptureSucceededPayload(targetUnitId = targetUnitId),
        )
        return currentSnapshot
    }

    /**
     * 记录一个在轮到执行前已经失效的 action。
     *
     * 这里统一产生日志和结构化事件,避免“失效动作”在不同 handler 里各写一套格式。
     */
    internal fun recordSkippedAction(
        action: BattleSessionAction,
        reason: String,
    ) {
        val actionType = BattleSessionActionEventKind.valueOf(action.kind.name)
        val submittingUnitId = (action as? BattleSessionSubmittingAction)?.submittingUnitId
        val sideId = (action as? BattleSessionSideAction)?.sideId
        recordLog(
            "Skipped ${action.kind.name.lowercase()} action" +
                (submittingUnitId?.let { " from $it" } ?: "") +
                (sideId?.let { " on side $it" } ?: "") +
                ". reason=$reason",
        )
        recordEvent(
            BattleSessionActionSkippedPayload(
                actionType = actionType,
                submittingUnitId = submittingUnitId,
                sideId = sideId,
                reason = reason,
            ),
        )
    }

    /**
     * 从当前 session RNG 中消费一个 `[0, bound)` 范围内的整数。
     *
     * 每次调用都会推进 battle state 中持久化的随机游标,
     * 因而该结果可以随着 session state 一起被导出和恢复。
     */
    internal fun nextRandomInt(bound: Int): Int {
        val randomResult = currentSnapshot.battle.randomState.nextInt(bound)
        currentSnapshot =
            currentSnapshot.copy(
                battle =
                    currentSnapshot.battle.copy(
                        randomState = randomResult.nextState,
                    ),
            )
        return randomResult.value
    }

    /**
     * 生成一个 `[1, 100]` 的百分比判定值。
     */
    internal fun nextPercentageRoll(): Int = nextRandomInt(100) + 1

    /**
     * 生成一个 `[0, 65535]` 的捕捉摇晃判定值。
     */
    internal fun nextCaptureShakeRoll(): Int = nextRandomInt(65536)

    /**
     * 标记当前 battle 已完成结算。
     */
    fun markSettled(): BattleRuntimeSnapshot = setupCoordinator.markSettled()

    /**
     * 推导当前回合内状态。
     *
     * 说明:
     * - 全局生命周期负责回答 battle 是否可运行、是否已结束。
     * - turn status 只回答运行中的这一回合正卡在哪个输入阶段。
     */
    internal fun turnStatus(): BattleSessionTurnStatus? =
        BattleSessionTurnStatus.from(
            lifecycle = currentSnapshot.battle.lifecycle,
            hasPendingReplacement = replacementRequests.isNotEmpty(),
            readyToResolve = choiceStatuses().isNotEmpty() && isTurnReady(),
        )

    /**
     * 确认当前会话仍处于初始化阶段。
     *
     * 初始化阶段只允许做 side/unit 注册和 battle 业务配置;
     * 一旦进入 `RUNNING`,这些结构性输入就不应再被修改。
     */
    internal fun ensureInitializing() {
        require(currentSnapshot.battle.lifecycle == BattleLifecycle.INITIALIZING) {
            "Battle session must be in lifecycle '${BattleLifecycle.INITIALIZING.name}' " +
                "but was '${currentSnapshot.battle.lifecycle.name}'."
        }
    }

    /**
     * 确认当前会话正处于运行阶段。
     *
     * 这里优先使用显式 lifecycle,而不是继续依赖 `started / ended` 的布尔组合,
     * 这样调用边界会更清晰,也便于后续继续收口状态机。
     */
    internal fun ensureRunning() {
        require(currentSnapshot.battle.lifecycle == BattleLifecycle.RUNNING) {
            "Battle session must be in lifecycle '${BattleLifecycle.RUNNING.name}' " +
                "but was '${currentSnapshot.battle.lifecycle.name}'."
        }
    }

    /**
     * 确认当前会话已经结束但尚未完成战后结算。
     *
     * 资源扣减、奖励发放、落库存等副作用都应该只在这个阶段发生。
     */
    internal fun ensureEndedUnsettled() {
        require(currentSnapshot.battle.lifecycle == BattleLifecycle.ENDED_UNSETTLED) {
            "Battle session must be in lifecycle '${BattleLifecycle.ENDED_UNSETTLED.name}' " +
                "but was '${currentSnapshot.battle.lifecycle.name}'."
        }
    }

    /**
     * 确认当前回合仍在等待行动输入。
     *
     * 一旦进入替补阶段或已经凑齐本回合输入,就不允许继续提交普通 choice,
     * 否则外部调用方很难判断“当前失败是规则限制还是时机不对”。
     */
    internal fun ensureAwaitingChoices() {
        val status = turnStatus()
        require(status == BattleSessionTurnStatus.AWAITING_CHOICES) {
            "Battle session must be in turn status '${BattleSessionTurnStatus.AWAITING_CHOICES.name}' " +
                "but was '${status?.name ?: "null"}'."
        }
    }

    /**
     * 确认当前回合正处于等待替补阶段。
     */
    internal fun ensureAwaitingReplacement() {
        val status = turnStatus()
        require(status == BattleSessionTurnStatus.AWAITING_REPLACEMENT) {
            "Battle session must be in turn status '${BattleSessionTurnStatus.AWAITING_REPLACEMENT.name}' " +
                "but was '${status?.name ?: "null"}'."
        }
    }

    /**
     * 确认当前回合已经可以进入正式结算。
     */
    internal fun ensureReadyToResolve() {
        val status = turnStatus()
        require(status == BattleSessionTurnStatus.READY_TO_RESOLVE) {
            "Battle session must be in turn status '${BattleSessionTurnStatus.READY_TO_RESOLVE.name}' " +
                "but was '${status?.name ?: "null"}'."
        }
    }

    internal fun ensureTurnReady() {
        ensureReadyToResolve()
        choiceValidator.ensureTurnReady()
    }

    internal fun ensureUnitCanSubmitChoice(unitId: String) = choiceValidator.ensureUnitCanSubmitChoice(unitId)

    internal fun ensureSideCanSubmitRunChoice(sideId: String) = choiceValidator.ensureSideCanSubmitRunChoice(sideId)

    internal fun ensureTargetIsLegalForAction(
        effectId: String,
        actorUnitId: String,
        targetUnitId: String,
    ) = choiceValidator.ensureTargetIsLegalForAction(effectId, actorUnitId, targetUnitId)

    internal fun ensureCaptureIsLegal(
        playerId: String,
        sourceUnitId: String,
        targetUnitId: String,
    ) = choiceValidator.ensureCaptureIsLegal(playerId, sourceUnitId, targetUnitId)

    internal fun ensureMoveChoiceIsLegal(
        moveId: String,
        actorUnitId: String,
    ) = choiceValidator.ensureMoveChoiceIsLegal(moveId, actorUnitId)

    internal fun ensureItemChoiceIsLegal(
        itemId: String,
        actorUnitId: String,
    ) = choiceValidator.ensureItemChoiceIsLegal(itemId, actorUnitId)

    internal fun executeResolvedEffect(
        effectId: String,
        actorUnitId: String,
        targetUnitId: String,
        accuracy: Int?,
        evasion: Int?,
        basePower: Int,
        damage: Int,
        attributes: Map<String, Any?>,
    ): MoveResolutionResult =
        actionExecutionSupport.executeResolvedEffect(
            session = this,
            effectId = effectId,
            actorUnitId = actorUnitId,
            targetUnitId = targetUnitId,
            accuracy = accuracy,
            evasion = evasion,
            basePower = basePower,
            damage = damage,
            attributes = attributes,
        )

    internal fun applyDirectDamage(
        sourceId: String,
        targetId: String,
        damage: Int,
    ): BattleRuntimeSnapshot = actionExecutionSupport.applyDirectDamage(this, sourceId, targetId, damage)

    internal fun applyMutations(
        snapshot: BattleRuntimeSnapshot,
        selfId: String?,
        targetId: String?,
        sourceId: String?,
        mutations: List<io.github.lishangbu.avalon.game.battle.engine.core.mutation.BattleMutation>,
    ): BattleRuntimeSnapshot = mutationProcessor.apply(snapshot, selfId, targetId, sourceId, mutations)

    internal fun resolveFaintAndReplacement(snapshot: BattleRuntimeSnapshot = currentSnapshot): BattleRuntimeSnapshot = replacementResolver.resolveFaintAndReplacement(snapshot)

    internal fun updateWinnerIfNeeded(snapshot: BattleRuntimeSnapshot): BattleRuntimeSnapshot = replacementResolver.updateWinnerIfNeeded(snapshot)

    internal fun processBeforeTurn(snapshot: BattleRuntimeSnapshot = currentSnapshot): BattleRuntimeSnapshot {
        currentSnapshot = hookLifecycleProcessor.processBeforeTurn(snapshot)
        return currentSnapshot
    }

    internal fun processSwitchOut(
        unitId: String,
        snapshot: BattleRuntimeSnapshot = currentSnapshot,
    ): BattleRuntimeSnapshot {
        currentSnapshot = hookLifecycleProcessor.processSwitchOut(unitId, snapshot)
        return currentSnapshot
    }

    internal fun processSwitchIn(
        unitId: String,
        snapshot: BattleRuntimeSnapshot = currentSnapshot,
    ): BattleRuntimeSnapshot {
        currentSnapshot = hookLifecycleProcessor.processSwitchIn(unitId, snapshot)
        return currentSnapshot
    }

    internal fun processFaintHooks(snapshot: BattleRuntimeSnapshot = currentSnapshot): BattleRuntimeSnapshot {
        currentSnapshot = hookLifecycleProcessor.processFaintHooks(snapshot)
        return currentSnapshot
    }

    internal fun clearForceSwitchRequests(
        snapshot: BattleRuntimeSnapshot = currentSnapshot,
        unitIds: Collection<String>,
    ): BattleRuntimeSnapshot {
        if (unitIds.isEmpty()) {
            return snapshot
        }
        val nextUnits = snapshot.units.toMutableMap()
        var changed = false
        unitIds.distinct().forEach { unitId ->
            val unit = nextUnits[unitId] ?: return@forEach
            if (!unit.forceSwitchRequested) {
                return@forEach
            }
            nextUnits[unitId] = unit.copy(forceSwitchRequested = false)
            changed = true
        }
        val nextSnapshot =
            if (changed) {
                snapshot.copy(units = nextUnits)
            } else {
                snapshot
            }
        currentSnapshot = nextSnapshot
        return nextSnapshot
    }

    internal fun recordMoveExecution(
        moveId: String,
        attackerId: String,
        targetId: String,
        result: MoveResolutionResult,
    ) = actionExecutionSupport.recordMoveExecution(this, moveId, attackerId, targetId, result)

    internal fun applySwitchAction(action: BattleSessionSwitchAction): BattleRuntimeSnapshot = actionExecutionSupport.applySwitchAction(this, action)

    internal fun applyRunAction(action: BattleSessionRunAction): BattleRuntimeSnapshot = actionExecutionSupport.applyRunAction(this, action)

    internal fun recordResourceUsage(usage: BattleSessionResourceUsage) {
        publicationRecorder.recordResourceUsage(usage)
    }

    internal fun recordLog(message: String) {
        publicationRecorder.recordLog(message)
    }

    internal fun recordEvent(payload: BattleSessionEventPayload) {
        publicationRecorder.recordEvent(payload)
    }

    /**
     * 发布一条 session 内部事件。
     *
     * @param publication 本次需要投影到各个读模型的事件载体。
     */
    internal fun publish(publication: BattleSessionPublication) {
        publicationRecorder.publish(publication)
    }

    /**
     * 供 projector 追加一条 battle log。
     *
     * @param message 已经完成格式化的 battle log 文本。
     */
    internal fun appendBattleLog(message: String) {
        publicationRecorder.appendBattleLog(message)
    }

    /**
     * 供 projector 追加一条结构化事件。
     *
     * @param event 已经完成组装的结构化事件对象。
     */
    internal fun appendEventLog(event: BattleSessionEvent) {
        publicationRecorder.appendEventLog(event)
    }

    /**
     * 供 projector 追加一条资源账本记录。
     *
     * @param usage 已经完成组装的资源账本条目。
     */
    internal fun appendResourceUsage(usage: BattleSessionResourceUsage) {
        publicationRecorder.appendResourceUsage(usage)
    }

    internal fun submittedSideId(action: BattleSessionAction): String? = choiceValidator.submittedSideId(action)

    internal fun sideIdOfUnit(unitId: String): String? = choiceValidator.sideIdOfUnit(unitId)

    internal fun submittedUnitIdsForSide(side: SideState): List<String> = choiceValidator.submittedUnitIdsForSide(side)
}