//
// Created by dancrosby on 3/4/20.
//

#include "AIScoreCalculator.hpp"

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

#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIVictoryConditionScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCommandChooser.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/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 {

#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 const std::vector _averageSequence = {0.5};
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);

static auto CommandSorter(
        const AIScoreCalculator::IndexAndScore &l,
        const AIScoreCalculator::IndexAndScore &r) -> bool {
    if (l.lookaheadScore < r.lookaheadScore) return true;
    if (l.lookaheadScore > r.lookaheadScore) return false;

    // At this point the scores are tied
    if (l.immediateScore < r.immediateScore) return true;
    if (l.immediateScore > r.immediateScore) return false;

    return false;
}

auto IsDeterministic(const CommandType type) -> bool {
    switch (type) {
        case net::eagle0::shardok::common::MOVE_COMMAND:
        case net::eagle0::shardok::common::CONTROL_COMMAND:
        case net::eagle0::shardok::common::METEOR_START_COMMAND:
        case net::eagle0::shardok::common::METEOR_TARGET_COMMAND:
        case net::eagle0::shardok::common::METEOR_CANCEL_COMMAND:
        case net::eagle0::shardok::common::END_TURN_COMMAND:
        case net::eagle0::shardok::common::PLACE_UNIT_COMMAND:
        case net::eagle0::shardok::common::PLACE_HIDDEN_UNIT_COMMAND:
        case net::eagle0::shardok::common::UNIT_STOP_COMMAND:
        case net::eagle0::shardok::common::UNIT_REST_COMMAND:
        case net::eagle0::shardok::common::FLEE_COMMAND:
        case net::eagle0::shardok::common::REINFORCE_COMMAND:
        case net::eagle0::shardok::common::RETREAT_COMMAND:
        case net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND:
        case net::eagle0::shardok::common::HIDE_COMMAND:
        case net::eagle0::shardok::common::FORTIFY_COMMAND:
        case net::eagle0::shardok::common::BECOME_OUTLAW_COMMAND:
        case net::eagle0::shardok::common::HOLY_WAVE_COMMAND:
        case net::eagle0::shardok::common::REPAIR_COMMAND: return true;

        default: return false;
    }
}

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 AttackerUnitsScore(
        const GameStateW &gameState,
        int roundsRemaining,
        const SettingsGetter &settings,
        bool attackerWantsCastles,
        bool defenderShouldScatter,
        const vector<TargetPriorityList> &attackerTargetPriorities,
        const ALCache &alCache,
        const APDCache &apdCache,
        const MapId &mapId) -> ScoreValue {
    // Cache frequently accessed FlatBuffer fields to avoid repeated offset calculations
    const auto *cachedGameState = gameState.Get();
    const auto *cachedUnits = cachedGameState->units();
    const auto *cachedHexMap = cachedGameState->hex_map();
    const int16_t cachedRowCount = cachedHexMap->row_count();
    const int16_t cachedColumnCount = cachedHexMap->column_count();
    const int cachedCurrentRound = cachedGameState->current_round();

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

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

    // 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(cachedGameState, 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 = alCache->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,
                                  settings.GetBattalionType(
                                          static_cast<BattalionTypeId>(battTypeId)),
                                  apdCache->GetRaw(
                                          cachedHexMap,
                                          mapId,
                                          settings.GetBattalionType(
                                                  static_cast<BattalionTypeId>(battTypeId)),
                                          false),
                                  settings.GetBattalionType(
                                                  static_cast<BattalionTypeId>(battTypeId))
                                                  ->allowsBraveWater
                                          ? apdCache->GetRaw(
                                                    cachedHexMap,
                                                    mapId,
                                                    settings.GetBattalionType(
                                                            static_cast<BattalionTypeId>(
                                                                    battTypeId)),
                                                    true,
                                                    braveWaterCost)
                                          : nullptr,
                                  isLateGame);

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

        attackerUnitsValue += distanceMultiplier * uv;
    }

    auto attackLocationsForDefender = alCache->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,
                apdCache->GetRaw(
                        cachedHexMap,
                        mapId,
                        settings.GetBattalionType(static_cast<BattalionTypeId>(battTypeId)),
                        false),
                settings);

        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(),
                        apdCache->GetRaw(
                                cachedHexMap,
                                mapId,
                                settings.GetBattalionType(
                                        static_cast<BattalionTypeId>(attackerBattTypeId)),
                                false),
                        settings.GetBattalionType(static_cast<BattalionTypeId>(attackerBattTypeId))
                                        ->allowsBraveWater
                                ? apdCache->GetRaw(
                                          cachedHexMap,
                                          mapId,
                                          settings.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 &&
                closestDistanceToEnemy != ActionPointDistances::IMPOSSIBLE &&
                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(),
                                    apdCache->GetRaw(
                                            cachedHexMap,
                                            mapId,
                                            settings.GetBattalionType(static_cast<BattalionTypeId>(
                                                    defenderBattTypeId)),
                                            false),
                                    settings.GetBattalionType(static_cast<BattalionTypeId>(
                                                                      defenderBattTypeId))
                                                    ->allowsBraveWater
                                            ? apdCache->GetRaw(
                                                      cachedHexMap,
                                                      mapId,
                                                      settings.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 AIScoreCalculator::FleeStrategyScoreForState(
        const GameStateW &gameState,
        const PlayerId playerId) -> ScoreValue {
    ScoreValue scoreValue = 0.0;

    for (const auto *unit : *gameState->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;
}

auto AIScoreCalculator::DefenderScatterStrategyScoreForState(
        const GameStateW &gameState,
        const int roundsRemaining,
        const SettingsGetter &settings,
        const ALCache &alCache,
        const APDCache &apdCache) -> 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 (gameState->player_infos()->Get(winningPid)->is_defender()) return INT_MAX;
            return INT_MIN;
        }
        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,
            settings,
            /* attackerWantsCastles=*/false,
            /* defenderShouldScatter=*/true,
            {},
            alCache,
            apdCache,
            mapId);

    return unitsTotal;
}

