BareGit
#include "config.h"

#include <array>
#include <cstdlib>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <variant>

#include <toml++/toml.hpp>

#include "email_address.h"

namespace
{

const std::set<std::string> CONFIG_KEYS = {
    "administrator_email",
    "avif_quality",
    "base_url",
    "card_storage_root",
    "database_path",
    "email",
    "listen_address",
    "listen_port",
    "maximum_accumulated_pulls",
    "static_root",
    "thumbnail_long_side",
};

const std::set<std::string> EMAIL_CONFIG_KEYS = {
    "daily_attempt_limit",
    "from_address",
    "from_name",
    "link_file",
    "mailjet_api_key_environment",
    "mailjet_secret_key_environment",
    "transport",
};

mw::E<std::string> requiredString(
    const toml::table& table,
    std::string_view name)
{
    auto value = table[name].value<std::string>();
    if(!value)
    {
        return std::unexpected(mw::runtimeError(
            "Configuration key '" + std::string(name) +
            "' must be a string"));
    }
    return std::move(*value);
}

mw::E<std::int64_t> requiredInteger(
    const toml::table& table,
    std::string_view name)
{
    auto value = table[name].value<std::int64_t>();
    if(!value)
    {
        return std::unexpected(mw::runtimeError(
            "Configuration key '" + std::string(name) +
            "' must be an integer"));
    }
    return *value;
}

std::filesystem::path normalizedPath(
    const std::filesystem::path& config_directory,
    const std::string& value)
{
    std::filesystem::path path(value);
    if(path.is_relative())
    {
        path = config_directory / path;
    }
    return std::filesystem::absolute(path).lexically_normal();
}

bool pathContains(
    const std::filesystem::path& parent,
    const std::filesystem::path& child)
{
    auto parent_position = parent.begin();
    auto child_position = child.begin();
    while(parent_position != parent.end() &&
          child_position != child.end() &&
          *parent_position == *child_position)
    {
        ++parent_position;
        ++child_position;
    }
    return parent_position == parent.end();
}

mw::E<void> createDirectory(
    const std::filesystem::path& path,
    std::string_view description)
{
    std::error_code filesystem_error;
    std::filesystem::create_directories(path, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to create " + std::string(description) + ": " +
            filesystem_error.message()));
    }
    return {};
}

} // namespace

