//
//  EagleInterfaceGrpcServer.cpp
//  shardok_engine
//
//  Created by Dan Crosby on 1/28/19.
//  Copyright © 2019 none. All rights reserved.
//

#include "EagleInterfaceGrpcServer.hpp"

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

#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
#include "src/main/cpp/net/eagle0/shardok/controller/ShardokGameController.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/GameOverResponsePopulator.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"

namespace shardok {
using grpc::Status;
using grpc::StatusCode;
using net::eagle0::common::GameSetupInfo;
using net::eagle0::common::VictoryCondition;
using net::eagle0::shardok::api::PlacementCommand;
using net::eagle0::shardok::common::EndGameCondition;
using std::begin;
using std::end;

class NewGameException : public std::exception {
private:
    Status status;

public:
    explicit NewGameException(const StatusCode s, const string &m) : status(s, m) {}

    auto GetStatus() const -> Status { return status; }
};

auto EagleInterfaceImpl::ControllerForGame(const GameId &gameId, const GameSetupInfo &setupInfo)
        -> std::shared_ptr<ShardokGameController> {
    auto controller = gamesManager->GetController(gameId);

    // If game exists but Eagle is requesting a fresh start with no prior history
    // (happens after admin rewind), replace the old game
    if (controller != nullptr && setupInfo.has_new_game_request() &&
        setupInfo.known_result_count() == 0) {
        printf("Replacing existing game %s with fresh game (rewind detected)\n", gameId.c_str());
        gamesManager->RemoveController(gameId);
        controller = nullptr;
    }

    if (controller == nullptr) {
        if (setupInfo.current_game_state().empty()) {
            StartGame(setupInfo.new_game_request());
        } else {
            gamesManager->UnlockedCreateFromGameStateBytes(
                    byte_vector(setupInfo.current_game_state()),
                    setupInfo.known_result_count(),
                    "");
        }

        controller = gamesManager->GetController(gameId);
    }

    if (controller == nullptr) { throw ShardokClientErrorException("Not found"); }

    return controller;
}

const std::string &kShardokGameRequestExtension = *(new string(".e0gr"));

EagleInterfaceImpl::EagleInterfaceImpl(
        shared_ptr<ShardokGamesManager> manager,
        std::string authToken)
    : gamesManager(std::move(manager)),
      tokenValidator_(std::move(authToken)) {
    if (tokenValidator_.IsEnabled()) {
        std::cout << "Token authentication enabled" << std::endl;
    } else {
        std::cout << "Token authentication disabled (no token configured)" << std::endl;
    }
}

auto ConvertPlayerInfo(
        const google::protobuf::RepeatedPtrField<net::eagle0::common::PlayerSetupInfo> &allPis,
        PlayerId shardokPid) -> PlayerInfoWithUnits {
    const net::eagle0::common::PlayerSetupInfo &pi = allPis[shardokPid];

    const bool isAi =
            pi.type() ==
            net::eagle0::common::PlayerSetupInfo::PlayerType::PlayerSetupInfo_PlayerType_AI;

    const int32_t eagleFactionId = pi.eagle_faction_id();

    vector<PlayerId> allPids{};
    for (int i = 0; i < allPis.size(); i++) { allPids.push_back(i); }

    vector<Unit> shardokUnits;
    shardokUnits.reserve(pi.units().size());
    std::ranges::transform(
            pi.units(),
            std::back_inserter(shardokUnits),
            [shardokPid, allPids](const net::eagle0::common::CommonUnit &unit) {
                return ConvertUnit(unit, shardokPid, allPids);
            });

    vector<net::eagle0::shardok::common::VictoryCondition> victoryConditions;
    victoryConditions.reserve(pi.victory_conditions().size());
    std::ranges::transform(
            pi.victory_conditions(),
            std::back_inserter(victoryConditions),
            [](const int32_t vc) { return net::eagle0::shardok::common::VictoryCondition(vc); });

    vector<PlayerId> alliedPids{};
    alliedPids.reserve(pi.allies_size());
    for (const auto &ally : pi.allies()) {
        const int32_t alliedFid = ally.eagle_faction_id();
        for (int i = 0; i < allPis.size(); i++) {
            if (allPis[i].eagle_faction_id() == alliedFid) {
                alliedPids.push_back(i);
                break;
            }
        }
    }

    return PlayerInfoWithUnits(
            shardokPid,
            isAi,
            eagleFactionId,
            pi.defender(),
            pi.food(),
            victoryConditions,
            alliedPids,
            shardokUnits,
            pi.cannot_become_outlaw());
}

void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
    // Get the custom map, or the map name if there isn't one
    const string &mapName = request.hex_map_name();
    const string &customMapBytes = request.custom_map_bytes();
    if (mapName.empty() && customMapBytes.empty()) {
        throw NewGameException(
                StatusCode::INVALID_ARGUMENT,
                "Must include map path unless a custom map is present");
    }

