//
// Command evaluator for AI lookahead search.
// Extracted from AIScoreCalculator to separate concerns.
//

#include "AICommandEvaluator.hpp"

#include <chrono>
#include <cmath>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>

#include "AICommandFilter.hpp"
#include "TranspositionTable.hpp"
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/score/AIScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"

namespace shardok {

constexpr bool kMultithread = true;
constexpr bool kLogging = false;

[[nodiscard]] static auto AverageGenerator() -> std::shared_ptr<SequenceRandomGenerator> {
    return std::make_shared<SequenceRandomGenerator>(std::vector{0.5});
}

[[nodiscard]] static auto DescribeCommandForLog(const CommandSPtr& command) -> std::string {
    if (!command) { return "<null command>"; }

    std::ostringstream out;
    out << "type=" << command->GetCommandType()
        << " player=" << static_cast<int>(command->GetPlayerId())
        << " actor=" << command->GetActorUnitId() << " target=("
        << static_cast<int>(command->GetTargetRow()) << ','
        << static_cast<int>(command->GetTargetColumn()) << ')';

    if (command->HasOdds()) { out << " odds_percentile=" << command->GetOddsPercentile(); }

    return out.str();
}

[[nodiscard]] static auto DescribeCommandAt(
        const CommandListSPtr& commands,
        const size_t commandIndex) -> std::string {
    if (!commands) { return "command=<null command list>"; }
    if (commandIndex >= commands->size()) {
        std::ostringstream out;
        out << "command=<index " << commandIndex << " out of " << commands->size() << ">";
        return out.str();
    }

    std::ostringstream out;
    out << "command={" << DescribeCommandForLog(commands->at(commandIndex)) << '}';
    return out.str();
}

[[nodiscard]] static auto WithExceptionContext(
        const std::string& context,
        const std::exception& exception) -> std::runtime_error {
    return std::runtime_error(context + ": " + exception.what());
}

static 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::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::HOLY_WAVE_COMMAND:
        case net::eagle0::shardok::common::EVACUATE_PRISONERS_COMMAND:
        case net::eagle0::shardok::common::REPAIR_COMMAND: return true;
        default: return false;
    }
}

static auto RandomnessSampleForRepeat(const int repeatIteration, const int maxRepeatCount)
        -> double {
    if (maxRepeatCount <= 1) { return 0.5; }
    return static_cast<double>(repeatIteration) / static_cast<double>(maxRepeatCount - 1);
}

static auto SuccessSampleForOdds(const double successChance) -> double {
    return successChance / 2.0;
}

static auto FailureSampleForOdds(const double successChance) -> double {
    return successChance + ((1.0 - successChance) / 2.0);
}

static auto CommandSorter(
        const AICommandEvaluator::IndexAndScore& l,
        const AICommandEvaluator::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;
}

AICommandEvaluator::AICommandEvaluator(
        const AIScoreCalculator& scorer,
        const APDCache& apdCache,
        BattalionTypeGetter battalionTypeGetter)
    : scorer_(scorer),
      apdCache_(apdCache),
      battalionTypeGetter_(std::move(battalionTypeGetter)) {}

