//
// AI Battle Simulator Implementation
//

#include "AiBattleSimulator.hpp"

#include <iostream>

#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai_testing_common/AIClientFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai_testing_common/GamePhaseRunner.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai_testing_common/GameSettingsFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
#pragma clang diagnostic pop

namespace shardok {
namespace ai_battle_simulator {

namespace {

constexpr PlayerId ATTACKER_ID = 0;
constexpr PlayerId DEFENDER_ID = 1;

// Default values for unit configuration - matching client-initiated battles
constexpr double DEFAULT_BATTALION_ARMAMENT = 102.0;
constexpr double DEFAULT_BATTALION_TRAINING = 102.0;
constexpr double DEFAULT_BATTALION_MORALE = 76.0;

constexpr int DEFAULT_HERO_STRENGTH = 102;
constexpr int DEFAULT_HERO_AGILITY = 102;
constexpr int DEFAULT_HERO_WISDOM = 102;
constexpr int DEFAULT_HERO_CHARISMA = 102;
constexpr int DEFAULT_HERO_CONSTITUTION = 102;
constexpr int DEFAULT_HERO_BRAVERY = 102;
constexpr int DEFAULT_HERO_INTEGRITY = 0;
constexpr int DEFAULT_HERO_AMBITION = 0;
constexpr int DEFAULT_HERO_VIGOR = 102;

// Convert proto AI algorithm type to ShardokAI enum
AIAlgorithmType ConvertAIAlgorithmType(
        net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType protoType) {
    switch (protoType) {
        case net::eagle0::shardok::ai_battle_simulator::MCTS: return AIAlgorithmType::MCTS;
        case net::eagle0::shardok::ai_battle_simulator::ITERATIVE_DEEPENING:
        default: return AIAlgorithmType::ITERATIVE_DEEPENING;
    }
}

// Get value with default if not set (proto3 uses 0 as default, we check for that)
template<typename T>
T GetOrDefault(T value, T defaultValue) {
    return (value == 0) ? defaultValue : value;
}

}  // namespace

AiBattleSimulator::AiBattleSimulator(const BattleConfigProto& config, GameSettingsSPtr gameSettings)
    : config_(config),
      gameSettings_(std::move(gameSettings)) {
    if (!gameSettings_) {
        // Initialize default game settings
        gameSettings_ = ai_testing_common::GameSettingsFactory::CreateDefault();
    }
}

BattleResult AiBattleSimulator::RunBattle() {
    std::cout << "Starting AI vs AI battle simulation...\n";
    std::cout << "  Map: " << config_.map_name() << "\n";
    std::cout << "  Month: " << config_.month() << "\n";
    std::cout << "  Max rounds: " << config_.max_rounds() << "\n";
    std::cout << "  Attacker AI: "
              << net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType_Name(
                         config_.attacker().ai_algorithm())
              << "\n";
    std::cout << "  Defender AI: "
              << net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType_Name(
                         config_.defender().ai_algorithm())
              << "\n";

    // Create initial game state
    auto gameState = CreateInitialGameState();

    // Get hex map from game state
    const auto* hexMap = gameState->hex_map();

    // Create AI clients for both players
    auto attackerAI = CreateAIClient(ATTACKER_ID, hexMap);
    auto defenderAI = CreateAIClient(DEFENDER_ID, hexMap);

    // Create the engine once for the entire simulation
    ShardokEngine engine(gameSettings_, gameState);

    // Run setup phase
    std::cout << "\nStarting setup phase...\n";
    auto setupResult = RunSetupPhase(engine, *attackerAI, *defenderAI);
    if (setupResult.endReason != BattleResult::EndReason::DRAW) {
        // Game ended during setup (shouldn't happen normally)
        return setupResult;
    }

    // Run battle phase
    std::cout << "\nStarting battle phase...\n";
    return RunBattlePhase(engine, *attackerAI, *defenderAI);
}

GameStateW AiBattleSimulator::CreateInitialGameState() const {
    // Load map
    auto hexMapProto = LoadMap(config_.map_name());

    // Create player info protos
    std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos;

    // Attacker info
    net::eagle0::shardok::common::PlayerInfo attackerInfo;
    attackerInfo.set_player_id(ATTACKER_ID);
    attackerInfo.set_is_defender(false);
    attackerInfo.set_starting_food(1000);
    attackerInfo.add_victory_conditions(
            net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
    attackerInfo.add_victory_conditions(
            net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
    playerInfoProtos.push_back(attackerInfo);

    // Defender info
    net::eagle0::shardok::common::PlayerInfo defenderInfo;
    defenderInfo.set_player_id(DEFENDER_ID);
    defenderInfo.set_is_defender(true);
    defenderInfo.set_starting_food(1000);
    defenderInfo.add_victory_conditions(
            net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
    defenderInfo.add_victory_conditions(
            net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
    playerInfoProtos.push_back(defenderInfo);

    // Create units from config
    std::vector<net::eagle0::shardok::storage::fb::Unit> units;

    // Attacker units
    for (int i = 0; i < config_.attacker().units_size(); ++i) {
        const auto& unitConfig = config_.attacker().units(i);

        net::eagle0::shardok::storage::fb::Unit unit{};
        unit.mutate_player_id(ATTACKER_ID);
        unit.mutate_unit_id(i);
        unit.mutate_eagle_player_id(ATTACKER_ID);
        unit.mutable_location() = net::eagle0::shardok::storage::fb::Coords(-1, -1);
        unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
        unit.mutate_remaining_action_points(12);
        unit.mutate_hidden(false);
        unit.mutate_fortified(false);
        unit.mutate_can_flee(true);
        unit.mutate_can_start_fire(false);
        unit.mutate_can_archery(false);
        unit.mutate_stun_rounds_remaining(0);
        unit.mutate_commanding_unit_id(-1);
        unit.mutate_targeted_unit(-1);
        unit.mutate_starting_position_index(unitConfig.starting_position_index());
        unit.mutate_has_moved_in_zoc(false);
        unit.mutate_volleys_remaining(0);
        unit.mutate_food_remaining(1000.0f);

        // Battalion
        net::eagle0::shardok::storage::fb::Battalion battalion;
        const auto battalionTypeId =
                static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(
                        unitConfig.battalion_type_id());
        battalion.mutate_type(battalionTypeId);

        // Get battalion type to determine default capacity
        const auto& battalionType = gameSettings_->GetGetter().GetBattalionType(battalionTypeId);
        const double defaultSize = battalionType->capacity;

        battalion.mutate_size(GetOrDefault(unitConfig.battalion().size(), defaultSize));
        battalion.mutate_armament(
                GetOrDefault(unitConfig.battalion().armament(), DEFAULT_BATTALION_ARMAMENT));
        battalion.mutate_training(
                GetOrDefault(unitConfig.battalion().training(), DEFAULT_BATTALION_TRAINING));
        battalion.mutate_morale(
                GetOrDefault(unitConfig.battalion().morale(), DEFAULT_BATTALION_MORALE));
        unit.mutable_battalion() = battalion;

        // Hero
        if (unitConfig.profession() > 0) {
            unit.mutate_has_attached_hero(true);

            net::eagle0::shardok::storage::fb::Hero hero;
            hero.mutate_strength(GetOrDefault(unitConfig.hero().strength(), DEFAULT_HERO_STRENGTH));
            hero.mutate_strength_xp(0);
            hero.mutate_agility(GetOrDefault(unitConfig.hero().agility(), DEFAULT_HERO_AGILITY));
            hero.mutate_agility_xp(0);
            hero.mutate_wisdom(GetOrDefault(unitConfig.hero().wisdom(), DEFAULT_HERO_WISDOM));
            hero.mutate_wisdom_xp(0);
            hero.mutate_charisma(GetOrDefault(unitConfig.hero().charisma(), DEFAULT_HERO_CHARISMA));
            hero.mutate_charisma_xp(0);
            hero.mutate_constitution(
                    GetOrDefault(unitConfig.hero().constitution(), DEFAULT_HERO_CONSTITUTION));
            hero.mutate_constitution_xp(0);
            const int vigor = GetOrDefault(unitConfig.hero().vigor(), DEFAULT_HERO_VIGOR);
            hero.mutate_vigor(vigor);
            hero.mutate_starting_vigor(vigor);
            hero.mutate_spent_vigor(0);
            hero.mutate_bravery(GetOrDefault(unitConfig.hero().bravery(), DEFAULT_HERO_BRAVERY));
            hero.mutate_integrity(
                    GetOrDefault(unitConfig.hero().integrity(), DEFAULT_HERO_INTEGRITY));
            hero.mutate_ambition(GetOrDefault(unitConfig.hero().ambition(), DEFAULT_HERO_AMBITION));
            hero.mutate_eagle_hero_id(i + 1);
            hero.mutate_is_vip(false);

            hero.mutable_profession_info().mutate_profession(
                    static_cast<net::eagle0::shardok::storage::fb::Profession>(
                            unitConfig.profession()));
            hero.mutable_profession_info().mutate_meteor_cast_state(
                    net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);

            hero.mutable_control_info().mutate_controlled_unit_id(-1);
            hero.mutable_control_info().mutate_controlled_this_round(false);

            unit.mutable_attached_hero() = hero;
        } else {
            unit.mutate_has_attached_hero(false);
        }

        // Initialize opponent knowledge
        unit.mutable_opponent_knowledge()->Mutate(0, 0);
        unit.mutable_opponent_knowledge()->Mutate(1, 0);

        units.push_back(unit);
    }

    // Defender units
    int defenderUnitIdStart = config_.attacker().units_size();
    for (int i = 0; i < config_.defender().units_size(); ++i) {
        const auto& unitConfig = config_.defender().units(i);

        net::eagle0::shardok::storage::fb::Unit unit{};
        unit.mutate_player_id(DEFENDER_ID);
        unit.mutate_unit_id(defenderUnitIdStart + i);
        unit.mutate_eagle_player_id(DEFENDER_ID);
        unit.mutable_location() = net::eagle0::shardok::storage::fb::Coords(-1, -1);
        unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
        unit.mutate_remaining_action_points(0);  // Player 1 starts with 0 AP (not their turn yet)
        unit.mutate_hidden(false);
        unit.mutate_fortified(false);
        unit.mutate_can_flee(true);
        unit.mutate_can_start_fire(false);
        unit.mutate_can_archery(false);
        unit.mutate_stun_rounds_remaining(0);
        unit.mutate_commanding_unit_id(-1);
        unit.mutate_targeted_unit(-1);
        unit.mutate_starting_position_index(unitConfig.starting_position_index());
        unit.mutate_has_moved_in_zoc(false);
        unit.mutate_volleys_remaining(0);
        unit.mutate_food_remaining(1000.0f);

        // Battalion
        net::eagle0::shardok::storage::fb::Battalion battalion;
        const auto battalionTypeId =
                static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(
                        unitConfig.battalion_type_id());
        battalion.mutate_type(battalionTypeId);

        // Get battalion type to determine default capacity
        const auto& battalionType = gameSettings_->GetGetter().GetBattalionType(battalionTypeId);
        const double defaultSize = battalionType->capacity;

        battalion.mutate_size(GetOrDefault(unitConfig.battalion().size(), defaultSize));
        battalion.mutate_armament(
                GetOrDefault(unitConfig.battalion().armament(), DEFAULT_BATTALION_ARMAMENT));
        battalion.mutate_training(
                GetOrDefault(unitConfig.battalion().training(), DEFAULT_BATTALION_TRAINING));
        battalion.mutate_morale(
                GetOrDefault(unitConfig.battalion().morale(), DEFAULT_BATTALION_MORALE));
        unit.mutable_battalion() = battalion;

        // Hero
        if (unitConfig.profession() > 0) {
            unit.mutate_has_attached_hero(true);

            net::eagle0::shardok::storage::fb::Hero hero;
            hero.mutate_strength(GetOrDefault(unitConfig.hero().strength(), DEFAULT_HERO_STRENGTH));
            hero.mutate_strength_xp(0);
            hero.mutate_agility(GetOrDefault(unitConfig.hero().agility(), DEFAULT_HERO_AGILITY));
            hero.mutate_agility_xp(0);
            hero.mutate_wisdom(GetOrDefault(unitConfig.hero().wisdom(), DEFAULT_HERO_WISDOM));
            hero.mutate_wisdom_xp(0);
            hero.mutate_charisma(GetOrDefault(unitConfig.hero().charisma(), DEFAULT_HERO_CHARISMA));
            hero.mutate_charisma_xp(0);
            hero.mutate_constitution(
                    GetOrDefault(unitConfig.hero().constitution(), DEFAULT_HERO_CONSTITUTION));
            hero.mutate_constitution_xp(0);
            const int vigor = GetOrDefault(unitConfig.hero().vigor(), DEFAULT_HERO_VIGOR);
            hero.mutate_vigor(vigor);
            hero.mutate_starting_vigor(vigor);
            hero.mutate_spent_vigor(0);
            hero.mutate_bravery(GetOrDefault(unitConfig.hero().bravery(), DEFAULT_HERO_BRAVERY));
            hero.mutate_integrity(
                    GetOrDefault(unitConfig.hero().integrity(), DEFAULT_HERO_INTEGRITY));
            hero.mutate_ambition(GetOrDefault(unitConfig.hero().ambition(), DEFAULT_HERO_AMBITION));
            hero.mutate_eagle_hero_id(defenderUnitIdStart + i + 1);
            hero.mutate_is_vip(false);

            hero.mutable_profession_info().mutate_profession(
                    static_cast<net::eagle0::shardok::storage::fb::Profession>(
                            unitConfig.profession()));
            hero.mutable_profession_info().mutate_meteor_cast_state(
                    net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);

            hero.mutable_control_info().mutate_controlled_unit_id(-1);
            hero.mutable_control_info().mutate_controlled_this_round(false);

            unit.mutable_attached_hero() = hero;
        } else {
            unit.mutate_has_attached_hero(false);
        }

        // Initialize opponent knowledge
        unit.mutable_opponent_knowledge()->Mutate(0, 0);
        unit.mutable_opponent_knowledge()->Mutate(1, 0);

        units.push_back(unit);
    }

    // Create game state
    const bool isWinter = (config_.month() >= 10 || config_.month() <= 2);
    return shardok::fb::SetupInitialGameState(
            "ai_battle_sim",
            hexMapProto,
            playerInfoProtos,
            units,
            config_.month(),
            isWinter,
            gameSettings_->GetGetter());
}

std::unique_ptr<ShardokAIClient> AiBattleSimulator::CreateAIClient(
        PlayerId playerId,
        const HexMap* hexMap) const {
    const auto& playerConfig = (playerId == ATTACKER_ID) ? config_.attacker() : config_.defender();
    const bool isDefender = (playerId == DEFENDER_ID);

    AIAlgorithmType algorithmType = ConvertAIAlgorithmType(playerConfig.ai_algorithm());

    return ai_testing_common::AIClientFactory::Create(
            playerId,
            isDefender,
            hexMap,
            gameSettings_->GetGetter(),
            algorithmType);
}

BattleResult AiBattleSimulator::RunSetupPhase(
        ShardokEngine& engine,
        ShardokAIClient& attackerAI,
        ShardokAIClient& defenderAI) {
    auto getAI = [&](PlayerId playerId) -> ShardokAIClient& {
        return (playerId == ATTACKER_ID) ? attackerAI : defenderAI;
    };

    auto phaseResult = ai_testing_common::GamePhaseRunner::RunSetupPhase(engine, getAI);

    std::cout << "Setup phase complete. Commands executed: " << phaseResult.commandsExecuted
              << "\n";

    // Check if game ended unexpectedly
    if (phaseResult.gameEnded) {
        return CreateResultFromGameState(
                engine.GetCurrentGameState(),
                0,
                phaseResult.commandsExecuted);
    }

    // Return a "not finished" result
    BattleResult result;
    result.winner = -1;
    result.totalRounds = 0;
    result.totalCommands = phaseResult.commandsExecuted;
    result.endReason = BattleResult::EndReason::DRAW;  // Temporary placeholder
    result.description = "Setup phase completed";
    return result;
}

BattleResult AiBattleSimulator::RunBattlePhase(
        ShardokEngine& engine,
        ShardokAIClient& attackerAI,
        ShardokAIClient& defenderAI) {
    int totalCommands = 0;
    int currentRound = 1;

    while (!engine.GameIsOver() && currentRound <= config_.max_rounds()) {
        auto currentState = engine.GetCurrentGameState();
        PlayerId currentPlayer = currentState->current_player();

        auto availableCommands = engine.GetAvailableCommandProtos(currentPlayer, false);

        if (availableCommands.empty()) {
            std::cout << "No commands available for player " << (int)currentPlayer << "\n";
            break;
        }

        // Choose which AI to use
        ShardokAIClient& activeAI = (currentPlayer == ATTACKER_ID) ? attackerAI : defenderAI;

        // Get AI decision
        auto choiceResults = activeAI.ChooseCommandIndex(engine);

        // Apply command
        engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
        totalCommands++;

        // Check round progression
        auto newState = engine.GetCurrentGameState();
        if (newState->current_round() > currentRound) {
            std::cout << "Round " << currentRound
                      << " completed. Commands this round: " << totalCommands << "\n";
            currentRound = newState->current_round();
        }
    }

    std::cout << "Battle phase complete. Total rounds: " << currentRound
              << ", Total commands: " << totalCommands << "\n";

    return CreateResultFromGameState(engine.GetCurrentGameState(), currentRound, totalCommands);
}

bool AiBattleSimulator::IsGameOver(const GameStateW& state) const {
    const auto stateValue = state->status()->state();
    return stateValue == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY ||
           stateValue == net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW;
}

BattleResult AiBattleSimulator::CreateResultFromGameState(
        const GameStateW& state,
        int totalRounds,
        int totalCommands) const {
    BattleResult result;
    result.totalRounds = totalRounds;
    result.totalCommands = totalCommands;

    const auto* status = state->status();
    const auto stateValue = status->state();

    // Determine winner and end reason
    if (stateValue == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
        // Extract winner from winning_shardok_ids
        const auto* winningIds = status->winning_shardok_ids();
        if (winningIds && winningIds->size() > 0) {
            result.winner = winningIds->Get(0);

            // Determine how the game ended
            if (result.winner == ATTACKER_ID) {
                // Attacker won
                if (totalRounds >= config_.max_rounds()) {
                    result.endReason = BattleResult::EndReason::CRITICAL_TILES_CONTROLLED;
                    result.description = "Attacker won by controlling critical tiles";
                } else {
                    result.endReason = BattleResult::EndReason::LAST_PLAYER_STANDING;
                    result.description = "Attacker won by eliminating all defenders";
                }
            } else if (result.winner == DEFENDER_ID) {
                // Defender won
                if (totalRounds >= config_.max_rounds()) {
                    result.endReason = BattleResult::EndReason::MAX_ROUNDS_REACHED;
                    result.description = "Defender won by surviving maximum rounds";
                } else {
                    result.endReason = BattleResult::EndReason::LAST_PLAYER_STANDING;
                    result.description = "Defender won by eliminating all attackers";
                }
            } else {
                result.winner = -1;
                result.endReason = BattleResult::EndReason::DRAW;
                result.description = "Game ended with unknown winner";
            }
        } else {
            result.winner = -1;
            result.endReason = BattleResult::EndReason::DRAW;
            result.description = "Game ended with no winner";
        }
    } else if (stateValue == net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
        result.winner = -1;
        result.endReason = BattleResult::EndReason::DRAW;
        result.description = "Game ended in a draw";
    } else {
        // Game not over yet (shouldn't happen)
        result.winner = -1;
        result.endReason = BattleResult::EndReason::DRAW;
        result.description = "Game incomplete";
    }

    // Collect surviving units (for winner, or all units if draw)
    const auto* units = state->units();
    if (units) {
        for (const auto* unit : *units) {
            if (!unit) { continue; }

            // Only include units that are still alive
            // Based on GameStateFilter.cpp logic
            const bool isAlive =
                    unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT ||
                    unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT ||
                    unit->status() ==
                            net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT;

            if (!isAlive) { continue; }

            // For victories, only include winner's units; for draws, include all
            if (result.winner != -1 && unit->player_id() != result.winner) { continue; }

            SurvivingUnit survivor;
            survivor.unitId = unit->unit_id();
            survivor.battalionSize = unit->battalion().size();
            survivor.profession = unit->has_attached_hero()
                                          ? unit->attached_hero().profession_info().profession()
                                          : 0;

            // Get battalion type name
            const auto& battalionType =
                    gameSettings_->GetGetter().GetBattalionType(unit->battalion().type());
            survivor.battalionTypeName = battalionType->name;

            result.survivingUnits.push_back(survivor);
        }
    }

    return result;
}

}  // namespace ai_battle_simulator
}  // namespace shardok
