//
// Standard implementation of AIScoreCalculator
//

#include "StandardAIScoreCalculator.hpp"

#include <atomic>
#include <chrono>
#include <future>
#include <unordered_map>

#include "AIAttackGroups.hpp"
#include "AIAttackLocations.hpp"
#include "AIScoreCalculator.hpp"
#include "AIScoreUtilities.hpp"
#include "AIStrategy.hpp"
#include "AIUnitScoreCalculator.hpp"
#include "AIVictoryConditionScoreCalculator.hpp"
#include "AIWaterCrossingCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"

namespace shardok {

using net::eagle0::shardok::storage::fb::BattalionTypeId;

// Forward declare the implementation class
class StandardAIScoreCalculator;

// Anonymous namespace for helper functions that don't need access to scorer
namespace {

#define LOGGING_ 0
#define PERFORMANCE_LOGGING_ 0

// Performance logging for AttackerScoreForState
struct AttackerScorePerformanceLogger {
    static constexpr int LOG_INTERVAL = 100000;

    static std::atomic<int> callCount;
    static std::atomic<double> intervalTime;
    static std::atomic<double> totalTime;

    static void LogCall(double duration) {
        callCount.fetch_add(1);
        intervalTime.fetch_add(duration);
        totalTime.fetch_add(duration);

        if (callCount.load() % LOG_INTERVAL == 0) {
            double intervalAvg = intervalTime.load() / LOG_INTERVAL;
            double overallAvg = totalTime.load() / callCount.load();
            printf("AttackerScoreForState: %d calls, last %d avg: %.1f µs, overall avg: %.1f µs\n",
                   callCount.load(),
                   LOG_INTERVAL,
                   intervalAvg * 1000000.0,
                   overallAvg * 1000000.0);
            intervalTime.store(0.0);  // Reset for next interval
        }
    }
};

std::atomic<int> AttackerScorePerformanceLogger::callCount{0};
std::atomic<double> AttackerScorePerformanceLogger::intervalTime{0.0};
std::atomic<double> AttackerScorePerformanceLogger::totalTime{0.0};

// RAII timer for automatic performance logging
class AttackerScoreTimer {
private:
    std::chrono::high_resolution_clock::time_point startTime;

public:
    AttackerScoreTimer() : startTime(std::chrono::high_resolution_clock::now()) {}

    ~AttackerScoreTimer() {
        auto endTime = std::chrono::high_resolution_clock::now();
        auto duration =
                std::chrono::duration_cast<std::chrono::duration<double>>(endTime - startTime);
        AttackerScorePerformanceLogger::LogCall(duration.count());
    }
};

// Memoization cache for EffectiveDistance calls
struct EffectiveDistanceCache {
    struct CacheKey {
        UnitId unitId;
        Coords target;
        bool operator==(const CacheKey &other) const {
            return unitId == other.unitId && target == other.target;
        }
    };

    struct CacheKeyHash {
        size_t operator()(const CacheKey &key) const {
            return std::hash<UnitId>{}(key.unitId) ^ (std::hash<int>{}(key.target.row()) << 1) ^
                   (std::hash<int>{}(key.target.column()) << 2);
        }
    };

    mutable gtl::flat_hash_map<CacheKey, DIST_T, CacheKeyHash> cache;

