BareGit
#include "configuration.h"

#include <algorithm>
#include <cctype>
#include <exception>
#include <fstream>
#include <iterator>
#include <limits>
#include <string_view>
#include <unordered_set>
#include <utility>

#include <ryml.hpp>
#include <ryml_std.hpp>
#include <mw/utils.hpp>

namespace
{

using Node = ryml::ConstNodeRef;

Node child(Node node, std::string_view key)
{
    return node.find_child(c4::csubstr(key.data(), key.size()));
}

mw::Error configError(std::string_view context, std::string_view message)
{
    return mw::runtimeError(std::string(context) + ": " +
                            std::string(message));
}

mw::E<std::vector<char>> readFile(const std::filesystem::path& path)
{
    std::ifstream file(path, std::ios::binary);
    if(!file)
    {
        return std::unexpected(configError(
            path.string(), "failed to open configuration file"));
    }
    std::vector<char> content(
        (std::istreambuf_iterator<char>(file)),
        std::istreambuf_iterator<char>());
    if(file.bad())
    {
        return std::unexpected(configError(
            path.string(), "failed to read configuration file"));
    }
    return content;
}

mw::E<std::string> requiredString(Node node, std::string_view key,
                                  std::string_view context)
{
    auto value = child(node, key);
    if(!value.readable() || !value.has_val())
    {
        return std::unexpected(configError(
            context, std::string("missing string field ") + std::string(key)));
    }
    std::string result;
    value.load(&result);
    return result;
}

mw::E<std::optional<std::string>> optionalString(
    Node node, std::string_view key, std::string_view context)
{
    auto value = child(node, key);
    if(!value.readable())
    {
        return std::optional<std::string>{};
    }
    if(!value.has_val())
    {
        return std::unexpected(configError(
            context, std::string("invalid string field ") + std::string(key)));
    }
    std::string result;
    value.load(&result);
    return std::optional<std::string>{std::move(result)};
}

template<typename Integer>
mw::E<Integer> requiredInteger(Node node, std::string_view key,
                               std::string_view context)
{
    auto value = child(node, key);
    if(!value.readable() || !value.has_val())
    {
        return std::unexpected(configError(
            context, std::string("missing integer field ") + std::string(key)));
    }
    Integer result{};
    if(!value.deserialize(&result))
    {
        return std::unexpected(configError(
            context, std::string("invalid integer field ") + std::string(key)));
    }
    return result;
}

template<typename Integer>
mw::E<std::optional<Integer>> optionalInteger(
    Node node, std::string_view key, std::string_view context)
{
    auto value = child(node, key);
    if(!value.readable())
    {
        return std::optional<Integer>{};
    }
    if(!value.has_val())
    {
        return std::unexpected(configError(
            context, std::string("invalid integer field ") + std::string(key)));
    }
    Integer result{};
    if(!value.deserialize(&result))
    {
        return std::unexpected(configError(
            context, std::string("invalid integer field ") + std::string(key)));
    }
    return std::optional<Integer>{result};
}

std::string lower(std::string value)
{
    std::transform(value.begin(), value.end(), value.begin(),
                   [](unsigned char character) {
                       return static_cast<char>(std::tolower(character));
                   });
    return value;
}

mw::E<std::chrono::steady_clock::duration> parseInterval(
    Node service, std::string_view context)
{
    auto interval = child(service, "interval");
    if(!interval.readable() || !interval.is_map())
    {
        return std::unexpected(configError(
            context, "missing interval map"));
    }
    ASSIGN_OR_RETURN(const auto value,
                     requiredInteger<std::int64_t>(interval, "value", context));
    ASSIGN_OR_RETURN(auto unit, requiredString(interval, "unit", context));
    if(value <= 0)
    {
        return std::unexpected(configError(
            context, "interval value must be positive"));
    }
    const auto normalized_unit = lower(std::move(unit));
    std::int64_t multiplier = 0;
    if(normalized_unit == "second" || normalized_unit == "seconds")
    {
        multiplier = 1;
    }
    else if(normalized_unit == "minute" || normalized_unit == "minutes")
    {
        multiplier = 60;
    }
    else if(normalized_unit == "hour" || normalized_unit == "hours")
    {
        multiplier = 60 * 60;
    }
    else if(normalized_unit == "day" || normalized_unit == "days")
    {
        multiplier = 24 * 60 * 60;
    }
    else
    {
        return std::unexpected(configError(
            context, "interval unit must be second, minute, hour, or day"));
    }
    const auto max_seconds = std::chrono::duration_cast<std::chrono::seconds>(
        std::chrono::steady_clock::duration::max()).count();
    if(value > max_seconds / multiplier)
    {
        return std::unexpected(configError(context, "interval is too large"));
    }
    return std::chrono::duration_cast<std::chrono::steady_clock::duration>(
        std::chrono::seconds(value * multiplier));
}

mw::E<EndpointConfig> parseEndpoint(Node endpoint, std::string_view context)
{
    ASSIGN_OR_RETURN(auto protocol,
                     requiredString(endpoint, "protocol", context));
    protocol = lower(std::move(protocol));
    if(protocol == "http" || protocol == "https")
    {
        ASSIGN_OR_RETURN(auto url,
                         optionalString(endpoint, "url", context));
        return EndpointConfig{HttpEndpoint{url.value_or("")}};
    }
    if(protocol == "tcp")
    {
        ASSIGN_OR_RETURN(auto host,
                         requiredString(endpoint, "host", context));
        ASSIGN_OR_RETURN(const auto port,
                         requiredInteger<std::uint64_t>(
                             endpoint, "port", context));
        if(port == 0 || port > std::numeric_limits<std::uint16_t>::max())
        {
            return std::unexpected(configError(
                context, "TCP port must be between 1 and 65535"));
        }
        return EndpointConfig{TcpEndpoint{
            std::move(host), static_cast<std::uint16_t>(port)}};
    }
    if(protocol == "udp")
    {
        ASSIGN_OR_RETURN(auto host,
                         requiredString(endpoint, "host", context));
        ASSIGN_OR_RETURN(const auto port,
                         requiredInteger<std::uint64_t>(
                             endpoint, "port", context));
        if(port == 0 || port > std::numeric_limits<std::uint16_t>::max())
        {
            return std::unexpected(configError(
                context, "UDP port must be between 1 and 65535"));
        }
        ASSIGN_OR_RETURN(auto payload,
                         optionalString(endpoint, "payload", context));
        return EndpointConfig{UdpEndpoint{
            std::move(host), static_cast<std::uint16_t>(port),
            payload.value_or("")}};
    }
    if(protocol == "icmp")
    {
        ASSIGN_OR_RETURN(auto host,
                         requiredString(endpoint, "host", context));
        return EndpointConfig{IcmpEndpoint{std::move(host)}};
    }
    return std::unexpected(configError(
        context, "protocol must be HTTP, TCP, UDP, or ICMP"));
}

mw::E<ServiceConfig> parseService(Node service, std::string id,
                                  std::string_view context)
{
    if(!service.is_map())
    {
        return std::unexpected(configError(context, "service must be a map"));
    }
    ServiceConfig result;
    result.id = std::move(id);
    ASSIGN_OR_RETURN(result.name, requiredString(service, "name", context));
    ASSIGN_OR_RETURN(result.description,
                     requiredString(service, "description", context));
    ASSIGN_OR_RETURN(result.url, optionalString(service, "url", context));
    auto endpoint = child(service, "endpoint");
    if(!endpoint.readable() || !endpoint.is_map())
    {
        return std::unexpected(configError(
            context, "missing endpoint map"));
    }
    ASSIGN_OR_RETURN(result.endpoint,
                     parseEndpoint(endpoint, context));
    auto timeout = optionalInteger<std::int64_t>(
        service, "timeout_second", context);
    if(!timeout)
    {
        return std::unexpected(timeout.error());
    }
    if(!*timeout)
    {
        timeout = optionalInteger<std::int64_t>(
            endpoint, "timeout_second", context);
        if(!timeout)
        {
            return std::unexpected(timeout.error());
        }
    }
    const auto timeout_value = timeout->value_or(5);
    if(timeout_value <= 0)
    {
        return std::unexpected(configError(
            context, "timeout_second must be positive"));
    }
    result.timeout = std::chrono::seconds(timeout_value);
    ASSIGN_OR_RETURN(result.interval, parseInterval(service, context));
    return result;
}

mw::E<Configuration> parseTree(ryml::Tree& tree)
{
    const auto root = tree.rootref();
    if(!root.is_map())
    {
        return std::unexpected(configError(
            "configuration", "root must be a map"));
    }
    Configuration result;
    ASSIGN_OR_RETURN(const auto worker_count,
                     requiredInteger<std::uint64_t>(
                         root, "worker_count", "configuration"));
    if(worker_count == 0 ||
       worker_count > std::numeric_limits<std::size_t>::max())
    {
        return std::unexpected(configError(
            "configuration", "worker_count must be positive"));
    }
    result.worker_count = static_cast<std::size_t>(worker_count);
    ASSIGN_OR_RETURN(result.database_path,
                     requiredString(root, "database_path", "configuration"));
    if(result.database_path.empty() ||
       result.database_path.find('\0') != std::string::npos)
    {
        return std::unexpected(configError(
            "configuration", "database_path must not be empty"));
    }
    ASSIGN_OR_RETURN(auto unix_socket,
                     optionalString(root, "unix_socket", "configuration"));
    if(unix_socket && unix_socket->find('\0') != std::string::npos)
    {
        return std::unexpected(configError(
            "configuration", "unix_socket contains a null character"));
    }
    if(unix_socket)
    {
        result.unix_socket = std::move(*unix_socket);
    }
    ASSIGN_OR_RETURN(auto socket_permission,
                     optionalInteger<unsigned int>(
                         root, "socket_permission", "configuration"));
    if(socket_permission && *socket_permission > 0777)
    {
        return std::unexpected(configError(
            "configuration",
            "socket_permission must be an octal mode from 0o000 through "
            "0o777"));
    }
    result.socket_permission = std::move(socket_permission);
    auto groups = child(root, "groups");
    if(!groups.readable() || !groups.is_seq())
    {
        return std::unexpected(configError(
            "configuration", "groups must be a sequence"));
    }
    std::unordered_set<std::string> service_ids;
    std::unordered_set<std::string> group_names;
    std::size_t group_index = 0;
    for(const auto& group : groups)
    {
        const auto group_location =
            "groups[" + std::to_string(group_index) + "]";
        ++group_index;
        if(!group.is_map())
        {
            return std::unexpected(configError(
                group_location, "group must be a map"));
        }
        ASSIGN_OR_RETURN(auto group_name,
                         requiredString(group, "name", group_location));
        if(group_name.empty())
        {
            return std::unexpected(configError(
                group_location, "group name must not be empty"));
        }
        if(!group_names.insert(group_name).second)
        {
            return std::unexpected(configError(
                group_location, "group name must be unique"));
        }
        const auto group_context = group_location + "." + group_name;
        ServiceGroupConfig group_config;
        group_config.name = std::move(group_name);
        auto services = child(group, "services");
        if(!services.readable() || !services.is_map())
        {
            return std::unexpected(configError(
                group_context, "services must be a map"));
        }
        for(const auto& service : services)
        {
            if(!service.has_key() || !service.is_map())
            {
                return std::unexpected(configError(
                    group_context, "service must be a map"));
            }
            const std::string service_id(
                service.key().begin(), service.key().end());
            if(service_id.empty())
            {
                return std::unexpected(configError(
                    group_context, "service id must not be empty"));
            }
            const auto context = group_context + "." + service_id;
            ASSIGN_OR_RETURN(auto service_config,
                             parseService(service, service_id, context));
            if(!service_ids.insert(service_config.id).second)
            {
                return std::unexpected(configError(
                    context, "service id must be unique"));
            }
            group_config.services.push_back(std::move(service_config));
        }
        result.groups.push_back(std::move(group_config));
    }
    return result;
}

}

mw::E<Configuration> Configuration::fromYaml(
    const std::filesystem::path& path)
{
    ASSIGN_OR_RETURN(auto buffer, readFile(path));
    try
    {
        auto tree = ryml::parse_in_place(ryml::to_substr(buffer));
        return parseTree(tree);
    }
    catch(const std::exception& error)
    {
        return std::unexpected(configError(
            path.string(), std::string("invalid YAML: ") + error.what()));
    }
}