#pragma once

#include <balancer/kernel/http/parser/http.h>
#include <balancer/kernel/memory/chunks.h>

#include <util/generic/bt_exception.h>
#include <util/generic/maybe.h>
#include <util/generic/strbuf.h>
#include <util/generic/yexception.h>
#include <util/memory/blob.h>
#include <util/system/byteorder.h>

#include <type_traits>

namespace NSrvKernel::NHTTP2 {

    using EErrorType = THTTP2Error::EErrorType;
    using EErrorCode = THTTP2Error::EErrorCode;

    using TStreamErrorReason = THTTP2StreamError::TErrorReason;
    using TConnErrorReason = THTTP2ConnectionError::TErrorReason;
    using TUnspecifiedReason = THTTP2Error::TUnspecifiedReason;
    using TConnectionError = THTTP2ConnectionError;
    using TStreamError = THTTP2StreamError;

    using EFlowControlError = THTTP2Error::EFlowControlError;

    using EConnNoError = TConnectionError::ENoError;
    using EConnProtocolError = TConnectionError::EProtocolError;
    using EConnInternalError = TConnectionError::EInternalError;
    using EConnStreamClosed = TConnectionError::EStreamClosed;
    using ECompressionError = TConnectionError::ECompressionError;

    using EStreamNoError = TStreamError::ENoError;
    using EStreamProtocolError = TStreamError::EProtocolError;
    using EStreamInternalError = TStreamError::EInternalError;
    using EStreamStreamClosed = TStreamError::EStreamClosed;
    using ERefusedStream = TStreamError::ERefusedStream;


    const char REQ_HEADER_AUTHORITY[] = ":authority";
    const char REQ_HEADER_METHOD[] = ":method";
    const char REQ_HEADER_PATH[] = ":path";
    const char REQ_HEADER_SCHEME[] = ":scheme";
    const char RESP_HEADER_STATUS[] = ":status";

    const char HEADER_CONTENT_LENGTH[] = "content-length";
    const char HEADER_CONTENT_TYPE[] = "content-type";
    const char HEADER_CONNECTION[] = "connection";
    const char HEADER_KEEP_ALIVE[] = "keep-alive";
    const char HEADER_PROXY_CONNECTION[] = "proxy-connection";
    const char HEADER_TRAILER[] = "trailer";

    // A priority override for clients which do not send priorities (MS Edge)
    const char RESP_HEADER_X_YANDEX_H2_PRIO_EDGE[] = "x-yandex-h2-prio-edge";

    const char REQ_HEADER_UPGRADE[] = "upgrade";
    const char REQ_HEADER_TE[] = "te";
    const char REQ_HEADER_TE_TRAILERS[] = "trailers";
    const char REQ_HEADER_USER_AGENT[] = "user-agent";
    const char REQ_HEADER_HOST[] = "host";
    const char REQ_HEADER_COOKIE[] = "cookie";

    const char METHOD_HEAD[] = "HEAD";

    const ui32 RFC_HEADER_FIELD_OVERHEAD = 32;
    const ui32 RFC_HEADER_TABLE_SIZE_DEFAULT = 4096;
    const ui32 IMPL_HEADER_TABLE_SIZE_DEFAULT = RFC_HEADER_TABLE_SIZE_DEFAULT * 4;
    const ui32 RFC_STATIC_TABLE_INDEX_MAX = 61;

    // Nginx does it, so it's definitely ok.
    const ui32 IMPL_HPACK_INT_SUFFIX_BYTES_LIMIT = 3;
    // The maximum suffix value the hpack int may have given the limit on its length.
    const ui32 IMPL_HPACK_INT_SUFFIX_LIMIT = (1u << 7 * IMPL_HPACK_INT_SUFFIX_BYTES_LIMIT) - 1;
    // We do not support index values bigger than 3-byte suffix plus 4-bit prefix.
    const ui32 IMPL_HPACK_HEADER_TABLE_INDEX_LIMIT = IMPL_HPACK_INT_SUFFIX_LIMIT + 0xF;
    // We do not guarantee support for header tables bigger than this.
    const ui32 IMPL_HEADER_TABLE_SIZE_LIMIT = (IMPL_HPACK_HEADER_TABLE_INDEX_LIMIT - RFC_STATIC_TABLE_INDEX_MAX) * RFC_HEADER_FIELD_OVERHEAD;

