//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012- OpenVPN Inc.
//
//    SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
//

#ifndef OPENVPN_PKI_X509TRACK_H
#define OPENVPN_PKI_X509TRACK_H

#include <string>
#include <vector>

#include <openvpn/common/exception.hpp>
#include <openvpn/common/options.hpp>
#include <openvpn/common/arraysize.hpp>
#include <openvpn/common/string.hpp>
#include <openvpn/common/to_string.hpp>

namespace openvpn::X509Track {

enum Type
{
    UNDEF = -1,
    SERIAL,
    SERIAL_HEX,
    SHA1,
    CN,
    C,
    L,
    ST,
    O,
    OU,
    EMAIL,
    N_TYPES,
};

static const char *const names[] = {
    // CONST GLOBAL
    "SERIAL",
    "SERIAL_HEX",
    "SHA1",
    "CN",
    "C",
    "L",
    "ST",
    "O",
    "OU",
    "emailAddress",
};

OPENVPN_EXCEPTION(x509_track_error);

inline const char *name(const Type type)
{
    static_assert(N_TYPES == array_size(names), "x509 names array inconsistency");
    if (type >= 0 && type < N_TYPES)
        return names[type];
    else
        return "UNDEF";
}

inline Type parse_type(const std::string &name)
{
    for (size_t i = 0; i < N_TYPES; ++i)
        if (name == names[i])
            return Type(i);
    return UNDEF;
}

struct Config
{
    Config(const Type type_arg, const bool full_chain_arg)
        : type(type_arg),
          full_chain(full_chain_arg)
    {
    }

    Config(const std::string &spec)
    {
        full_chain = (spec.length() > 0 && spec[0] == '+');
        type = parse_type(spec.substr(full_chain ? 1 : 0));
        if (type == UNDEF)
            throw Exception("cannot parse attribute '" + spec + "'");
    }

    std::string to_string() const
    {
        std::string ret;
        if (full_chain)
            ret += '+';
        ret += name(type);
        return ret;
    }

    bool depth_match(const int depth) const
    {
        return !depth || full_chain;
    }

    Type type;
    bool full_chain;
};

struct ConfigSet : public std::vector<Config>
{
    ConfigSet()
    {
    }

    ConfigSet(const OptionList &opt,
              const bool include_serial,
              const bool include_serial_hex)
    {
        const auto *xt = opt.get_index_ptr("x509-track");
        if (xt)
        {
            for (const auto &i : *xt)
            {
                try
                {
                    const Option &o = opt[i];
                    o.touch();
                    emplace_back(o.get(1, 64));
                }
                catch (const std::exception &e)
                {
                    throw x509_track_error(e.what());
                }
            }
        }

        if (include_serial && !exists(SERIAL))
            emplace_back(SERIAL, true);
        if (include_serial_hex && !exists(SERIAL_HEX))
            emplace_back(SERIAL_HEX, true);
    }

    bool exists(const Type t) const
    {
        for (auto &c : *this)
            if (c.type == t)
                return true;
        return false;
    }

    std::string to_string() const
    {
        std::string ret;
        for (auto &c : *this)
        {
            ret += c.to_string();
            ret += '\n';
        }
        return ret;
    }
};

struct KeyValue
{
    KeyValue(const Type type_arg,
             const int depth_arg,
             std::string value_arg)
        : type(type_arg),
          depth(depth_arg),
          value(std::move(value_arg))
    {
    }

    std::string to_string(const bool omi_form) const
    {
        std::string ret;
        ret.reserve(128);
        if (omi_form)
            ret += ">CLIENT:ENV,";
        ret += key_name();
        ret += '=';
        ret += string::reduce_spaces(value, ' ');
        return ret;
    }

    std::string key_name() const
    {
        switch (type)
        {
        case SERIAL:
            return "tls_serial_" + openvpn::to_string(depth);
        case SERIAL_HEX:
            return "tls_serial_hex_" + openvpn::to_string(depth);
        default:
            return "X509_" + openvpn::to_string(depth) + '_' + name(type);
        }
    }

    Type type = UNDEF;
    int depth = 0;
    std::string value;
};

struct Set : public std::vector<KeyValue>
{
    std::string to_string(const bool omi_form) const
    {
        std::string ret;
        ret.reserve(512);
        for (auto &kv : *this)
        {
            ret += kv.to_string(omi_form);
            if (omi_form)
                ret += '\r';
            ret += '\n';
        }
        return ret;
    }
};

} // namespace openvpn::X509Track

#endif
