#pragma once

#include "http2_settings.h"
#include "http2_base.h"

#include <balancer/kernel/http2/server/common/http2_common.h>

namespace NSrvKernel::NHTTP2 {

    class TFlowBase : TNonCopyable {
    public:
        explicit TFlowBase(TLogger& logger) noexcept;

        virtual ~TFlowBase() = default;

        TError Consume(ui32 consumeSize) noexcept;

        TError Update(ui32 updateSize) noexcept;

        TError UpdateInitialSize() noexcept;

        [[nodiscard]] ui32 GetAvailableSize() const noexcept;

    protected:
        [[nodiscard]] i64 GetCurrentSize() const noexcept;

        TError DoValidateUpdate(ui32 updateSize) const noexcept;

        [[nodiscard]] virtual ui32 GetInitialSize() const noexcept = 0;

        virtual TError ThrowOnConsumeUnderflow(i64 current, i64 consumed) const noexcept = 0;

        virtual TError ThrowOnUpdateOverflow(i64 current, i64 updated) const noexcept = 0;

        virtual TError ThrowOnZeroUpdate() const noexcept = 0;

        virtual TError OnUpdate() noexcept = 0;

        virtual void OnConsume() noexcept {}

    protected:
        TLogger& Logger_;

    private:
        i64 WindowDelta_ = 0;
    };


    // IFlowCallback ===================================================================================================
    class IFlowCallback {
    public:
        virtual ~IFlowCallback() {}

        virtual void OnFlowBlockedForever() noexcept = 0;

        virtual TError OnFlowUnblocked() noexcept = 0;
    };


    // TSendFlow =======================================================================================================
    // We send, the client receives

    template <class TException, class EProtocolError>
    class TSendFlow final : public TFlowBase {
    public:
        explicit TSendFlow(
            TLogger& logger,
            IFlowCallback& callback,
            const TClientSettings& settings
        ) noexcept
            : TFlowBase(logger)
            , Callback_(callback)
            , ClientSettings_(settings)
        {}

        void PrintTo(IOutputStream& out) const {
            Y_HTTP2_PRINT_OBJ(out, GetCurrentSize(), ReadFin_);
        }

        [[nodiscard]] bool IsBlockedForever() const noexcept {
            return IsBlocked() && ReadFin_;
        }

        [[nodiscard]] bool IsBlocked() const noexcept {
            return !GetAvailableSize();
        }

        void OnReadFin() noexcept {
            Y_HTTP2_METH_E(Logger_);
            ReadFin_ = true;

            if (!GetAvailableSize()) {
                Callback_.OnFlowBlockedForever();
            }
        }

    private:
        ui32 GetInitialSize() const noexcept override {
            return ClientSettings_.InitialWindowSize;
        }

        TError ThrowOnConsumeUnderflow(i64 current, i64 consumed) const noexcept override {
            Y_FAIL("%s", (
                TStringBuilder() << "Broken invariant: current=" << current << ", consumed=" << consumed
            ).c_str());
        }

        TError OnUpdate() noexcept override {
            Y_HTTP2_METH_E(Logger_);

            if (GetAvailableSize()) {
                Y_PROPAGATE_ERROR(Callback_.OnFlowUnblocked());
            }
            return {};
        }

        void OnConsume() noexcept override {
            Y_HTTP2_METH_E(Logger_);

            if (!GetAvailableSize() && ReadFin_) {
                Callback_.OnFlowBlockedForever();
            }
        }

        TError ThrowOnUpdateOverflow(i64 current, i64 updated) const noexcept override {
            return Y_MAKE_ERROR(
                TException(EErrorCode::FLOW_CONTROL_ERROR, EFlowControlError::WindowOverflow)
                    << " current=" << current << ", updated=" << updated
            );
        }

        TError ThrowOnZeroUpdate() const noexcept override {
            return Y_MAKE_ERROR(
                TException(EErrorCode::PROTOCOL_ERROR, EProtocolError::ZeroWindowUpdate)
            );
        }

    private:
        IFlowCallback& Callback_;
        const TClientSettings& ClientSettings_;
        bool ReadFin_ = false;
    };


    using TStreamSendFlow = TSendFlow<TStreamError, EStreamProtocolError>;
    using TConnSendFlow = TSendFlow<TConnectionError, EConnProtocolError>;


    // TRecvFlow =======================================================================================================
    // We receive, the client sends
    template <class TException>
    class TRecvFlow final : public TFlowBase {
    public:
        explicit TRecvFlow(
            TLogger& logger,
            const TServerSettings& serverSettings
        ) noexcept
            : TFlowBase(logger)
            , ServerSettings_(serverSettings)
        {}

        void PrintTo(IOutputStream& out) const {
            Y_HTTP2_PRINT_OBJ(out, GetCurrentSize());
        }

    private:
        [[nodiscard]] ui32 GetInitialSize() const noexcept override {
            return ServerSettings_.GetCurrent().InitialWindowSize;
        }

        TError ThrowOnUpdateOverflow(i64 current, i64 updated) const noexcept override {
            Y_FAIL("%s", (
                TStringBuilder() << "Broken invariant: current=" << current << ", updated=" << updated
            ).c_str());
        }

        TError ThrowOnZeroUpdate() const noexcept override {
            Y_FAIL("Broken invariant");
        }

        TError OnUpdate() noexcept override {
            return {};
        }

        TError ThrowOnConsumeUnderflow(i64 current, i64 consumed) const noexcept override {
            return Y_MAKE_ERROR(
                TException(EErrorCode::FLOW_CONTROL_ERROR, EFlowControlError::ConsumeUnderflow)
                    << " current=" << current << ", consumed=" << consumed
            );
        }

    private:
        const TServerSettings& ServerSettings_;
    };


    using TStreamRecvFlow = TRecvFlow<TStreamError>;
    using TConnRecvFlow = TRecvFlow<TConnectionError>;
}
