#pragma once

#include "client_proto.h"
#include "headers.h"
#include "httpparser.h"
#include "parse_options.h"

#include <balancer/kernel/custom_io/chunkio.h>
#include <balancer/kernel/custom_io/stream.h>
#include <balancer/kernel/http/headers.h>
#include <balancer/kernel/http/string_storage/string_storage.h>
#include <balancer/kernel/helpers/cast.h>
#include <balancer/kernel/helpers/errors.h>
#include <balancer/kernel/io/iobase.h>
#include <balancer/kernel/memory/chunks.h>

#include <util/generic/intrlist.h>
#include <util/generic/maybe.h>
#include <util/generic/flags.h>
#include <util/memory/pool.h>
#include <util/string/cast.h>

#include <utility>

namespace NSrvKernel {
    class TRequest;
    class TResponse;

    constexpr size_t HTTP_MAX_REQUEST_LINE_SIZE_DEFAULT = 1 << 16;
    constexpr size_t HTTP_MAX_HEADERS_SIZE_DEFAULT = 1 << 16;
    constexpr size_t HTTP_MAX_HEADERS_COUNT_DEFAULT = 1 << 9;
    constexpr size_t HTTP_REQUEST_COMMON_PART_SIZE = 12; // SP + len(" HTTP/x.x\r\n")
    constexpr size_t HTTP_RESPONSE_COMMON_PART_SIZE = 15; // len("HTTP/x.x xxx ") + CRLF

    inline size_t HeadersSerializedSize(const THeaders& headers) {
        size_t size = headers.Size() * (HDRD.length() + CRLF.length());
        return std::accumulate(headers.cbegin(), headers.cend(), size, [](size_t sz, const auto& header) {
            size_t nameSize = header.first.size() * header.second.size();
            for (const auto& headerValue : header.second) {
                nameSize += headerValue.size();
            }
            return nameSize + sz;
        });
    }

    enum class EMethod : ui64 {
        UNDEFINED = 0ull,
        OPTIONS = 1ull << 0,
        GET = 1ull << 1,
        HEAD = 1ull << 2,
        POST = 1ull << 3,
        PUT = 1ull << 4,
        PATCH = 1ull << 5,
        DELETE = 1ull << 6,
        TRACE = 1ull << 7,
        CONNECT = 1ull << 8,
        // BALANCER-3153 webdav support below
        ACL = 1ull << 9,
        BASELINE_CONTROL /* "BASELINE-CONTROL" */ = 1ull << 10,
        BIND = 1ull << 11,
        CHECKIN = 1ull << 12,
        CHECKOUT = 1ull << 13,
        COPY = 1ull << 14,
        LABEL = 1ull << 15,
        LINK = 1ull << 16,
        LOCK = 1ull << 17,
        MERGE = 1ull << 18,
        MKACTIVITY = 1ull << 19,
        MKCALENDAR = 1ull << 20,
        MKCOL = 1ull << 21,
        MKREDIRECTREF = 1ull << 22,
        MKWORKSPACE = 1ull << 23,
        MOVE = 1ull << 24,
        ORDERPATCH = 1ull << 25,
        PROPFIND = 1ull << 26,
        PROPPATCH = 1ull << 27,
        REBIND = 1ull << 28,
        REPORT = 1ull << 29,
        SEARCH = 1ull << 30,
        UNBIND = 1ull << 31,
        UNCHECKOUT = 1ull << 32,
        UNLINK = 1ull << 33,
        UNLOCK = 1ull << 34,
        UPDATE = 1ull << 35,
        UPDATEREDIRECTREF = 1ull << 36,
        VERSION_CONTROL /* "VERSION-CONTROL" */ = 1ull << 37,
    };
    Y_DECLARE_FLAGS(EMethods, EMethod);

    constexpr auto HttpBasicMethods = EMethods()
        | EMethod::OPTIONS
        | EMethod::GET
        | EMethod::HEAD
        | EMethod::POST
        | EMethod::PUT
        | EMethod::PATCH
        | EMethod::DELETE
        | EMethod::TRACE
        | EMethod::CONNECT
    ;
    constexpr auto HttpWebDavMethods = ~HttpBasicMethods;


