#pragma once

#include "client_settings.h"
#include "matcher.h"
#include "tickets.h"

#include <balancer/kernel/coro/coroutine.h>
#include <balancer/kernel/fs/shared_files.h>
#include <balancer/kernel/fs/threadedfile.h>
#include <balancer/kernel/fs/watched_state.h>
#include <balancer/kernel/helpers/common_parsers.h>
#include <balancer/kernel/helpers/errors.h>
#include <balancer/kernel/memory/chunk.h>
#include <balancer/kernel/module/events.h>
#include <balancer/kernel/module/ssl_props.h>
#include <balancer/kernel/ssl/sslio.h>
#include <balancer/kernel/stats/manager.h>

#include <library/cpp/config/sax.h>
#include <library/cpp/containers/flat_hash/flat_hash.h>
#include <library/cpp/logger/log.h>
#include <util/generic/string.h>

#include <openssl/x509.h>
#include <balancer/kernel/ctl/ctl.h>

namespace NModSsl {
    struct TSslItemSettings;
    class TSslItem;

    // Right now noexcept is not defined for std::function
    using TCallBackEvent = void (TSslItem::*)(NSrvKernel::TEventData&) noexcept;

    /*
     * Helper class with information about event callback
     */
    struct TSslItemEvent {
        TString Regexp;
        TString Name;
        TCallBackEvent Event;
    public:
        TSslItemEvent(TString regexp, TString name, TCallBackEvent event)
        : Regexp(std::move(regexp))
        , Name(std::move(name))
        , Event(event)
        {}
    };

    struct TSslItemTls {
        TSslItemTls(NSrvKernel::TSharedCounter selected)
            : Selected(std::move(selected))
        {}

        THolder<NSrvKernel::TSslServerContext> Ctx;
        mutable NSrvKernel::TSharedCounter Selected;

        TLog* Log = nullptr;
        TLog* SecretsLog = nullptr;
        NSrvKernel::TWatchedState<double> SecretsLogFreq = 1.0;

        TSslTicketKeyStorage SslTicketKeys;
        ui64 SslTicketsReloadOkCount = 0;
        ui64 SslTicketsValidationFailCount = 0;
        ui64 SslTicketsReloadFailCount = 0;

        using TCiphersStat = TMap<TString, ui64>;
        mutable TCiphersStat CiphersStat;

        NSrvKernel::TThreadedQueue* Queue = nullptr;
        NSrvKernel::TCoroutine TicketKeysLoader;
    };

    class TSslItemBase {
    public:
        explicit TSslItemBase(size_t countOfWorkers) : Tls_(countOfWorkers) {}

        void Init(NSrvKernel::IWorkerCtl& process) noexcept {
            Y_VERIFY(!Tls_[process.WorkerId()].Get());
            Tls_[process.WorkerId()] = DoInit(process);
        }

    protected:
        TSslItemTls& GetTls(const NSrvKernel::IWorkerCtl& process) {
            Y_VERIFY(Tls_[process.WorkerId()].Get());
            return *Tls_[process.WorkerId()];
        }

        const TSslItemTls& GetTls(const NSrvKernel::IWorkerCtl& process) const {
            Y_VERIFY(Tls_[process.WorkerId()].Get());
            return *Tls_[process.WorkerId()];
        }

    private:
        THolder<TSslItemTls> virtual DoInit(NSrvKernel::IWorkerCtl& process) noexcept = 0;

    private:
        TVector<THolder<TSslItemTls>> Tls_;
    };

    /*
     * This class provides OpenSSL context, managing of OCSP/Tickets state
     * and additional information which is used in ssl_sni module to choose
     * the right context.
     */
    class TSslItem final : public TSslItemBase {
    private:
        const NFH::TFlatHashMap<ui64, TVector<TSslItem>>& Items_;
        ui64 ItemsSection_;
        const TString Name_;
        THolder<NSrvKernel::TSslServerCtxInitParams> CtxInitParams_;