    DIST_T GetOrCompute(
            const Unit *unit,
            const Coords &target,
            const ActionPointDistances *notBravingApd,
            const ActionPointDistances *bravingApd,
            const HexMap *hexMap) const {
        CacheKey key{unit->unit_id(), target};
        auto it = cache.find(key);
        if (it != cache.end()) { return it->second; }

        CoordsSet targetSet(hexMap);
        targetSet.Add(target);

        DIST_T result = EffectiveDistance(unit, notBravingApd, bravingApd, targetSet);
        cache[key] = result;
        return result;
    }
};

#define MULTITHREAD true

constexpr double UNITS_BASE_MULTIPLIER = 0.05;

constexpr double FLEE_UNIT_SCORE = -10000;
constexpr double FLEE_CONTROLLING_UNIT_SCORE = -10000;
constexpr double CAPTURED_UNIT_SCORE = -10000;
constexpr double CAPTURED_VIP_SCORE = -25000;

constexpr double kMaxProximityBuf = 1.5;
constexpr double kDistanceDebufRatio = 8.0;

using std::async;
using std::future;

using flatbuffers::FlatBufferBuilder;
using flatbuffers::Offset;

using net::eagle0::shardok::api::HeroView;
using net::eagle0::shardok::api::UnitView;
using GameState = fb::GameState;
using Unit = fb::Unit;

static auto RecursiveAttackerMultiplierForTargetDistance(
        const Unit *attackingUnit,
        vector<TargetAndAttackLocations>::const_iterator &priorityListNext,
        const vector<TargetAndAttackLocations>::const_iterator &priorityListEnd,
        const vector<const Unit *> &occupants,
        const HexMap *map,
        const BattalionTypeSPtr &battType,
        const ActionPointDistances *notBravingApd,
        const ActionPointDistances *bravingApd,
        bool isLateGame) -> double;

static auto RecursiveAttackerMultiplierForTargetDistance(
        const Unit *attackingUnit,
        vector<TargetAndAttackLocations>::const_iterator &priorityListNext,
        const vector<TargetAndAttackLocations>::const_iterator &priorityListEnd,
        const vector<const Unit *> &occupants,
        const HexMap *map,
        const BattalionTypeSPtr &battType,
        const ActionPointDistances *notBravingApd,
        const ActionPointDistances *bravingApd,
        const bool isLateGame) -> double {
    if (priorityListNext == priorityListEnd) return 1.0;

    const auto &[target, attackLocations] = *priorityListNext;
    const Coords &topPriorityTarget = target;

    // If the target is unoccupied or is occupied by this player, give the maximum multiplier, but
    // also add the bonus for the next up in the priority list
    if (const Unit *occupant = occupants
                [topPriorityTarget.row() * map->column_count() + topPriorityTarget.column()];
        !occupant || occupant->player_id() == attackingUnit->player_id()) {
        return kMaxProximityBuf + RecursiveAttackerMultiplierForTargetDistance(
                                          attackingUnit,
                                          ++priorityListNext,
                                          priorityListEnd,
                                          occupants,
                                          map,
                                          battType,
                                          notBravingApd,
                                          bravingApd,
                                          isLateGame);
    }

    // Use optimized EffectiveDistance with pre-computed ActionPointDistances
    // attackLocations is already the CoordsSet of attack locations for this target
    const DIST_T distance =
            EffectiveDistance(attackingUnit, notBravingApd, bravingApd, attackLocations);

    return kMaxProximityBuf / (1 + distance / kDistanceDebufRatio);
}

// Overload that accepts pre-computed ActionPointDistances
auto AttackerMultiplierForTargetDistance(
        const Unit *attackingUnit,
        const vector<TargetAndAttackLocations> &priorityList,
        const vector<const Unit *> &occupants,
        const HexMap *map,
        const BattalionTypeSPtr &battType,
        const ActionPointDistances *notBravingApd,
        const ActionPointDistances *bravingApd,
        const bool isLateGame) -> double {
    auto iter = begin(priorityList);
    return RecursiveAttackerMultiplierForTargetDistance(
            attackingUnit,
            iter,
            end(priorityList),
            occupants,
            map,
            battType,
            notBravingApd,
            bravingApd,
            isLateGame);
}

auto FleeStrategyScoreForState(const GameStateW &gameState, const PlayerId playerId) -> ScoreValue {
    ScoreValue scoreValue = 0.0;

    const auto *gameStatePtr = gameState.Get();
    const auto *units = gameStatePtr->units();

    for (const auto *unit : *units) {
        if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;

        if (unit->player_id() == playerId &&
            unit->battalion().type() != net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
            scoreValue += FLEE_UNIT_SCORE;

            if (unit->has_attached_hero() &&
                unit->attached_hero().control_info().controlled_unit_id() != -1) {
                scoreValue += FLEE_CONTROLLING_UNIT_SCORE;
            }
        }
    }

    return scoreValue;
}

}  // anonymous namespace

/// Standard implementation of AIScoreCalculator that uses the default scoring algorithm.
/// Stores specific setting values needed for scoring rather than the entire SettingsGetter.
class StandardAIScoreCalculator : public AIScoreCalculator {
public:
    StandardAIScoreCalculator(
            int maxRounds,
            ActionPoints braveWaterCost,
            int meteorRange,
            double meteorCastVigorCost,
            int minimumFleeOddsThreshold,
            int desperateFleeThreshold,
            std::unordered_map<BattalionTypeId, BattalionTypeSPtr> battalionTypes,
            const APDCache &apdCache,
            const ALCache &alCache)
        : maxRounds_(maxRounds),
          braveWaterCost_(braveWaterCost),
          meteorRange_(meteorRange),
          meteorCastVigorCost_(meteorCastVigorCost),
          minimumFleeOddsThreshold_(minimumFleeOddsThreshold),
          desperateFleeThreshold_(desperateFleeThreshold),
          battalionTypes_(std::move(battalionTypes)),
          apdCache_(apdCache),
          alCache_(alCache) {}

