//
//  ShardokGameController.cpp
//  eagle0_engine
//
//  Created by Dan Crosby on 7/9/17.
//  Copyright © 2017 none. All rights reserved.
//

#include "ShardokGameController.hpp"

#include <execinfo.h>

#include <algorithm>
#include <iterator>
#include <ranges>
#include <thread>

#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"

namespace shardok {
using std::condition_variable;
using std::mutex;
using std::scoped_lock;
using std::string;
using std::unique_lock;
using std::weak_ptr;

using net::eagle0::shardok::common::GameStatus;

class InvalidTokenException : public std::exception {};

class MismatchedPlayerIdException : public std::exception {
private:
    PlayerId shardokPlayerId;
    int eagleFactionId;

    string message;

public:
    MismatchedPlayerIdException(const PlayerId shardokPid, const int eagleFid)
        : shardokPlayerId(shardokPid),
          eagleFactionId(eagleFid) {
        message = "Mismatched playerId " + std::to_string(shardokPlayerId) +
                  " with eagle factionId " + std::to_string(eagleFactionId);
    }

    [[nodiscard]] auto what() const noexcept -> const char * override { return message.c_str(); }
};

class MissingPlayerIdException : public std::exception {
private:
    PlayerId shardokPlayerId;

    string message;

public:
    MissingPlayerIdException(const PlayerId shardokPid) : shardokPlayerId(shardokPid) {
        message = "Missing playerId " + std::to_string(shardokPlayerId);
    }

    [[nodiscard]] auto what() const noexcept -> const char * override { return message.c_str(); }
};

vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
        const unique_ptr<ShardokEngine> &e) {
    vector<shared_ptr<ShardokAIClient>> aic;

    mcts::MCTSConfig mctsConfig;
    mctsConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::MINIMAX;
    mctsConfig.maxPlayerFlips = 1;
    mctsConfig.maxSimulationFlips = 1;
    mctsConfig.simulationPolicy = mcts::MCTSSimulationPolicy::WEIGHTED_HEURISTIC;

    // Check if all players are AI - if so, use faster time budgets
    const auto &playerInfos = e->GetPlayerInfos();
    const bool isAllAiBattle =
            std::ranges::all_of(playerInfos, [](const auto &pi) { return pi.is_ai(); });

    for (const auto &pi : playerInfos) {
        if (pi.is_ai()) {
            auto newClient = std::make_shared<ShardokAIClient>(
                    pi.player_id(),
                    pi.is_defender(),
                    isAllAiBattle,
                    e->GetCurrentGameState()->hex_map(),
                    e->GetGameSettings()->GetGetter(),
                    AIAlgorithmType::ITERATIVE_DEEPENING,
                    ScoringCalculatorType::STANDARD,
                    mctsConfig);

            // MCTS config is dynamically adjusted in ShardokAIClient based on proximity:
            // - Far from enemy: AVERAGING with maxPlayerFlips=0 (natural wandering prevention)
            // - Close to enemy: MINIMAX with maxPlayerFlips=1 (adversarial lookahead)

            aic.push_back(newClient);
        }
    }

    return aic;
}

void ShardokGameController::LockedNotifyClients() const { updateCondition.notify_all(); }

auto ShardokGameController::LockedAIClientForPid(PlayerId pid) const
        -> shared_ptr<ShardokAIClient> {
    const auto it = std::ranges::find_if(aiClients, [pid](const auto &client) {
        return client->GetPlayerId() == pid;
    });
    return (it != aiClients.end()) ? *it : nullptr;
}

