//
//  ShardokAIClient.cpp
//  eagle0
//
//  Created by Dan Crosby on 10/18/17.
//  Copyright © 2017 none. All rights reserved.
//

#include "ShardokAIClient.hpp"

#define DEBUG_FLEE_DECISIONS

#include <google/protobuf/util/message_differencer.h>

#include "AIAttackerStrategySelector.hpp"
#include "AIDefenderStrategySelector.hpp"
#include "AIFleeDecisionCalculator.hpp"
#include "AIScoreUtilities.hpp"
#include "AITimeBudget.hpp"
#include "IterativeDeepeningAI.hpp"
#include "src/main/cpp/net/eagle0/common/TimeUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateGuesser.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"

namespace shardok {

using net::eagle0::shardok::api::ActionResultView;
using net::eagle0::shardok::api::GameStateView;

static constexpr bool kPerformanceLogging = true;

void ApplyUpdate(GameStateView &currentView, const ActionResultView &update) {}

auto RoundsRemaining(const GameSettingsSPtr &settings, const GameStateView &gsv) -> int {
    const int maxRounds = settings->GetGetter().Backing().max_rounds();

    return maxRounds - gsv.current_round();
}

ShardokAIClient::ShardokAIClient(
        const PlayerId playerId,
        const bool isDefender,
        const HexMap *hexMap,
        const SettingsGetter &settings)
    : playerId(playerId),
      isDefender(isDefender),
      alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
      waterCrossingCommandChooser(playerId, apdCache) {
    // Pre-generate the most common cache entries for better performance
    const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);

    // Pre-fetch for all battalion types, both with and without brave water
    using BattalionTypeId = net::eagle0::shardok::storage::fb::BattalionTypeId;

    for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
         typeId <= BattalionTypeId::BattalionTypeId_MAX;
         typeId++) {
        const auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
        const auto battalionType = settings.GetBattalionType(battalionTypeId);

        // Pre-fetch without brave water (braveWaterActionPointCost = -1)
        apdCache->GetRaw(hexMap, mapId, battalionType, false, -1);

        // Pre-fetch with brave water (includeBravingWater = true, braveWaterActionPointCost = 0)
        apdCache->GetRaw(hexMap, mapId, battalionType, true, 0);
    }

    // Consolidate all the pre-fetched entries into the persistent cache
    apdCache->ConsolidateThreadLocalCache_Racy();
}

void CheckCommand(const CommandProto &realDescriptor, const CommandProto &guessedDescriptor) {
    string diff;
    auto differencer = google::protobuf::util::MessageDifferencer();
    differencer.IgnoreField(CommandProto::descriptor()->FindFieldByNumber(
            CommandProto::kFollowUpCommandTypesFieldNumber));
    differencer.ReportDifferencesToString(&diff);
    if (!differencer.Compare(realDescriptor, guessedDescriptor)) {
        printf("diff: %s\n\n", diff.c_str());

        printf("Selected command descriptor\n%s\ndoes not match guessed\n%s\n\n",
               realDescriptor.DebugString().c_str(),
               guessedDescriptor.DebugString().c_str());
        throw ShardokInternalErrorException("Illegal state for AI client");
    }
}

auto ShardokAIClient::StandardChooseCommandIndex(
        const GameSettingsSPtr &settings,
        const GameStateW &guessedState,
        const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
    const auto settingsGetter = settings->GetGetter();
    const auto guessedEngine = ShardokEngine(settings, guessedState);

    // Calculate time budget based on game situation using new settings
    const auto timeBudget = CalculateTimeBudget(playerId, settings, guessedState);

    const auto guessedCommands = guessedEngine.GetAvailableCommandProtos(playerId, false);
    const auto commandCount = guessedCommands.size();

    assert(commandCount == realAvailableCommands.size());
    for (int i = 0; i < commandCount; i++) {
        CheckCommand(realAvailableCommands[i], guessedCommands[i]);
    }

    // Determine strategy once for consistent scoring throughout iterative deepening
    const auto castleCoords = AllCastleCoords(guessedState->hex_map());
    const AIStrategy strategy = isDefender ? AIDefenderStrategySelector::BestDefenderStrategy(
                                                     guessedState,
                                                     castleCoords,
                                                     apdCache,
                                                     settingsGetter)
                                           : AIAttackerStrategySelector::BestAttackerStrategy(
                                                     playerId,
                                                     guessedState,
                                                     castleCoords,
                                                     apdCache,
                                                     alCache,
                                                     settingsGetter,
                                                     waterCrossingCommandChooser,
                                                     realAvailableCommands);

    // Use iterative deepening AI for Phase 2 implementation
    IterativeDeepeningAI
            iterativeAI(playerId, isDefender, strategy, castleCoords, apdCache, alCache);
    auto search_result =
            iterativeAI.IterativeSearch(settings, guessedState, realAvailableCommands, timeBudget);

    CommandChoiceResults result{};
    result.chosenIndex = search_result.bestCommandIndex;
    result.availableCommandCount = search_result.availableCommandCount;
    result.depthAchieved = search_result.depthAchieved;
    result.commandCountEvaluated = search_result.commandCountEvaluated;
    result.completionReason = search_result.completionReason;

    if constexpr (kPerformanceLogging) {
        if (result.commandCountEvaluated < result.availableCommandCount) {
            printf("ID AI: Depth %d - evaluated %lu/%zu commands\n",
                   result.depthAchieved,
                   result.commandCountEvaluated,
                   result.availableCommandCount);
        }
        printf("ID AI: Search complete - achieved depth %d for best command %zu\n",
               result.depthAchieved,
               result.chosenIndex);

        fflush(stdout);
    }

    return result;
}

auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
        const GameSettingsSPtr &settings,
        const GameStateW &guessedState,
        const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
    if (const auto dismissCommand = std::ranges::find_if(
                realAvailableCommands,
                [](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
                    return cmd.type() == net::eagle0::shardok::common::DISMISS_UNIT_COMMAND;
                });
        dismissCommand == realAvailableCommands.end()) {
        return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
    } else {
        CommandChoiceResults results{};
        results.chosenIndex =
                static_cast<size_t>(std::distance(realAvailableCommands.begin(), dismissCommand));
        results.availableCommandCount = realAvailableCommands.size();
        results.depthAchieved = 1;          // Simple heuristic choice
        results.commandCountEvaluated = 1;  // Only evaluated one command type
        results.completionReason =
                EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;  // Heuristic choice
        return results;
    }
}

auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
        const GameSettingsSPtr &settings,
        const GameStateW &guessedState,
        const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
    const auto fleeCommand = std::ranges::find_if(
            realAvailableCommands,
            [](const net::eagle0::shardok::api::CommandDescriptor &cmd) {
                return cmd.type() == net::eagle0::shardok::common::FLEE_COMMAND;
            });

    if (fleeCommand == realAvailableCommands.end()) {
        return LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
    }

    // Use the flee decision calculator
    const auto fleeDecision = AIFleeDecisionCalculator::EvaluateFleeVsFight(
            playerId,
            settings,
            guessedState,
            realAvailableCommands,
            fleeCommand,
#ifdef DEBUG_FLEE_DECISIONS
            true  // Enable debug logging
#else
            false
#endif
    );

    if (fleeDecision.shouldFlee) {
        CommandChoiceResults results{};
        results.chosenIndex = fleeDecision.commandIndex;
        results.availableCommandCount = realAvailableCommands.size();
        results.depthAchieved = 1;          // Heuristic choice
        results.commandCountEvaluated = 1;  // Only evaluated one command type
        results.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
        return results;
    } else {
        // Fight instead of flee
        return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
    }
}

auto ShardokAIClient::ChooseCommandIndex(
        const GameSettingsSPtr &settings,
        const GameStateView &gsv,
        const vector<CommandProto> &realAvailableCommands) const -> CommandChoiceResults {
    static int typeChosenCount[net::eagle0::shardok::common::CommandType_MAX + 1];
    static int totalChoices = 0;

    CommandChoiceResults results{};

    const auto guessedState = GameStateGuesser::GuessedState(playerId, settings->GetGetter(), gsv);

    if (const int roundsRemaining = RoundsRemaining(settings, gsv);
        !isDefender && roundsRemaining <= 1) {
        results =
                FinalRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
    } else if (!isDefender && roundsRemaining <= 3) {
        results =
                LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
    } else {
        results = StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
    }

    const auto chosenType = realAvailableCommands[results.chosenIndex].type();
    typeChosenCount[static_cast<int>(chosenType)]++;
    totalChoices++;

    if (totalChoices % 1000 == 0) {
        printf("TYPES CHOSEN:\n");
        vector<std::pair<int, CommandType>> choices{};
        for (int i = 0; i <= net::eagle0::shardok::common::CommandType_MAX; i++) {
            if (typeChosenCount[i] != 0) {
                choices.emplace_back(typeChosenCount[i], static_cast<CommandType>(i));
            }
        }

        std::ranges::sort(choices);
        std::ranges::reverse(choices);
        for (const auto &[index, choice] : choices) {
            printf("%5d %s\n", index, CommandType_Name(choice).c_str());
        }
        printf("\n\n");
    }

    return results;
}

auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const
        -> CommandChoiceResults {
    if (const auto &availableCommands = engine.GetAvailableCommandProtos(playerId, false);
        availableCommands.empty()) {
        printf("no commands for player %d\n", playerId);
        throw ShardokInternalErrorException(
                "Asked to choose a command, but there are none available");
    } else {
        const auto &settings = engine.GetGameSettings();
        const auto &gsv = engine.GetGameStateView(GetPlayerId());

        const auto results = ChooseCommandIndex(settings, gsv, availableCommands);
        apdCache->ConsolidateThreadLocalCache_Racy();
        return results;
    }
}

}  // namespace shardok