    [[nodiscard]] auto GuessedStateScore(
            bool isDefender,
            const GameStateW &state,
            const AIStrategy &aiStrategy,
            const CoordsSet &allCastleCoords) const -> ScoreValue override;

private:
    // Internal accessor methods
    [[nodiscard]] auto GetBattalionType(BattalionTypeId typeId) const -> BattalionTypeSPtr {
        auto it = battalionTypes_.find(typeId);
        if (it == battalionTypes_.end()) {
            throw ShardokInternalErrorException("Unknown battalion type ID");
        }
        return it->second;
    }
    [[nodiscard]] auto GetApdCache() const -> const APDCache & { return apdCache_; }
    [[nodiscard]] auto GetAlCache() const -> const ALCache & { return alCache_; }
    [[nodiscard]] auto GetBraveWaterCost() const -> ActionPoints { return braveWaterCost_; }
    [[nodiscard]] auto GetMaxRounds() const -> int { return maxRounds_; }
    [[nodiscard]] auto GetMeteorRange() const -> int { return meteorRange_; }
    [[nodiscard]] auto GetMeteorCastVigorCost() const -> double { return meteorCastVigorCost_; }
    [[nodiscard]] auto GetMinimumFleeOddsThreshold() const -> int {
        return minimumFleeOddsThreshold_;
    }
    [[nodiscard]] auto GetDesperateFleeThreshold() const -> int { return desperateFleeThreshold_; }

    // Implementation methods (converted from internal namespace functions)
    [[nodiscard]] auto AttackerUnitsScore(
            const GameStateW &gameState,
            int roundsRemaining,
            bool attackerWantsCastles,
            bool defenderShouldScatter,
            const vector<TargetPriorityList> &attackerTargetPriorities,
            const MapId &mapId) const -> ScoreValue;

    [[nodiscard]] auto DefenderScatterStrategyScoreForState(
            const GameStateW &gameState,
            int roundsRemaining) const -> ScoreValue;

    [[nodiscard]] auto DefenderHoldCastlesStrategyScoreForState(
            const GameStateW &gameState,
            const CoordsSet &castleCoords,
            int roundsRemaining) const -> ScoreValue;

    [[nodiscard]] auto DefenderScoreForState(
            const GameStateW &gameState,
            const AIStrategy &defenderStrategy,
            const CoordsSet &castleCoords,
            int roundsRemaining) const -> ScoreValue;

    [[nodiscard]] auto AttackerScoreForState(
            const GameStateW &gameState,
            const AIStrategy &attackerStrategy,
            const CoordsSet &castleCoords,
            int roundsRemaining) const -> ScoreValue;

    // Scalar settings extracted from SettingsGetter
    int maxRounds_;
    ActionPoints braveWaterCost_;
    int meteorRange_;
    double meteorCastVigorCost_;
    int minimumFleeOddsThreshold_;
    int desperateFleeThreshold_;

    // Battalion type lookup map
    std::unordered_map<BattalionTypeId, BattalionTypeSPtr> battalionTypes_;

