#include "http2_conn.h"
#include "http2_headers.h"

#include <balancer/kernel/http2/server/common/http2_headers_fsm.h>
#include <balancer/kernel/http2/server/utils/http2_log.h>

#include <util/generic/strbuf.h>

namespace NSrvKernel::NHTTP2 {

    namespace {
        [[nodiscard]]
        bool IsNewClientStreamAllowed(EShutdownPhase phase) noexcept {
            switch (phase) {
            case EShutdownPhase::Running:
            case EShutdownPhase::GoAwayNoIdSent:
                return true;
            case EShutdownPhase::GoAwayLastIdSent:
                return false;
            }
        }

        [[nodiscard]]
        ui32 GetStreamSendBufferSize(const TAuxServerSettings& settings, ui32 openCount) noexcept {
            // This allocation policy is explained in BALANCER-1352
            ui32 curStep = openCount / std::max<ui32>(settings.StreamSendBufferLoadStep, 1);

            // Note: If the value of the right operand is negative or is greater or equal to the number of bits
            //      in the promoted left operand, the behavior is undefined.
            curStep = std::min<ui32>(
                curStep,
                sizeof(settings.StreamSendBufferHiMax) * 8 - 1
            );

            return std::max(
                settings.StreamSendBufferLoMax,
                settings.StreamSendBufferHiMax >> curStep
            );
        }

        class TGracefulShutdownHandler : public IGracefulShutdownHandler {
        public:
            TGracefulShutdownHandler(TConnection& conn) : Conn_(conn) {}

            void OnShutdown() noexcept override {
                Conn_.OnShutdown();
            }
        private:
            TConnection& Conn_;
        };
    }

    void TConnError::PrintTo(IOutputStream& out) const {
        Y_HTTP2_PRINT_OBJ(out, GoAway, Reason);
    }

    // TConnection =====================================================================================================

    void TConnection::InitStatic() noexcept {
        InitHeadersFSMs();
    }

    TConnection::TConnection(
        const IHTTP2Module& parent,
        const TConnDescr& mainDescr,
        TLogger& logger,
        const TSettings& serverSettings,
        const TAuxServerSettings& auxServerSettings
    ) noexcept
        : Parent_(parent)
        , MainDescr_(mainDescr)
        , MainCont_(*mainDescr.Process().Executor().Running())
        , Logger_(logger)
        , Stats_(Parent_.GetStats(mainDescr))
        , ServerSettings_(serverSettings)
        , AuxServerSettings_(auxServerSettings)
        , ClientInput_(
            *MainDescr_.Input,
            logger,
            Stats_,
            ServerSettings_
        )
        , ClientOutput_(
            *this,
            *MainDescr_.Output,
            mainDescr.Process().Executor(),
            Stats_,
            logger,
            ClientSettings_,
            AuxServerSettings_
        )
        , HPackDecoder_(
            GetHPackDecoderSettings(ServerSettings_.GetCurrent(), AuxServerSettings_)
        )
        , RecvFlow_(
            logger,
            ServerSettings_
        )
        , EdgePrioFix_(AuxServerSettings_)
        , Streams_(
            *this,
            AuxServerSettings_
        )
        , Executor_(mainDescr.Process().Executor())
    {}

    TConnection::~TConnection() {
        Y_HTTP2_METH_E(Logger_);
    }

    void TConnection::PrintTo(IOutputStream& out) const {
        Y_HTTP2_PRINT_OBJ(
            out,
            Streams_,
            ConnState_,
            ConnError_,
            (bool)ConnException_
        );
    }

