BareGit
#include "config_file.hpp"
#include "engine_worker.hpp"
#include "game_http_server.hpp"
#include "game_manager.hpp"
#include "mcp_server.hpp"
#include "server_config.hpp"

#include <cstdio>
#include <cstdlib>
#include <chrono>
#include <csignal>
#include <exception>
#include <filesystem>
#include <limits>
#include <optional>
#include <string>
#include <sys/resource.h>
#include <thread>
#include <utility>

namespace
{

volatile std::sig_atomic_t stop_requested = 0;

void requestStop(int)
{
    stop_requested = 1;
}

enum class ParseResult
{
    SUCCESS,
    HELP,
    ERROR,
};

bool parsePositive(const std::string& text, std::size_t& value)
{
    if(text.empty() || text.front() == '-')
    {
        return false;
    }
    try
    {
        std::size_t consumed = 0;
        const unsigned long long parsed = std::stoull(text, &consumed);
        if(consumed != text.size() || parsed == 0
           || parsed > static_cast<unsigned long long>(
               std::numeric_limits<std::size_t>::max())
           || parsed > static_cast<unsigned long long>(
               std::numeric_limits<std::int64_t>::max()))
        {
            return false;
        }
        value = static_cast<std::size_t>(parsed);
        return true;
    }
    catch(const std::exception&)
    {
        return false;
    }
}

bool validResourceBounds(const nethack_mcp::ServerConfig& config)
{
    constexpr std::size_t MAX_ACTIVE_GAMES = 1024;
    constexpr std::size_t MAX_HTTP_WORKERS = 1024;
    constexpr std::size_t MAX_HTTP_CONNECTIONS = 1'000'000;
    constexpr std::size_t MAX_RATE_LIMIT_CLIENTS = 1'000'000;
    constexpr std::size_t MAX_MCP_BODY_BYTES = 64U * 1024U * 1024U;
    constexpr std::size_t MAX_WORKER_OUTPUT_BYTES = 1024U * 1024U * 1024U;
    constexpr std::int64_t MAX_DURATION_SECONDS = 315'360'000;
    const auto valid_duration = [](std::chrono::seconds duration) {
        return duration.count() > 0
            && duration.count() <= MAX_DURATION_SECONDS;
    };

    return config.max_active_games <= MAX_ACTIVE_GAMES
        && config.max_concurrent_requests <= MAX_HTTP_WORKERS
        && config.max_open_connections <= MAX_HTTP_CONNECTIONS
        && config.new_games_per_client <= 1'000'000
        && config.control_failures_per_client <= 10'000
        && config.max_rate_limit_clients <= MAX_RATE_LIMIT_CLIENTS
        && config.max_mcp_body_bytes <= MAX_MCP_BODY_BYTES
        && config.max_worker_output_bytes <= MAX_WORKER_OUTPUT_BYTES
        && valid_duration(config.new_game_rate_window)
        && valid_duration(config.control_failure_window)
        && valid_duration(config.idle_timeout)
        && valid_duration(config.max_game_duration)
        && valid_duration(config.lifecycle_sweep_interval);
}

bool parsePort(const std::string& text, int& port)
{
    std::size_t parsed = 0;
    if(!parsePositive(text, parsed) || parsed > 65535)
    {
        return false;
    }
    port = static_cast<int>(parsed);
    return true;
}

void printUsage()
{
    std::fputs(
        "Usage: nethack_mcp [options]\n"
        "  --config PATH                       Read TOML configuration\n"
        "  --listen-address ADDRESS            HTTP address or UDS path\n"
        "  --port PORT                         HTTP port (8765)\n"
        "  --data-root PATH                    Temporary game files\n"
        "  --database PATH                     Persistent SQLite records\n"
        "  --runtime-dir PATH                  NetHack runtime data files\n"
        "  --public-base-url URL               Canonical URL ending in /\n"
        "  --max-active-games N                Active game capacity\n"
        "  --new-games-per-client N            Creation quota per rate window\n"
        "  --new-game-rate-window-seconds N    Creation quota window\n"
        "  --control-failures-per-client N     Bad-token limit (10)\n"
        "  --control-failure-window-seconds N Bad-token window (60)\n"
        "  --max-rate-limit-clients N          Tracked client cap (4096)\n"
        "  --max-concurrent-requests N         HTTP request worker limit\n"
        "  --max-open-connections N            Total HTTP connection limit\n"
        "  --idle-timeout-seconds N            Idle game lifetime (600)\n"
        "  --max-game-duration-seconds N       Absolute game lifetime (86400)\n"
        "  --lifecycle-sweep-seconds N         Expiry sweep interval (15)\n"
        "  --max-mcp-body-bytes N              MCP request size (1048576)\n"
        "  --max-worker-output-bytes N         Worker log byte limit\n"
        "  --help                              Show this help\n",
        stdout);
}

ParseResult parseOptions(int argc, char* argv[],
                         nethack_mcp::ServerConfig& config,
                         std::string& error)
{
    config.data_root = std::filesystem::temp_directory_path()
        / "nethack-mcp";
    const char* home = std::getenv("HOME");
    const std::filesystem::path home_path = home != nullptr && home[0] != '\0'
        ? std::filesystem::path(home) : std::filesystem::current_path();
    config.database_path = home_path / ".local" / "share" / "nethack-mcp"
        / "games.sqlite3";
    config.runtime_directory = NETHACK_RUNTIME_DIR;

    for(int index = 1; index < argc; ++index)
    {
        if(std::string(argv[index]) == "--help")
        {
            printUsage();
            return ParseResult::HELP;
        }
    }

    std::optional<std::filesystem::path> config_path;
    for(int index = 1; index < argc; ++index)
    {
        if(std::string(argv[index]) == "--config")
        {
            if(config_path.has_value() || index + 1 >= argc)
            {
                error = "--config requires exactly one file path";
                return ParseResult::ERROR;
            }
            config_path = argv[++index];
        }
    }

    nethack_mcp::ConfigExplicitSettings explicit_settings;
    if(config_path.has_value()
       && !nethack_mcp::readConfigFile(*config_path, config,
                                      explicit_settings, error))
    {
        return ParseResult::ERROR;
    }

    for(int index = 1; index < argc; ++index)
    {
        const std::string argument = argv[index];
        if(argument == "--config")
        {
            ++index;
            continue;
        }
        if(index + 1 >= argc)
        {
            return ParseResult::ERROR;
        }
        const std::string value = argv[++index];
        std::size_t number = 0;
        if(argument == "--listen-address")
        {
            if(value.empty()) return ParseResult::ERROR;
            config.listen_address = value;
        }
        else if(argument == "--port")
        {
            if(!parsePort(value, config.port)) return ParseResult::ERROR;
        }
        else if(argument == "--data-root")
        {
            config.data_root = value;
        }
        else if(argument == "--database")
        {
            config.database_path = value;
        }
        else if(argument == "--runtime-dir")
        {
            config.runtime_directory = value;
        }
        else if(argument == "--public-base-url")
        {
            config.public_base_url = value;
            explicit_settings.public_base_url = true;
        }
        else if(argument == "--max-active-games")
        {
            if(!parsePositive(value, config.max_active_games))
                return ParseResult::ERROR;
            explicit_settings.max_active_games = true;
        }
        else if(argument == "--new-games-per-client")
        {
            if(!parsePositive(value, config.new_games_per_client))
                return ParseResult::ERROR;
            explicit_settings.new_games_per_client = true;
        }
        else if(argument == "--new-game-rate-window-seconds")
        {
            if(!parsePositive(value, number)) return ParseResult::ERROR;
            config.new_game_rate_window = std::chrono::seconds(number);
            explicit_settings.new_game_rate_window = true;
        }
        else if(argument == "--control-failures-per-client")
        {
            if(!parsePositive(value, config.control_failures_per_client))
                return ParseResult::ERROR;
        }
        else if(argument == "--control-failure-window-seconds")
        {
            if(!parsePositive(value, number)) return ParseResult::ERROR;
            config.control_failure_window = std::chrono::seconds(number);
        }
        else if(argument == "--max-rate-limit-clients")
        {
            if(!parsePositive(value, config.max_rate_limit_clients))
                return ParseResult::ERROR;
            explicit_settings.max_rate_limit_clients = true;
        }
        else if(argument == "--max-concurrent-requests")
        {
            if(!parsePositive(value, config.max_concurrent_requests))
                return ParseResult::ERROR;
            explicit_settings.max_concurrent_requests = true;
        }
        else if(argument == "--max-open-connections")
        {
            if(!parsePositive(value, config.max_open_connections))
                return ParseResult::ERROR;
            explicit_settings.max_open_connections = true;
        }
        else if(argument == "--idle-timeout-seconds")
        {
            if(!parsePositive(value, number)) return ParseResult::ERROR;
            config.idle_timeout = std::chrono::seconds(number);
        }
        else if(argument == "--max-game-duration-seconds")
        {
            if(!parsePositive(value, number)) return ParseResult::ERROR;
            config.max_game_duration = std::chrono::seconds(number);
        }
        else if(argument == "--lifecycle-sweep-seconds")
        {
            if(!parsePositive(value, number)) return ParseResult::ERROR;
            config.lifecycle_sweep_interval = std::chrono::seconds(number);
        }
        else if(argument == "--max-mcp-body-bytes")
        {
            if(!parsePositive(value, config.max_mcp_body_bytes))
                return ParseResult::ERROR;
        }
        else if(argument == "--max-worker-output-bytes")
        {
            if(!parsePositive(value, config.max_worker_output_bytes))
                return ParseResult::ERROR;
            explicit_settings.max_worker_output_bytes = true;
        }
        else
        {
            return ParseResult::ERROR;
        }
    }

    if(!explicit_settings.public_base_url)
    {
        config.public_base_url = "http://127.0.0.1:"
            + std::to_string(config.port) + "/";
    }
    if(config.public_base_url.empty()
       || (config.public_base_url.rfind("https://", 0) != 0
           && config.public_base_url.rfind("http://", 0) != 0)
       || config.public_base_url.back() != '/')
    {
        return ParseResult::ERROR;
    }
    const std::size_t scheme_end = config.public_base_url.find("://");
    const std::size_t path_start = config.public_base_url.find(
        '/', scheme_end + 3);
    if(path_start != config.public_base_url.size() - 1)
    {
        return ParseResult::ERROR;
    }
    const std::string authority = config.public_base_url.substr(
        scheme_end + 3, path_start - (scheme_end + 3));
    if(authority.empty())
    {
        return ParseResult::ERROR;
    }
    const std::string host_name = authority.substr(0, authority.find(':'));
    const bool loopback = host_name == "127.0.0.1"
        || host_name == "localhost";
    if(!loopback && config.public_base_url.rfind("https://", 0) != 0)
    {
        return ParseResult::ERROR;
    }
    if(!loopback
       && (!explicit_settings.max_active_games
           || !explicit_settings.new_games_per_client
           || !explicit_settings.new_game_rate_window
           || !explicit_settings.max_rate_limit_clients
           || !explicit_settings.max_concurrent_requests
           || !explicit_settings.max_open_connections
           || !explicit_settings.max_worker_output_bytes))
    {
        return ParseResult::ERROR;
    }
    if(config.max_open_connections <= config.max_concurrent_requests
       || !validResourceBounds(config))
    {
        return ParseResult::ERROR;
    }
    return ParseResult::SUCCESS;
}

bool configureDescriptorLimit(const nethack_mcp::ServerConfig& config)
{
    struct rlimit limits{};
    if(::getrlimit(RLIMIT_NOFILE, &limits) != 0)
    {
        return false;
    }
    constexpr rlim_t RESERVED_DESCRIPTORS = 64;
    const rlim_t maximum = std::numeric_limits<rlim_t>::max();
    const rlim_t active_games = static_cast<rlim_t>(config.max_active_games);
    const rlim_t open_connections =
        static_cast<rlim_t>(config.max_open_connections);
    if(static_cast<std::size_t>(active_games) != config.max_active_games
       || static_cast<std::size_t>(open_connections)
           != config.max_open_connections
       || active_games > (maximum - RESERVED_DESCRIPTORS) / 2)
    {
        return false;
    }
    const rlim_t worker_descriptors = 2 * active_games;
    if(open_connections > maximum - worker_descriptors
                              - RESERVED_DESCRIPTORS)
    {
        return false;
    }
    const rlim_t desired = open_connections + worker_descriptors
        + RESERVED_DESCRIPTORS;
    if(desired > limits.rlim_max)
    {
        return false;
    }
    limits.rlim_cur = desired;
    return ::setrlimit(RLIMIT_NOFILE, &limits) == 0;
}

} // namespace

