#pragma once

#include <algorithm>
#include <cstddef>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>

namespace ta::application::config
{
class ConfigCodec
{
public:
    struct Entry
    {
        std::string key;
        std::string value;
    };

    struct DecodeResult
    {
        std::unordered_map<std::string, std::string> values;
        bool success = false;
        std::size_t error_line = 0;
        std::string error;
    };

    [[nodiscard]] static DecodeResult decode(const std::string& contents)
    {
        DecodeResult result{};
        std::istringstream input(contents);
        std::string line;

        std::size_t line_number = 0;
        while (std::getline(input, line))
        {
            ++line_number;
            trim_carriage_return(line);
            if (line.empty() || line.front() == '#')
                continue;

            if (result.values.size() >= max_entries)
                return fail(line_number, "entry limit exceeded");

            const std::size_t separator = line.find('\t');
            if (separator == std::string::npos || line.find('\t', separator + 1) != std::string::npos)
                return fail(line_number, "expected one tab separator");

            const auto key = unescape(std::string_view(line).substr(0, separator));
            const auto value = unescape(std::string_view(line).substr(separator + 1));
            if (!key || key->empty())
                return fail(line_number, "invalid or empty key");
            if (!value)
                return fail(line_number, "invalid escaped value");
            if (key->size() > max_key_length || value->size() > max_value_length)
                return fail(line_number, "key or value is too long");
            if (!result.values.emplace(*key, *value).second)
                return fail(line_number, "duplicate key");
        }

        if (input.bad())
            return fail(line_number, "document read failed");

        result.success = true;
        return result;
    }

    [[nodiscard]] static std::string encode(std::vector<Entry> entries)
    {
        std::sort(entries.begin(), entries.end(), [](const Entry& left, const Entry& right)
        {
            return left.key < right.key;
        });

        std::string result;
        for (const Entry& entry : entries)
        {
            result += escape(entry.key);
            result.push_back('\t');
            result += escape(entry.value);
            result.push_back('\n');
        }
        return result;
    }

private:
    static constexpr std::size_t max_entries = 4096;
    static constexpr std::size_t max_key_length = 256;
    static constexpr std::size_t max_value_length = 64 * 1024;


    [[nodiscard]] static DecodeResult fail(const std::size_t line, std::string message)
    {
        DecodeResult result{};
        result.error_line = line;
        result.error = std::move(message);
        return result;
    }

    static void trim_carriage_return(std::string& line)
    {
        if (!line.empty() && line.back() == '\r')
            line.pop_back();
    }

    [[nodiscard]] static std::string escape(const std::string_view value)
    {
        std::string result;
        result.reserve(value.size());
        for (const char character : value)
        {
            switch (character)
            {
            case '\\': result += "\\\\"; break;
            case '\t': result += "\\t"; break;
            case '\n': result += "\\n"; break;
            case '\r': result += "\\r"; break;
            default: result.push_back(character); break;
            }
        }
        return result;
    }

    [[nodiscard]] static std::optional<std::string> unescape(const std::string_view value)
    {
        std::string result;
        result.reserve(value.size());
        bool escaped = false;

        for (const char character : value)
        {
            if (!escaped)
            {
                if (character == '\\')
                    escaped = true;
                else
                    result.push_back(character);
                continue;
            }

            switch (character)
            {
            case '\\': result.push_back('\\'); break;
            case 't': result.push_back('\t'); break;
            case 'n': result.push_back('\n'); break;
            case 'r': result.push_back('\r'); break;
            default: return std::nullopt;
            }
            escaped = false;
        }

        return escaped ? std::nullopt : std::optional<std::string>(std::move(result));
    }
};
}
