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

#include "EagleInterfaceGrpcServer.hpp"

#include <thread>
#include <utility>

#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#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/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 (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;
}

static auto PopulateGameOverResponse(
        const string &gameId,
        const GameStatus &gameStatus,
        const vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
        const vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
        net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;

static auto IncludeUnitProtoInReturn(
        const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;

static auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;

static auto FromInternalStatus(
        const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
        -> net::eagle0::common::UnitStatus;

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

EagleInterfaceImpl::EagleInterfaceImpl(shared_ptr<ShardokGamesManager> manager)
    : gamesManager(std::move(manager)) {}

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); }

    const vector<Unit> shardokUnits = common::MapToVector(
            pi.units(),
            [shardokPid, allPids](const net::eagle0::common::CommonUnit &unit) {
                return ConvertUnit(unit, shardokPid, allPids);
            });

    const vector<net::eagle0::shardok::common::VictoryCondition> victoryConditions =
            common::MapToVector(pi.victory_conditions(), [](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);
}

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));
    }

    // FIXME: validate sanity of the victory conditions

    // 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.SerializeAsString());
}

auto EagleInterfaceImpl::PostCommand(
        ServerContext *context,
        const PostCommandRequest *request,
        GameStatusResponse *response) -> Status {
    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!" << std::endl;
        std::cerr << "Exception: " << e.what();

        throw e;
    } catch (ShardokInternalErrorException &e) {
        std::cerr << "ShardokInternalErrorException thrown!" << std::endl;
        std::cerr << "Exception: " << e.what();

        throw e;
    } catch (std::exception &e) {
        std::cerr << "Unknown exception in client->PostCommand" << std::endl;
        std::cerr << "Exception: " << e.what();

        throw e;
    }

    return Status::OK;
}

auto EagleInterfaceImpl::PostPlacementCommands(
        ServerContext *context,
        const PlacementCommandsRequest *request,
        GameStatusResponse *response) -> Status {
    shared_ptr<ShardokGameController> controller;
    try {
        controller = ControllerForGame(request->game_id(), request->game_setup_info());
    } catch (NewGameException &e) { return e.GetStatus(); }
    try {
        const auto upis = common::MapToVector(
                request->placement_commands(),
                [](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!" << std::endl;
        std::cerr << "Exception: " << e.what();

        throw e;
    } catch (ShardokInternalErrorException &e) {
        std::cerr << "ShardokInternalErrorException thrown!" << std::endl;
        std::cerr << "Exception: " << e.what();

        throw e;
    } catch (std::exception &e) {
        std::cerr << "Unknown exception in client->PostCommand" << std::endl;
        std::cerr << "Exception: " << e.what();

        throw e;
    }

    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());
    }
}

auto EagleInterfaceImpl::GetGameStatus(
        ServerContext *context,
        const GameStatusRequest *request,
        GameStatusResponse *response) -> Status {
    shared_ptr<ShardokGameController> controller;
    try {
        controller = ControllerForGame(request->game_id(), request->game_setup_info());
    } catch (NewGameException &e) { return e.GetStatus(); }

    PopulateGameStatusResponse(
            controller,
            request->game_setup_info().known_result_count(),
            response);

    return Status::OK;
}

auto EagleInterfaceImpl::GetHexMap(
        ServerContext *context,
        const HexMapRequest *request,
        HexMapResponse *response) -> Status {
    // 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 {
    for (const string &mapName : GetMapNames()) { response->add_map_names(mapName); }

    return Status::OK;
}

auto PopulateGameOverResponse(
        const string &gameId,
        const GameStatus &gameStatus,
        const vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
        const vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
        net::eagle0::common::GameOverResponse *gameOverResponse) -> bool {
    const auto &endCondition = gameStatus.end_game_condition();

    int32_t winningFactionId = -1;

    for (const auto &rpi : resolvedPlayerInfos) {
        //    for (int shardokPid = 0; shardokPid < request.user_infos_size(); shardokPid++) {
        auto shardokPid = rpi.player_id();
        const int32_t eagleFactionId = rpi.eagle_faction_id();

        //  const net::eagle0::common::PlayerSetupInfo &playerSetupInfo =
        //  request.user_infos(shardokPid);
        auto *resolutionInfo = gameOverResponse->add_user_infos();

        // Set the endgame condition (win/lose/draw) appropriately
        resolutionInfo->set_eagle_faction_id(eagleFactionId);
        if (endCondition.condition_case() == EndGameCondition::kDraw) {
            // Everyone gets a draw
            resolutionInfo->mutable_end_game_condition()->set_draw(
                    net::eagle0::common::DrawType(endCondition.draw()));
        } else if (common::Contains(gameStatus.winning_shardok_ids(), shardokPid)) {
            // The winning player gets the victory set
            resolutionInfo->mutable_end_game_condition()->set_victory(
                    net::eagle0::common::VictoryCondition(endCondition.victory()));
            winningFactionId = eagleFactionId;
        } else if (common::ContainsWhere(
                           common::FlatMapToVector(
                                   gameStatus.winning_shardok_ids(),
                                   [&rpi](const PlayerId pid) { return rpi.allies(); }),
                           [shardokPid](const net::eagle0::shardok::common::AlliedPlayer &ap) {
                               return ap.player_id() == shardokPid;
                           })) {
            // Allies of the winning player get an allied victory set
            resolutionInfo->mutable_end_game_condition()->set_ally_victory(
                    net::eagle0::common::VictoryCondition(endCondition.victory()));
        } else {
            resolutionInfo->mutable_end_game_condition()->set_loss(
                    net::eagle0::common::VictoryCondition(endCondition.victory()));
        }

        // copy in all the resolvedUnits that correspond to this player
        for (const auto &internalRu : resolvedUnits) {
            const auto unit = (Unit *)internalRu.unit_bytes().data();
            if (!unit->has_attached_hero()) continue;
            if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
            if (unit->player_id() != shardokPid) continue;

            auto *ru = resolutionInfo->add_units();
            *ru->mutable_unit() = FromUnitFb(*unit);
            ru->set_status(FromInternalStatus(internalRu.status()));
        }
    }

    printf("Finishing game %s: ", gameId.c_str());
    if (endCondition.condition_case() == EndGameCondition::kDraw) {
        printf("It's a draw!\n");
    } else {
        printf("%d wins\n", winningFactionId);
    }

    return true;
}

