#pragma once

#include "logging.hpp"
#include "persistent_data.hpp"

#include <openssl/crypto.h>
#include <openssl/ssl.h>

#include <boost/asio/ip/address.hpp>
#include <boost/asio/ssl/verify_context.hpp>

#include <memory>
#include <span>

inline std::shared_ptr<persistent_data::UserSession>
    verifyMtlsUser(const boost::asio::ip::address& clientIp,
                   boost::asio::ssl::verify_context& ctx)
{
    // do nothing if TLS is disabled
    if (!persistent_data::SessionStore::getInstance()
             .getAuthMethodsConfig()
             .tls)
    {
        BMCWEB_LOG_DEBUG("TLS auth_config is disabled");
        return nullptr;
    }

    X509_STORE_CTX* cts = ctx.native_handle();
    if (cts == nullptr)
    {
        BMCWEB_LOG_DEBUG("Cannot get native TLS handle.");
        return nullptr;
    }

    // Get certificate
    X509* peerCert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
    if (peerCert == nullptr)
    {
        BMCWEB_LOG_DEBUG("Cannot get current TLS certificate.");
        return nullptr;
    }

    // Check if certificate is OK
    int ctxError = X509_STORE_CTX_get_error(cts);
    if (ctxError != X509_V_OK)
    {
        BMCWEB_LOG_INFO("Last TLS error is: {}", ctxError);
        return nullptr;
    }
    // Check that we have reached final certificate in chain
    int32_t depth = X509_STORE_CTX_get_error_depth(cts);
    if (depth != 0)

    {
        BMCWEB_LOG_DEBUG(
            "Certificate verification in progress (depth {}), waiting to reach final depth",
            depth);
        return nullptr;
    }

    BMCWEB_LOG_DEBUG("Certificate verification of final depth");

    // Verify KeyUsage
    bool isKeyUsageDigitalSignature = false;
    bool isKeyUsageKeyAgreement = false;

    ASN1_BIT_STRING* usage = static_cast<ASN1_BIT_STRING*>(
        X509_get_ext_d2i(peerCert, NID_key_usage, nullptr, nullptr));

    if ((usage == nullptr) || (usage->data == nullptr))
    {
        BMCWEB_LOG_DEBUG("TLS usage is null");
        return nullptr;
    }

    for (auto usageChar :
         std::span(usage->data, static_cast<size_t>(usage->length)))
    {
        if (KU_DIGITAL_SIGNATURE & usageChar)
        {
            isKeyUsageDigitalSignature = true;
        }
        if (KU_KEY_AGREEMENT & usageChar)
        {
            isKeyUsageKeyAgreement = true;
        }
    }
    ASN1_BIT_STRING_free(usage);

    if (!isKeyUsageDigitalSignature || !isKeyUsageKeyAgreement)
    {
        BMCWEB_LOG_DEBUG("Certificate ExtendedKeyUsage does "
                         "not allow provided certificate to "
                         "be used for user authentication");
        return nullptr;
    }

    // Determine that ExtendedKeyUsage includes Client Auth

    stack_st_ASN1_OBJECT* extUsage = static_cast<stack_st_ASN1_OBJECT*>(
        X509_get_ext_d2i(peerCert, NID_ext_key_usage, nullptr, nullptr));

    if (extUsage == nullptr)
    {
        BMCWEB_LOG_DEBUG("TLS extUsage is null");
        return nullptr;
    }

    bool isExKeyUsageClientAuth = false;
    for (int i = 0; i < sk_ASN1_OBJECT_num(extUsage); i++)
    {
        // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
        int nid = OBJ_obj2nid(sk_ASN1_OBJECT_value(extUsage, i));
        if (NID_client_auth == nid)
        {
            isExKeyUsageClientAuth = true;
            break;
        }
    }
    sk_ASN1_OBJECT_free(extUsage);

    // Certificate has to have proper key usages set
    if (!isExKeyUsageClientAuth)
    {
        BMCWEB_LOG_DEBUG("Certificate ExtendedKeyUsage does "
                         "not allow provided certificate to "
                         "be used for user authentication");
        return nullptr;
    }
    std::string sslUser;
    // Extract username contained in CommonName
    sslUser.resize(256, '\0');

    int status = X509_NAME_get_text_by_NID(X509_get_subject_name(peerCert),
                                           NID_commonName, sslUser.data(),
                                           static_cast<int>(sslUser.size()));

    if (status == -1)
    {
        BMCWEB_LOG_DEBUG("TLS cannot get username to create session");
        return nullptr;
    }

    size_t lastChar = sslUser.find('\0');
    if (lastChar == std::string::npos || lastChar == 0)
    {
        BMCWEB_LOG_DEBUG("Invalid TLS user name");
        return nullptr;
    }
    sslUser.resize(lastChar);
    std::string unsupportedClientId;
    return persistent_data::SessionStore::getInstance().generateUserSession(
        sslUser, clientIp, unsupportedClientId,
        persistent_data::PersistenceType::TIMEOUT);
}