    // Caches (stored as references)
    const APDCache &apdCache_;
    const ALCache &alCache_;
};

// Implementation of StandardAIScoreCalculator methods

auto StandardAIScoreCalculator::AttackerUnitsScore(
        const GameStateW &gameState,
        int roundsRemaining,
        bool attackerWantsCastles,
        bool defenderShouldScatter,
        const vector<TargetPriorityList> &attackerTargetPriorities,
        const MapId &mapId) const -> ScoreValue {
    // Cache frequently accessed FlatBuffer fields to avoid repeated offset calculations
    const auto *gameStateRawPtr = gameState.Get();
    const auto *cachedUnits = gameStateRawPtr->units();
    const auto *cachedHexMap = gameStateRawPtr->hex_map();

    const int16_t cachedRowCount = cachedHexMap->row_count();
    const int16_t cachedColumnCount = cachedHexMap->column_count();
    const int cachedCurrentRound = gameStateRawPtr->current_round();

    bool isLateGame = cachedCurrentRound > 18;  // Inline IsLateGame for efficiency

    // APDCache now has built-in thread-local caching - no need for PreCachedAPDs
    ActionPoints braveWaterCost = GetBraveWaterCost();

    // Memoization cache for EffectiveDistance calls
    EffectiveDistanceCache distanceCache;

    std::vector<const Unit *> attackerUnits{};
    std::vector<const Unit *> defenderUnits{};
    // Pre-allocate vectors based on estimated unit ratios to avoid reallocations
    const size_t estimatedUnitCount = cachedUnits->size();
    attackerUnits.reserve(estimatedUnitCount - 1);
    defenderUnits.reserve(estimatedUnitCount - 1);

    double attackerUnitsValue = 0;
    double defenderUnitsValue = 0;

    auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);

    for (const Unit *unit : *cachedUnits) {
        const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
        if (pi == nullptr) { continue; }

        switch (unit->status()) {
            case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT: {
                if (pi->is_defender()) {
                    defenderUnits.push_back(unit);
                } else {
                    attackerUnits.push_back(unit);
                }
                break;
            }
            case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT: {
                double thisScore = unit->has_attached_hero() && unit->attached_hero().is_vip()
                                           ? CAPTURED_VIP_SCORE
                                           : CAPTURED_UNIT_SCORE;
                if (pi->is_defender()) {
                    defenderUnitsValue += thisScore;
                } else {
                    attackerUnitsValue += thisScore;
                }
                break;
            }

            case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
            case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
            case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
            case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
            case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
            case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
            case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT: break;

            case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
                throw ShardokInternalErrorException("Unknown unit status");
        }
    }

    double defenderAdvantage = 1.0 + static_cast<double>(cachedCurrentRound) / 31.0;

    // Can we cache this somehow, it won't usually change within your turn
    auto attackLocationsForAttacker = GetAlCache()->CachedLocations(defenderUnits, isLateGame);
    const auto &locationsCausingDanger = attackLocationsForAttacker.AllLocations();

    // Process attacker units using cached ActionPointDistances
    for (const Unit *unit : attackerUnits) {
        const int battTypeId = unit->battalion().type();

        const auto &priorityList = std::ranges::find_if(
                attackerTargetPriorities,
                [&unit](const TargetPriorityList &tpl) {
                    return tpl.attackingUnitId == unit->unit_id();
                });

        // If there are any tiles being targeted, give this unit a multiplier based on how close
        // they are to being able to attack it
        double distanceMultiplier =
                priorityList == end(attackerTargetPriorities)
                        ? 1.0
                        : AttackerMultiplierForTargetDistance(
                                  unit,
                                  priorityList->priorityOrder,
                                  occupants,
                                  cachedHexMap,
                                  GetBattalionType(static_cast<BattalionTypeId>(battTypeId)),
                                  GetApdCache()->GetRaw(
                                          cachedHexMap,
                                          mapId,
                                          GetBattalionType(
                                                  static_cast<BattalionTypeId>(battTypeId)),
                                          false),
                                  GetBattalionType(static_cast<BattalionTypeId>(battTypeId))
                                                  ->allowsBraveWater
                                          ? GetApdCache()->GetRaw(
                                                    cachedHexMap,
                                                    mapId,
                                                    GetBattalionType(static_cast<BattalionTypeId>(
                                                            battTypeId)),
                                                    true,
                                                    braveWaterCost)
                                          : nullptr,
                                  isLateGame);

        auto uv = UnitValue(
                unit,
                true,
                attackerUnits,
                attackerWantsCastles,
                /* includeCastleBonus=*/true,
                defenderUnits,
                cachedHexMap,
                roundsRemaining,
                attackLocationsForAttacker,
                locationsCausingDanger,
                GetApdCache()->GetRaw(
                        cachedHexMap,
                        mapId,
                        GetBattalionType(static_cast<BattalionTypeId>(battTypeId)),
                        false),
                GetMeteorRange(),
                GetMeteorCastVigorCost());

        attackerUnitsValue += distanceMultiplier * uv;
    }