        THolder<IServernameMatcher> Matcher_;
        int Priority_ =  0;

        THolder<TSslItemSettings> PrimaryCertData_;
        THolder<TSslItemSettings> SecondaryCertData_;
        TString Ciphers_;
        TString Suites_ = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256";
        TString Curves_ = "X25519:prime256v1";
        bool NeedLog_ = false;
        TString LogName_;
        bool NeedSecretLog_ = false;
        TString SecretsLogName_;

        TDuration Timeout_;

        TTicketFiles Tickets;
        bool SslTicketsValidate_ = false;

        THolder<const TClientSettings> ClientCertSettings_;

        bool DisableSslv3_ = false;
        bool DisableTlsv1_3_ = true;

        TMaybe<long> MaxSendFragment_;

        ui64 SslProtocols_ = SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2;

        ui64 MaxOcspFileSize_ = 1 << 20;
        bool HasClientCert_ = false;

        TVector<TSslItemEvent> Events_;
        bool Ja3Enabled_ = false;
        bool ValidateCertDate_ = false;

        const NSrvKernel::IAlpnProtos* Protos_ = nullptr;

        TString SecretsLogFreqFileName_;
        double SecretsLogFreqDefault_ = 1.0;
        NSrvKernel::TSharedCounter Selected_;
        bool LogCiphersStats_ = false;

        NSrvKernel::TSslEarlyDataParams ItemEarlyDataParams_ = {false, 16384, 16384};
    public:
        TSslItem(
            const NFH::TFlatHashMap<ui64, TVector<TSslItem>>& items,
            ui64 section, const TString& name, const NSrvKernel::TModuleParams& mp,
            bool ja3Eanbled, bool validateCertDate);

        /**
         * Parse events for primary certificate
         *
         * @param[in] handler                event name from config
         * @param[in] regexp                 regexp for matching event by name
         */
        void DoConsumeEventPrimary(const TString& handler, const TString& regexp);
        /**
         * Parse events for secondary certificate
         *
         * @param[in] handler                event name from config
         * @param[in] regexp                 regexp for matching event by name
         */
        void DoConsumeEventSecondary(const TString &handler, const TString &regexp);


        /**
         * Reload tickets from disk and write output to stream.
         *
         * @param[in] out                    output stream
         * @param[in] forceReload            force reload operation
         */
        void ReloadTicketKeys(IOutputStream &out, bool forceReload) noexcept;
        /**
         * Reload tickets from disk. Event handling function.
         *
         * @param[in] event                  event information
         */
        void ReloadTicketKeys(NSrvKernel::TEventData& event) noexcept;
        /**
         * Force reload tickets from disk. Event handling function.
         *
         * @param[in] event                  event information
         */
        void ForceReloadTicketKeys(NSrvKernel::TEventData& event) noexcept;
        /**
         * Reload tickets from disk. Callback function.
         */
        void DoReloadTicketKeys() noexcept;
        /**
         * Reload tickets from disk and write output to stream.
         *
         * @param[in] out                    output stream
         * @param[in] newKeys                list with new keys
         * @param[in] forceReload            force reload operation
         *
         * @return                           status of new keys
         */
         bool ValidateReloadTicketKeys(IOutputStream &out, const TSslTicketKeyStorage& newKeys, bool forceReload, TSslItemTls& tls) noexcept;


        /**
         * Reload primary OCSP from disk. Event handling function.
         *
         * @param[in] event                  event information
         */
        void ReloadPrimaryOcspResponse(NSrvKernel::TEventData& event) noexcept;
        /**
         * Reload secondary OCSP from disk. Event handling function.
         *
         * @param[in] event                  event information
         */
        void ReloadSecondaryOcspResponse(NSrvKernel::TEventData& event) noexcept;
        /**
         * Reload OCSP from disk. Callback function.
         *
         * @param[in] data                   TSslItemSettings object(primary or secondary cert)
         */
        bool DoReloadOcspResponse(TSslItemSettings& data, const NSrvKernel::IWorkerCtl& process) noexcept;
        