    const int month = request.month();
    if (month == 0) {
        throw NewGameException(StatusCode::INVALID_ARGUMENT, "Must include battle month");
    }

    // Also grab a collection of all the hero IDs for logging
    vector<HeroId> heroIds{};
    for (const auto &info : request.user_infos()) {
        for (const auto &unit : info.units()) { heroIds.push_back(unit.hero().eagle_hero_id()); }
    }
    std::sort(heroIds.begin(), heroIds.end());

    // Convert each interface player struct into the Shardok storage equivalents
    vector<PlayerInfoWithUnits> playersInfo{};
    playersInfo.reserve(request.user_infos_size());
    for (int i = 0; i < request.user_infos_size(); i++) {
        playersInfo.push_back(ConvertPlayerInfo(request.user_infos(), (PlayerId)i));
    }

    // Validate that exactly one player has WIN_AFTER_MAX_ROUNDS
    int winAfterMaxRoundsCount = 0;
    for (const auto &psi : playersInfo) {
        for (const auto &vc : psi.playerInfo.victory_conditions()) {
            if (vc == net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS) {
                winAfterMaxRoundsCount++;
            }
        }
    }
    if (winAfterMaxRoundsCount != 1) {
        throw NewGameException(
                StatusCode::INVALID_ARGUMENT,
                "Exactly one player must have WIN_AFTER_MAX_ROUNDS victory condition, found " +
                        std::to_string(winAfterMaxRoundsCount));
    }

    // Grab the nonempty gameId
    const string &gameId = request.game_id();
    if (gameId.empty()) {
        throw NewGameException(StatusCode::INVALID_ARGUMENT, "Must include game ID");
    }

    string unitsString;
    for (const auto &hid : heroIds) { unitsString += std::to_string(hid) + " "; }

    printf("Starting game %s with units %s\n", gameId.c_str(), unitsString.c_str());
    gamesManager->UnlockedCreateSpecifiedGame(
            gameId,
            playersInfo,
            mapName,
            customMapBytes,
            month,
            request.tutorial_battle_config(),
            request.SerializeAsString());
}

auto EagleInterfaceImpl::PostCommand(
        ServerContext *context,
        const PostCommandRequest *request,
        GameStatusResponse *response) -> Status {
    // Validate auth token
    if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }

    shared_ptr<ShardokGameController> controller;
    try {
        controller = ControllerForGame(request->game_id(), request->game_setup_info());
    } catch (NewGameException &e) { return e.GetStatus(); }
    try {
        controller->PostCommand(
                (PlayerId)request->player_id(),
                request->eagle_faction_id(),
                request->token(),
                request->index(),
                request->has_roll() ? std::optional<int32_t>(request->roll().value())
                                    : std::optional<int32_t>());

        PopulateGameStatusResponse(
                controller,
                request->game_setup_info().known_result_count(),
                response);
    } catch (ShardokClientErrorException &e) {
        std::cerr << "Bad request from client: " << e.what() << std::endl;
        return Status(StatusCode::INVALID_ARGUMENT, e.what());
    } catch (ShardokInternalErrorException &e) {
        std::cerr << "Internal error: " << e.what() << std::endl;
        return Status(StatusCode::INTERNAL, e.what());
    } catch (std::exception &e) {
        std::cerr << "Unknown exception: " << e.what() << std::endl;
        return Status(StatusCode::INTERNAL, e.what());
    }

    return Status::OK;
}