void ShardokGameController::DoAIThread() {
    printf("Entering AI thread.\n");
    if (aiClients.empty()) { printf("No AI players, exiting AI thread.\n"); }

    while (aiThreadKeepGoing) {
        // Phase 1: Gather data for AI decision (brief lock)
        shared_ptr<ShardokAIClient> aiClient;
        PlayerId playerId;
        GameSettingsSPtr settings;
        net::eagle0::shardok::api::GameStateView gsv;
        CommandListSPtr availableCommands;
        size_t expectedHistoryCount;

        {
            unique_lock lk(masterLock);

            if (engine->GameIsOver()) {
                aiThreadKeepGoing = false;
                continue;
            }

            playerId = engine->GetCurrentPlayerId();
            aiClient = LockedAIClientForPid(playerId);

            if (!aiClient) {
                // Not an AI player's turn - wait for signal
                aiCondition.wait(lk);
                aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
                continue;
            }

            // Get copies of everything the AI needs
            settings = engine->GetGameSettings();
            gsv = engine->GetGameStateView(playerId);
            availableCommands = engine->GetAvailableCommandsForAIPlayer(playerId);
            expectedHistoryCount = engine->GetUnfilteredHistoryCount();
        }
        // Lock released - polls can now get through

        if (availableCommands->empty()) {
            printf("no commands for player %d\n", playerId);
            continue;
        }

        try {
            // Phase 2: AI thinks (NO LOCK - this is the slow part)
            const auto results = aiClient->ChooseCommandIndex(settings, gsv, availableCommands);

            // Phase 3: Post the command (brief lock)
            {
                unique_lock lk(masterLock);

                // Verify state hasn't changed while we were thinking
                if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
                    // State changed (e.g., human posted command) - re-evaluate
                    printf("AI: State changed while thinking, re-evaluating\n");
                    continue;
                }

                if (engine->GameIsOver()) {
                    aiThreadKeepGoing = false;
                    continue;
                }

                engine->PostCommand(playerId, results.chosenIndex);
                LockedNotifyClients();
                aiThreadKeepGoing = !engine->GameIsOver();
            }
        } catch (const std::exception &e) {
            fprintf(stderr,
                    "AI thread exception in game %s, player %d, round %d: %s\n",
                    cachedGameId.c_str(),
                    playerId,
                    gsv.current_round(),
                    e.what());

            void *backtraceArray[20];
            const int backtraceSize = backtrace(backtraceArray, 20);
            backtrace_symbols_fd(backtraceArray, backtraceSize, STDERR_FILENO);

            aiThreadErrorMessage = "AI error in game " + cachedGameId + ", player " +
                                   std::to_string(playerId) + ", round " +
                                   std::to_string(gsv.current_round()) + ": " + e.what();
            aiThreadFailed.store(true);
            updateCondition.notify_all();
            break;
        }
    }
    printf("Exiting AI thread.\n");
}

ShardokGameController::~ShardokGameController() {
    {
        unique_lock lk(masterLock);
        aiThreadKeepGoing = false;
        aiCondition.notify_one();
    }
    aiThread.join();
}

void CheckFactionId(
        const unique_ptr<ShardokEngine> &engine,
        const PlayerId shardokPlayerId,
        const int eagleFactionId) {
    bool found = false;
    for (const auto &pi : *engine->GetCurrentGameState()->player_infos()) {
        if (pi->player_id() != shardokPlayerId) continue;
        found = true;
        if (pi->eagle_faction_id() != eagleFactionId) {
            throw MismatchedPlayerIdException(shardokPlayerId, eagleFactionId);
        }
    }

    if (!found) throw MissingPlayerIdException(shardokPlayerId);
}

void ShardokGameController::PostCommand(
        const PlayerId shardokPlayerId,
        const int eagleFactionId,
        const int64_t token,
        const int64_t commandIndex,
        const optional<int32_t> roll) {
    scoped_lock<mutex> guard(masterLock);

    CheckFactionId(engine, shardokPlayerId, eagleFactionId);

    const auto expectedToken = static_cast<int64_t>(engine->GetUnfilteredHistoryCount());
    if (token < expectedToken) {
        printf("Double token in postCommand\n");
        // The client is missing some updates; probably it's a double-submit
        // Let's just ignore and see how it goes
    } else if (token > expectedToken) {
        // This shouldn't be possible
        throw InvalidTokenException();
    } else {
        engine->PostCommand(shardokPlayerId, commandIndex, nullptr, roll);
        LockedNotifyClients();
        engine->PostWhileCurrentPlayerHasOnlyOneOption(nullptr);
    }

    LockedNotifyClients();
    aiCondition.notify_one();
}