    class TRequestLine {
    public:
        TRequestLine() noexcept = default;

        TRequestLine(const TRequestLine& rhs) = default;
        TRequestLine& operator=(const TRequestLine& rhs) = default;

        TRequestLine(TRequestLine&& rhs) noexcept = default;
        TRequestLine& operator=(TRequestLine&& rhs) noexcept = default;

        void Swap(TRequestLine& rhs) noexcept {
            DoSwap(Method, rhs.Method);
            DoSwap(Path, rhs.Path);
            DoSwap(CGI, rhs.CGI);
            DoSwap(MajorVersion, rhs.MajorVersion);
            DoSwap(MinorVersion, rhs.MinorVersion);
            DoSwap(Buffer_, rhs.Buffer_);
        }

        template <typename T>
        void PrintNoCRLF(T&& out) const {
            PrintNoCRLFWithProto(out, EClientProto::CP_HTTP);
        }

        template <typename T>
        void PrintNoCRLFWithProto(T&& out, EClientProto proto) const {
            out << Method << SP << Path << CGI << " HTTP/";
            switch (proto) {
            case EClientProto::CP_HTTP:
                out << MajorVersion << '.' << MinorVersion;
                break;
            case EClientProto::CP_HTTP2:
                out << '2';
                break;
            }
        }

        void UnparseRaw(IOutputStream& out) const noexcept {
            out << Method << SP << Path << CGI << " HTTP/" << MajorVersion << '.' << MinorVersion;
        }

        void Unparse(IOutputStream& out) const noexcept {
            UnparseRaw(out);
            out << CRLF;
        }

        bool Match(const TFsm& fsm) const noexcept {
            TMatcher matcher(fsm);
            NSrvKernel::Match(matcher, ::ToString(Method));
            matcher.Match(SP);
            NSrvKernel::Match(matcher, Path.AsStringBuf());
            if (matcher.Final()) {
                return true;
            }
            NSrvKernel::Match(matcher, CGI.AsStringBuf());
            if (matcher.Final()) {
                return true;
            }
            matcher.Match(" HTTP/");
            matcher.Match(::ToString(MajorVersion));
            matcher.Match(".");
            matcher.Match(::ToString(MinorVersion));

            return matcher.Final();
        }

        TError SetURL(TString lst) noexcept;

        TString GetURL() const noexcept {
            auto result = TString(Path.AsStringBuf());
            result += CGI.AsStringBuf();
            return result;
        }

        bool MatchUrl(const TFsm& fsm) const noexcept {
            TMatcher matcher{ fsm };
            NSrvKernel::Match(matcher, Path.AsStringBuf());
            if (matcher.Final()) {
                return true;
            }
            NSrvKernel::Match(matcher, CGI.AsStringBuf());
            return matcher.Final();
        }

        size_t EncodedSize() const noexcept {
            return ::ToString(Method).size() + Path.size() + CGI.size()
                                       + HTTP_REQUEST_COMMON_PART_SIZE;
        }

        bool UriParsed() const noexcept {
            return UriParsed_;
        }

        void SetUriParsed(bool parsed) noexcept {
            UriParsed_ = parsed;
        }

        void SetBuffer(TString&& buffer) noexcept {
            Buffer_ = MakeAtomicShared<TString>(std::move(buffer));
        }

    public:
        EMethod Method = EMethod::UNDEFINED;
        TStringStorage Path;
        TStringStorage CGI;
        ui16 MajorVersion = 1;
        ui16 MinorVersion = 1;

    private:
        bool UriParsed_ = false;
        TAtomicSharedPtr<TString> Buffer_;
    };

    class TRequest {
    public:
        using THeadLine = TRequestLine;

    public:
        TRequest() noexcept = default;