auto EagleInterfaceImpl::PostPlacementCommands(
        ServerContext *context,
        const PlacementCommandsRequest *request,
        GameStatusResponse *response) -> Status {
    // Validate auth token
    if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }

    shared_ptr<ShardokGameController> controller;
    try {
        controller = ControllerForGame(request->game_id(), request->game_setup_info());
    } catch (NewGameException &e) { return e.GetStatus(); }
    try {
        vector<UnitPlacementInfo> upis;
        upis.reserve(request->placement_commands().size());
        std::ranges::transform(
                request->placement_commands(),
                std::back_inserter(upis),
                [](const net::eagle0::shardok::api::PlacementCommand &cmd) {
                    return UnitPlacementInfo(
                            (UnitId)cmd.unit_id(),
                            Coords(cmd.row(), cmd.column()));
                });
        controller->PostPlacementCommands(
                (PlayerId)request->player_id(),
                request->eagle_faction_id(),
                request->token(),
                upis);

        PopulateGameStatusResponse(
                controller,
                request->game_setup_info().known_result_count(),
                response);
    } catch (ShardokClientErrorException &e) {
        std::cerr << "Bad request from client: " << e.what() << std::endl;
        return Status(StatusCode::INVALID_ARGUMENT, e.what());
    } catch (ShardokInternalErrorException &e) {
        std::cerr << "Internal error: " << e.what() << std::endl;
        return Status(StatusCode::INTERNAL, e.what());
    } catch (std::exception &e) {
        std::cerr << "Unknown exception: " << e.what() << std::endl;
        return Status(StatusCode::INTERNAL, e.what());
    }

    return Status::OK;
}

void EagleInterfaceImpl::PopulateGameStatusResponse(
        shared_ptr<ShardokGameController> &controller,
        const int64_t startingActionId,
        GameStatusResponse *response) {
    response->set_game_id(controller->GetGameId());

    auto results = controller->GetUpdates(startingActionId);

    if (results.mainResults.empty()) {
        // check for game over
        if (controller->GameIsOver()) {
            PopulateGameOverResponse(
                    controller->GetGameId(),
                    controller->GameOverStatus(),
                    controller->ResolvedPlayerInfos(),
                    controller->EndGameUnits(),
                    response->mutable_game_over_response());
            auto status = controller->GameOverStatus();
            // player resolution infos
            //            response->mutable_game_over_response()->mutable_user_infos() ...
        }
    } else {
        response->mutable_game_update_response()->mutable_update_responses()->Add(
                begin(results.mainResults),
                end(results.mainResults));
        for (const auto &op : results.filteredResults) {
            auto *newFilteredResponse =
                    response->mutable_game_update_response()->add_filtered_update_responses();
            newFilteredResponse->set_eagle_faction_id(op.eagleFactionId);
            newFilteredResponse->mutable_action_result_views()->Add(
                    begin(op.resultViews),
                    end(op.resultViews));

            if (op.availableCommands) {
                *newFilteredResponse->mutable_available_commands() = *op.availableCommands;
            }
        }
        response->mutable_game_update_response()->set_total_action_result_count(
                results.newUnfilteredCount);

        *response->mutable_game_update_response()->mutable_current_game_state() =
                string(controller->GetCurrentGameStateBytes());

        response->mutable_game_update_response()->set_current_round(results.currentRound);
        for (const auto &[playerId, troopCount] : results.playerTroopCounts) {
            auto *ptc = response->mutable_game_update_response()->add_player_troop_counts();
            ptc->set_player_id(playerId);
            ptc->set_troop_count(troopCount);
        }
    }
}