    TError TConnection::RunConnection() noexcept {
        Y_HTTP2_METH(Logger_, ServerSettings_, AuxServerSettings_);

        ClientOutput_.Start();
        ClientOutput_.SendSettings(ServerSettings_);

        Y_TRY(TError, error) {
            bool preface = false;
            Y_PROPAGATE_ERROR(
                ClientInput_.RecvPreface().AssignTo(preface)
            );
            if (preface) {
                TGracefulShutdownHandler gracefulShutdownHandler(*this);
                MainDescr_.Process().AddGracefulShutdownHandler(&gracefulShutdownHandler, true);

                ui32 framesNoYield = 0;
                while (auto frame = RecvClientFrame()) {
                    CheckCpuLimit();
                    Y_PROPAGATE_ERROR(
                        ProcessClientFrame(std::move(*frame))
                    );
                    // BALANCER-1314
                    // Preventing coroutine starvation by
                    // 1. forcing a context switch after each CPU-hungry frame processing
                    // 2. forcing a context switch once in a while
                    if (frame->Heading.IsPriority()      // May trigger a priority tree and priority queue rebuilds
                        || frame->Heading.IsHeaders()    // Same as above and an expensive decoding
                        || frame->Heading.IsSettings()   // May trigger a priority queue rebuild
                        || (framesNoYield += 1) >= AuxServerSettings_.FramesNoYieldMax)
                    {
                        Y_HTTP2_EVENT(Logger_, "Forcing yield after frame", frame->Heading, framesNoYield);
                        framesNoYield = 0;
                        GetMainCont().Yield();
                    }
                }
            }
            return {};
        } Y_CATCH {
            Y_HTTP2_LOG_CATCH_E(Logger_, TLOG_ERR, *error);
            if (const auto* connErr = error.GetAs<TConnectionError>()) {
                OnConnError(connErr->ErrorCode, connErr->AsStrBuf(), connErr->ErrorReason);
            } else {
                OnConnReset(std::move(error), EErrorSource::Other);
            }
        }

        // everything is noexcept
        if (IsConnError()) {
            ProcessConnError();
        } else if (!ProcessReadFin()) {
            ProcessConnError();
        }

        // MINOTAUR-2222 While mass reaping connections we may starve other coroutines.
        Executor_.Running()->Yield();
        return std::move(ConnException_);
    }

    TMaybe<TFrame> TConnection::RecvClientFrame() noexcept {
        Y_HTTP2_METH_E(Logger_);

        if (!IsReadOpen()) {
            return Nothing();
        }

        TMaybe<TFrame> ret;
        Y_TRY(TError, error) {
            Y_PROPAGATE_ERROR(
                ClientInput_.RecvFrame().AssignTo(ret)
            );
            if (!ret && IsReadOpen()) {
                ConnState_ = EConnState::ReadFin;
            }
            return {};
        } Y_CATCH {
            Y_HTTP2_CATCH(Logger_, *error, this);
            if (const auto* connErr = error.GetAs<TConnectionError>()) {
                OnConnError(connErr->ErrorCode, connErr->AsStrBuf(), connErr->ErrorReason);
            } else {
                OnConnReset(std::move(error), EErrorSource::RecvFrame);
            }
        }

        return ret;
    }

    TError TConnection::ProcessClientFrame(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading);

