#pragma once

#include <balancer/kernel/cookie/utils/utils.h>
#include <balancer/kernel/regexp/regexp_pire.h>

#include <util/datetime/base.h>
#include <util/generic/maybe.h>
#include <util/generic/strbuf.h>
#include <util/generic/xrange.h>
#include <util/memory/blob.h>
#include <util/string/split.h>
#include <util/string/strip.h>

#include <string>
#include <array>

namespace NSrvKernel {
    constexpr TStringBuf CookieStrictSep = "; ";

    inline void AppendCookieC(TString& out, TStringBuf cookie) noexcept {
        cookie = StripString(cookie);
        if (!cookie || cookie == "="sv) {
            return;
        }
        if (out) {
            out.append(CookieStrictSep);
        }
        out.append(cookie);
    }


    struct TNameValue {
        TMaybe<TStringBuf> Name;
        TStringBuf Value;

    public:
        Y_BALANCER_TUPLE_EQ(TNameValue, Name, Value)

        explicit operator bool() const noexcept {
            return !empty();
        }

        bool empty() const noexcept {
            return Name.GetOrElse({}).empty() && Value.empty();
        }

        TString Render() const noexcept {
            if (Name) {
                return TString::Join(*Name, '=', Value);
            } else {
                return TString(Value);
            }
        }

        [[nodiscard]]
        static TNameValue Parse(TStringBuf cookie) noexcept {
            cookie = StripString(cookie);
            TStringBuf name, val;
            if (cookie.TrySplit('=', name, val)) {
                name = StripStringRight(name);
                val = StripStringLeft(val);
                return {name, val};
            } else {
                return {Nothing(), cookie};
            }
        }
    };


    inline void AppendCookieKV(TString& out, TNameValue nv) noexcept {
        if (nv.Name) {
            nv.Name = StripString(*nv.Name);
        }
        nv.Value = StripString(nv.Value);
        if (!(nv.Name && *nv.Name) && !nv.Value) {
            return;
        }
        if (out) {
            out.append(CookieStrictSep);
        }
        if (nv.Name) {
            out.append(*nv.Name).append('=');
        }
        out.append(nv.Value);
    }


    [[nodiscard]]
    inline TStringBuf StripCookieHeader(TStringBuf cookie) noexcept {
        return StripString(cookie, [](const char* c) {
            return IsAsciiSpace(*c) || ';' == *c || '=';
        });
    }


    enum class ECookieIter {
        Stop, Continue
    };

    // NOTE: Both FF and Chromium sort the same-name cookies first by len(path) (desc) and then by creation date (asc)

    template <class F>
    inline bool ForEachCookieC(F&& func, TStringBuf headerValue) noexcept {
        for (auto tokIt : StringSplitter(headerValue).Split(';')) {
            auto tok = StripString(tokIt.Token());
            if (tok && tok != "="sv) {
                if (func(tok) == ECookieIter::Stop) {
                    return true;
                }
            }
        }
        return false;
    }

    template <class F>
    inline bool ForEachCookieKV(F&& func, TStringBuf headerValue) noexcept {
        return ForEachCookieC([&](TStringBuf cookie) noexcept {
            return func(TNameValue::Parse(cookie));
        }, headerValue);
    }


    template <class F>
    inline void EveryCookieC(F&& func, TStringBuf headerValue) noexcept {
        ForEachCookieC([&](TStringBuf token) noexcept {
            func(token);
            return ECookieIter::Continue;
        }, headerValue);
    }

    template <class F>
    inline void EveryCookieKV(F&& func, TStringBuf headerValue) noexcept {
        ForEachCookieKV([&](const TNameValue& nv) noexcept {
            func(std::move(nv));
            return ECookieIter::Continue;
        }, headerValue);
    }

    [[nodiscard]]
    inline TMaybe<TStringBuf> FindCookieK(
        TStringBuf headerValue, TStringBuf name, ECookieIter firstRes = ECookieIter::Stop) noexcept
    {
        TMaybe<TStringBuf> res;
        ForEachCookieKV([&](const TNameValue& nv) {
            if (nv.Name == name) {
                res = nv.Value;
                return firstRes;
            }
            return ECookieIter::Continue;
        }, headerValue);
        return res;
    }

    template <class TCallback>
    inline bool FindCookieKFsm(TStringBuf headerValue, const TFsm& fsm, TCallback&& cb) noexcept {
        return ForEachCookieKV([&](const TNameValue& nv) {
            TMatcher matcher(fsm);
            if (Match(matcher, nv.Name.GetOrElse({})).Final()) {
                return cb(nv, *matcher.MatchedRegexps().first);
            }
            return ECookieIter::Continue;
        }, headerValue);
    }
}