        explicit TRequest(const TRequestLine& requestLine) noexcept
            : RequestLine_(requestLine)
        {
            Props_.SetReasonableDefaults(this);
        }

        TRequest(const TRequest& rhs) = default;
        TRequest& operator=(const TRequest& rhs) = default;

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

        void Swap(TRequest& rhs) noexcept {
            DoSwap(RequestLine_, rhs.RequestLine_);
            DoSwap(Props_, rhs.Props_);
            DoSwap(Headers_, rhs.Headers_);
        }

        TError Parse(TStringBuf request) {
            TChunksInput input(TChunkList().PushNonOwning(request));
            TChunkList unparsed;
            Y_PROPAGATE_ERROR(Read(&input, unparsed, TInstant::Max()));
            Y_REQUIRE(unparsed.Empty(), THttpParseError(unparsed, HTTP_BAD_REQUEST) << "failed to parse request");
            return {};
        }

        TError Read(IIoInput* in, TChunkList& unparsed, const THttpParseOptions& options, TInstant deadline) noexcept {
            TString buffer;

            TParseMessagePicoContext<TRequest> context{&unparsed, &buffer, this, options};
            Y_PROPAGATE_ERROR(context.Read(in, deadline));

            RequestLine_.SetBuffer(std::move(buffer));
            return {};
        }

        TError Read(IIoInput* in, TChunkList& unparsed, TInstant deadline) noexcept {
            THttpParseOptions options;
            return Read(in, unparsed, options, deadline);
        }

        TString FirstLine() const noexcept {
            TString firstLine;
            TStringOutput output(firstLine);
            RequestLine().UnparseRaw(output);
            output.Flush();
            return firstLine;
        }

        void BuildLine(IOutputStream& out) const noexcept {
            RequestLine_.Unparse(out);
        }

        void BuildTo(IOutputStream& out) const noexcept {
            BuildLine(out);
            Headers_.BuildTo(out);
        }

        const TRequestLine& RequestLine() const noexcept {
            return RequestLine_;
        }

        TRequestLine& RequestLine() noexcept {
            return RequestLine_;
        }

        TRequestLine& HeadLine() noexcept {
            return RequestLine_;
        }

        const THeaders& Headers() const noexcept {
            return Headers_;
        }

        THeaders& Headers() noexcept {
            return Headers_;
        }

        const TBaseProperties& Props() const noexcept {
            return Props_;
        }

        TBaseProperties& Props() noexcept {
            return Props_;
        }

        size_t EncodedSize() const noexcept {
            // TODO(tender-bum): also count implicit headers (Content-Length, Transfer-Encoding...)
            return RequestLine_.EncodedSize() + HeadersSerializedSize(Headers_);
        }

    private:
        TRequestLine RequestLine_;
        TBaseProperties Props_;
        THeaders Headers_;
    };

    class TResponseLine {
    public:
        TResponseLine() noexcept = default;

        template <typename T>
        TResponseLine(ui16 statusCode, const T& reason, ui8 majorVersion = 1, ui8 minorVersion = 1)
            : MajorVersion(majorVersion)
            , MinorVersion(minorVersion)
            , StatusCode(statusCode)
            , Reason(TStringStorage(reason))
        {
        }

        TResponseLine(const TResponseLine& rhs) = default;
        TResponseLine& operator=(const TResponseLine& rhs) = default;

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

        void Swap(TResponseLine& rhs) noexcept {
            DoSwap(MajorVersion, rhs.MajorVersion);
            DoSwap(MinorVersion, rhs.MinorVersion);
            DoSwap(StatusCode, rhs.StatusCode);
            DoSwap(Reason, rhs.Reason);
            DoSwap(Buffer_, rhs.Buffer_);
        }

        void Unparse(IOutputStream& out) const noexcept {
            out << "HTTP/"
                << ui16(MajorVersion)
                << '.'
                << ui16(MinorVersion)
                << SP
                << StatusCode
                << SP
                << Reason
                << CRLF;
        }