    const ui32 RFC_WINDOW_SIZE_MAX = (1u << 31) - 1;
    const ui32 RFC_WINDOW_SIZE_MASK = RFC_WINDOW_SIZE_MAX;
    const ui32 RFC_STREAM_ID_MAX = (1u << 31) - 1;
    const ui32 RFC_STREAM_ID_MASK = RFC_STREAM_ID_MAX;
    const ui32 RFC_FRAME_HEADING_SIZE = 9;
    const ui32 RFC_PRIORITY_SIZE = 5;
    // RFC 7540: For any given request, a lower limit than what is advertised MAY be enforced.
    //           The initial value of this setting is unlimited.
    const ui32 RFC_MAX_HEADER_LIST_SIZE_DEFAULT = -1;
    // Disallowing infinite header lists yet allowing more than the http module would, to avoid breaking the connection
    const ui32 IMPL_MAX_HEADER_LIST_SIZE_DEFAULT = std::max<size_t>(
        NSrvKernel::HTTP_MAX_HEADERS_SIZE_DEFAULT, 1 << 19
    );

    const ui32 RFC_MAX_FRAME_SIZE_MIN = 1u << 14;
    const ui32 RFC_MAX_FRAME_SIZE_DEFAULT = RFC_MAX_FRAME_SIZE_MIN;
    const ui32 RFC_MAX_FRAME_SIZE_MAX = (1u << 24) - 1;
    const ui32 IMPL_FRAME_SIZE_MAX_MIN = 1u << 12;
    const ui32 IMPL_DATA_FRAME_SIZE_MAX_MIN = IMPL_FRAME_SIZE_MAX_MIN;

    const ui32 RFC_FIRST_CLIENT_STREAM_ID = 1;
    const ui32 RFC_FIRST_SERVER_STREAM_ID = 2;

    // RFC 7540: Add one to the value to obtain a
    //           weight between 1 and 256.
    // RFC 7540: default weight of 16.
    const ui32 RFC_PRIO_WEIGHT_MIN = 1;
    const ui32 RFC_PRIO_WEIGHT_DEFAULT = 16;
    const ui32 RFC_PRIO_WEIGHT_MAX = 256;
    // Impl: We operate in raw weights, converting them to the protocol weights only as required.
    const ui8 RFC_PRIO_RAW_WEIGHT_DEFAULT = RFC_PRIO_WEIGHT_DEFAULT - 1;
    const ui8 RFC_PRIO_RAW_WEIGHT_MAX = RFC_PRIO_WEIGHT_MAX - 1;


    template <class TCode>
    TCode GetErrorCode(ui32 rawCode) noexcept {
        return (TCode)Min(rawCode, (ui32)TCode::UNKNOWN);
    }

    // TRegionBase =====================================================================================================

    template <class T>
    class TRegionBase : public TArrayRef<T> {
        using TBase = TArrayRef<T>;
        static_assert(sizeof(T) == 1, "invalid type");
        size_t Consumed_ = 0;

    private:
        using TBase::TBase;

    public:
        TRegionBase(T* begin, size_t len) noexcept
            : TBase(begin, len)
        {}

        TRegionBase(T* begin, T* end) noexcept
            : TRegionBase(begin, end - begin)
        {}

        template <size_t Sz>
        TRegionBase(T (&array)[Sz]) noexcept
            : TRegionBase(&array, Sz)
        {}

        void Consume(size_t len) noexcept {
            Y_VERIFY(len <= TBase::size());
            ConsumeUnsafe(len);
        }

        void ConsumeUnsafe(size_t len) noexcept {
            Consumed_ += len;
            AssignBase({TBase::data() + len, TBase::size() - len});
        }

        [[nodiscard]] size_t SizeAvailable() const noexcept {
            return TBase::size();
        }