auto AICommandEvaluator::PerformLookahead(
        const PlayerId pid,
        const bool isDefender,
        const int remainingLookahead,
        const int maxRepeatCount,
        const std::shared_ptr<ShardokEngine>& innerEngine,
        const ScoreValue currentUtility,
        const AIStrategy& attackerStrategy,
        const CoordsSet& allCastleCoords,
        std::chrono::steady_clock::time_point deadline) const -> std::future<EvaluationResult> {
    // Check transposition table before expensive computation
    auto cachedScore =
            g_transpositionTable.probe(innerEngine->GetCurrentGameState(), remainingLookahead, pid);

    if (cachedScore.has_value()) {
        // Return cached result immediately
        std::promise<EvaluationResult> p;
        p.set_value(EvaluationResult{.score = *cachedScore, .completed = true});
        return p.get_future();
    }
    const auto nextUtility = currentUtility;

    // Check if we've reached the depth limit before making recursive calls
    if (remainingLookahead <= 0) {
        // Store the current utility in the transposition table and return it
        // Note: Store with depth 1 since depth 0 indicates an empty entry in the transposition
        // table
        g_transpositionTable.store(innerEngine->GetCurrentGameState(), 1, pid, nextUtility);

        std::promise<EvaluationResult> p;
        p.set_value(EvaluationResult{.score = nextUtility, .completed = true});
        return p.get_future();
    }

    if (const CommandListSPtr nextCommands = innerEngine->GetAvailableCommandsForAIPlayer(pid);
        nextCommands && !nextCommands->empty()) {
        // Get the future from FindBestCommand without calling .get()
        auto bestCommandFuture = FindBestCommand(
                pid,
                isDefender,
                remainingLookahead - 1,
                maxRepeatCount,
                *innerEngine,
                attackerStrategy,
                nextUtility,
                allCastleCoords,
                deadline);

        // Return a future that chains the best command evaluation
        return std::async(
                std::launch::deferred,
                [bestCommandFuture = std::move(bestCommandFuture),
                 innerEngine,
                 nextCommands,
                 pid,
                 nextUtility,
                 remainingLookahead]() mutable -> EvaluationResult {
                    IndexAndScore bestCommand;
                    try {
                        bestCommand = bestCommandFuture.get();
                    } catch (const std::exception& e) {
                        std::ostringstream context;
                        context << "AI PerformLookahead failed while resolving best command"
                                << " player=" << static_cast<int>(pid)
                                << " remaining_lookahead=" << remainingLookahead
                                << " next_command_count=" << nextCommands->size();
                        throw WithExceptionContext(context.str(), e);
                    }
                    if (!bestCommand.completed) {
                        return EvaluationResult{.score = nextUtility, .completed = false};
                    }
                    if (bestCommand.index >= nextCommands->size()) {
                        std::cerr << "AI lookahead produced command index " << bestCommand.index
                                  << " with only " << nextCommands->size()
                                  << " available commands for player " << static_cast<int>(pid)
                                  << " at remaining lookahead " << remainingLookahead << "\n";
                        return EvaluationResult{.score = nextUtility, .completed = false};
                    }

                    ScoreValue resultScore;
                    if (const auto& nextCommand = nextCommands->at(bestCommand.index);
                        nextCommand->GetCommandType() !=
                        net::eagle0::shardok::common::END_TURN_COMMAND) {
                        resultScore = bestCommand.immediateScore;
                    } else {
                        resultScore = nextUtility;
                    }

                    // Store in transposition table before returning
                    g_transpositionTable.store(
                            innerEngine->GetCurrentGameState(),
                            remainingLookahead,
                            pid,
                            resultScore);

                    return EvaluationResult{.score = resultScore, .completed = true};
                });
    }

    // No commands available, store and return the current utility as a future
    g_transpositionTable
            .store(innerEngine->GetCurrentGameState(), remainingLookahead, pid, nextUtility);

    std::promise<EvaluationResult> p;
    p.set_value(EvaluationResult{.score = nextUtility, .completed = true});
    return p.get_future();
}