auto AIScoreCalculator::DefenderHoldCastlesStrategyScoreForState(
        const GameStateW &gameState,
        const CoordsSet &castleCoords,
        const int roundsRemaining,
        const SettingsGetter &settings,
        const ALCache &alCache,
        const APDCache &apdCache) -> ScoreValue {
    const auto unitsTotal = -AttackerUnitsScore(
            gameState,
            roundsRemaining,
            settings,
            /* attackerWantsCastles=*/true,
            /*defenderShouldScatter=*/false,
            {},
            alCache,
            apdCache,
            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,
            apdCache,
            alCache,
            settings);

    const double unitsMultiplier = static_cast<double>(roundsRemaining) /
                                   static_cast<double>(settings.Backing().max_rounds());

    return UNITS_BASE_MULTIPLIER * unitsMultiplier * unitsTotal + victoryConditionTotal;
}

auto AIScoreCalculator::DefenderScoreForState(
        const GameStateW &gameState,
        const AIStrategy &defenderStrategy,
        const CoordsSet &castleCoords,
        const int roundsRemaining,
        const SettingsGetter &settings,
        const ALCache &alCache,
        const APDCache &apdCache) -> 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,
                    settings,
                    alCache,
                    apdCache);
        case AIStrategy::STRATEGY_SCATTER:
            return DefenderScatterStrategyScoreForState(
                    gameState,
                    roundsRemaining,
                    settings,
                    alCache,
                    apdCache);
        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 AIScoreCalculator::AttackerScoreForState(
        const GameStateW &gameState,
        const AIStrategy &attackerStrategy,
        const CoordsSet &castleCoords,
        const int roundsRemaining,
        const SettingsGetter &settings,
        const ALCache &alCache,
        const APDCache &apdCache) -> 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,
            settings,
            attackerStrategy.strategyType == AIStrategy::STRATEGY_HOLD_CASTLES,
            /* defenderShouldScatter=*/false,
            attackerStrategy.targetPriorities,
            alCache,
            apdCache,
            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 += AIWaterCrossingCommandChooser(pi->player_id(), apdCache)
                                                 .WaterCrossingScore(
                                                         settings,
                                                         gameState,
                                                         castleCoords,
                                                         attackerStrategy.targetLocations);

            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,
                        apdCache,
                        alCache,
                        settings);
                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>(settings.Backing().max_rounds());

    return UNITS_BASE_MULTIPLIER * unitsMultiplier * unitsTotal + victoryConditionTotal;
}

