#pragma once

#include "common.h"

#include <balancer/kernel/http/parser/http.h>
#include <balancer/kernel/module/iface.h>
#include <balancer/kernel/net/ip_matcher.h>
#include <balancer/kernel/regexp/regexp_pire.h>
#include <util/string/split.h>
#include <balancer/kernel/matcher/expsplitparams.pb.h>

namespace NSrvKernel {

    using IRequestMatcher = IMatcher<TConnDescr>;

    class TIpMatcherBuilder final : public IConfig::IFunc {
    public:
        TIpMatcherBuilder(IConfig* config) {
            config->ForEach(this);
        }

        TIpMatcher Build() {
            if (!SourceMask_) {
                ythrow TConfigParseError() << "unspecified source_mask for source_ip matcher";
            }

            return TIpMatcher(std::move(*SourceMask_));
        }

    private:
        START_PARSE {
            ON_KEY("source_mask", SourceMask_) {
                return;
            }
        } END_PARSE

        TMaybe<TString> SourceMask_;
    };

    class TRequestIpMatcher final : public IRequestMatcher {
    public:
        TRequestIpMatcher(IConfig* config) {
            TIpMatcherBuilder ipMatcherBuilder(config);
            Matcher_ = ipMatcherBuilder.Build();
        }

        TRequestIpMatcher(TString sourceMask) : Matcher_(std::move(sourceMask)) {}

        [[nodiscard]] bool Match(const TConnDescr& descr) const noexcept override {
            return Matcher_->Match(*descr.Properties->Parent.RemoteAddress->Addr());
        }

        bool NeedsHttp() const noexcept override {
            return false;
        }

        void Init(IWorkerCtl*) override {}

    private:
        TMaybe<TIpMatcher> Matcher_;
    };


    class TMethodMatcher final : public IRequestMatcher {
    public:
        TMethodMatcher(IConfig* config) {
            config->ForEach(this);
        }

    private:
        [[nodiscard]] bool Match(const TConnDescr& descr) const noexcept override {
            return Methods_.HasFlags(descr.Request->RequestLine().Method);
        }

        bool NeedsHttp() const noexcept override {
            return true;
        }

        void Init(IWorkerCtl*) override {}

        START_PARSE {
            if (key == "methods") {
                ParseMap(value->AsSubConfig(), [this](const auto& key, auto* val) {
                    Y_UNUSED(key);
                    Methods_ |= FromString<EMethod>(to_upper(val->AsString()));
                });
                return;
            }
        } END_PARSE

    private:
        EMethods Methods_;
    };

    class TExpDeviceMatcher final : public IRequestMatcher {
    public:
        TExpDeviceMatcher(IConfig* config) {
            config->ForEach(this);
        }

    private:
        [[nodiscard]] bool Match(const TConnDescr& descr) const noexcept override {
            auto re = descr.Request;
            auto header = re->Headers().FindValues(HeaderName_);
            if (header != re->Headers().End()) {
                for (auto& value : header->second) {
                    TExpSplitParamsProto params;
                    Y_PROTOBUF_SUPPRESS_NODISCARD params.ParseFromString(TString(value.AsStringBuf()));

                    if (params.GetBrowserName() == Device_) {
                        return true;
                    }
                }
            }

            return false;
        }

        bool NeedsHttp() const noexcept override {
            return true;
        }

        void Init(IWorkerCtl*) override {}

        START_PARSE {
                ON_KEY("device", Device_) {
                    return;
                }

                ON_KEY("header_name", HeaderName_) {
                    return;
                }
            } END_PARSE

    private:
        TString Device_;
        TString HeaderName_ = "ExpSplitParamsProto";
    };


    class TExpBoxesMatcher final : public IRequestMatcher {
    public:
        TExpBoxesMatcher(IConfig* config) {
            config->ForEach(this);
        }