    auto attackLocationsForDefender = GetAlCache()->CachedLocations(attackerUnits, isLateGame);
    const auto &locationsCausingDangerForAttacker = attackLocationsForDefender.AllLocations();

    for (const Unit *unit : defenderUnits) {
        auto defenderUnitId = unit->unit_id();
        const int battTypeId = unit->battalion().type();

        auto dv = UnitValue(
                unit,
                false,
                attackerUnits,
                attackerWantsCastles,
                /* includeCastleBonus=*/!defenderShouldScatter,
                defenderUnits,
                cachedHexMap,
                roundsRemaining,
                attackLocationsForDefender,
                locationsCausingDangerForAttacker,
                GetApdCache()->GetRaw(
                        cachedHexMap,
                        mapId,
                        GetBattalionType(static_cast<BattalionTypeId>(battTypeId)),
                        false),
                GetMeteorRange(),
                GetMeteorCastVigorCost());

        double distanceMultiplier = 1.0;

        // If the defender is trying to scatter, than we want to be as far away from the nearest
        // attacker as possible, AND as far away from the nearest friendly as possible
        if (unit->location().row() > -1 && defenderShouldScatter) {
            CoordsSet myLocationSet(cachedHexMap);
            myLocationSet.Add(unit->location());

            DIST_T closestDistanceToEnemy = 999;
            for (const auto &attackerUnit : attackerUnits) {
                const int attackerBattTypeId = attackerUnit->battalion().type();
                const DIST_T thisDistance = distanceCache.GetOrCompute(
                        attackerUnit,
                        unit->location(),
                        GetApdCache()->GetRaw(
                                cachedHexMap,
                                mapId,
                                GetBattalionType(static_cast<BattalionTypeId>(attackerBattTypeId)),
                                false),
                        GetBattalionType(static_cast<BattalionTypeId>(attackerBattTypeId))
                                        ->allowsBraveWater
                                ? GetApdCache()->GetRaw(
                                          cachedHexMap,
                                          mapId,
                                          GetBattalionType(
                                                  static_cast<BattalionTypeId>(attackerBattTypeId)),
                                          true,
                                          braveWaterCost)
                                : nullptr,
                        cachedHexMap);
                if (thisDistance < closestDistanceToEnemy) {
                    closestDistanceToEnemy = thisDistance;
                }
            }

            // If the best we can do puts us very close to the enemy, and the unit is almost
            // destroyed, return a negative value; better to flee
            if (unit->can_flee() && closestDistanceToEnemy < 5 && unit->battalion().size() < 10) {
                distanceMultiplier = -1;
            } else {
                DIST_T closestDistanceToFriendly = 1;
                if (defenderUnits.size() > 1) {
                    for (const auto &defenderUnit : defenderUnits) {
                        if (defenderUnit->unit_id() != defenderUnitId) {
                            const int defenderBattTypeId = defenderUnit->battalion().type();
                            const DIST_T thisDistance = distanceCache.GetOrCompute(
                                    defenderUnit,
                                    unit->location(),
                                    GetApdCache()->GetRaw(
                                            cachedHexMap,
                                            mapId,
                                            GetBattalionType(static_cast<BattalionTypeId>(
                                                    defenderBattTypeId)),
                                            false),
                                    GetBattalionType(
                                            static_cast<BattalionTypeId>(defenderBattTypeId))
                                                    ->allowsBraveWater
                                            ? GetApdCache()->GetRaw(
                                                      cachedHexMap,
                                                      mapId,
                                                      GetBattalionType(static_cast<BattalionTypeId>(
                                                              defenderBattTypeId)),
                                                      true,
                                                      braveWaterCost)
                                            : nullptr,
                                    cachedHexMap);
                            if (thisDistance < closestDistanceToEnemy) {
                                closestDistanceToFriendly = thisDistance;
                            }
                        }
                    }
                }

                distanceMultiplier =
                        (closestDistanceToEnemy + closestDistanceToFriendly / 5.0) / 5.0;
            }
        }

        defenderUnitsValue += distanceMultiplier * dv;
    }

    defenderUnitsValue *= defenderAdvantage;

    return attackerUnitsValue - defenderUnitsValue;
}