auto EagleInterfaceImpl::GetHexMap(
        ServerContext *context,
        const HexMapRequest *request,
        HexMapResponse *response) -> Status {
    // Validate auth token
    if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }

    // TODO: return appropriate status code if a bad map name is sent
    *response->mutable_map() = LoadMap(request->map_name());

    return Status::OK;
}

auto EagleInterfaceImpl::GetHexMapNames(
        ServerContext *context,
        const HexMapNamesRequest * /*request*/,
        HexMapNamesResponse *response) -> Status {
    // Validate auth token
    if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }

    for (const string &mapName : GetMapNames()) { response->add_map_names(mapName); }

    return Status::OK;
}

auto EagleInterfaceImpl::SubscribeToGame(
        ServerContext *context,
        const GameSubscriptionRequest *request,
        grpc::ServerWriter<GameStatusResponse> *writer) -> Status {
    // Validate auth token
    if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }

    shared_ptr<ShardokGameController> controller;
    try {
        controller = ControllerForGame(request->game_id(), request->game_setup_info());
    } catch (NewGameException &e) { return e.GetStatus(); } catch (ShardokClientErrorException &e) {
        std::cerr << "Bad request from client: " << e.what() << std::endl;
        return Status(StatusCode::INVALID_ARGUMENT, e.what());
    } catch (ShardokInternalErrorException &e) {
        std::cerr << "Internal error: " << e.what() << std::endl;
        return Status(StatusCode::INTERNAL, e.what());
    } catch (std::exception &e) {
        std::cerr << "Unknown exception in SubscribeToGame: " << e.what() << std::endl;
        return Status(StatusCode::INTERNAL, e.what());
    }

    if (!controller) { return Status(StatusCode::NOT_FOUND, "Game not found"); }

    // Send initial state
    GameStatusResponse initialResponse;
    PopulateGameStatusResponse(
            controller,
            request->game_setup_info().known_result_count(),
            &initialResponse);
    if (!writer->Write(initialResponse)) {
        return Status::OK;  // Client disconnected
    }

    // If game was already over, we're done
    if (initialResponse.has_game_over_response()) { return Status::OK; }

    // Track the actual count after initial response to avoid duplicate sends
    const int64_t countAfterInitialResponse =
            initialResponse.has_game_update_response()
                    ? initialResponse.game_update_response().total_action_result_count()
                    : request->game_setup_info().known_result_count();

    // Create a subscriber that writes to the gRPC stream
    class GrpcStreamSubscriber : public StreamSubscriber {
    private:
        grpc::ServerWriter<GameStatusResponse> *writer_;
        ServerContext *context_;
        std::atomic<bool> active_{true};
        std::string gameId_;

    public:
        GrpcStreamSubscriber(
                grpc::ServerWriter<GameStatusResponse> *w,
                ServerContext *ctx,
                std::string gameId)
            : writer_(w),
              context_(ctx),
              gameId_(std::move(gameId)) {}

        void OnUpdate(
                const vector<ActionResult> &mainResults,
                const vector<OnePlayerUpdates> &filteredResults,
                int32_t newUnfilteredCount,
                const byte_vector &currentGameState,
                int32_t currentRound,
                const vector<std::pair<int32_t, int32_t>> &playerTroopCounts) override {
            if (!active_) return;

            GameStatusResponse response;
            response.set_game_id(gameId_);

            response.mutable_game_update_response()->mutable_update_responses()->Add(
                    begin(mainResults),
                    end(mainResults));

            // Add filtered results for each player with their faction IDs
            for (const auto &playerUpdate : filteredResults) {
                auto *filtered =
                        response.mutable_game_update_response()->add_filtered_update_responses();
                filtered->set_eagle_faction_id(playerUpdate.eagleFactionId);
                filtered->mutable_action_result_views()->Add(
                        begin(playerUpdate.resultViews),
                        end(playerUpdate.resultViews));
                if (playerUpdate.availableCommands) {
                    *filtered->mutable_available_commands() = *playerUpdate.availableCommands;
                }
            }

            response.mutable_game_update_response()->set_total_action_result_count(
                    newUnfilteredCount);
            *response.mutable_game_update_response()->mutable_current_game_state() =
                    std::string(currentGameState.begin(), currentGameState.end());

            response.mutable_game_update_response()->set_current_round(currentRound);
            for (const auto &[playerId, troopCount] : playerTroopCounts) {
                auto *ptc = response.mutable_game_update_response()->add_player_troop_counts();
                ptc->set_player_id(playerId);
                ptc->set_troop_count(troopCount);
            }

            if (!writer_->Write(response)) { active_ = false; }
        }

        void OnGameOver(const GameOverInfo &info) override {
            if (!active_) return;

            GameStatusResponse response;
            response.set_game_id(gameId_);

            PopulateGameOverResponse(
                    gameId_,
                    info.gameStatus,
                    info.playerInfos,
                    info.endGameUnits,
                    response.mutable_game_over_response());

            writer_->Write(response);
            active_ = false;
        }

        void OnBattleReset() override {
            if (!active_) return;

            GameStatusResponse response;
            response.set_game_id(gameId_);
            response.mutable_battle_reset_response();

            if (!writer_->Write(response)) { active_ = false; }
        }

        [[nodiscard]] auto IsActive() const -> bool override {
            return active_ && !context_->IsCancelled();
        }
    };

    auto subscriber =
            std::make_shared<GrpcStreamSubscriber>(writer, context, controller->GetGameId());

    controller->RegisterSubscriber(subscriber);

    // Wait for updates and push them until game ends or subscriber disconnects.
    // For tutorial battles, a BATTLE_RESET result means the defender lost and we
    // should restart the game from the original request.
    int64_t actionCount = countAfterInitialResponse;
    WaitResult result = WaitResult::GAME_OVER;

    while (true) {
        result = controller->WaitForUpdatesAndPush(subscriber, actionCount);

        if (result != WaitResult::BATTLE_RESET) break;

        printf("SubscribeToGame: Tutorial battle reset, restarting game %s\n",
               controller->GetGameId().c_str());

        // Save the serialized request before removing the controller
        const std::string savedRequest = controller->SerializedRequest();
        const GameId gameId = controller->GetGameId();

        controller->UnregisterSubscriber(subscriber.get());

        gamesManager->RemoveController(gameId);

        // Deserialize and restart the game from the original request
        NewGameRequest newGameRequest;
        newGameRequest.ParseFromString(savedRequest);
        StartGame(newGameRequest);

        controller = gamesManager->GetController(gameId);
        if (!controller) {
            printf("SubscribeToGame: Failed to restart game after reset\n");
            return Status(StatusCode::INTERNAL, "Failed to restart tutorial battle");
        }

        // Send initial state from the new game
        GameStatusResponse resetInitialResponse;
        PopulateGameStatusResponse(controller, 0, &resetInitialResponse);
        if (!writer->Write(resetInitialResponse)) break;

        // Reset action count based on the new game's state
        actionCount =
                resetInitialResponse.has_game_update_response()
                        ? resetInitialResponse.game_update_response().total_action_result_count()
                        : 0;

        // Re-register subscriber with the new controller
        controller->RegisterSubscriber(subscriber);
    }

    controller->UnregisterSubscriber(subscriber.get());

    if (controller->HasAIThreadError()) {
        std::cerr << "SubscribeToGame: AI thread error: " << controller->GetAIThreadError()
                  << std::endl;
        return Status(StatusCode::INTERNAL, controller->GetAIThreadError());
    }

    if (result == WaitResult::GAME_OVER) {
        printf("SubscribeToGame: Game ended normally\n");
    } else {
        printf("SubscribeToGame: Subscriber disconnected\n");
    }

    return Status::OK;
}

}  // namespace shardok

#ifndef NDEBUG
template class std::vector<net::eagle0::shardok::storage::ResolvedUnit>;
#endif