auto IncludeUnitProtoInReturn(
        const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool {
    switch (internalStatus) {
        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT: return true;

        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
            return false;

        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
        case net::eagle0::shardok::storage::
                ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
        case net::eagle0::shardok::storage::
                ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
            throw ShardokInternalErrorException("Bad unit status on resolved unit");
    }

    return true;
}

auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit {
    net::eagle0::common::CommonUnit commonUnit{};
    commonUnit.set_eagle_player_id(unit.eagle_player_id());
    commonUnit.mutable_hero()->set_eagle_hero_id(unit.attached_hero().eagle_hero_id());
    commonUnit.mutable_hero()->set_profession(
            (net::eagle0::common::CommonProfession)unit.attached_hero()
                    .profession_info()
                    .profession());

    commonUnit.mutable_hero()->set_strength(unit.attached_hero().strength());
    commonUnit.mutable_hero()->set_strength_xp(unit.attached_hero().strength_xp());

    commonUnit.mutable_hero()->set_agility(unit.attached_hero().agility());
    commonUnit.mutable_hero()->set_agility_xp(unit.attached_hero().agility_xp());

    commonUnit.mutable_hero()->set_constitution(unit.attached_hero().constitution());
    commonUnit.mutable_hero()->set_constitution_xp(unit.attached_hero().constitution_xp());

    commonUnit.mutable_hero()->set_charisma(unit.attached_hero().charisma());
    commonUnit.mutable_hero()->set_charisma_xp(unit.attached_hero().charisma_xp());

    commonUnit.mutable_hero()->set_wisdom(unit.attached_hero().wisdom());
    commonUnit.mutable_hero()->set_wisdom_xp(unit.attached_hero().wisdom_xp());

    commonUnit.mutable_hero()->set_integrity(unit.attached_hero().integrity());
    commonUnit.mutable_hero()->set_ambition(unit.attached_hero().ambition());
    commonUnit.mutable_hero()->set_gregariousness(unit.attached_hero().gregariousness());
    commonUnit.mutable_hero()->set_bravery(unit.attached_hero().bravery());

    commonUnit.mutable_hero()->set_vigor(unit.attached_hero().vigor());
    commonUnit.mutable_hero()->set_spent_vigor(unit.attached_hero().spent_vigor());

    commonUnit.mutable_battalion()->set_eagle_battalion_id(unit.battalion().eagle_battalion_id());
    commonUnit.mutable_battalion()->set_size(unit.battalion().size());
    commonUnit.mutable_battalion()->set_type(
            (net::eagle0::common::CommonBattalionTypeId)unit.battalion().type());
    commonUnit.mutable_battalion()->set_morale(unit.battalion().morale());
    commonUnit.mutable_battalion()->set_armament(unit.battalion().armament());
    commonUnit.mutable_battalion()->set_training(unit.battalion().training());

    return commonUnit;
}

auto FromInternalStatus(
        const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
        -> net::eagle0::common::UnitStatus {
    switch (internalStatus) {
        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
            return net::eagle0::common::UnitStatus::CAPTURED_UNIT;

        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
            return net::eagle0::common::UnitStatus::NORMAL_UNIT;

        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
            return net::eagle0::common::UnitStatus::FLED_UNIT;

        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
            return net::eagle0::common::UnitStatus::RETREATED_UNIT;

        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
            return net::eagle0::common::UnitStatus::NEVER_ENTERED_UNIT;

        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT:
            return net::eagle0::common::UnitStatus::OUTLAWED_UNIT;

        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
        case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
        case net::eagle0::shardok::storage::
                ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
        case net::eagle0::shardok::storage::
                ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
            throw ShardokInternalErrorException("Bad unit status on resolved unit");
    }

    throw ShardokInternalErrorException("Bad unit status on resolved unit");
}

}  // namespace shardok

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