        [[nodiscard]] size_t SizeConsumed() const noexcept {
            return Consumed_;
        }

        [[nodiscard]] size_t SizeTotal() const noexcept {
            return SizeAvailable() + SizeConsumed();
        }

        void Clear() noexcept {
            AssignBase({nullptr, nullptr});
        }

        [[nodiscard]] TStringBuf AsStringBuf(size_t len = TStringBuf::npos) const noexcept {
            return {
                (const char*)TBase::data(),
                std::min(len, SizeAvailable())
            };
        }

    protected:
        void AssignBase(TBase base) noexcept {
            (TBase&)(*this) = base;
        }
    };


    // TInputRegion ====================================================================================================

    class TInputRegion : public TRegionBase<const ui8> {
    public:
        using TRegionBase::TRegionBase;

        TInputRegion(const char* begin, size_t len) noexcept
            : TInputRegion((const ui8*)begin, len)
        {}

        TInputRegion(const char* begin, const char* end) noexcept
            : TInputRegion(begin, end - begin)
        {}

        TInputRegion(TStringBuf sb) noexcept
            : TInputRegion(sb.data(), sb.size())
        {}

        TInputRegion(const TBlob& b) noexcept
            : TInputRegion((const ui8*)b.Data(), b.Size())
        {}

        TInputRegion(const TChunk& chunk) noexcept
            : TInputRegion(chunk.AsStringBuf())
        {}

        template <size_t Bytes, class T>
        [[nodiscard]] inline T ReadUIntUnsafe() noexcept {
            T value = 0;
            static_assert(Bytes <= sizeof(value), "invalid read size");
            static_assert(std::is_unsigned<T>::value, "expected unsigned");
            memcpy((ui8*)(&value) + sizeof(value) - Bytes, data(), Bytes);
            value = HostToInet(value);
            ConsumeUnsafe(Bytes);
            return value;
        }

        template <size_t Bytes, class T>
        [[nodiscard]] T ReadUInt() noexcept {
            Y_VERIFY(SizeAvailable() >= Bytes);
            return ReadUIntUnsafe<Bytes, T>();
        }
    };

    template <>
    [[nodiscard]]
    inline ui8 TInputRegion::ReadUIntUnsafe<1, ui8>() noexcept {
        ui8 b = *data();
        ConsumeUnsafe(1);
        return b;
    }


    // TOutputRegion ===================================================================================================

    class TOutputRegion : public TRegionBase<ui8> {
    public:
        using TRegionBase::TRegionBase;

        TOutputRegion(char* begin, size_t len) noexcept
            : TOutputRegion((ui8*)begin, len)
        {}

        TOutputRegion(char* begin, char* end) noexcept
            : TOutputRegion(begin, end - begin)
        {}

        TOutputRegion(TBuffer& b) noexcept
            : TOutputRegion(b.Data(), b.Size())
        {}

        TOutputRegion(TString& s) noexcept
            : TOutputRegion(s.begin(), s.size())
        {}

        TOutputRegion(TChunkData& data) noexcept
            : TOutputRegion(data.Data(), data.Length())
        {}

        template <size_t Bytes, class T>
        inline void WriteUIntUnsafe(T value) noexcept {
            static_assert(Bytes <= sizeof(value), "invalid write size");
            static_assert(std::is_unsigned<T>::value, "expected unsigned");
            value = HostToInet(value);
            memcpy(data(), (ui8*)(&value) + sizeof(value) - Bytes, Bytes);
            ConsumeUnsafe(Bytes);
        }

        template <size_t Bytes, class T>
        void WriteUInt(T value) noexcept {
            Y_VERIFY(SizeAvailable() >= Bytes);
            WriteUIntUnsafe<Bytes>(value);
        }

        void Copy(TStringBuf str) noexcept {
            Y_VERIFY(SizeAvailable() >= str.size());
            CopyUnsafe(str);
        }

        void CopyUnsafe(TStringBuf str) noexcept {
            if (str.size()) {
                memcpy(data(), str.data(), str.size());
                ConsumeUnsafe(str.size());
            }
        }
    };