auto StandardAIScoreCalculator::DefenderScatterStrategyScoreForState(
        const GameStateW &gameState,
        const int roundsRemaining) const -> ScoreValue {
    const auto *gameStatePtr = gameState.Get();
    const auto *status = gameStatePtr->status();

    if (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
        const auto *winningIds = status->winning_shardok_ids();
        const auto *playerInfos = gameStatePtr->player_infos();

        for (const PlayerId winningPid : *winningIds) {
            if (winningPid < 0) continue;
            if (playerInfos->Get(winningPid)->is_defender()) return INT_MAX;
            return INT_MIN;
        }
        return INT_MAX;
    }
    if (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) { return 0; }

    const auto *hexMap = gameStatePtr->hex_map();

    const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);

    const auto unitsTotal = -AttackerUnitsScore(
            gameState,
            roundsRemaining,
            /* attackerWantsCastles=*/false,
            /* defenderShouldScatter=*/true,
            {},
            mapId);

    return unitsTotal;
}

auto StandardAIScoreCalculator::DefenderHoldCastlesStrategyScoreForState(
        const GameStateW &gameState,
        const CoordsSet &castleCoords,
        const int roundsRemaining) const -> ScoreValue {
    const auto unitsTotal = -AttackerUnitsScore(
            gameState,
            roundsRemaining,
            /* attackerWantsCastles=*/true,
            /*defenderShouldScatter=*/false,
            {},
            ActionPointDistancesCache::GetMapId(gameState->hex_map()));

    // Check the victory condition types
    ScoreValue victoryConditionTotal = 0.0;

    const PlayerInfo *defenderPi = nullptr;
    for (const PlayerInfo *pi : *gameState->player_infos()) {
        if (pi->is_defender()) defenderPi = pi;
    }

    victoryConditionTotal +=
            DefenderHoldsCriticalTilesVictoryScore(gameState, castleCoords, defenderPi);

    const double unitsMultiplier =
            static_cast<double>(roundsRemaining) / static_cast<double>(GetMaxRounds());

    return UNITS_BASE_MULTIPLIER * unitsMultiplier * unitsTotal + victoryConditionTotal;
}

auto StandardAIScoreCalculator::DefenderScoreForState(
        const GameStateW &gameState,
        const AIStrategy &defenderStrategy,
        const CoordsSet &castleCoords,
        const int roundsRemaining) const -> ScoreValue {
    if (gameState->status()->state() ==
        net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
        for (const PlayerId winningPid : *gameState->status()->winning_shardok_ids()) {
            if (winningPid < 0) continue;
            if (defenderStrategy.strategyType == AIStrategy::STRATEGY_FLEE) return 0;
            if (gameState->player_infos()->Get(winningPid)->is_defender()) return INT_MAX;
            return INT_MIN;
        }
        return INT_MIN;
    }
    if (gameState->status()->state() ==
        net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
        return 0;
    }

    switch (defenderStrategy.strategyType) {
        case AIStrategy::STRATEGY_ATTACK_CASTLES:
            throw ShardokInternalErrorException("Defender cannot use AttackCastlesStrategy");
        case AIStrategy::STRATEGY_ATTACK_UNITS:
            throw ShardokInternalErrorException("Defender cannot use AttackUnitsStrategy");
        case AIStrategy::STRATEGY_CROSS_RIVERS:
            throw ShardokInternalErrorException("Defender cannot use CrossRiversStrategy");
        case AIStrategy::STRATEGY_HOLD_CASTLES:
            return DefenderHoldCastlesStrategyScoreForState(
                    gameState,
                    castleCoords,
                    roundsRemaining);
        case AIStrategy::STRATEGY_SCATTER:
            return DefenderScatterStrategyScoreForState(gameState, roundsRemaining);
        case AIStrategy::STRATEGY_FLEE:
            for (const auto *pi : *gameState->player_infos()) {
                if (pi->is_defender()) {
                    return FleeStrategyScoreForState(gameState, pi->player_id());
                }
            }
            throw ShardokInternalErrorException("Unable to find defender for FleeStrategy");
    }
    throw ShardokInternalErrorException("Escaped AIStrategy switch");
}