        /**
         * Update ciphers statistics
         *
         * @param[in] io                     OpenSSL io class
         */
        void LogCipherStat(const NSrvKernel::TConnDescr& descr, const NSrvKernel::TSslIo& io) const;
        /**
         * Check does this item's regexp matches servername
         *
         * @param[in] name                   servername
         *
         * @return                           boolean
         */
        bool Match(const TStringBuf name) const noexcept;
        /**
         * Find OpenSSL context matches servername
         *
         * @param[in] name                   servername
         *
         * @return                           ssl context
         */
        ssl_ctx_st* MatchCtx(const TStringBuf servername) const;


        /**
         * Use this to set max send fragment value before
         * initialization in DoInit.
         *
         * @param[in] maxSendFragment        value in range 512 - 16384
         */
        void SetMaxSendFragment(long maxSendFragment) noexcept;
        /**
         * Use this to update max send fragment value after
         * initialization in DoInit.
         *
         * @param[in] maxSendFragment        value in range 512 - 16384
         */
        void TryUpdateMaxSendFragment(long maxSendFragment) noexcept;
        /**
         * Set ALPN proto to HTTP2
         *
         * @param[in] protos                 value in range 512 - 16384
         */
        void EnableH2(const NSrvKernel::IAlpnProtos* protos) noexcept;
        /**
         * Set keylog line
         *
         * @param[in] line                   keylog line
         */
        void SetKeylogLine(const TStringBuf line) noexcept;
    private:
        /**
        * Initialization of OpenSSL context and coroutine tasks.
        *
        * @param[in] executor               Coroutine executor
        * @param[in] sharedFiles            Shared files controlling thread
        */
        THolder<TSslItemTls> DoInit(NSrvKernel::IWorkerCtl& process) noexcept final;
        /**
         * Reload primary OCSP from disk. Helper function.
         *
         * @param[in] event                  event information
         */
        void ReloadOcspResponseFor(TSslItemSettings& data, IOutputStream& out) noexcept;

        void AddSecondaryCert(NSrvKernel::TSslServerContext* ctx) const;

        void SetClientSettings(NSrvKernel::TSslServerContext* ctx) const;

    public:
        const NSrvKernel::TSslContext& Ctx(const NSrvKernel::IWorkerCtl& process) const noexcept {
            Y_VERIFY(GetTls(process).Ctx);
            return *GetTls(process).Ctx.Get();
        }

        const TSslTicketKeyStorage* SslTicketKeys(const NSrvKernel::IWorkerCtl& process) const noexcept {
            return &GetTls(process).SslTicketKeys;
        }

        int Priority() const noexcept {
            return Priority_;
        }

        bool IsDefault() const noexcept {
            return Name_ == "default";
        }

        bool HasClientCert() const noexcept {
            return HasClientCert_;
        }

        TSslItemSettings* PrimaryCertData() {
            Y_VERIFY(PrimaryCertData_);
            return PrimaryCertData_.Get();
        }

        TSslItemSettings* SecondaryCertData() {
            return SecondaryCertData_.Get();
        }

        const TSslItemSettings* PrimaryCertData() const noexcept {
            Y_VERIFY(PrimaryCertData_);
            return PrimaryCertData_.Get();
        }

        const TSslItemSettings* SecondaryCertData() const noexcept {
            return SecondaryCertData_.Get();
        }

        TVector<TSslItemEvent>& Events() {
            return Events_;
        }

        void SetItemsSection(ui64 section) {
            ItemsSection_ = section;
        }

        bool IsEarlyDataEnabled() const noexcept {
            return ItemEarlyDataParams_.Enabled;
        }

        void SetItemEarlyDataEnabled(bool enabled) noexcept {
            ItemEarlyDataParams_.Enabled = enabled;
        }

