//
//  shardok_server_main.cpp
//  eagle0
//
//  Created by Dan Crosby on 11/2/17.
//  Copyright © 2017 none. All rights reserved.
//

#include <condition_variable>
#include <cstddef>
#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <sstream>
#include <thread>

#include "src/main/cpp/net/eagle0/common/ProtobufWarningSuppression.hpp"

#pragma GCC diagnostic push
SUPPRESS_PROTOBUF_WARNINGS

#include <execinfo.h>
#include <grpcpp/channel.h>
#include <grpcpp/security/server_credentials.h>
#include <signal.h>
#include <unistd.h>

#pragma GCC diagnostic pop

#include "src/main/cpp/net/eagle0/shardok/server/EagleInterfaceGrpcServer.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/ServerConfiguration.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/ShardokGamesManager.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/TokenAuthInterceptor.hpp"

using ::GameStatePersister;
using shardok::EagleInterfaceImpl;
using shardok::ShardokGamesManager;

struct ServerThreadInfo {
    std::thread thread;
    std::shared_ptr<grpc::Service> service;
};

auto StartInThread(std::unique_ptr<grpc::ServerBuilder> serverBuilder) -> ServerThreadInfo;

auto CreateServerBuilder(
        const std::string &serverAddress,
        const std::string &certPath,
        const std::string &keyPath) -> std::unique_ptr<grpc::ServerBuilder>;

auto CreateEagleInterfaceService(
        const std::shared_ptr<ServerConfiguration> &config,
        std::shared_ptr<ShardokGamesManager> shardokGamesManager) -> ServerThreadInfo;

auto ReadFileContents(const std::string &path) -> std::string;

void handler(const int sig) {
    void *array[10];
    size_t size;

    // get void*'s for all entries on the stack
    size = backtrace(array, 10);

    // print out all the frames to stderr
    std::cerr << "Error: signal " << sig << ":\n";
    backtrace_symbols_fd(array, size, STDERR_FILENO);
    exit(1);
}

auto main(const int argc, char **argv) -> int {
    signal(SIGSEGV, handler);  // install our handler

    FilesystemUtils::SetExecPath(argv[0]);

    std::vector<std::string> extraSettings;
    extraSettings.reserve(argc - 1);
    for (int i = 1; i < argc; ++i) { extraSettings.emplace_back(argv[i]); }

    const std::shared_ptr<ServerConfiguration> config = std::make_shared<ServerConfiguration>(
            FilesystemUtils::StaticEagle0FilesDirectory() + "/eagle0.conf");

    const auto shardokGamesManager = std::make_shared<ShardokGamesManager>(extraSettings);

    ServerThreadInfo eagleInterfaceInfo = CreateEagleInterfaceService(config, shardokGamesManager);

    eagleInterfaceInfo.thread.join();
}

auto ReadFileContents(const std::string &path) -> std::string {
    std::ifstream file(path);
    if (!file.is_open()) { return ""; }
    std::stringstream buffer;
    buffer << file.rdbuf();
    return buffer.str();
}

auto CreateServerBuilder(
        const std::string &serverAddress,
        const std::string &certPath,
        const std::string &keyPath) -> std::unique_ptr<grpc::ServerBuilder> {
    auto serverBuilder = std::make_unique<grpc::ServerBuilder>();

    std::shared_ptr<grpc::ServerCredentials> credentials;

    // Check if TLS is configured
    if (!certPath.empty() && !keyPath.empty()) {
        const std::string certContents = ReadFileContents(certPath);
        const std::string keyContents = ReadFileContents(keyPath);

        if (certContents.empty() || keyContents.empty()) {
            std::cerr << "Error: Could not read TLS certificate or key file" << '\n';
            std::cerr << "  cert path: " << certPath << '\n';
            std::cerr << "  key path: " << keyPath << '\n';
            std::cerr << "Falling back to insecure connection" << '\n';
            credentials = grpc::InsecureServerCredentials();
        } else {
            grpc::SslServerCredentialsOptions::PemKeyCertPair keyCert;
            keyCert.private_key = keyContents;
            keyCert.cert_chain = certContents;

            grpc::SslServerCredentialsOptions sslOpts;
            sslOpts.pem_key_cert_pairs.push_back(keyCert);

            credentials = grpc::SslServerCredentials(sslOpts);
            std::cout << "TLS enabled with certificate from: " << certPath << '\n';
        }
    } else {
        credentials = grpc::InsecureServerCredentials();
        std::cout << "Creating insecure connection (no TLS configured)" << '\n';
    }

    serverBuilder->AddListeningPort(serverAddress, credentials);
    return serverBuilder;
}

auto CreateEagleInterfaceService(
        const std::shared_ptr<ServerConfiguration> &config,
        std::shared_ptr<ShardokGamesManager> shardokGamesManager) -> ServerThreadInfo {
    const std::string eagleInterfaceServerAddress =
            config->stringForKey(ServerConfiguration::kEagleInterfaceGrpcAddress);

    // TLS configuration
    const std::string certPath = config->stringForKey(ServerConfiguration::kSslCertPath);
    const std::string keyPath = config->stringForKey(ServerConfiguration::kSslPrivateKeyPath);

    auto eagleInterfaceServerBuilder =
            CreateServerBuilder(eagleInterfaceServerAddress, certPath, keyPath);

    // Auth token configuration
    const std::string authTokenPath = config->stringForKey(ServerConfiguration::kAuthTokenPath);
    const std::string authToken = shardok::ReadAuthTokenFromFile(authTokenPath);

    const auto eagleInterfaceService =
            std::make_shared<EagleInterfaceImpl>(shardokGamesManager, authToken);
    eagleInterfaceServerBuilder->RegisterService(eagleInterfaceService.get());

    ServerThreadInfo eagleInterfaceThreadInfo =
            StartInThread(std::move(eagleInterfaceServerBuilder));
    eagleInterfaceThreadInfo.service = eagleInterfaceService;
    std::cout << "Eagle interface server listening on " << eagleInterfaceServerAddress << '\n';

    return eagleInterfaceThreadInfo;
}

auto StartInThread(std::unique_ptr<grpc::ServerBuilder> serverBuilder) -> ServerThreadInfo {
    ServerThreadInfo info;
    std::mutex m{};
    std::condition_variable cv{};
    bool finished = false;

    std::unique_lock<std::mutex> lk(m);
    std::thread t([serverBuilder = std::move(serverBuilder), &finished, &m, &cv]() {
        const std::unique_ptr<grpc::Server> server(serverBuilder->BuildAndStart());

        {
            std::lock_guard<std::mutex> lk(m);
            finished = true;
            cv.notify_one();
        }

        server->Wait();
    });

    info.thread = std::move(t);
    cv.wait(lk, [&finished] { return finished; });
    lk.unlock();

    return info;
}