auto AICommandEvaluator::EvaluateWithRandomness(
        const PlayerId pid,
        const bool isDefender,
        const uint32_t commandIndex,
        const int remainingLookahead,
        const int maxRepeatCount,
        const std::shared_ptr<RandomGenerator>& randomGenerator,
        const ShardokEngine& guessedEngine,
        const AIStrategy& attackerStrategy,
        const CoordsSet& allCastleCoords,
        std::chrono::steady_clock::time_point deadline) const -> ImmediateAndLookaheadScore {
    ImmediateAndLookaheadScore returnValue{};

    // Check if we've exceeded the deadline
    if (std::chrono::steady_clock::now() > deadline) {
        std::promise<EvaluationResult> p;
        p.set_value(EvaluationResult{.score = 0.0, .completed = false});
        returnValue.immediateScore = 0.0;
        returnValue.completed = false;
        returnValue.lookaheadScore = p.get_future();
        return returnValue;
    }

    const auto guessedCommands = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
    auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
    try {
        innerEngine->PostCommand(pid, commandIndex, randomGenerator);
    } catch (const std::exception& e) {
        std::ostringstream context;
        context << "AI EvaluateWithRandomness failed while posting simulated command"
                << " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
                << " guessed_command_count=" << guessedCommands->size()
                << " remaining_lookahead=" << remainingLookahead << ' '
                << DescribeCommandAt(guessedCommands, commandIndex);
        throw WithExceptionContext(context.str(), e);
    }

    ScoreValue innerUtility;
    try {
        innerUtility = scorer_.GuessedStateScore(
                isDefender,
                innerEngine->GetCurrentGameState(),
                attackerStrategy,
                allCastleCoords);
    } catch (const std::exception& e) {
        std::ostringstream context;
        context << "AI EvaluateWithRandomness failed while scoring simulated state"
                << " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
                << " guessed_command_count=" << guessedCommands->size()
                << " remaining_lookahead=" << remainingLookahead << ' '
                << DescribeCommandAt(guessedCommands, commandIndex);
        throw WithExceptionContext(context.str(), e);
    }

    returnValue.immediateScore = innerUtility;
    returnValue.completed = true;

    if (remainingLookahead <= 0) {
        std::promise<EvaluationResult> p;
        returnValue.lookaheadScore = p.get_future();
        p.set_value(EvaluationResult{.score = innerUtility, .completed = true});
    } else {
        auto lookaheadLambda = [this,
                                pid,
                                isDefender,
                                remainingLookahead,
                                maxRepeatCount,
                                commandIndex,
                                innerEngine,
                                attackerStrategy,
                                innerUtility,
                                guessedCommands,
                                allCastleCoords,
                                deadline]() -> EvaluationResult {
            try {
                auto lookaheadFuture = PerformLookahead(
                        pid,
                        isDefender,
                        remainingLookahead,
                        maxRepeatCount,
                        innerEngine,
                        innerUtility,
                        attackerStrategy,
                        allCastleCoords,
                        deadline);
                return lookaheadFuture.get();
            } catch (const std::exception& e) {
                std::ostringstream context;
                context << "AI EvaluateWithRandomness failed during recursive lookahead"
                        << " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
                        << " remaining_lookahead=" << remainingLookahead << ' '
                        << DescribeCommandAt(guessedCommands, commandIndex);
                throw WithExceptionContext(context.str(), e);
            }
        };

        if constexpr (kMultithread) {
            auto launchPolicy =
                    remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
            returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
        } else {
            std::promise<EvaluationResult> p;
            returnValue.lookaheadScore = p.get_future();
            auto lambdaResult = lookaheadLambda();
            p.set_value(lambdaResult);
        }
    }

    return returnValue;
}

