#pragma once

#include <balancer/kernel/http/parser/http.h>
#include <balancer/kernel/http2/server/common/http2_common.h>
#include <balancer/kernel/log/errorlog.h>
#include <balancer/kernel/memory/chunks.h>
#include <balancer/kernel/module/iface.h>

#include <library/cpp/coroutine/engine/impl.h>
#include <library/cpp/logger/backend.h>
#include <library/cpp/logger/log.h>
#include <library/cpp/logger/priority.h>
#include <library/cpp/logger/record.h>

#include <util/generic/hash.h>
#include <util/generic/maybe.h>
#include <util/generic/strbuf.h>
#include <util/generic/vector.h>
#include <util/generic/yexception.h>

#include <algorithm>
#include <utility>

namespace NSrvKernel::NHTTP2 {

    // The annoyingly lengthy list of macros ===========================================================================

#define Y_HTTP2_PRINT_OBJ(out, ...) PrintObjState(out, TStringBuf(#__VA_ARGS__), __VA_ARGS__)
#define Y_HTTP2_GEN_PRINT(TClass, ...) \
    template <> \
    void Out<NSrvKernel::NHTTP2::TClass>(IOutputStream& out, const NSrvKernel::NHTTP2::TClass& obj) { \
        obj.PrintTo(out); \
    }

#define Y_HTTP2_LOG_EVENT(logger, logLevel, message, ...) \
    do if (logger.GetLog(logLevel)) { logger.LogEvent(logLevel, TStringBuf(message), TStringBuf(#__VA_ARGS__), __VA_ARGS__); } while (0)
#define Y_HTTP2_EVENT(logger, message, ...) Y_HTTP2_LOG_EVENT(logger, TLOG_DEBUG, message, __VA_ARGS__)

#define Y_HTTP2_LOG_EVENT_E(logger, logLevel, message) \
    do if (logger.GetLog(logLevel)) { logger.LogEvent(logLevel, TStringBuf(message), TStringBuf()); } while (0)
#define Y_HTTP2_EVENT_E(logger, message) Y_HTTP2_LOG_EVENT_E(logger, TLOG_DEBUG, message)

#define Y_HTTP2_LOG_BLOCK(logger, logLevel, name, ...) \
    auto Y_CAT(blockGuard, __LINE__) = (logger.GetLog(logLevel) ? logger.LogBlockStart(logLevel, TStringBuf(name), TStringBuf(#__VA_ARGS__), __VA_ARGS__) : logger.EmptyBlockGuard())
#define Y_HTTP2_BLOCK(logger, name, ...) Y_HTTP2_LOG_BLOCK(logger, TLOG_DEBUG, name, __VA_ARGS__)

#define Y_HTTP2_LOG_BLOCK_E(logger, logLevel, name) \
    auto Y_CAT(blockGuard, __LINE__) = (logger.GetLog(logLevel) ? logger.LogBlockStart(logLevel, TStringBuf(name), TStringBuf()) : logger.EmptyBlockGuard())
#define Y_HTTP2_BLOCK_E(logger, name) Y_HTTP2_LOG_BLOCK_E(logger, TLOG_DEBUG, name)

#define Y_HTTP2_LOG_FUNC(logger, logLevel, ...) \
    auto Y_CAT(blockGuard, __LINE__) = (logger.GetLog(logLevel) ? logger.LogFunctionStart(logLevel, TStringBuf(Y_FUNC_SIGNATURE), TStringBuf(#__VA_ARGS__), __VA_ARGS__) : logger.EmptyBlockGuard())
#define Y_HTTP2_FUNC(logger, ...) Y_HTTP2_LOG_FUNC(logger, TLOG_DEBUG, __VA_ARGS__)
#define Y_HTTP2_METH(logger, ...) Y_HTTP2_LOG_FUNC(logger, TLOG_DEBUG, this, __VA_ARGS__)

#define Y_HTTP2_LOG_FUNC_E(logger, logLevel) \
    auto Y_CAT(blockGuard, __LINE__) = (logger.GetLog(logLevel) ? logger.LogFunctionStart(logLevel, Y_FUNC_SIGNATURE, TStringBuf()) : logger.EmptyBlockGuard())
#define Y_HTTP2_FUNC_E(logger) Y_HTTP2_LOG_FUNC_E(logger, TLOG_DEBUG)
#define Y_HTTP2_METH_E(logger) Y_HTTP2_LOG_FUNC(logger, TLOG_DEBUG, this)

#define Y_HTTP2_LOG_CATCH(logger, logLevel, exc, ...) \
    auto Y_CAT(blockGuard, __LINE__) = (logger.GetLog(logLevel) ? logger.LogCatchStart(logLevel, exc, TStringBuf(#__VA_ARGS__), __VA_ARGS__) : logger.EmptyBlockGuard())
#define Y_HTTP2_CATCH(logger, exc, ...) Y_HTTP2_LOG_CATCH(logger, TLOG_ERR, exc, __VA_ARGS__)

#define Y_HTTP2_LOG_CATCH_E(logger, logLevel, exc) \
    auto Y_CAT(blockGuard, __LINE__) = (logger.GetLog(logLevel) ? logger.LogCatchStart(logLevel, exc, TStringBuf()) : logger.EmptyBlockGuard())
#define Y_HTTP2_CATCH_E(logger, exc) Y_HTTP2_LOG_CATCH_E(logger, TLOG_ERR, exc)

#define Y_HTTP2_LOG_CATCH_ALL(logger, logLevel, ...) \
    auto Y_CAT(blockGuard, __LINE__) = (logger.GetLog(logLevel) ? logger.LogCatchAllStart(logLevel, TStringBuf(#__VA_ARGS__), __VA_ARGS__) : logger.EmptyBlockGuard())
#define Y_HTTP2_CATCH_ALL(logger, ...) Y_HTTP2_LOG_CATCH_ALL(logger, TLOG_ERR, __VA_ARGS__)

#define Y_HTTP2_LOG_CATCH_ALL_E(logger, logLevel) \
    auto Y_CAT(blockGuard, __LINE__) = (logger.GetLog(logLevel) ? logger.LogCatchAllStart(logLevel, TStringBuf()) : logger.EmptyBlockGuard())
#define Y_HTTP2_CATCH_ALL_E(logger) Y_HTTP2_LOG_CATCH_ALL_E(logger, TLOG_ERR)


    // TMultilogBackend ================================================================================================

    class TBilogBackend : public TLogBackend {
    public:
        explicit TBilogBackend(TLog* first, TLog* second) noexcept;

        void WriteData(const TLogRecord& rec) noexcept override;

        void ReopenLog() override;

        ELogPriority FiltrationLevel() const noexcept override;

    private:
        std::pair<ELogPriority, TLog*> const First_;
        std::pair<ELogPriority, TLog*> const Second_;
        ELogPriority MaxPriority_ = TLOG_EMERG;
    };


    // TLogLabel =======================================================================================================

    class TLogLabel {
    public:
        TLogLabel(TStringBuf moduleName, const NSrvKernel::TConnDescr& mainDescr) noexcept;

        // For mocking in unittests
        TLogLabel(const TString& prefix, const TString& defaultSuffix) noexcept;

        template <class TLe>
        void Print(TLe&& le, ELogPriority logLevel) noexcept {
            try {
                le << GetPrefix();
                NLogImpl::DoPrintNow(le, Now());
                le << GetSuffix(logLevel);
            } catch (...) {
                Y_VERIFY_DEBUG(false, "exception during logging: %s", CurrentExceptionMessage().data());
            }
        }

    private:
        TStringBuf GetPrefix() noexcept;

        TStringBuf GetSuffix(ELogPriority logLevel) noexcept;

    private:
        const TStringBuf ModuleName_;
        const TConnDescr* const MainDescr_ = nullptr;
        TString Prefix_;
        TString DefaultSuffix_;
        TVector<TString> Suffixes_ = TVector<TString>(LOG_MAX_PRIORITY + 1);
    };

    TString PrintCont(const TCont*) noexcept;

    void PrintCont(IOutputStream& out, const TCont*) noexcept;

    TSourceLocation GetShortLocation(TSourceLocation log) noexcept;

    TStringBuf StripPrettyFunction(TStringBuf func) noexcept;

    TString FormatException(const yexception& e) noexcept;

    // Print ===========================================================================================================

    namespace NImpl {
        // TODO (velavokr): can we get away with one overload?
        inline void DoPrintValue(IOutputStream& log, const TCont& cont) noexcept {
            PrintCont(log, &cont);
        }
        inline void DoPrintValue(IOutputStream& log, TCont& cont) noexcept {
            PrintCont(log, &cont);
        }

        template <class T>
        inline void DoPrintValue(IOutputStream& log, T&& value) noexcept {
            try {
                try {
                    log << value;
                } catch (...) {
                    log << "FAILED_PRINT(" << CurrentExceptionMessage() << ')';
                }
            } catch (...) {
                Y_VERIFY_DEBUG(false, "exception during logging: %s", CurrentExceptionMessage().data());
            }
        }

        template <class T>
        inline void DoPrintValue(IOutputStream& log, T* value) noexcept {
            try {
                try {
                    if (value) {
                        DoPrintValue(log, *value);
                    } else {
                        log << "nullptr";
                    }
                } catch (...) {
                    log << "FAILED_PRINT(" << CurrentExceptionMessage() << ')';
                }
            } catch (...) {
                Y_VERIFY_DEBUG(false, "exception during logging: %s", CurrentExceptionMessage().data());
            }
        }

        inline void DoPrintArgs(IOutputStream&, TStringBuf) noexcept {
        }

        template <class T0>
        inline void DoPrintArgs(IOutputStream& log, TStringBuf name, T0&& value) noexcept {
            try {
                try {
                    log << name << '=';
                } catch (...) {
                    log << "FAILED_PRINT(" << CurrentExceptionMessage() << ')';
                }
            } catch (...) {
                Y_VERIFY_DEBUG(false, "exception during logging: %s", CurrentExceptionMessage().data());
            }
            DoPrintValue(log, std::forward<T0>(value));
        }

        template <class T0, class ... T>
        inline void DoPrintArgs(IOutputStream& log, TStringBuf args, T0&& t0, T&& ... t) noexcept {
            try {
                DoPrintArgs(log, args.NextTok(", "), std::forward<T0>(t0));
                log << ", ";
                DoPrintArgs(log, args, std::forward<T>(t)...);
            } catch (...) {
                Y_VERIFY_DEBUG(false, "exception during logging: %s", CurrentExceptionMessage().data());
            }
        }
    }


    template <class ... T>
    inline void PrintObjState(IOutputStream& log, TStringBuf args, T&& ... t) noexcept {
        try {
            log << '{';
            NImpl::DoPrintArgs(log, args, std::forward<T>(t)...);
            log << '}';
        } catch (...) {
            Y_VERIFY_DEBUG(false, "exception during logging: %s", CurrentExceptionMessage().data());
        }
    }

    // TLogger =========================================================================================================

    class TLogger;

    class TLoggerBlockGuard {
    public:
        TLoggerBlockGuard() = default;

        TLoggerBlockGuard(TLogger& logger, ELogPriority logLevel, TStringBuf func) noexcept;

        ~TLoggerBlockGuard();

    private:
        TLogger* const Logger_ = nullptr;
        const TStringBuf Func_;
        const ELogPriority LogLevel_ = LOG_MAX_PRIORITY;
        const bool Unwinding_ = false;
    };


    class TLogger {
        friend class TLoggerBlockGuard;

        class TContName {
        public:
            TContName(TString name) noexcept
                : Name_(std::move(name))
            {
                Name_.append('\t');
                NameSize_ = Name_.size();
            }

            const TString& GetName() const noexcept {
                return Name_;
            }

            void Push() noexcept {
                Name_.append('.');
            }

            [[nodiscard]]
            bool Pop() noexcept {
                if (Name_.size() > NameSize_) {
                    Name_.pop_back();
                }
                return Name_.size() > NameSize_;
            }

        private:
            TString Name_;
            size_t NameSize_ = 0;
        };

        using TContCache = THashMap<const TCont*, TContName>;

    public:
        static auto EmptyBlockGuard() noexcept {
            return TLoggerBlockGuard();
        }

    public:
        TLogger(TLog* log, TLogLabel& logLabel, const TContExecutor& executor) noexcept;

        ~TLogger();

        TLog* GetLog(ELogPriority logLevel) const noexcept {
            return LogLevel_ < logLevel ? nullptr : Log_;
        }

        template <class... T>
        auto LogBlockStart(ELogPriority logLevel, TStringBuf blockName, TStringBuf args, T&&... t) noexcept {
            if (auto* log = GetLog(logLevel)) {
                return DoLogBlockStart(*log, logLevel, blockName, {}, {}, args, std::forward<T>(t)...);
            } else {
                return TLoggerBlockGuard();
            }
        }

        template <class... T>
        auto LogFunctionStart(ELogPriority logLevel, TStringBuf prettyFunction, TStringBuf args, T&&... t) noexcept {
            if (auto* log = GetLog(logLevel)) {
                return DoLogBlockStart(*log, logLevel, StripPrettyFunction(prettyFunction), {}, {}, args, std::forward<T>(t)...);
            } else {
                return TLoggerBlockGuard();
            }
        }

        template <class TEx, class... T>
        auto LogCatchStart(ELogPriority logLevel, const TEx& exc, TStringBuf args, T&&... t) noexcept {
            if (auto* log = GetLog(logLevel)) {
                return DoLogBlockStart(
                    *log, logLevel, "catch", {TypeName(exc)},
                    {TStringBuf(exc.what())}, args, std::forward<T>(t)...
                );
            } else {
                return TLoggerBlockGuard();
            }
        }

        template <class... T>
        auto LogCatchAllStart(ELogPriority logLevel, TStringBuf args, T&&... t) noexcept {
            if (auto* log = GetLog(logLevel)) {
                return DoLogBlockStart(
                    *log, logLevel, "catch ...", {},
                    {CurrentExceptionMessage()}, args, std::forward<T>(t)...
                );
            } else {
                return TLoggerBlockGuard();
            }
        }

        template <class... T>
        void LogEvent(ELogPriority logLevel, TStringBuf message, TStringBuf args, T&&... t) noexcept {
            if (auto* log = GetLog(logLevel)) {
                DoLogMessage(*log, logLevel, ".[Event]", {message}, {}, args, std::forward<T>(t)...);
            }
        }

    private:
        template <class... T>
        void DoLogMessage(TLog& log, ELogPriority logLevel, TStringBuf name, std::initializer_list<TStringBuf> specs,
                          std::initializer_list<TStringBuf> messages, TStringBuf args, T&&... t) noexcept
        {
            try {
                auto logElement = log << logLevel;
                try {
                    LogLabel_.Print(logElement, logLevel);

                    if (const auto* val = Cache_.FindPtr(GetCurrentCont())) {
                        logElement << val->GetName();
                    } else {
                        PrintCont(logElement, GetCurrentCont());
                        logElement << '\t';
                    }

                    logElement << (UncaughtException() ? "[UNWIND]" : TStringBuf());
                    logElement << name;

                    for (auto spec : specs) {
                        logElement << ' ' << spec;
                    }

                    logElement << '\t';

                    for (auto message : messages) {
                        logElement << message << ' ';
                    }

                    if (args) {
                        PrintObjState(logElement, args, std::forward<T>(t)...);
                        logElement << ' ';
                    }

                    logElement << '\n';
                } catch (...) {
                    logElement << "FAILED_PRINT(" << CurrentExceptionMessage() << ')';
                }
            } catch (...) {
                Y_VERIFY_DEBUG(false, "exception during logging: %s", CurrentExceptionMessage().data());
            }
        }

        template <class... T>
        TLoggerBlockGuard DoLogBlockStart(TLog& log, ELogPriority logLevel, TStringBuf name, std::initializer_list<TStringBuf> specs,
                                    std::initializer_list<TStringBuf> messages, TStringBuf args, T&&... t) noexcept
        {
            Push();
            try {
                DoLogMessage(log, logLevel, name, specs, messages, args, std::forward<T>(t)...);
            } catch (...) {
                Y_VERIFY_DEBUG(false, "exception during logging: %s", CurrentExceptionMessage().data());
            }
            return {*this, logLevel, name};
        }

        void DoLogBlockEnd(TLog& log, ELogPriority logLevel, TStringBuf name, bool wasUnwinding) noexcept;

        void Push() noexcept;

        void Pop() noexcept;

        const TCont* GetCurrentCont() const noexcept;

    private:
        TLog* Log_ = nullptr;
        TLogLabel& LogLabel_;
        const TContExecutor& Executor_;
        TContCache Cache_;
        const ELogPriority LogLevel_;
    };


    namespace NImpl {
        enum class EEscapeMode {
            Unicode, C, Hex, Base64
        };

        TString DoEscape(TStringBuf str, EEscapeMode) noexcept;

        TString DoEscape(const TChunkList&, EEscapeMode) noexcept;

        TString DoEscape(const TChunk&, EEscapeMode) noexcept;

        TString DoEscape(const TChunkPtr&, EEscapeMode) noexcept;

        TString DoEscape(const THeader&, EEscapeMode) noexcept;
    }

    template <class T>
    auto EscUnicode(const T& t) noexcept {
        using namespace NImpl;
        return DoEscape(t, EEscapeMode::Unicode);
    }

    template <class T>
    auto EscC(const T& t) noexcept {
        using namespace NImpl;
        return DoEscape(t, EEscapeMode::C);
    }

    template <class T>
    auto EscHex(const T& t) noexcept {
        using namespace NImpl;
        return DoEscape(t, EEscapeMode::Hex);
    }

    template <class T>
    auto EscBase64(const T& t) noexcept {
        using namespace NImpl;
        return DoEscape(t, EEscapeMode::Base64);
    }
}