        bool NullContent() const {
            return IsIn({HTTP_NO_CONTENT, HTTP_NOT_MODIFIED}, StatusCode);
        }

        size_t EncodedSize() const noexcept {
            return HTTP_RESPONSE_COMMON_PART_SIZE + Reason.size();
        }

        void SetBuffer(TString&& buffer) {
            Buffer_ = MakeAtomicShared<TString>(std::move(buffer));
        }

    public:
        ui8 MajorVersion = 1;
        ui8 MinorVersion = 1;
        ui16 StatusCode = 0;
        TStringStorage Reason;

    private:
        TAtomicSharedPtr<TString> Buffer_;
    };

    class TResponse {
    public:
        using THeadLine = TResponseLine;

    public:
        TResponse() noexcept = default;

        explicit TResponse(const TResponseLine& responseLine) noexcept
            : ResponseLine_(responseLine)
        {
            Props_.SetReasonableDefaults(this);
        }

        template <typename T>
        TResponse(ui16 status, const T& reason) noexcept
            : TResponse(TResponseLine(status, reason))
        {
        }

        TResponse(const TResponse& rhs) = default;
        TResponse& operator=(const TResponse& rhs) = default;

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

        void Swap(TResponse& rhs) noexcept {
            DoSwap(ResponseLine_, rhs.ResponseLine_);
            DoSwap(Headers_, rhs.Headers_);
            DoSwap(Props_, rhs.Props_);
        }

        TError Parse(TStringBuf response) {
            TChunksInput input(TChunkList().PushNonOwning(response));
            TChunkList unparsed;
            Y_PROPAGATE_ERROR(Read(&input, unparsed, TInstant::Max()));
            Y_REQUIRE(unparsed.Empty(), THttpParseError(unparsed, HTTP_BAD_REQUEST) << "failed to parse response");
            return {};
        }

        TError Read(IIoInput* in, TChunkList& unparsed, const THttpParseOptions& options, TInstant deadline) noexcept {
            TString buffer;

            TParseMessagePicoContext<TResponse> context{&unparsed, &buffer, this, options};
            Y_PROPAGATE_ERROR(context.Read(in, deadline));

            ResponseLine_.SetBuffer(std::move(buffer));
            return {};
        }

        TError Read(IIoInput* in, TChunkList& unparsed, TInstant deadline) noexcept {
            THttpParseOptions options;
            return Read(in, unparsed, options, deadline);
        }

        void BuildLine(IOutputStream& out) const noexcept {
            ResponseLine_.Unparse(out);
        }

        void BuildTo(IOutputStream& out) const noexcept {
            BuildLine(out);
            Headers_.BuildTo(out);
        }

        const TResponseLine& ResponseLine() const noexcept {
            return ResponseLine_;
        }

        TResponseLine& ResponseLine() noexcept {
            return ResponseLine_;
        }

        TResponseLine& HeadLine() noexcept {
            return ResponseLine_;
        }

        size_t EncodedSize() const noexcept {
            // TODO(tender-bum): also count implicit headers (Content-Length, Transfer-Encoding...)
            return ResponseLine_.EncodedSize() + HeadersSerializedSize(Headers_);
        }

        const THeaders& Headers() const noexcept {
            return Headers_;
        }

        THeaders& Headers() noexcept {
            return Headers_;
        }

        const TBaseProperties& Props() const noexcept {
            return Props_;
        }

        TBaseProperties& Props() noexcept {
            return Props_;
        }

    private:
        TResponseLine ResponseLine_;
        THeaders Headers_;
        TBaseProperties Props_;
    };

    [[nodiscard]] bool IsHttp(const TChunkList& lst) noexcept;

    [[nodiscard]] bool IsHTTP2(const TRequest*) noexcept;

    [[nodiscard]] bool IsHTTP2CancelledByClient(const TRequest*) noexcept;

    [[nodiscard]] bool IsHTTP2ClosedByClient(const TRequest* mess) noexcept;

    [[nodiscard]] bool NullResponseBody(ui16 status) noexcept;

}