mw::E<Config> loadConfig(const std::filesystem::path& config_path)
{
    toml::table table;
    try
    {
        table = toml::parse_file(config_path.string());
    }
    catch(const toml::parse_error& error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to parse configuration: " +
            std::string(error.description())));
    }

    for(const auto& [key, value] : table)
    {
        [[maybe_unused]] const toml::node& node = value;
        if(!CONFIG_KEYS.contains(std::string(key.str())))
        {
            return std::unexpected(mw::runtimeError(
                "Unknown configuration key '" +
                std::string(key.str()) + "'"));
        }
    }

    const toml::table* email_table = table["email"].as_table();
    if(email_table == nullptr)
    {
        return std::unexpected(mw::runtimeError(
            "Configuration key 'email' must be a table"));
    }
    for(const auto& [key, value] : *email_table)
    {
        [[maybe_unused]] const toml::node& node = value;
        if(!EMAIL_CONFIG_KEYS.contains(std::string(key.str())))
        {
            return std::unexpected(mw::runtimeError(
                "Unknown email configuration key '" +
                std::string(key.str()) + "'"));
        }
    }

    auto base_url_text = requiredString(table, "base_url");
    auto listen_address_text = requiredString(table, "listen_address");
    auto static_root_text = requiredString(table, "static_root");
    auto database_path_text = requiredString(table, "database_path");
    auto card_storage_text = requiredString(table, "card_storage_root");
    auto avif_quality = requiredInteger(table, "avif_quality");
    auto thumbnail_long_side = requiredInteger(
        table, "thumbnail_long_side");
    auto administrator_email_text = requiredString(
        table, "administrator_email");
    auto maximum_accumulated_pulls = requiredInteger(
        table, "maximum_accumulated_pulls");
    if(!base_url_text || !listen_address_text || !static_root_text ||
       !database_path_text || !card_storage_text || !avif_quality ||
       !thumbnail_long_side || !administrator_email_text ||
       !maximum_accumulated_pulls)
    {
        if(!base_url_text)
        {
            return std::unexpected(std::move(base_url_text.error()));
        }
        if(!listen_address_text)
        {
            return std::unexpected(std::move(listen_address_text.error()));
        }
        if(!static_root_text)
        {
            return std::unexpected(std::move(static_root_text.error()));
        }
        if(!database_path_text)
        {
            return std::unexpected(std::move(database_path_text.error()));
        }
        if(!card_storage_text)
        {
            return std::unexpected(std::move(card_storage_text.error()));
        }
        if(!avif_quality)
        {
            return std::unexpected(std::move(avif_quality.error()));
        }
        if(!thumbnail_long_side)
        {
            return std::unexpected(std::move(
                thumbnail_long_side.error()));
        }
        if(!administrator_email_text)
        {
            return std::unexpected(std::move(
                administrator_email_text.error()));
        }
        return std::unexpected(std::move(
            maximum_accumulated_pulls.error()));
    }

    auto base_url = mw::URL::fromStr(*base_url_text);
    if(!base_url ||
       (base_url->scheme() != "http" && base_url->scheme() != "https") ||
       base_url->host().empty())
    {
        return std::unexpected(mw::runtimeError(
            "base_url must be an absolute HTTP or HTTPS URL"));
    }
    std::string base_path = base_url->path();
    if(base_path.empty())
    {
        base_path = "/";
    }
    while(base_path.size() > 1 && base_path.ends_with('/'))
    {
        base_path.pop_back();
    }
    if(!base_path.ends_with('/'))
    {
        base_path.push_back('/');
    }
    base_url->path(base_path.c_str());

    std::optional<mw::HTTPServer::ListenAddress> listen_address;
    if(listen_address_text->starts_with("unix:"))
    {
        const std::string socket_path = listen_address_text->substr(5);
        if(socket_path.empty())
        {
            return std::unexpected(mw::runtimeError(
                "A Unix listen address requires a socket path"));
        }
        listen_address.emplace(mw::SocketFileInfo(socket_path));
    }
    else
    {
        auto listen_port = requiredInteger(table, "listen_port");
        if(!listen_port || *listen_port < 1 || *listen_port > 65535)
        {
            return std::unexpected(mw::runtimeError(
                "listen_port must be an integer from 1 through 65535"));
        }
        listen_address.emplace(mw::IPSocketInfo{
            *listen_address_text, static_cast<int>(*listen_port)});
    }
    if(*avif_quality < 0 || *avif_quality > 100)
    {
        return std::unexpected(mw::runtimeError(
            "avif_quality must be from 0 through 100"));
    }
    if(*thumbnail_long_side < 1 ||
       *thumbnail_long_side > UINT32_MAX)
    {
        return std::unexpected(mw::runtimeError(
            "thumbnail_long_side must be a positive 32-bit integer"));
    }
    if(*maximum_accumulated_pulls < 1 ||
       *maximum_accumulated_pulls > UINT32_MAX)
    {
        return std::unexpected(mw::runtimeError(
            "maximum_accumulated_pulls must be a positive 32-bit integer"));
    }
    auto administrator_email = normalizeEmail(*administrator_email_text);
    if(!administrator_email)
    {
        return std::unexpected(mw::runtimeError(
            "administrator_email is invalid"));
    }

    const std::filesystem::path config_directory =
        std::filesystem::absolute(config_path).parent_path();
    const std::filesystem::path static_root = normalizedPath(
        config_directory, *static_root_text);
    const std::filesystem::path database_path = normalizedPath(
        config_directory, *database_path_text);
    const std::filesystem::path card_storage_root = normalizedPath(
        config_directory, *card_storage_text);

    auto transport_text = requiredString(*email_table, "transport");
    if(!transport_text)
    {
        return std::unexpected(std::move(transport_text.error()));
    }
    EmailConfig email;
    if(*transport_text == "file")
    {
        auto link_file = requiredString(*email_table, "link_file");
        if(!link_file)
        {
            return std::unexpected(std::move(link_file.error()));
        }
        email.transport = EmailTransport::FILE;
        email.link_file = std::filesystem::path(*link_file);
        if(!email.link_file.is_absolute())
        {
            return std::unexpected(mw::runtimeError(
                "email.link_file must be absolute with an existing parent"));
        }
        email.link_file = std::filesystem::absolute(
            email.link_file).lexically_normal();
        if(!std::filesystem::is_directory(email.link_file.parent_path()))
        {
            return std::unexpected(mw::runtimeError(
                "email.link_file must be absolute with an existing parent"));
        }
        if(pathContains(static_root, email.link_file) ||
           pathContains(card_storage_root, email.link_file))
        {
            return std::unexpected(mw::runtimeError(
                "email.link_file must be outside public asset roots"));
        }
        if(email_table->contains("mailjet_api_key_environment") ||
           email_table->contains("mailjet_secret_key_environment") ||
           email_table->contains("daily_attempt_limit") ||
           email_table->contains("from_address") ||
           email_table->contains("from_name"))
        {
            return std::unexpected(mw::runtimeError(
                "File email transport rejects Mailjet settings"));
        }
        if(base_url->scheme() == "http" &&
           std::holds_alternative<mw::IPSocketInfo>(*listen_address))
        {
            const std::string& address =
                std::get<mw::IPSocketInfo>(*listen_address).address;
            if(address != "127.0.0.1" && address != "::1" &&
               address != "localhost")
            {
                return std::unexpected(mw::runtimeError(
                    "HTTP file transport requires a loopback listener"));
            }
        }
    }
    else if(*transport_text == "mailjet")
    {
        auto from_address = requiredString(*email_table, "from_address");
        auto from_name = requiredString(*email_table, "from_name");
        auto api_environment = requiredString(
            *email_table, "mailjet_api_key_environment");
        auto secret_environment = requiredString(
            *email_table, "mailjet_secret_key_environment");
        auto daily_limit = requiredInteger(
            *email_table, "daily_attempt_limit");
        if(!from_address || !from_name || !api_environment ||
           !secret_environment || !daily_limit || from_name->empty() ||
           api_environment->empty() || secret_environment->empty() ||
           *daily_limit < 1 ||
           *daily_limit > UINT32_MAX || base_url->scheme() != "https")
        {
            return std::unexpected(mw::runtimeError(
                "Mailjet email configuration is invalid"));
        }
        if(!normalizeEmail(*from_address))
        {
            return std::unexpected(mw::runtimeError(
                "email.from_address is invalid"));
        }
        const char* api_key = std::getenv(api_environment->c_str());
        const char* secret_key = std::getenv(secret_environment->c_str());
        if(api_key == nullptr || *api_key == '\0' ||
           secret_key == nullptr || *secret_key == '\0')
        {
            return std::unexpected(mw::runtimeError(
                "Mailjet credential environment variables are missing"));
        }
        email.transport = EmailTransport::MAILJET;
        email.from_address = std::move(*from_address);
        email.from_name = std::move(*from_name);
        email.mailjet_api_key_environment = std::move(*api_environment);
        email.mailjet_secret_key_environment = std::move(*secret_environment);
        email.daily_attempt_limit = static_cast<std::uint32_t>(*daily_limit);
    }
    else
    {
        return std::unexpected(mw::runtimeError(
            "email.transport must be 'mailjet' or 'file'"));
    }
    std::error_code filesystem_error;
    if(!std::filesystem::is_directory(static_root, filesystem_error) ||
       filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "static_root must name an existing directory"));
    }
    if(pathContains(static_root, card_storage_root) ||
       pathContains(card_storage_root, static_root))
    {
        return std::unexpected(mw::runtimeError(
            "static_root and card_storage_root must not overlap"));
    }
    if(pathContains(static_root, database_path) ||
       pathContains(card_storage_root, database_path))
    {
        return std::unexpected(mw::runtimeError(
            "database_path must not be inside a mounted asset root"));
    }
    if(email.transport == EmailTransport::FILE &&
       (pathContains(static_root, email.link_file) ||
        pathContains(card_storage_root, email.link_file)))
    {
        return std::unexpected(mw::runtimeError(
            "email.link_file must not be inside an asset root"));
    }

    auto database_directory = createDirectory(
        database_path.parent_path(), "the database directory");
    if(!database_directory)
    {
        return std::unexpected(std::move(database_directory.error()));
    }
    auto card_directory = createDirectory(
        card_storage_root, "the card storage directory");
    if(!card_directory)
    {
        return std::unexpected(std::move(card_directory.error()));
    }
    auto published_directory = createDirectory(
        card_storage_root / "published", "the published card directory");
    if(!published_directory)
    {
        return std::unexpected(std::move(published_directory.error()));
    }

    return Config{
        std::move(*base_url),
        std::move(*listen_address),
        static_root,
        database_path,
        card_storage_root,
        static_cast<int>(*avif_quality),
        static_cast<std::uint32_t>(*thumbnail_long_side),
        std::move(administrator_email->email),
        std::move(administrator_email->key),
        static_cast<std::uint32_t>(*maximum_accumulated_pulls),
        std::move(email),
    };
}