    template <>
    inline void TOutputRegion::WriteUIntUnsafe<1, ui8>(ui8 value) noexcept {
        *data() = value;
        ConsumeUnsafe(1);
    };


    // TStackRegion ====================================================================================================

    template <size_t Sz, class TRegion>
    class TStackRegion {
    public:
        TStackRegion() noexcept
            : Region_(Buffer_, Sz)
        {
            memset(Buffer_, 0, Sz);
        }

        [[nodiscard]] ui8* Data() noexcept {
            return Buffer_;
        }

        [[nodiscard]] const ui8* Data() const noexcept {
            return Buffer_;
        }

        [[nodiscard]] constexpr size_t Size() const noexcept {
            return Sz;
        }

        [[nodiscard]] TRegion& GetRegion() noexcept {
            return Region_;
        }

        [[nodiscard]] TChunkPtr CopyToChunk() const noexcept {
            return NewChunk(TBlob::Copy(Buffer_, Region_.SizeConsumed()));
        }

    private:
        ui8 Buffer_[Sz];
        TRegion Region_;
    };


    // TUnitedChunkList ================================================================================================

    class TUnitedChunkList {
    public:
        TUnitedChunkList() = default;

        TUnitedChunkList(const TChunkList& lst) noexcept;

        TUnitedChunkList(TChunkList&& lst) noexcept;

        [[nodiscard]]
        TStringBuf AsStringBuf() const noexcept {
            return DataView_;
        }

        explicit operator bool() const noexcept {
            return (bool)AsStringBuf();
        }

    private:
        void Copy(const TChunkList& lst) noexcept;

    private:
        TStringBuf DataView_;
        TMaybe<TBlob> Data_;
    };


    enum class EHPackHeaderRule {
        Index, DoNotIndex, NeverIndex
    };

    // THeader  ================================================================================================

    struct THeader : public TIntrusiveListItem<THeader> {
        THeader() noexcept = default;

        THeader(const THeader& hdr) noexcept
            : Key(hdr.Key.Copy())
            , Value(hdr.Value.Copy())
        {}

        THeader(THeader&& rhs) noexcept
            : Key(std::move(rhs.Key))
            , Value(std::move(rhs.Value))
        {}

        THeader(TChunkList key, TChunkList value) noexcept
            : Key(std::move(key))
            , Value(std::move(value))
        {}

        THeader& operator=(const THeader& rhs) noexcept {
            auto tmp = rhs;
            return *this = std::move(tmp);
        }

        THeader& operator=(THeader&& rhs) noexcept {
            Key = std::move(rhs.Key);
            Value = std::move(rhs.Value);
            return *this;
        }

    public:
        TChunkList Key;
        TChunkList Value;
        TMaybe<EHPackHeaderRule> HPackHeaderRule;
    };

    // THeadersList  ================================================================================================

    class THeadersList : public TIntrusiveListWithAutoDelete<THeader, TDelete> {
    private:
        THeadersList(const THeadersList& hs) noexcept {
            for (const auto& hdr: hs) {
                PushBack(new THeader(hdr));
            }
        }

        THeadersList& operator=(const THeadersList& rhs) noexcept {
            THeadersList tmp = rhs;
            return *this = std::move(tmp);
        }

    public:
        THeadersList() noexcept = default;

        THeadersList(THeadersList&&) noexcept = default;

        ~THeadersList() = default;

        THeadersList& operator=(THeadersList&&) noexcept = default;

        THeadersList Copy() const noexcept {
            return *this;
        }

        void erase(const_iterator header) {
            delete &*header;
        }

         std::pair<const TChunkList*, TChunkList*> Add(TChunkList&& key, TChunkList&& val) noexcept {
            THeader* const hdr = new THeader(std::move(key), std::move(val));
            PushBack(hdr);

            return {&hdr->Key, &hdr->Value};
        }

        void BuildTo(IOutputStream& out) const noexcept {
            for (const auto& hdr: *this) {
                out << hdr.Key << HDRD << hdr.Value << CRLF;
            }
        }
    };
}