void ShardokGameController::PostPlacementCommands(
        const PlayerId shardokPlayerId,
        const int eagleFactionId,
        const int64_t token,
        const vector<UnitPlacementInfo> &infos) {
    {
        scoped_lock<mutex> guard(masterLock);

        CheckFactionId(engine, shardokPlayerId, eagleFactionId);

        const auto expectedToken = static_cast<int64_t>(engine->GetUnfilteredHistoryCount());
        if (token < expectedToken) {
            printf("Double token in postPlacementCommands\n");
            // The client is missing some updates; probably it's a double-submit
            // Let's just ignore and see how it goes
        } else if (token > expectedToken) {
            // This shouldn't be possible
            throw InvalidTokenException();
        } else {
            // The token matches, execute the commands
            engine->PostPlacementCommands(shardokPlayerId, infos);
            engine->PostFinishedPlacementCommand(shardokPlayerId);
            engine->PostWhileCurrentPlayerHasOnlyOneOption(nullptr);
        }

        LockedNotifyClients();
        aiCondition.notify_one();
    }
}

auto ShardokGameController::GetCurrentGameStateBytes() -> byte_vector {
    scoped_lock<mutex> guard(masterLock);
    return engine->GetCurrentGameStateBytes();
}

auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpdates {
    AllUpdates updates{};

    incomingRegistrations++;
    vector<net::eagle0::shardok::storage::ShardokActionWithResultingState> awrs;
    {
        unique_lock<mutex> guard(masterLock);

        updates.newUnfilteredCount = engine->GetUnfilteredHistoryCount();

        awrs = engine->GetGameHistory(startingActionId);
        incomingRegistrations--;

        // Note: Previously this had a 5-second wait for AI players to support long-polling.
        // With streaming (WaitForUpdatesAndPush), the caller already waits for updates,
        // so this wait is no longer needed. GetGameStatus is deprecated in favor of streaming.

        updates.mainResults.reserve(awrs.size());
        std::ranges::transform(
                awrs,
                std::back_inserter(updates.mainResults),
                [](const ShardokActionWithResultingState &a) { return a.action_result(); });

        const auto playerInfos = engine->GetPlayerInfos();
        updates.filteredResults.reserve(playerInfos.size() + 1);
        for (const auto &pi : playerInfos) {
            if (pi.is_ai()) continue;

            auto fid = pi.eagle_faction_id();

            auto actionResultViews = engine->FilterNewResults(pi.player_id(), startingActionId);

            shared_ptr<AvailableCommands> acs = nullptr;

            const auto availableCommands = engine->GetAvailableCommandProtos(pi.player_id(), true);
            const auto previewCommands = engine->GetPreviewCommands(pi.player_id());

            if (!availableCommands.empty() || !previewCommands.empty()) {
                acs = std::make_shared<AvailableCommands>();
                *acs->mutable_current_command() = {
                        std::begin(availableCommands),
                        std::end(availableCommands)};
                *acs->mutable_preview_command() = {
                        std::begin(previewCommands),
                        std::end(previewCommands)};
            }

            updates.filteredResults.emplace_back(fid, actionResultViews, acs);
        }

        updates.filteredResults.emplace_back(
                -1,
                engine->FilterNewResults(-1, startingActionId),
                nullptr);

        auto gameStateBytes = engine->GetCurrentGameStateBytes();
        updates.currentGameState.swap(gameStateBytes);

        // Extract battle progress data from the FlatBuffer game state
        const auto *gs = engine->GetCurrentGameState().Get();
        updates.currentRound = gs->current_round();
        for (const auto *player : *gs->player_infos()) {
            int32_t troopCount = 0;
            for (const auto *unit : *gs->units()) {
                if (unit->player_id() != player->player_id()) continue;
                switch (unit->status()) {
                    case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
                    case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
                    case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
                        troopCount += unit->battalion().size();
                        break;
                    default: break;
                }
            }
            updates.playerTroopCounts.emplace_back(player->player_id(), troopCount);
        }
    }

    return updates;
}