    private:
        [[nodiscard]] bool Match(const TConnDescr& descr) const noexcept override {
            auto re = descr.Request;
            auto header = re->Headers().FindValues(HeaderName_);
            if (header != re->Headers().End()) {
                for (auto& value : header->second) {
                    // 12,0,35;77257,0,43;224959,0,61;225413,0,27
                    for (const auto& it : StringSplitter(value.AsStringBuf()).Split(';')) {
                        const TStringBuf kv = it.Token();
                        if (kv.StartsWith(TestId_)) {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        bool NeedsHttp() const noexcept override {
            return true;
        }

        void Init(IWorkerCtl*) override {}

        START_PARSE {
                ON_KEY("test_id", TestId_) {
                    TestId_ = TestId_ + ',';
                    return;
                }

                ON_KEY("header_name", HeaderName_) {
                    return;
                }
            } END_PARSE

    private:
        TString TestId_;
        TString HeaderName_ = "X-Yandex-ExpBoxes";
    };


    class TProtoMatcher final : public IRequestMatcher {
    public:
        enum class EProto {
            Http1x /* "http1x" */,
            Http2  /* "http2" */,
            Unknown /* "unknown" */
        };

        TProtoMatcher(IConfig* config) {
            config->ForEach(this);
        }

        TProtoMatcher(EProto proto)
            : Proto_(proto)
        {}

        [[nodiscard]] bool Match(const TConnDescr& descr) const noexcept override {
            return GetProto(descr) == Proto_;
        }

        bool NeedsHttp() const noexcept override {
            return false;
        }

        void Init(IWorkerCtl*) override {}

    private:
        START_PARSE {
            ON_KEY("proto", Proto_) {
                return;
            }
        } END_PARSE

    private:
        static EProto GetProto(const TConnDescr& descr) noexcept;

    private:
        EProto Proto_ = EProto::Unknown;
    };


    class TFsmMatcher final : public IRequestMatcher, public TFsm {
    public:
        enum class EMatchType {
            RequestLine      /* "match" */,
            Path             /* "path", "URI" */,
            Cgi              /* "cgi", "CGI" */,
            Url              /* "url" */,
            Host             /* "host" */,
            Cookie           /* "cookie" */,
            Header           /* "header" */,
            Upgrade          /* "upgrade" */,
            CgiParam         /* "cgi_param" */,
            NormalizedPath   /* "normalized_path" */,
            None,
        };

    public:
        TFsmMatcher(IConfig* config)
            : TFsm(TFsm::False())
        {
            config->ForEach(this);

            Y_ENSURE_EX(MatchType_ != EMatchType::None,
                TConfigParseError() << "unspecified match type for fsm_matcher");

            TFsm::TOptions options;

            options.SetCaseInsensitive(CaseInsensitive_.GetOrElse(false));
            options.SetSurround(Surround_);

            if (!IsStringASCII(Regexp_.begin(), Regexp_.end()) && IsUtf(Regexp_)) {
                options.SetCharset(CODES_UTF8);
            }

            static_cast<TFsm&>(*this) = TFsm(Regexp_, options);
        }

        [[nodiscard]] bool Match(const TConnDescr& descr) const noexcept override;

    private:
        START_PARSE {
            if (key == "case_insensitive") {
                CaseInsensitive_ = value->AsBool();
                return;
            }

            if (key == "surround") {
                Surround_ = value->AsBool();
                return;
            }

            Y_ENSURE_EX(MatchType_ == EMatchType::None,
                TConfigParseError() << "multiple match types in one fsm");

            MatchType_ = FromString<EMatchType>(key);

            struct TKVMatchConfig final : public IConfig::IFunc {
                TString Name_;
                TString Regexp_;
                bool NameSurround_ = false;

                TKVMatchConfig(TStringBuf type, IConfig* config) {
                    config->ForEach(this);

                    if (Name_.empty()) {
                        ythrow TConfigParseError() << type << " fsm must have a name";
                    }

                    if (Regexp_.empty()) {
                        ythrow TConfigParseError() << type << " fsm must have a value";
                    }
                }

                START_PARSE {
                    ON_KEY("name", Name_) {
                        return;
                    }

                    ON_KEY("value", Regexp_) {
                        return;
                    }

                    ON_KEY("name_surround", NameSurround_) {
                        return;
                    }
                } END_PARSE;
            };

            if (EMatchType::Header == MatchType_) {
                TKVMatchConfig conf("header", value->AsSubConfig());

                TFsm::TOptions headerNameOptions;
                headerNameOptions.SetCaseInsensitive(true);
                headerNameOptions.SetSurround(conf.NameSurround_);
                KVMatcherNameFsm_.Reset(new TFsm(conf.Name_, headerNameOptions));
                Regexp_ = conf.Regexp_;
            } else if (EMatchType::Upgrade == MatchType_) {
                TFsm::TOptions headerNameOptions;
                headerNameOptions.SetCaseInsensitive(true);
                headerNameOptions.SetSurround(false);
                KVMatcherNameFsm_.Reset(new TFsm("upgrade", headerNameOptions));
                Regexp_ = value->AsString();
                if (!CaseInsensitive_) {
                    CaseInsensitive_ = true;
                }
            } else if (EMatchType::CgiParam == MatchType_) {
                TKVMatchConfig conf("cgi", value->AsSubConfig());

                TFsm::TOptions cgiNameOptions;
                cgiNameOptions.SetCaseInsensitive(true);
                cgiNameOptions.SetSurround(conf.NameSurround_);
                KVMatcherNameFsm_.Reset(new TFsm(conf.Name_, cgiNameOptions));
                Regexp_ = conf.Regexp_;
            } else {
                Regexp_ = value->AsString();
            }

            return;
        } END_PARSE

        bool NeedsHttp() const noexcept override {
            return true;
        }

    private:
        void Init(IWorkerCtl*) override {}

    private:
        EMatchType MatchType_ = EMatchType::None;
        TString Regexp_;
        THolder<TFsm> KVMatcherNameFsm_;

        TMaybe<bool> CaseInsensitive_;
        bool Surround_ = false;
    };

    class TLocalFileMatcher final : public IRequestMatcher {
    public:
        TLocalFileMatcher(IConfig* config, ICtl* ctl)
            : FileCheckers_(ctl->GetCountOfChildren() + 1)
        {
            config->ForEach(this);

            if (Path_.empty()) {
                ythrow TConfigParseError{} << "empty path is not allowed";
            }
        }

        void Init(IWorkerCtl* process) override {
            if (Y_LIKELY(process != nullptr)) {
                FileCheckers_[process->WorkerId()] = process->SharedFiles()->FileChecker(Path_, TDuration::Seconds(1));
            }
        }

        bool Match(const TConnDescr& descr) const noexcept override {
            return FileCheckers_[descr.Process().WorkerId()].Exists();
        }

        bool NeedsHttp() const noexcept override {
            return false;
        }

    private:
        START_PARSE {
            ON_KEY("path", Path_) {
                return;
            }
        } END_PARSE

    private:
        TString Path_;
        TVector<TSharedFileExistsChecker> FileCheckers_;
    };

};