[[nodiscard]] auto AIScoreCalculator::GuessedStateScore(
        const bool isDefender,
        const GameStateW &state,
        const AIStrategy &aiStrategy,
        const CoordsSet &allCastleCoords,
        const SettingsGetter &settingsGetter,
        const APDCache &apdCache,
        const ALCache &alCache) -> ScoreValue {
    const int roundsRemaining = settingsGetter.Backing().max_rounds() - state->current_round();

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

void PrintCommand(
        const uint32_t index,
        const CommandProto &cmd,
        const GameStateW &gs,
        const ScoreValue utility) {
    printf("i%d %s\n  ", index, net::eagle0::shardok::common::CommandType_Name(cmd.type()).c_str());

    if (cmd.has_actor()) {
        const auto actor = cmd.actor().value();
        auto &loc = gs->units()->Get(actor)->location();
        printf("a%d(%d, %d) ", cmd.actor().value(), loc.row(), loc.column());
    }
    if (cmd.has_target()) { printf("t(%d, %d) ", cmd.target().row(), cmd.target().column()); }
    printf("u%f\n", utility);
}

auto AIScoreCalculator::BasicLookaheadCalculator(
        const PlayerId pid,
        const bool isDefender,
        const int remainingLookahead,
        const int maxRepeatCount,
        const shared_ptr<ShardokEngine> &innerEngine,
        const ScoreValue currentUtility,
        const AIStrategy &attackerStrategy,
        const SettingsGetter &settingsGetter,
        const CoordsSet &allCastleCoords,
        const APDCache &apdCache,
        const ALCache &alCache) -> ScoreValue {
    const auto nextUtility = currentUtility;

    if (const CommandListSPtr nextCommands = innerEngine->GetAvailableCommandsForAIPlayer(pid);
        nextCommands && !nextCommands->empty()) {
        const auto [index, type, lookaheadScore, immediateScore] = BestCommandIndex(
                pid,
                isDefender,
                remainingLookahead - 1,
                maxRepeatCount,
                *innerEngine,
                attackerStrategy,
                nextUtility,
                settingsGetter,
                allCastleCoords,
                apdCache,
                alCache);

        if (auto &nextCommand = innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(index);
            nextCommand->GetCommandType() != net::eagle0::shardok::common::END_TURN_COMMAND) {
            return immediateScore;
        }
    }

    return nextUtility;
}

auto AIScoreCalculator::CalcOne(
        PlayerId pid,
        bool isDefender,
        uint32_t commandIndex,
        int remainingLookahead,
        int maxRepeatCount,
        const std::shared_ptr<RandomGenerator> &randomGenerator,
        const ShardokEngine &guessedEngine,
        const AIStrategy &attackerStrategy,
        const SettingsGetter &settingsGetter,
        const CoordsSet &allCastleCoords,
        const APDCache &apdCache,
        const ALCache &alCache) -> ImmediateAndLookaheadScore {
    ImmediateAndLookaheadScore returnValue{};

    auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
    innerEngine->PostCommand(pid, commandIndex, randomGenerator);

    auto innerUtility = GuessedStateScore(
            isDefender,
            innerEngine->GetCurrentGameState(),
            attackerStrategy,
            allCastleCoords,
            settingsGetter,
            apdCache,
            alCache);

    returnValue.immediateScore = innerUtility;

    if (remainingLookahead <= 0) {
        std::promise<ScoreValue> p;
        returnValue.lookaheadScore = p.get_future();
        p.set_value(innerUtility);
    } else {
        auto lookaheadLambda = [pid,
                                isDefender,
                                remainingLookahead,
                                maxRepeatCount,
                                innerEngine,
                                attackerStrategy,
                                innerUtility,
                                &settingsGetter,
                                &allCastleCoords,
                                &apdCache,
                                &alCache]() -> ScoreValue {
            return BasicLookaheadCalculator(
                    pid,
                    isDefender,
                    remainingLookahead,
                    maxRepeatCount,
                    innerEngine,
                    innerUtility,
                    attackerStrategy,
                    settingsGetter,
                    allCastleCoords,
                    apdCache,
                    alCache);
        };

#if MULTITHREAD
        returnValue.lookaheadScore = std::async(std::launch::async, lookaheadLambda);
#else
        std::promise<ScoreValue> p;
        returnValue.lookaheadScore = p.get_future();
        auto lambdaResult = lookaheadLambda();
        p.set_value(lambdaResult);
#endif
    }

    return returnValue;
}

[[nodiscard]] auto AIScoreCalculator::BestCommandIndex(
        const PlayerId pid,
        const bool isDefender,
        const int remainingLookahead,
        const int maxRepeatCount,
        const ShardokEngine &guessedEngine,
        const AIStrategy &attackerStrategy,
        const ScoreValue currentUtility,
        const SettingsGetter &settingsGetter,
        const CoordsSet &allCastleCoords,
        const APDCache &apdCache,
        const ALCache &alCache) -> IndexAndScore {
    const CommandListSPtr guessedDescriptors = guessedEngine.GetAvailableCommandsForAIPlayer(pid);

    // Filter out obviously bad commands to reduce search space
    const std::vector<size_t> filteredIndices = AICommandFilter::FilterCommands(
            guessedDescriptors,
            pid,
            isDefender,
            guessedEngine.GetCurrentGameState(),
            settingsGetter,
            apdCache);

    const auto &gameState = guessedEngine.GetCurrentGameState();
    // Calculate minimum hex distance to enemies for this player
    double minDistToEnemies = std::numeric_limits<double>::max();
    const auto *units = gameState->units();

    for (int i = 0; i < units->size(); ++i) {
        if (const auto *playerUnit = units->Get(i); playerUnit->player_id() == pid) {
            const auto &playerCoords = playerUnit->location();

            for (int j = 0; j < units->size(); ++j) {
                if (const auto *enemyUnit = units->Get(j); enemyUnit->player_id() != pid) {
                    const auto &enemyCoords = enemyUnit->location();

                    // Proper hex distance calculation using cube coordinates
                    const Cube playerCube = OffsetToCube(playerCoords);
                    const Cube enemyCube = OffsetToCube(enemyCoords);
                    const int hexDistance = CubeDistance(playerCube, enemyCube);

                    minDistToEnemies = std::min(minDistToEnemies, static_cast<double>(hexDistance));
                }
            }
        }
    }

    if (minDistToEnemies == std::numeric_limits<double>::max()) {
        minDistToEnemies = 0.0;  // No enemies found
    }

#if LOGGING_
    // Log command count and distance metrics for performance analysis
    const auto allCommandCount = guessedDescriptors->size();
    const auto filteredCommandCount = filteredIndices.size();
    const int currentRound = gameState->current_round();

    printf("AI_COMMAND_COUNT: Round %d, Player %d, Defender %d, MinDist %.1f, Commands %zu -> %zu "
           "(%.1f%% filtered)\n",
           currentRound,
           static_cast<int>(pid),
           isDefender ? 1 : 0,
           minDistToEnemies,
           allCommandCount,
           filteredCommandCount,
           100.0 * (allCommandCount - filteredCommandCount) / allCommandCount);
#endif

    const auto commandCount = filteredIndices.size();

    vector<IndexAndScore> allIndices(commandCount);

    // Primary index is the command index; vector may contain repeated attempts
    vector<vector<future<ScoreValue>>> scoreFutures(commandCount);

    for (uint32_t index = 0; index < commandCount; index++) {
        const auto originalIndex = filteredIndices[index];
        const auto &guessedDescriptor = guessedDescriptors->at(originalIndex);
        const auto guessedCommandType = guessedDescriptor->GetCommandType();

        allIndices[index].index = originalIndex;
        allIndices[index].type = guessedCommandType;

        if (guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
            std::promise<ScoreValue> p;
            scoreFutures[index].push_back(p.get_future());
            p.set_value(currentUtility);
            allIndices[index].immediateScore = currentUtility;
        } else if (IsDeterministic(guessedCommandType)) {
            auto [immediateScore, lookaheadScore] =
                    CalcOne(pid,
                            isDefender,
                            originalIndex,
                            remainingLookahead,
                            maxRepeatCount,
                            _averageGenerator,
                            guessedEngine,
                            attackerStrategy,
                            settingsGetter,
                            allCastleCoords,
                            apdCache,
                            alCache);

            allIndices[index].immediateScore = immediateScore;
            scoreFutures[index].push_back(std::move(lookaheadScore));
        } else if (guessedDescriptor->HasOdds()) {
            const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
            const double successChance = static_cast<double>(successChancePercentile) / 100.0;

            // Success attempt uses 1.0 - (successChance / 2) as the roll (so 30% chance -> rolling
            // 85)
            auto [successImmediateScore, successLookaheadScore] =
                    CalcOne(pid,
                            isDefender,
                            originalIndex,
                            remainingLookahead,
                            maxRepeatCount,
                            std::make_shared<SequenceRandomGenerator>(
                                    std::vector{1.0 - successChance / 2.0}),
                            guessedEngine,
                            attackerStrategy,
                            settingsGetter,
                            allCastleCoords,
                            apdCache,
                            alCache);

            // second attempt uses the average of (1 - successChance) and 0 as the roll (so 30%
            // chance -> rolling 15)
            auto [failureImmediateScore, failureLookaheadScore] =
                    CalcOne(pid,
                            isDefender,
                            originalIndex,
                            remainingLookahead,
                            maxRepeatCount,
                            std::make_shared<SequenceRandomGenerator>(
                                    std::vector{(1.0 - successChance) / 2.0}),
                            guessedEngine,
                            attackerStrategy,
                            settingsGetter,
                            allCastleCoords,
                            apdCache,
                            alCache);

            allIndices[index].immediateScore =
                    std::lerp(failureImmediateScore, successImmediateScore, successChance);

            auto successSF = successLookaheadScore.share();
            auto failureSF = failureLookaheadScore.share();
            scoreFutures[index].push_back(std::async(
                    std::launch::deferred,
                    [successSF, failureSF, successChance]() -> double {
                        return std::lerp(failureSF.get(), successSF.get(), successChance);
                    }));
        } else {
            ScoreValue sum = 0.0;
            for (int repeatIteration = 0; repeatIteration < maxRepeatCount; repeatIteration++) {
                // In each iteration, use a double from [0, 1] as the random roll
                auto sequence = std::vector{
                        static_cast<double>(repeatIteration) /
                        static_cast<double>(maxRepeatCount - 1)};
                auto [immediateScore, lookaheadScore] =
                        CalcOne(pid,
                                isDefender,
                                originalIndex,
                                remainingLookahead,
                                maxRepeatCount,
                                std::make_shared<SequenceRandomGenerator>(sequence),
                                guessedEngine,
                                attackerStrategy,
                                settingsGetter,
                                allCastleCoords,
                                apdCache,
                                alCache);

                sum += immediateScore;
                scoreFutures[index].push_back(std::move(lookaheadScore));
            }
            allIndices[index].immediateScore = sum / maxRepeatCount;
        }
    }

    for (uint32_t i = 0; i < commandCount; i++) {
        const auto count = static_cast<ScoreValue>(scoreFutures[i].size());
        ScoreValue total = 0.0;
        for (auto &oneFuture : scoreFutures[i]) { total += oneFuture.get(); }
        allIndices[i].lookaheadScore = total / count;
    }

    return *std::ranges::max_element(allIndices, CommandSorter);
}

auto AIScoreCalculator::EvaluateCommand(
        const PlayerId pid,
        const bool isDefender,
        const uint32_t commandIndex,
        const int remainingLookahead,
        const int maxRepeatCount,
        const ShardokEngine &guessedEngine,
        const AIStrategy &attackerStrategy,
        const ScoreValue currentUtility,
        const SettingsGetter &settingsGetter,
        const CoordsSet &allCastleCoords,
        const APDCache &apdCache,
        const ALCache &alCache) -> CommandEvaluationResult {
    const CommandListSPtr guessedDescriptors = guessedEngine.GetAvailableCommandsForAIPlayer(pid);

    if (commandIndex >= guessedDescriptors->size()) { return {currentUtility, currentUtility}; }

    const auto &guessedDescriptor = guessedDescriptors->at(commandIndex);

    if (const auto guessedCommandType = guessedDescriptor->GetCommandType();
        guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
        return {currentUtility, currentUtility};
    } else if (IsDeterministic(guessedCommandType)) {
        auto [immediateScore, lookaheadScore] =
                CalcOne(pid,
                        isDefender,
                        commandIndex,
                        remainingLookahead,
                        maxRepeatCount,
                        _averageGenerator,
                        guessedEngine,
                        attackerStrategy,
                        settingsGetter,
                        allCastleCoords,
                        apdCache,
                        alCache);
        return {immediateScore, lookaheadScore.get()};
    } else if (guessedDescriptor->HasOdds()) {
        const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
        const double successChance = static_cast<double>(successChancePercentile) / 100.0;

        // Success attempt
        auto [successImmediateScore, successLookaheadScore] = CalcOne(
                pid,
                isDefender,
                commandIndex,
                remainingLookahead,
                maxRepeatCount,
                std::make_shared<SequenceRandomGenerator>(std::vector{1.0 - successChance / 2.0}),
                guessedEngine,
                attackerStrategy,
                settingsGetter,
                allCastleCoords,
                apdCache,
                alCache);

        // Failure attempt
        auto [failureImmediateScore, failureLookaheadScore] = CalcOne(
                pid,
                isDefender,
                commandIndex,
                remainingLookahead,
                maxRepeatCount,
                std::make_shared<SequenceRandomGenerator>(std::vector{(1.0 - successChance) / 2.0}),
                guessedEngine,
                attackerStrategy,
                settingsGetter,
                allCastleCoords,
                apdCache,
                alCache);

        // Return weighted average of success and failure
        return {std::lerp(failureImmediateScore, successImmediateScore, successChance),
                std::lerp(failureLookaheadScore.get(), successLookaheadScore.get(), successChance)};
    } else {
        // For non-deterministic commands without odds, use multiple attempts
        ScoreValue totalImmediateScore = 0.0;
        ScoreValue totalLookaheadScore = 0.0;
        for (int repeatIteration = 0; repeatIteration < maxRepeatCount; repeatIteration++) {
            auto sequence = std::vector{
                    static_cast<double>(repeatIteration) / static_cast<double>(maxRepeatCount - 1)};
            auto [immediateScore, lookaheadScore] =
                    CalcOne(pid,
                            isDefender,
                            commandIndex,
                            remainingLookahead,
                            maxRepeatCount,
                            std::make_shared<SequenceRandomGenerator>(sequence),
                            guessedEngine,
                            attackerStrategy,
                            settingsGetter,
                            allCastleCoords,
                            apdCache,
                            alCache);

            totalImmediateScore += immediateScore;
            totalLookaheadScore += lookaheadScore.get();
        }
        return {totalImmediateScore / maxRepeatCount, totalLookaheadScore / maxRepeatCount};
    }
}

[[nodiscard]] auto AIScoreCalculator::CommandScore(
        const PlayerId pid,
        const bool isDefender,
        const int remainingLookahead,
        const int maxRepeatCount,
        const ShardokEngine &guessedEngine,
        const AIStrategy &attackerStrategy,
        const ScoreValue currentUtility,
        const SettingsGetter &settingsGetter,
        const CoordsSet &allCastleCoords,
        const APDCache &apdCache,
        const ALCache &alCache,
        const size_t commandIndex) -> ScoreValue {
    const auto result = EvaluateCommand(
            pid,
            isDefender,
            commandIndex,
            remainingLookahead,
            maxRepeatCount,
            guessedEngine,
            attackerStrategy,
            currentUtility,
            settingsGetter,
            allCastleCoords,
            apdCache,
            alCache);
    return result.lookaheadScore;
}

}  // namespace shardok