auto ShardokGameController::GameIsOver() -> bool {
    scoped_lock<mutex> guard(masterLock);

    const auto &gameStatus = engine->GetGameStatus();
    return ShardokEngine::GameIsOver(gameStatus);
}

auto ShardokGameController::GameOverStatus() -> net::eagle0::shardok::common::GameStatus {
    scoped_lock<mutex> guard(masterLock);

    return fb::ToProto(engine->GetGameStatus());
}

auto ShardokGameController::EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit> {
    scoped_lock<mutex> guard(masterLock);
    return engine->EndGameUnits();
}

auto ShardokGameController::ResolvedPlayerInfos()
        -> vector<net::eagle0::shardok::common::PlayerInfo> {
    scoped_lock<mutex> guard(masterLock);
    return engine->GetPlayerInfos();
}

void ShardokGameController::RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber) {
    scoped_lock<mutex> guard(subscriberLock);
    subscribers.push_back(subscriber);
}

void ShardokGameController::UnregisterSubscriber(const StreamSubscriber *subscriber) {
    scoped_lock<mutex> guard(subscriberLock);
    subscribers.erase(
            std::remove_if(
                    subscribers.begin(),
                    subscribers.end(),
                    [subscriber](const std::weak_ptr<StreamSubscriber> &weakSub) {
                        auto sub = weakSub.lock();
                        return !sub || sub.get() == subscriber;
                    }),
            subscribers.end());
}

auto ShardokGameController::WaitForUpdatesAndPush(
        std::shared_ptr<StreamSubscriber> subscriber,
        int64_t startingActionId) -> WaitResult {
    int64_t lastPushedActionId = startingActionId;

    while (subscriber->IsActive()) {
        bool gameOver = false;
        bool tutorialReset = false;
        GameOverInfo gameOverInfo{};

        {
            unique_lock<mutex> guard(masterLock);

            // Wait for updates, game over, or AI thread failure
            updateCondition.wait(guard, [this, lastPushedActionId] {
                return engine->GetUnfilteredHistoryCount() >
                               static_cast<size_t>(lastPushedActionId) ||
                       engine->GameIsOver() || aiThreadFailed.load();
            });

            if (!subscriber->IsActive()) { return WaitResult::DISCONNECTED; }

            if (aiThreadFailed.load()) { return WaitResult::DISCONNECTED; }

            gameOver = engine->GameIsOver();

            if (gameOver) {
                // Check if this is a tutorial battle where the defender lost
                tutorialReset = engine->IsTutorialBattleEnabled() && engine->DidDefenderLose();

                if (!tutorialReset) {
                    gameOverInfo.gameStatus = fb::ToProto(engine->GetGameStatus());
                    gameOverInfo.playerInfos = engine->GetPlayerInfos();
                    gameOverInfo.endGameUnits = engine->EndGameUnits();
                }
            }
        }
        // Lock released - GetUpdates will acquire its own lock

        // IMPORTANT: Send any pending updates FIRST, including the final actions
        // that caused the game to end. Previously, we returned early on gameOver
        // without sending these final updates, causing the client to never see
        // the last few battle actions.
        AllUpdates updates = GetUpdates(lastPushedActionId);
        lastPushedActionId = updates.newUnfilteredCount;

        if (!updates.mainResults.empty()) {
            subscriber->OnUpdate(
                    updates.mainResults,
                    updates.filteredResults,
                    updates.newUnfilteredCount,
                    updates.currentGameState,
                    updates.currentRound,
                    updates.playerTroopCounts);
        }

        // Tutorial battle reset: defender lost, signal reset instead of game over
        if (tutorialReset) {
            subscriber->OnBattleReset();
            return WaitResult::BATTLE_RESET;
        }

        // Now send gameOver notification after all updates have been sent
        if (gameOver) {
            subscriber->OnGameOver(gameOverInfo);
            return WaitResult::GAME_OVER;
        }
    }

    return WaitResult::DISCONNECTED;  // Subscriber disconnected
}

}  // namespace shardok