auto StandardAIScoreCalculator::AttackerScoreForState(
        const GameStateW &gameState,
        const AIStrategy &attackerStrategy,
        const CoordsSet &castleCoords,
        const int roundsRemaining) const -> ScoreValue {
#if PERFORMANCE_LOGGING_
    AttackerScoreTimer timer;
#endif  // # PERFORMANCE_LOGGING_
    if (gameState->status()->state() ==
        net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
        for (const PlayerId winningPid : *gameState->status()->winning_shardok_ids()) {
            if (winningPid < 0) continue;
            if (attackerStrategy.strategyType == AIStrategy::STRATEGY_FLEE) return 0;
            if (gameState->player_infos()->Get(winningPid)->is_defender()) return INT_MIN;
            return INT_MAX;
        }
        return INT_MAX;
    }
    if (gameState->status()->state() ==
        net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
        return 0;
    }

    const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());

    const auto unitsTotal = AttackerUnitsScore(
            gameState,
            roundsRemaining,
            attackerStrategy.strategyType == AIStrategy::STRATEGY_HOLD_CASTLES,
            /* defenderShouldScatter=*/false,
            attackerStrategy.targetPriorities,
            mapId);

    // Check the victory condition types
    ScoreValue victoryConditionTotal = 0.0;
    for (const PlayerInfo *pi : *gameState->player_infos()) {
        if (pi->is_defender()) continue;

        switch (attackerStrategy.strategyType) {
            case AIStrategy::STRATEGY_CROSS_RIVERS:
                victoryConditionTotal += WaterCrossingScore(
                        pi->player_id(),
                        [this](BattalionTypeId typeId) { return GetBattalionType(typeId); },
                        gameState,
                        castleCoords,
                        attackerStrategy.targetLocations,
                        GetApdCache());
                break;

            case AIStrategy::STRATEGY_ATTACK_CASTLES:
            case AIStrategy::STRATEGY_ATTACK_UNITS:
                // already factored into AttackerUnitsScore
                break;

            case AIStrategy::STRATEGY_HOLD_CASTLES:
                victoryConditionTotal += AttackerHoldsCriticalTilesVictoryScore(
                        gameState,
                        castleCoords,
                        pi,
                        GetApdCache(),
                        GetAlCache(),
                        [this](BattalionTypeId typeId) { return GetBattalionType(typeId); },
                        GetBraveWaterCost());
                break;

            case AIStrategy::STRATEGY_SCATTER:
                throw ShardokInternalErrorException("Attacker cannot use ScatterStrategy");

            case AIStrategy::STRATEGY_FLEE:
                return FleeStrategyScoreForState(gameState, pi->player_id());
        }
    }

    const double unitsMultiplier =
            static_cast<double>(roundsRemaining) / static_cast<double>(GetMaxRounds());

    const double finalScore =
            UNITS_BASE_MULTIPLIER * unitsMultiplier * unitsTotal + victoryConditionTotal;

    return finalScore;
}

auto StandardAIScoreCalculator::GuessedStateScore(
        const bool isDefender,
        const GameStateW &state,
        const AIStrategy &aiStrategy,
        const CoordsSet &allCastleCoords) const -> ScoreValue {
    const int roundsRemaining = GetMaxRounds() - state->current_round();

    if (isDefender) {
        return DefenderScoreForState(state, aiStrategy, allCastleCoords, roundsRemaining);
    }
    return AttackerScoreForState(state, aiStrategy, allCastleCoords, roundsRemaining);
}

// Factory function implementation
auto MakeStandardAIScoreCalculator(
        const SettingsGetter &settingsGetter,
        const APDCache &apdCache,
        const ALCache &alCache) -> std::unique_ptr<AIScoreCalculator> {
    // Extract all battalion types
    std::unordered_map<BattalionTypeId, BattalionTypeSPtr> battalionTypes;
    for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
         typeId <= BattalionTypeId::BattalionTypeId_MAX;
         typeId++) {
        auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
        battalionTypes[battalionTypeId] = settingsGetter.GetBattalionType(battalionTypeId);
    }

    return std::make_unique<StandardAIScoreCalculator>(
            settingsGetter.Backing().max_rounds(),
            settingsGetter.Backing().brave_water_action_point_cost(),
            settingsGetter.Backing().meteor_range(),
            settingsGetter.Backing().meteor_cast_vigor_cost(),
            settingsGetter.Backing().ai_minimum_flee_odds_threshold(),
            settingsGetter.Backing().ai_desperate_flee_threshold(),
            std::move(battalionTypes),
            apdCache,
            alCache);
}

}  // namespace shardok