auto AICommandEvaluator::FindBestCommand(
        const PlayerId pid,
        const bool isDefender,
        const int remainingLookahead,
        const int maxRepeatCount,
        const ShardokEngine& guessedEngine,
        const AIStrategy& attackerStrategy,
        const ScoreValue currentUtility,
        const CoordsSet& allCastleCoords,
        std::chrono::steady_clock::time_point deadline) const -> std::future<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(),
            apdCache_,
            battalionTypeGetter_);

    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 (size_t i = 0; i < units->size(); ++i) {
        if (const auto* playerUnit = units->Get(static_cast<unsigned int>(i));
            playerUnit->player_id() == pid) {
            const auto& playerCoords = playerUnit->location();

            for (size_t j = 0; j < units->size(); ++j) {
                if (const auto* enemyUnit = units->Get(static_cast<unsigned int>(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 constexpr (kLogging) {
        // 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();

        const auto flags = std::cout.flags();
        const auto precision = std::cout.precision();
        std::cout << "AI_COMMAND_COUNT: Round " << currentRound << ", Player "
                  << static_cast<int>(pid) << ", Defender " << (isDefender ? 1 : 0) << ", MinDist "
                  << std::fixed << std::setprecision(1) << minDistToEnemies << ", Commands "
                  << allCommandCount << " -> " << filteredCommandCount << " ("
                  << 100.0 * (allCommandCount - filteredCommandCount) / allCommandCount
                  << "% filtered)\n";
        std::cout.flags(flags);
        std::cout.precision(precision);
    }

    const auto commandCount = filteredIndices.size();

    // Structure to hold all command evaluation data
    struct CommandEvaluation {
        size_t index{};
        CommandType type{};
        ScoreValue immediateScore{};
        bool immediateCompleted = true;
        std::vector<std::future<EvaluationResult>> lookaheadFutures;
    };

    std::vector<CommandEvaluation> commandEvaluations(commandCount);

    for (uint32_t index = 0; index < commandCount; ++index) {
        const auto originalIndex = filteredIndices[index];
        const CommandSPtr guessedDescriptor = [&] {
            try {
                return guessedDescriptors->at(originalIndex);
            } catch (const std::exception& e) {
                std::ostringstream context;
                context << "AI FindBestCommand failed while reading filtered command"
                        << " player=" << static_cast<int>(pid) << " filtered_index=" << index
                        << " original_index=" << originalIndex
                        << " guessed_command_count=" << guessedDescriptors->size()
                        << " filtered_command_count=" << filteredIndices.size();
                throw WithExceptionContext(context.str(), e);
            }
        }();
        const auto guessedCommandType = guessedDescriptor->GetCommandType();

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

        if (guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
            std::promise<EvaluationResult> p;
            commandEvaluations[index].lookaheadFutures.push_back(p.get_future());
            p.set_value(EvaluationResult{.score = currentUtility, .completed = true});
            commandEvaluations[index].immediateScore = currentUtility;
        } else if (IsDeterministic(guessedCommandType)) {
            auto evaluation = EvaluateWithRandomness(
                    pid,
                    isDefender,
                    originalIndex,
                    remainingLookahead,
                    maxRepeatCount,
                    AverageGenerator(),
                    guessedEngine,
                    attackerStrategy,
                    allCastleCoords,
                    deadline);

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

            // Success attempt uses the midpoint of the successful percentile range.
            auto successEvaluation = EvaluateWithRandomness(
                    pid,
                    isDefender,
                    originalIndex,
                    remainingLookahead,
                    maxRepeatCount,
                    std::make_shared<SequenceRandomGenerator>(
                            std::vector{SuccessSampleForOdds(successChance)}),
                    guessedEngine,
                    attackerStrategy,
                    allCastleCoords,
                    deadline);

            // Failure attempt uses the midpoint of the failing percentile range.
            auto failureEvaluation = EvaluateWithRandomness(
                    pid,
                    isDefender,
                    originalIndex,
                    remainingLookahead,
                    maxRepeatCount,
                    std::make_shared<SequenceRandomGenerator>(
                            std::vector{FailureSampleForOdds(successChance)}),
                    guessedEngine,
                    attackerStrategy,
                    allCastleCoords,
                    deadline);

            commandEvaluations[index].immediateCompleted =
                    failureEvaluation.completed && successEvaluation.completed;
            commandEvaluations[index].immediateScore = std::lerp(
                    failureEvaluation.immediateScore,
                    successEvaluation.immediateScore,
                    successChance);

            auto successSF = successEvaluation.lookaheadScore.share();
            auto failureSF = failureEvaluation.lookaheadScore.share();
            commandEvaluations[index].lookaheadFutures.push_back(std::async(
                    std::launch::deferred,
                    [successSF, failureSF, successChance]() -> EvaluationResult {
                        const auto failure = failureSF.get();
                        const auto success = successSF.get();
                        if (!failure.completed || !success.completed) {
                            return EvaluationResult{.score = 0.0, .completed = false};
                        }
                        return EvaluationResult{
                                .score = std::lerp(failure.score, success.score, successChance),
                                .completed = true};
                    }));
        } else {
            ScoreValue sum = 0.0;
            const int sampleCount = std::max(1, maxRepeatCount);
            commandEvaluations[index].lookaheadFutures.reserve(sampleCount);
            for (int repeatIteration = 0; repeatIteration < sampleCount; ++repeatIteration) {
                // In each iteration, use a double from [0, 1] as the random roll
                auto sequence =
                        std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
                auto evaluation = EvaluateWithRandomness(
                        pid,
                        isDefender,
                        originalIndex,
                        remainingLookahead,
                        maxRepeatCount,
                        std::make_shared<SequenceRandomGenerator>(std::move(sequence)),
                        guessedEngine,
                        attackerStrategy,
                        allCastleCoords,
                        deadline);

                if (!evaluation.completed) { commandEvaluations[index].immediateCompleted = false; }
                sum += evaluation.immediateScore;
                commandEvaluations[index].lookaheadFutures.push_back(
                        std::move(evaluation.lookaheadScore));
            }
            commandEvaluations[index].immediateScore = sum / sampleCount;
        }
    }

    // Return a future that will wait for all evaluations and find the best one
    return std::async(
            std::launch::deferred,
            [evals = std::move(commandEvaluations)]() mutable -> IndexAndScore {
                std::vector<IndexAndScore> allResults;
                allResults.reserve(evals.size());

                // Wait for all futures and compute final scores
                for (auto& eval : evals) {
                    ScoreValue totalLookaheadScore = 0.0;
                    bool completed = eval.immediateCompleted;
                    for (auto& future : eval.lookaheadFutures) {
                        EvaluationResult lookahead;
                        try {
                            lookahead = future.get();
                        } catch (const std::exception& e) {
                            std::ostringstream context;
                            context << "AI FindBestCommand failed while resolving lookahead future"
                                    << " command_index=" << eval.index
                                    << " command_type=" << eval.type
                                    << " lookahead_future_count=" << eval.lookaheadFutures.size();
                            throw WithExceptionContext(context.str(), e);
                        }
                        if (!lookahead.completed) { completed = false; }
                        totalLookaheadScore += lookahead.score;
                    }
                    ScoreValue avgLookaheadScore =
                            eval.lookaheadFutures.empty()
                                    ? eval.immediateScore
                                    : totalLookaheadScore / eval.lookaheadFutures.size();

                    if (!completed) { continue; }

                    allResults.push_back(IndexAndScore{
                            .index = eval.index,
                            .type = eval.type,
                            .lookaheadScore = avgLookaheadScore,
                            .immediateScore = eval.immediateScore,
                            .completed = true});
                }
                if (allResults.empty()) {
                    return IndexAndScore{
                            .index = 0,
                            .type = net::eagle0::shardok::common::UNKNOWN_COMMAND,
                            .lookaheadScore = 0.0,
                            .immediateScore = 0.0,
                            .completed = false};
                }
                // Find the best command using the existing sorter
                auto bestIt = std::ranges::max_element(allResults, CommandSorter);
                return *bestIt;
            });
}

auto AICommandEvaluator::EvaluateCommand(
        const PlayerId pid,
        const bool isDefender,
        const int remainingLookahead,
        const int maxRepeatCount,
        const ShardokEngine& guessedEngine,
        const AIStrategy& attackerStrategy,
        const ScoreValue currentUtility,
        const CoordsSet& allCastleCoords,
        const size_t commandIndex,
        std::chrono::steady_clock::time_point deadline) const -> std::future<EvaluationResult> {
    const CommandListSPtr guessedDescriptors = guessedEngine.GetAvailableCommandsForAIPlayer(pid);

    if (commandIndex >= guessedDescriptors->size()) {
        std::promise<EvaluationResult> p;
        p.set_value(EvaluationResult{.score = currentUtility, .completed = true});
        return p.get_future();
    }

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

    if (guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
        std::promise<EvaluationResult> p;
        p.set_value(EvaluationResult{.score = currentUtility, .completed = true});
        return p.get_future();
    }

    if (IsDeterministic(guessedCommandType)) {
        auto evaluation = EvaluateWithRandomness(
                pid,
                isDefender,
                commandIndex,
                remainingLookahead,
                maxRepeatCount,
                AverageGenerator(),
                guessedEngine,
                attackerStrategy,
                allCastleCoords,
                deadline);
        return std::move(evaluation.lookaheadScore);
    }

    if (guessedDescriptor->HasOdds()) {
        const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
        const double successChance = static_cast<double>(successChancePercentile) / 100.0;

        // Success attempt
        auto successEvaluation = EvaluateWithRandomness(
                pid,
                isDefender,
                commandIndex,
                remainingLookahead,
                maxRepeatCount,
                std::make_shared<SequenceRandomGenerator>(
                        std::vector{SuccessSampleForOdds(successChance)}),
                guessedEngine,
                attackerStrategy,
                allCastleCoords,
                deadline);

        // Failure attempt
        auto failureEvaluation = EvaluateWithRandomness(
                pid,
                isDefender,
                commandIndex,
                remainingLookahead,
                maxRepeatCount,
                std::make_shared<SequenceRandomGenerator>(
                        std::vector{FailureSampleForOdds(successChance)}),
                guessedEngine,
                attackerStrategy,
                allCastleCoords,
                deadline);

        // Return weighted average of success and failure
        auto successSF = successEvaluation.lookaheadScore.share();
        auto failureSF = failureEvaluation.lookaheadScore.share();
        return std::async(
                std::launch::deferred,
                [successSF, failureSF, successChance]() -> EvaluationResult {
                    const auto failure = failureSF.get();
                    const auto success = successSF.get();
                    if (!failure.completed || !success.completed) {
                        return EvaluationResult{.score = 0.0, .completed = false};
                    }
                    return EvaluationResult{
                            .score = std::lerp(failure.score, success.score, successChance),
                            .completed = true};
                });
    }

    // For non-deterministic commands without odds, use multiple attempts
    std::vector<std::future<EvaluationResult>> lookaheadFutures;
    const int sampleCount = std::max(1, maxRepeatCount);
    lookaheadFutures.reserve(sampleCount);

    for (int repeatIteration = 0; repeatIteration < sampleCount; ++repeatIteration) {
        auto sequence = std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
        auto evaluation = EvaluateWithRandomness(
                pid,
                isDefender,
                commandIndex,
                remainingLookahead,
                maxRepeatCount,
                std::make_shared<SequenceRandomGenerator>(std::move(sequence)),
                guessedEngine,
                attackerStrategy,
                allCastleCoords,
                deadline);

        lookaheadFutures.push_back(std::move(evaluation.lookaheadScore));
    }

    // Return a future that computes the average when needed
    return std::async(
            std::launch::deferred,
            [lookaheadFutures = std::move(lookaheadFutures),
             sampleCount]() mutable -> EvaluationResult {
                ScoreValue total = 0.0;
                bool completed = true;
                for (auto& future : lookaheadFutures) {
                    const auto result = future.get();
                    if (!result.completed) { completed = false; }
                    total += result.score;
                }
                if (!completed) { return EvaluationResult{.score = 0.0, .completed = false}; }
                return EvaluationResult{
                        .score = total / static_cast<ScoreValue>(sampleCount),
                        .completed = true};
            });
}

}  // namespace shardok