        void SetItemEarlyDataMaxData(ui32 data) noexcept {
            ItemEarlyDataParams_.MaxEarlyData = data;
        }

        void SetItemEarlyDataRecvMaxData(ui32 data) noexcept {
            ItemEarlyDataParams_.RecvMaxEarlyData = data;
        }

        const NSrvKernel::TSslEarlyDataParams& ItemEarlyDataParams() const noexcept {
            return ItemEarlyDataParams_;
        }
    public:
        void IncSelected(const NSrvKernel::IWorkerCtl& process) const noexcept {
            ++GetTls(process).Selected;
        }
    };

    /**
     * OpenSSL OCSP response callback wrapper.
     *
     * @param[in] data                   pointer to the class owner of context
     * @param[in] ssl                    OpenSSL info struct
     */
    static const NSrvKernel::TChunk* OcspResponseCallback(void* data, ssl_st* ssl) Y_DECLARE_UNUSED;
    /**
     * OpenSSL ticket keys callback wrapper.
     *
     * @param[in] data                   pointer to the class owner of context
     * @param[in] ssl                    OpenSSL info struct
     * @param[in] clientPresentedTicket  client ticket
     */
    static int SslTicketKeysCallback(void*, ssl_st* ssl, unsigned char* name, unsigned char* iv,
        evp_cipher_ctx_st* cipherCtx, hmac_ctx_st* hmacCtx, int init) Y_DECLARE_UNUSED;
    /**
      * Choose OpenSSL context by servername regexp
      *
      * @param[in] data                   pointer to the class owner of context
      * @param[in] servername             server name
      */
    static ssl_ctx_st* ServernameCallback(void* data, const TStringBuf& servername) Y_DECLARE_UNUSED;
    /**
      * Update keylog file
      *
      * @param[in] data                   pointer to the class owner of context
      * @param[in] line                   string with secrets info
      */
    static void KeylogCallback(void* data, const TStringBuf line) Y_DECLARE_UNUSED;

    /*
     * Helper class for storing basic information about OpenSSL context.
     */
    struct TSslItemSettingsTls {
        NSrvKernel::TChunkPtr OcspResponse;
        const X509 *RawCert = nullptr;
        NSrvKernel::TSharedFileExistsChecker OcspFileSwitchChecker;
        NSrvKernel::TCoroutine OcspResponseLoader;
    };

    struct TSslItemSettingsBase {
        explicit TSslItemSettingsBase(size_t countOfWorkers) : Tls_(countOfWorkers) {}

        TSslItemSettingsTls& GetTls(const NSrvKernel::IWorkerCtl& process) {
            return Tls_[process.WorkerId()];
        }

        const TSslItemSettingsTls& GetTls(const NSrvKernel::IWorkerCtl& process) const {
            return Tls_[process.WorkerId()];
        }

    private:
        TVector<TSslItemSettingsTls> Tls_;
    };

    struct TSslItemSettings : public TSslItemSettingsBase {
        TSslItemSettings(size_t countOfWorkers, TString cert, TString  priv, TString ca, TString ocsp,
            TString ocspFileSwitch)
            : TSslItemSettingsBase(countOfWorkers)
            , Cert(std::move(cert))
            , Priv(std::move(priv))
            , Ca(std::move(ca))
            , Ocsp(std::move(ocsp))
            , OcspFileSwitch(std::move(ocspFileSwitch))
        {}

        TString Cert;
        TString Priv;
        TString Ca;
        TString Ocsp;
        TString OcspFileSwitch;
    };

    /**
     * Preparse old context for primary cert.
     *
     * @param[in] conf                   pointer to config
     * @param[in] func                   events handling function
     * @param[in] suffix                 "primary" or "secondary"
     */
    THolder<TSslItemSettings> ParseSslItemSettings(
        NConfig::IConfig *conf, std::function<void(TString, TString)> func, TStringBuf suffix, size_t countOfWorkers);
}
