#pragma once

#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/tls1.h>

namespace NHttp {

struct TSslHelpers {
    struct TSslDestroy {
        static void Destroy(SSL_CTX* ctx) noexcept {
            SSL_CTX_free(ctx);
        }

        static void Destroy(SSL* ssl) noexcept {
            SSL_free(ssl);
        }

        static void Destroy(X509* cert) noexcept {
            X509_free(cert);
        }

        static void Destroy(EVP_PKEY* pkey) noexcept {
            EVP_PKEY_free(pkey);
        }

        static void Destroy(BIO* bio) noexcept {
            BIO_free(bio);
        }
    };

    template <typename T>
    using TSslHolder = THolder<T, TSslDestroy>;

    static TSslHolder<SSL_CTX> CreateSslCtx(const SSL_METHOD* method) {
        TSslHolder<SSL_CTX> ctx(SSL_CTX_new(method));

        if (ctx) {
            SSL_CTX_set_options(ctx.Get(), SSL_OP_NO_SSLv2);
            SSL_CTX_set_options(ctx.Get(), SSL_OP_NO_SSLv3);
            SSL_CTX_set_options(ctx.Get(), SSL_OP_MICROSOFT_SESS_ID_BUG);
            SSL_CTX_set_options(ctx.Get(), SSL_OP_NETSCAPE_CHALLENGE_BUG);
        }

        return ctx;
    }

    static TSslHolder<SSL_CTX> CreateClientContext() {
        return CreateSslCtx(SSLv23_client_method());
    }

    static TSslHolder<SSL_CTX> CreateServerContext(const TString& certificate, const TString& key) {
        TSslHolder<SSL_CTX> ctx = CreateSslCtx(SSLv23_server_method());
        SSL_CTX_set_ecdh_auto(ctx.Get(), 1);
        int res;
        res = SSL_CTX_use_certificate_chain_file(ctx.Get(), certificate.c_str());
        if (res < 0) {
            // TODO(xenoxeno): more diagnostics?
            return nullptr;
        }
        res = SSL_CTX_use_PrivateKey_file(ctx.Get(), key.c_str(), SSL_FILETYPE_PEM);
        if (res < 0) {
            // TODO(xenoxeno): more diagnostics?
            return nullptr;
        }
        return ctx;
    }

    static bool LoadX509Chain(TSslHolder<SSL_CTX>& ctx, const TString& pem) {
        TSslHolder<BIO> bio(BIO_new_mem_buf(pem.c_str(), pem.size()));
        if (bio == nullptr) {
            return false;
        }
        TSslHolder<X509> cert(PEM_read_bio_X509_AUX(bio.Get(), nullptr, nullptr, nullptr));
        if (cert == nullptr) {
            return false;
        }
        if (SSL_CTX_use_certificate(ctx.Get(), cert.Release()) <= 0) {
            return false;
        }
        SSL_CTX_clear_chain_certs(ctx.Get());
        while (true) {
            TSslHolder<X509> ca(PEM_read_bio_X509(bio.Get(), nullptr, nullptr, nullptr));
            if (ca == nullptr) {
                break;
            }
            if (!SSL_CTX_add0_chain_cert(ctx.Get(), ca.Release())) {
                return false;
            }
        }
        return true;
    }

    static bool LoadPrivateKey(TSslHolder<SSL_CTX>& ctx, const TString& pem) {
        TSslHolder<BIO> bio(BIO_new_mem_buf(pem.c_str(), pem.size()));
        if (bio == nullptr) {
            return false;
        }
        TSslHolder<EVP_PKEY> pkey(PEM_read_bio_PrivateKey(bio.Get(), nullptr, nullptr, nullptr));
        if (SSL_CTX_use_PrivateKey(ctx.Get(), pkey.Release()) <= 0) {
            return false;
        }
        return true;
    }

    static TSslHolder<SSL_CTX> CreateServerContext(const TString& pem) {
        TSslHolder<SSL_CTX> ctx = CreateSslCtx(SSLv23_server_method());
        SSL_CTX_set_ecdh_auto(ctx.Get(), 1);
        if (!LoadX509Chain(ctx, pem)) {
            return nullptr;
        }
        if (!LoadPrivateKey(ctx, pem)) {
            return nullptr;
        }
        return ctx;
    }

    static TSslHolder<SSL> ConstructSsl(SSL_CTX* ctx, BIO* bio) {
        TSslHolder<SSL> ssl(SSL_new(ctx));

        if (ssl) {
            BIO_up_ref(bio); // SSL_set_bio consumes only one reference if rbio and wbio are the same
            SSL_set_bio(ssl.Get(), bio, bio);
        }

        return ssl;
    }
};

}