int main(int argc, char* argv[])
{
    if(argc > 1 && std::string(argv[1]) == "--engine")
    {
        return nethack_mcp::runEngineWorker(argc, argv);
    }

    nethack_mcp::ServerConfig config;
    std::string parse_error;
    const ParseResult parsed = parseOptions(argc, argv, config, parse_error);
    if(parsed == ParseResult::HELP)
    {
        return 0;
    }
    if(parsed == ParseResult::ERROR)
    {
        if(parse_error.empty())
        {
            parse_error = "invalid or incomplete command line options";
        }
        std::fprintf(stderr, "%s; use --help\n", parse_error.c_str());
        return 2;
    }
    if(!configureDescriptorLimit(config))
    {
        std::fputs("could not apply the configured open-connection limit\n",
                   stderr);
        return 1;
    }

    const std::filesystem::path runtime_source = config.runtime_directory;
    auto manager_result = nethack_mcp::GameManager::create(
        std::move(config), runtime_source);
    if(!manager_result)
    {
        std::fprintf(stderr, "could not initialize game manager: %s\n",
                     mw::errorMsg(manager_result.error()).c_str());
        return 1;
    }
    std::unique_ptr<nethack_mcp::GameManager> manager =
        std::move(*manager_result);
    nethack_mcp::McpServer mcp(*manager);
    nethack_mcp::GameHttpServer http_server(*manager, mcp);
    std::string error;
    if(!http_server.startServer(error))
    {
        std::fprintf(stderr, "%s\n", error.c_str());
        manager->shutdown();
        return 1;
    }
    std::fprintf(stderr, "MCP: %smcp\n",
                 manager->publicBaseUrl().c_str());
    std::fprintf(stderr, "Spectators: %s\n",
                 manager->publicBaseUrl().c_str());
    std::signal(SIGINT, requestStop);
    std::signal(SIGTERM, requestStop);
    while(!stop_requested && http_server.running())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    const bool interrupted = stop_requested != 0;
    http_server.stopServer();
    manager->shutdown();
    return interrupted ? 0 : 1;
}