        Y_TRY(TError, error) {
            switch (frame.Heading.Type) {
            case EFrameType::DATA:
                return OnData(std::move(frame));
            case EFrameType::HEADERS:
                return OnHeaders(std::move(frame));
            case EFrameType::PRIORITY:
                return OnPriority(std::move(frame));
            case EFrameType::RST_STREAM:
                return OnRstStream(std::move(frame));
            case EFrameType::SETTINGS:
                return OnSettings(std::move(frame));
            case EFrameType::PING:
                return OnPing(std::move(frame));
            case EFrameType::GOAWAY:
                OnGoAway(std::move(frame));
                break;
            case EFrameType::WINDOW_UPDATE:
                return OnWindowUpdate(std::move(frame));
            case EFrameType::CONTINUATION:
                // RFC 7540: A CONTINUATION frame MUST be preceded by a HEADERS, PUSH_PROMISE or
                //           CONTINUATION frame without the END_HEADERS flag set.  A recipient
                //           that observes violation of this rule MUST respond with a connection
                //           error (Section 5.4.1) of type PROTOCOL_ERROR.
                return Y_MAKE_ERROR(
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame)
                );
            case EFrameType::PUSH_PROMISE:
                // RFC 7540: A client cannot push.  Thus, servers MUST treat the receipt of a
                //           PUSH_PROMISE frame as a connection error (Section 5.4.1) of type
                //           PROTOCOL_ERROR.
                return Y_MAKE_ERROR(
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame)
                );
            default:
                // RFC 7540: Implementations MUST discard frames
                //           that have unknown or unsupported types.
                break;
            }
            return {};
        } Y_CATCH {
            Y_HTTP2_LOG_CATCH_E(Logger_, TLOG_ERR, *error);
            if (const auto* streamErr = error.GetAs<TStreamError>()) {
                return OnStreamError(frame.Heading.StreamId,
                                     streamErr->ErrorCode,
                                     streamErr->ErrorReason);
            } else if (const auto* httpErr = error.GetAs<THttpError>()) {
                return OnStreamError(frame.Heading.StreamId,
                                     GetStreamErrorCode((HttpCodes) httpErr->Code()),
                                     GetStreamErrorReason((HttpCodes) httpErr->Code()));
            }
            return error;
        }
        return {};
    }

    TError TConnection::OnData(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading);

        // RFC 7540: Receiving any frame other than HEADERS or PRIORITY on a stream in
        //           idle state MUST be treated as a connection error (Section 5.4.1)
        //           of type PROTOCOL_ERROR.
        Y_REQUIRE(frame.Heading.StreamId <= MaxClientStreamId_,
            TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame));

        // RFC 7540: A receiver that receives a flow-controlled frame MUST always account
        //           for its contribution against the connection flow-control window,
        //           unless the receiver treats this as a connection error
        //           (Section 5.4.1).  This is necessary even if the frame is in error.
        //           The sender counts the frame toward the flow-control window, but if
        //           the receiver does not, the flow-control window at the sender and
        //           receiver can become different.
        const auto length = frame.Heading.Length;
        Y_PROPAGATE_ERROR(
            RecvFlow_.Consume(length)
        );

        auto streamPtr = Streams_.Find(frame.Heading.StreamId);
        if (streamPtr) {
            Y_REQUIRE(!streamPtr->IsClosedByClient(),
                TConnectionError(EErrorCode::STREAM_CLOSED, EConnStreamClosed::ClosedByClient));
            Y_REQUIRE(!streamPtr->IsIdle(),
                TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame));

            if (streamPtr->IsOpen()) {
                Y_PROPAGATE_ERROR(
                    streamPtr->OnData(std::move(frame))
                );
            }
        }

        if (length) {
            // Update only after the data was consumed by the stream.
            Y_PROPAGATE_ERROR(
                RecvFlow_.Update(length)
            );
            Y_PROPAGATE_ERROR(
                ClientOutput_.SendConnWindowUpdate(length)
            );
        }

        // According to RFC 7540 we should be throwing TConnectionError here.
        //      But since there is no way to prevent the stream GC from collecting
        //      the stream which might have just sent an RST_STREAM, should the client
        //      have sent DATA on that stream before receiving our RST_STREAM and
        //      should we have been too fast sending EOS (or just buffering them),
        //      we might end up killing a valid connection.
        // Thus we knowngly violate the RFC and send TStreamError instead.
        Y_REQUIRE(streamPtr,
            TStreamError(EErrorCode::STREAM_CLOSED, EStreamStreamClosed::AlreadyErased));
        return {};
    }

    TError TConnection::OnHeaders(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading);

        if (!StreamsCnt_) {
            Stats_.H2Conn.OnFirstStream();
        }
        StreamsCnt_ += 1;

        TMaybe<TPriority> priority;
        Y_PROPAGATE_ERROR(
            TPriority::Strip(frame).AssignTo(priority)
        );

        if (priority) {
            Y_HTTP2_EVENT(Logger_, "Received PRIORITY within HEADERS", frame.Heading.StreamId, priority);
            EdgePrioFix_.OnPriority(*priority);
        }

        if (frame.Heading.StreamId > MaxClientStreamId_) {
            // The function will exit either by receiving an EndHeaders or by reaching the headers size limit.
            TChunkList lst;
            Y_PROPAGATE_ERROR(
                RecvFullHeaderBlock(frame).AssignTo(lst)
            );

            // We must decode the headers even if the stream is bound to fail to sync the decoder state with the client
            THeadersList headerList;
            Y_PROPAGATE_ERROR(HPackDecoder_.Decode(TUnitedChunkList{std::move(lst)}.AsStringBuf()).AssignTo(headerList));

            Y_REQUIRE(ContainsSpecHeaders(headerList),
                TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::ExpectedSpecHeaders));

            // Sending RST_STREAM means we accepted the stream id
            MaxClientStreamId_ = frame.Heading.StreamId;

            Y_REQUIRE(Streams_.GetOpenCount() < ServerSettings_.GetCurrent().MaxConcurrentStreams,
                TStreamError(EErrorCode::REFUSED_STREAM, ERefusedStream::MaxStreams));

            Y_REQUIRE(IsNewClientStreamAllowed(ShutdownPhase_),
                TStreamError(EErrorCode::REFUSED_STREAM, ERefusedStream::Shutdown));

            // Will validate the headers
            THTTP11Request http11Request;
            Y_PROPAGATE_ERROR(
                ConvertRequest2To11(
                    Logger_, Stats_,
                    std::move(headerList),
                    frame.Heading.HasFlagEndStream(),
                    AuxServerSettings_.AllowSendingTrailers
                ).AssignTo(http11Request)
             );

            // streamPtr is guaranteed to be nonzero
            auto streamPtr = Streams_.Open(frame.Heading.StreamId);
            Y_VERIFY(streamPtr);
            Y_VERIFY(!streamPtr->IsClosed());

            if (Streams_.GetOpenCount() == 1) {
                Stats_.H2Conn.OnActive();
            }

            Y_PROPAGATE_ERROR(
                streamPtr->Open(
                    frame.Heading,
                    std::move(http11Request),
                    GetStreamSendBufferSize(AuxServerSettings_, Streams_.GetOpenCount())
                )
            );

            if (priority) {
                Y_PROPAGATE_ERROR(
                    streamPtr->OnPriority(
                        *priority,
                        Streams_.Find(priority->StreamDependency)
                    )
                );
            }
        } else {
            Y_REQUIRE(frame.Heading.HasFlagEndStream(),
                TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame));

            auto existingStreamPtr = Streams_.Find(frame.Heading.StreamId);

            if (existingStreamPtr) {
                Y_REQUIRE(!existingStreamPtr->IsClosedByClient(),
                    TConnectionError(EErrorCode::STREAM_CLOSED, EConnStreamClosed::ClosedByClient));
            }

            // The function will exit either by receiving an EndHeaders or by reaching the headers size limit.
            TChunkList lst;
            Y_PROPAGATE_ERROR(
                RecvFullHeaderBlock(frame).AssignTo(lst)
            );
            // We must decode the headers even if the stream is bound to fail to sync the decoder state with the client
            THeadersList headerList;
            Y_PROPAGATE_ERROR(HPackDecoder_.Decode(TUnitedChunkList{std::move(lst)}.AsStringBuf()).AssignTo(headerList));

            if (existingStreamPtr) {
                Y_REQUIRE(!existingStreamPtr->IsClosedByClient(),
                    TConnectionError(EErrorCode::STREAM_CLOSED, EConnStreamClosed::ClosedByClient));
            }

            Y_REQUIRE(!ContainsSpecHeaders(headerList),
                TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedSpecHeaders));

            if (existingStreamPtr) {
                if (priority) {
                    Y_PROPAGATE_ERROR(
                        existingStreamPtr->OnPriority(
                            *priority,
                            Streams_.Find(priority->StreamDependency)
                        )
                    );
                }

                return existingStreamPtr->OnTrailers(std::move(headerList));
            } else {
                // See the discussion on stream error vs connection error in OnData
                return Y_MAKE_ERROR(
                    TStreamError(EErrorCode::STREAM_CLOSED, EStreamStreamClosed::AlreadyErased)
                );
            }
        }
        return {};
    }

    TErrorOr<TChunkList> TConnection::RecvFullHeaderBlock(TFrame& headersFrame) noexcept {
        Y_HTTP2_METH(Logger_, headersFrame.Heading);

        Y_HTTP2_EVENT(Logger_, "Got HEADERS payload", EscBase64(headersFrame.Payload));

        TChunkList lst;
        lst.Push(std::move(headersFrame.Payload));

        if (!headersFrame.Heading.HasFlagEndHeaders()) {
            Y_HTTP2_BLOCK(Logger_, "Recv header block", headersFrame.Heading);

            ui32 headersLength = headersFrame.Heading.Length;

            while (true) {
                Y_HTTP2_BLOCK(Logger_, "Recv CONTINUATION", headersFrame.Heading);
                TMaybe<TFrame> contFrame;
                Y_PROPAGATE_ERROR(
                    ClientInput_.RecvFrame().AssignTo(contFrame)
                );

                Y_REQUIRE(contFrame,
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedEOF));

                Y_REQUIRE(contFrame->Heading.IsContinuation(),
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame));

                Y_REQUIRE(contFrame->Heading.StreamId == headersFrame.Heading.StreamId,
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame));

                headersLength += contFrame->Heading.Length;

                Y_HTTP2_EVENT(Logger_, "Got CONTINUATION payload", EscBase64(contFrame->Payload));

                Y_REQUIRE(GetMinHeaderListSize(headersLength) <= ServerSettings_.GetCurrent().MaxHeaderListSize,
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::TooBigHeaders));

                lst.Push(std::move(contFrame->Payload));

                if (contFrame->Heading.HasFlagEndHeaders()) {
                    break;
                } else {
                    // BALANCER-1361
                    // Preventing coroutine starvation on an input buffer full of empty CONTINUATIONs.
                    GetMainCont().Yield();
                }
            }
        }

        Y_HTTP2_EVENT(Logger_, "Got whole header block", lst.size());

        return lst;
    }

    TError TConnection::OnPriority(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading);

        const auto priority = TPriority::Parse(frame.Payload->AsStringBuf());
        Y_HTTP2_EVENT(Logger_, "Received PRIORITY", frame.Heading.StreamId, priority);

        EdgePrioFix_.OnPriority(priority);

        if (auto streamPtr = Streams_.FindOrCreateIdle(frame.Heading.StreamId)) {
            return streamPtr->OnPriority(priority, Streams_.Find(priority.StreamDependency));
        }
        return {};
    }

    TError TConnection::OnRstStream(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading);

        // RFC 7540: RST_STREAM frames MUST NOT be sent for a stream in the "idle" state.
        //           If a RST_STREAM frame identifying an idle stream is received, the
        //           recipient MUST treat this as a connection error (Section 5.4.1) of
        //           type PROTOCOL_ERROR.
        Y_REQUIRE(frame.Heading.StreamId <= MaxClientStreamId_,
            TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame));

        const auto errorCode = ParseRstStream(frame.Payload->AsStringBuf());
        Y_HTTP2_LOG_EVENT(Logger_, TLOG_ERR, "Received RST_STREAM", frame.Heading.StreamId, errorCode);

        if (auto streamPtr = Streams_.Find(frame.Heading.StreamId)) {
            if (streamPtr->IsCancelledByClient() && EErrorCode::PROTOCOL_ERROR == errorCode) {
                Stats_.RstStreamRecv.OnProtocolErrorAfterCancel();
            } else {
                Stats_.RstStreamRecv.OnError(errorCode);
            }
            streamPtr->OnClientRstStream(errorCode);
        } else {
            Stats_.RstStreamRecv.OnError(errorCode);
        }
        return {};
    }

    TError TConnection::OnSettings(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading);

        const auto oldWindow = ClientSettings_.InitialWindowSize;

        if (frame.Heading.HasFlagAck()) {
            ServerSettings_.AckNextWaiting();
            HPackDecoder_.UpdateSettings(
                GetHPackDecoderSettings(ServerSettings_.GetCurrent(), AuxServerSettings_)
            );
            Y_PROPAGATE_ERROR(RecvFlow_.UpdateInitialSize());
        } else {
            Y_PROPAGATE_ERROR(
                ClientSettings_.Parse(
                    Logger_,
                    frame.Payload->AsStringBuf(),
                    ClientOutput_.GetEncoder(),
                    AuxServerSettings_
                )
            );
            Y_PROPAGATE_ERROR(
                ClientOutput_.SendSettingsAck()
            );
        }

        // It is important to apply the settings only after the full frame was processed.
        // This way the heavy operations will only be triggered once per frame and not once per setting.

        if (ClientSettings_.InitialWindowSize > oldWindow) {
            Y_PROPAGATE_ERROR(
                ClientOutput_.GetFlow().UpdateInitialSize()
            );

            for (auto streamPtr : Streams_.GetSnapshotOfRunning()) {
                // Stream errors here are handled by the stream itself.
                // Everything coming out deserves tearing down the connection.
                Y_PROPAGATE_ERROR(
                    streamPtr->OnInitialWindowUpdate()
                );
            }
        }
        return {};
    }

    TError TConnection::OnPing(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading, EscHex(frame.Payload));

        if (frame.Heading.HasFlagAck()) {
            OnPingAck(TPing::Parse(frame.Payload->AsStringBuf()));
        } else {
            Y_PROPAGATE_ERROR(
                ClientOutput_.SendPingAck(std::move(frame.Payload))
            );
        }
        return {};
    }

    void TConnection::OnPingAck(TPing ping) noexcept {
        Y_HTTP2_METH(Logger_, ping);

        LastRTT_ = ping.GetRTT(AuxServerSettings_.ClientRTTEstimateMax);
        Y_HTTP2_EVENT(Logger_, "New RTT estimate", LastRTT_, ping.PingType);

        if (EShutdownPhase::GoAwayNoIdSent == ShutdownPhase_ && EPingType::GuardGoAwayNoId == ping.PingType) {
            ClientOutput_.SendGoAwayGraceful(
                TGoAway(MaxClientStreamId_, EErrorCode::NO_ERROR),
                EConnNoError::Shutdown
            );
            ShutdownPhase_ = EShutdownPhase::GoAwayLastIdSent;
            Y_HTTP2_EVENT(Logger_, "Next shutdown phase", ShutdownPhase_);
        }
    }

    void TConnection::OnGoAway(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading);

        // RFC 7540: Receivers of a GOAWAY frame MUST NOT open
        //           additional streams on the connection
        ClientSettings_.ReceivedGoaway = true;
        const auto goAway = TGoAway::Parse(frame.Payload->AsStringBuf());
        Stats_.GoAwayRecv.OnGoAway(goAway);
        // RFC 7540: A GOAWAY frame might not immediately precede closing of the
        //           connection; a receiver of a GOAWAY that has no more use for the
        //           connection SHOULD still send a GOAWAY frame before terminating the
        //           connection.
        Y_HTTP2_LOG_EVENT(Logger_, TLOG_ERR, "Received GOAWAY", goAway);
    }

    TError TConnection::OnWindowUpdate(TFrame frame) noexcept {
        Y_HTTP2_METH(Logger_, frame.Heading);

        const auto windowSizeIncrement = ParseWindowUpdate(frame.Payload->AsStringBuf());

        Y_HTTP2_EVENT(Logger_, "Received WINDOW_UPDATE", windowSizeIncrement);

        if (frame.Heading.StreamId) {
            // RFC 7540: Receiving any frame other than HEADERS or PRIORITY on a stream in
            //           idle state MUST be treated as a connection error (Section 5.4.1)
            //           of type PROTOCOL_ERROR.
            Y_REQUIRE(frame.Heading.StreamId <= MaxClientStreamId_,
                TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::UnexpectedFrame));

            // RFC 7540: WINDOW_UPDATE or RST_STREAM frames can be received in this[closed] state
            //           for a short period after a DATA or HEADERS frame containing an
            //           END_STREAM flag is sent.  Until the remote peer receives and
            //           processes RST_STREAM or the frame bearing the END_STREAM flag, it
            //           might send frames of these types.  Endpoints MUST ignore
            //           WINDOW_UPDATE or RST_STREAM frames received in this state
            if (auto streamPtr = Streams_.Find(frame.Heading.StreamId)) {
                return streamPtr->OnWindowUpdate(windowSizeIncrement);
            }
        } else {
            return ClientOutput_.GetFlow().Update(windowSizeIncrement);
        }
        return {};
    }

    // Connection state change handlers

    void TConnection::OnShutdown() noexcept {
        Y_HTTP2_METH(Logger_, ShutdownPhase_);

        if (EShutdownPhase::Running == ShutdownPhase_) {
            ClientOutput_.SendShutdownWarning();
            ShutdownPhase_ = EShutdownPhase::GoAwayNoIdSent;
            Y_HTTP2_EVENT(Logger_, "Next shutdown phase", ShutdownPhase_);
        }
    }

    void TConnection::CheckCpuLimit() noexcept {
        Y_HTTP2_METH_E(Logger_);
        if (ShutdownPhase_ != EShutdownPhase::GoAwayLastIdSent) {
            if (MainDescr_.CpuLimiter() && MainDescr_.CpuLimiter()->CheckHTTP2Closed()) {
                ClientOutput_.SendGoAwayGraceful(
                    TGoAway(MaxClientStreamId_, EErrorCode::NO_ERROR),
                    EConnNoError::CpuLimit
                );
                ShutdownPhase_ = EShutdownPhase::GoAwayLastIdSent;
                Y_HTTP2_EVENT_E(Logger_, "Forced shutdown by cpu limiter");
            }
        }
    }

    void TConnection::OnConnReset(TError err, EErrorSource ioDir) noexcept {
        Y_HTTP2_METH_E(Logger_);

        TrySetConnException(std::move(err), ioDir);
        ConnState_ = EConnState::Reset;
        GetMainCont().ReSchedule();
    }

    void TConnection::OnConnInternalError(TError err) noexcept {
        Y_HTTP2_METH_E(Logger_);

        if (EConnState::Reset != ConnState_) {
            OnConnError(EErrorCode::INTERNAL_ERROR, GetErrorMessage(err), EConnInternalError::UnknownError);
            TrySetConnException(std::move(err), EErrorSource::Other);
        } else {
            Y_HTTP2_EVENT_E(Logger_, "Already reset");
        }
    }

    void TConnection::OnConnError(EErrorCode errorCode, TStringBuf debugData, TConnErrorReason reason) noexcept {
        Y_HTTP2_METH(Logger_, errorCode, EscC(debugData), reason);

        if (EConnState::Reset != ConnState_) {
            if (ConnError_) {
                Y_HTTP2_EVENT(Logger_, "ConnError already set", ConnError_);
            } else {
                ConnError_.ConstructInPlace(
                    TGoAway(
                        MaxClientStreamId_,
                        errorCode,
                        AuxServerSettings_.GoAwayDebugDataEnabled ? debugData : ""
                    ),
                    reason
                );
            }
            GetMainCont().ReSchedule();
        } else {
            Y_HTTP2_EVENT_E(Logger_, "Already reset");
        }
    }

    TError TConnection::OnStreamError(ui32 streamId, EErrorCode errorCode, TStreamErrorReason reason) noexcept {
        Y_HTTP2_METH(Logger_, streamId, errorCode, reason);

        if (auto streamPtr = Streams_.Find(streamId)) {
            return streamPtr->OnStreamError({errorCode, reason});
        } else {
            return ClientOutput_.SendRstStream(streamId, errorCode, reason);
        }
    }

    // Stream callbacks

    TError TConnection::OnHTTP(const TConnDescr& connDescr) const {
        Y_HTTP2_METH_E(Logger_);
        return Parent_.OnHTTP(connDescr);
    }

    void TConnection::DisposeStream(TStream& stream) noexcept {
        Y_HTTP2_METH(Logger_, stream);
        // DisposeStream may be called from many different stream coroutines, thus it must not call Streams.ShrinkClosed
        Streams_.Close(stream);
        Stats_.Stream.OnDisposal();
        if (Streams_.GetOpenCount() == 0) {
            Stats_.H2Conn.OnInactive();
        }
    }

    // Helper methods

    void TConnection::TrySetConnException(TError err, EErrorSource errorSource) noexcept {
        Y_HTTP2_METH_E(Logger_);

        if (ConnException_) {
            Y_HTTP2_EVENT_E(Logger_, "ConnException already set");
        } else {
            if (auto* systemError = err.GetAs<TSystemError>()) {
                Y_UNUSED(Stats_.OnIOError(ErrnoToIOError(systemError->Status()), errorSource));
            } else if (err.GetAs<TSslError>()) {
                Stats_.OnIOError(EIOError::SSL, errorSource);
            } else {
                Stats_.OnIOError(EIOError::Other, errorSource);
            }
            ConnException_ = std::move(err);
        }
    }

    bool TConnection::ProcessReadFin() noexcept {
        Y_HTTP2_METH_E(Logger_);

        // The client won't send us a window update, so we should prevent the streams from blocking on flow windows.
        ClientOutput_.GetFlow().OnReadFin();

        for (auto streamPtr : Streams_.GetSnapshotOfRunning()) {
            streamPtr->OnReadFin();
        }

        if (IsConnError()) {
            return false;
        }

        {
            // Finish the streams. Finished streams will retire themselves and will be reaped later in the destructor.
            Y_HTTP2_BLOCK(Logger_, "Join all streams", this);

            // Takes an extra reference for each running stream.
            for (auto streamPtr : Streams_.GetSnapshotOfRunning()) {
                Y_HTTP2_BLOCK(Logger_, "Join stream", *streamPtr, this);
                streamPtr->Join();

                if (IsConnError()) {
                    // The extra references are dropped.
                    return false;
                }
            }
            // The extra references are dropped.
        }

        // Sending the final goaway.
        Y_HTTP2_BLOCK(Logger_, "Send GoAway", this);
        ClientOutput_.SendGoAwayGraceful(
            TGoAway(MaxClientStreamId_, EErrorCode::NO_ERROR),
            EConnNoError::ConnClose
        );

        // Cancel all waitings if any (there should be none by the moment anyway) and prevent further sending.
        ClientOutput_.Close();

        {
            Y_HTTP2_BLOCK(Logger_, "Join output", this);
            ClientOutput_.Join();
        }

        return !IsConnError();
    }

    void TConnection::ProcessConnError() noexcept {
        Y_HTTP2_METH_E(Logger_);

        if (EConnState::Reset != ConnState_) {
            Y_HTTP2_BLOCK(Logger_, "Send GoAway", this);
            if (ConnError_) {
                // RFC 7540: After sending the GOAWAY frame for an error condition,
                //           the endpoint MUST close the TCP connection.
                if (ConnException_) {
                    // If we encountered an internal problem, drop the current queue and report goaway immediately.
                    ClientOutput_.SendGoAwayUrgent(
                        TGoAway(MaxClientStreamId_, std::move(ConnError_->GoAway)), ConnError_->Reason
                    );
                } else {
                    // If the client has misbehaved but the connection is otherwise healthy, try reporting goaway gracefully.
                    ClientOutput_.SendGoAwayGraceful(
                        TGoAway(MaxClientStreamId_, std::move(ConnError_->GoAway)), ConnError_->Reason
                    );
                }
                ConnError_.Clear();
            } else {
                // Should not happen
                ClientOutput_.SendGoAwayUrgent(
                    TGoAway(MaxClientStreamId_, EErrorCode::INTERNAL_ERROR, "Cancelled"), EConnInternalError::UnknownError
                );
            }
        }

        // Cancel all waitings and prevent further sending. The streams should unblock now.
        ClientOutput_.Close();

        // Cancel all the streams.
        for (auto streamPtr : Streams_.GetSnapshotOfRunning()) {
            if (streamPtr->IsOpen()) {
                Stats_.Stream.OnConnAbort();
            }
            streamPtr->OnConnError(ConnException_);
        }

        {
            // Finish the streams. Finished streams will retire themselves and later be reaped in the desctructor.
            Y_HTTP2_BLOCK(Logger_, "Join all streams", this);

            // Takes an extra reference for each running stream.
            for (auto streamPtr : Streams_.GetSnapshotOfRunning()) {
                Y_HTTP2_BLOCK(Logger_, "Join stream", *streamPtr, this);
                streamPtr->Join();
            }
            // The extra references are dropped.
        }

        if (EConnState::Reset == ConnState_) {
            // If the connection is reset, there is no point in sending anything to the client.
            ClientOutput_.Cancel();
        }

        {
            Y_HTTP2_BLOCK(Logger_, "Join output", this);
            ClientOutput_.Join();
        }
    }
}

Y_HTTP2_GEN_PRINT(TConnection);
Y_HTTP2_GEN_PRINT(TConnError);
