BareGit
#include "config_file.hpp"

#include <toml++/toml.hpp>

#include <chrono>
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <string_view>

namespace nethack_mcp
{
namespace
{

bool fail(const std::filesystem::path& path, const std::string& message,
          std::string& error)
{
    error = path.string() + ": " + message;
    return false;
}

bool validateKeys(const toml::table& table,
                  std::initializer_list<std::string_view> allowed,
                  const std::string& section,
                  const std::filesystem::path& path,
                  std::string& error)
{
    std::size_t matched = 0;
    for(const std::string_view key : allowed)
    {
        if(table.get(key) != nullptr)
        {
            ++matched;
        }
    }
    if(matched != table.size())
    {
        return fail(path, "unknown setting in [" + section + "]", error);
    }
    return true;
}

bool readSection(const toml::table& root, std::string_view name,
                 const toml::table*& section,
                 const std::filesystem::path& path,
                 std::string& error)
{
    const toml::node* node = root.get(name);
    if(node == nullptr)
    {
        section = nullptr;
        return true;
    }
    section = node->as_table();
    if(section == nullptr)
    {
        return fail(path, "[" + std::string(name) + "] must be a table",
                    error);
    }
    return true;
}

bool readString(const toml::table& table, std::string_view key,
                const std::string& section, std::string& value,
                const std::filesystem::path& path, std::string& error)
{
    const toml::node* node = table.get(key);
    if(node == nullptr)
    {
        return true;
    }
    const auto parsed = node->value<std::string>();
    if(!parsed || parsed->empty())
    {
        return fail(path, "[" + section + "]." + std::string(key)
            + " must be a non-empty string", error);
    }
    value = *parsed;
    return true;
}

bool readPath(const toml::table& table, std::string_view key,
              const std::string& section, std::filesystem::path& value,
              const std::filesystem::path& path, std::string& error)
{
    std::string parsed;
    if(!readString(table, key, section, parsed, path, error))
    {
        return false;
    }
    if(table.get(key) != nullptr)
    {
        value = parsed;
    }
    return true;
}

bool readPositiveInteger(const toml::table& table, std::string_view key,
                         const std::string& section, std::size_t& value,
                         const std::filesystem::path& path,
                         std::string& error)
{
    const toml::node* node = table.get(key);
    if(node == nullptr)
    {
        return true;
    }
    const auto parsed = node->value<std::int64_t>();
    if(!parsed || *parsed <= 0
       || static_cast<std::uint64_t>(*parsed)
           > static_cast<std::uint64_t>(
               std::numeric_limits<std::size_t>::max()))
    {
        return fail(path, "[" + section + "]." + std::string(key)
            + " must be a positive integer", error);
    }
    value = static_cast<std::size_t>(*parsed);
    return true;
}

bool readPort(const toml::table& table, std::string_view key,
              int& value, const std::filesystem::path& path,
              std::string& error)
{
    const toml::node* node = table.get(key);
    if(node == nullptr)
    {
        return true;
    }
    const auto parsed = node->value<std::int64_t>();
    if(!parsed || *parsed <= 0 || *parsed > 65535)
    {
        return fail(path, "[server].port must be an integer from 1 to 65535",
                    error);
    }
    value = static_cast<int>(*parsed);
    return true;
}

bool readSocketPermission(const toml::table& table, std::string_view key,
                          std::optional<unsigned int>& value,
                          const std::filesystem::path& path,
                          std::string& error)
{
    const toml::node* node = table.get(key);
    if(node == nullptr)
    {
        return true;
    }
    const auto parsed = node->value<std::int64_t>();
    if(!parsed || *parsed < 0 || *parsed > 0777)
    {
        return fail(path,
                    "[server].listen_socket_permission must be an octal TOML "
                    "integer from 0o000 to 0o777", error);
    }
    value = static_cast<unsigned int>(*parsed);
    return true;
}

bool readSeconds(const toml::table& table, std::string_view key,
                 const std::string& section, std::chrono::seconds& value,
                 const std::filesystem::path& path, std::string& error)
{
    std::size_t parsed = 0;
    if(!readPositiveInteger(table, key, section, parsed, path, error))
    {
        return false;
    }
    if(table.get(key) != nullptr)
    {
        value = std::chrono::seconds(parsed);
    }
    return true;
}

bool readServerSection(const toml::table& table, ServerConfig& config,
                       ConfigExplicitSettings& explicit_settings,
                       const std::filesystem::path& path, std::string& error)
{
    if(!validateKeys(table, {
           "listen_address", "listen_socket_permission", "port",
           "public_base_url",
       }, "server", path, error)
       || !readString(table, "listen_address", "server",
                      config.listen_address, path, error)
       || !readSocketPermission(table, "listen_socket_permission",
                                config.listen_socket_permission, path, error)
       || !readPort(table, "port", config.port, path, error)
       || !readString(table, "public_base_url", "server",
                      config.public_base_url, path, error))
    {
        return false;
    }
    explicit_settings.public_base_url =
        table.get("public_base_url") != nullptr;
    return true;
}

bool readPathsSection(const toml::table& table, ServerConfig& config,
                      const std::filesystem::path& path, std::string& error)
{
    return validateKeys(table, {
               "data_root", "database", "runtime_dir",
           }, "paths", path, error)
        && readPath(table, "data_root", "paths", config.data_root,
                    path, error)
        && readPath(table, "database", "paths", config.database_path,
                    path, error)
        && readPath(table, "runtime_dir", "paths", config.runtime_directory,
                    path, error);
}

bool readLimitsSection(const toml::table& table, ServerConfig& config,
                       ConfigExplicitSettings& explicit_settings,
                       const std::filesystem::path& path, std::string& error)
{
    if(!validateKeys(table, {
           "max_active_games", "new_games_per_client",
           "new_game_rate_window_seconds", "control_failures_per_client",
           "control_failure_window_seconds", "max_rate_limit_clients",
           "max_concurrent_requests", "max_open_connections",
           "idle_timeout_seconds", "max_game_duration_seconds",
           "lifecycle_sweep_seconds", "max_mcp_body_bytes",
           "max_worker_output_bytes",
       }, "limits", path, error)
       || !readPositiveInteger(table, "max_active_games", "limits",
                               config.max_active_games, path, error)
       || !readPositiveInteger(table, "new_games_per_client", "limits",
                               config.new_games_per_client, path, error)
       || !readSeconds(table, "new_game_rate_window_seconds", "limits",
                       config.new_game_rate_window, path, error)
       || !readPositiveInteger(table, "control_failures_per_client", "limits",
                               config.control_failures_per_client, path, error)
       || !readSeconds(table, "control_failure_window_seconds", "limits",
                       config.control_failure_window, path, error)
       || !readPositiveInteger(table, "max_rate_limit_clients", "limits",
                               config.max_rate_limit_clients, path, error)
       || !readPositiveInteger(table, "max_concurrent_requests", "limits",
                               config.max_concurrent_requests, path, error)
       || !readPositiveInteger(table, "max_open_connections", "limits",
                               config.max_open_connections, path, error)
       || !readSeconds(table, "idle_timeout_seconds", "limits",
                       config.idle_timeout, path, error)
       || !readSeconds(table, "max_game_duration_seconds", "limits",
                       config.max_game_duration, path, error)
       || !readSeconds(table, "lifecycle_sweep_seconds", "limits",
                       config.lifecycle_sweep_interval, path, error)
       || !readPositiveInteger(table, "max_mcp_body_bytes", "limits",
                               config.max_mcp_body_bytes, path, error)
       || !readPositiveInteger(table, "max_worker_output_bytes", "limits",
                               config.max_worker_output_bytes, path, error))
    {
        return false;
    }
    explicit_settings.max_active_games =
        table.get("max_active_games") != nullptr;
    explicit_settings.new_games_per_client =
        table.get("new_games_per_client") != nullptr;
    explicit_settings.new_game_rate_window =
        table.get("new_game_rate_window_seconds") != nullptr;
    explicit_settings.max_rate_limit_clients =
        table.get("max_rate_limit_clients") != nullptr;
    explicit_settings.max_concurrent_requests =
        table.get("max_concurrent_requests") != nullptr;
    explicit_settings.max_open_connections =
        table.get("max_open_connections") != nullptr;
    explicit_settings.max_worker_output_bytes =
        table.get("max_worker_output_bytes") != nullptr;
    return true;
}

} // namespace

bool readConfigFile(const std::filesystem::path& path,
                    ServerConfig& config,
                    ConfigExplicitSettings& explicit_settings,
                    std::string& error)
{
    toml::table root;
    try
    {
        root = toml::parse_file(path.string());
    }
    catch(const std::exception& exception)
    {
        return fail(path, exception.what(), error);
    }

    if(!validateKeys(root, {"server", "paths", "limits"}, "root",
                     path, error))
    {
        return false;
    }

    const toml::table* server = nullptr;
    const toml::table* paths = nullptr;
    const toml::table* limits = nullptr;
    if(!readSection(root, "server", server, path, error)
       || !readSection(root, "paths", paths, path, error)
       || !readSection(root, "limits", limits, path, error))
    {
        return false;
    }
    if(server != nullptr
       && !readServerSection(*server, config, explicit_settings, path, error))
    {
        return false;
    }
    if(paths != nullptr && !readPathsSection(*paths, config, path, error))
    {
        return false;
    }
    return limits == nullptr
        || readLimitsSection(*limits, config, explicit_settings, path, error);
}

} // namespace nethack_mcp