#include "p_is_gdpr_cookie.h"

#include <balancer/modules/cookie_policy/common/controls_reader.h>
#include <balancer/kernel/client_hints/impl/client_hints.h>
#include <balancer/kernel/cookie/deletion/delete_cookie.h>
#include <balancer/kernel/cookie/gdpr/gdpr.h>
#include <balancer/kernel/cookie/gdpr/lifetime.h>

#include <algorithm>

namespace NModCookiePolicy {

    namespace {
        auto DelReqCookie(TVector<TRequestCookie>& cookies, TStringBuf name) noexcept {
            auto e = std::remove_if(cookies.begin(), cookies.end(), [&](const TRequestCookie& r) {
                return r.NameValue.Name == name;
            });
            auto cnt = cookies.end() - e;
            cookies.erase(e, cookies.end());
            return cnt;
        }

        TRequestCookieAction CreateReqCookie(TStringBuf name, TBlob value, TVector<TRequestCookie>& cookies) noexcept {
            cookies.emplace_back(TRequestCookie{
                .NameValue={.Name=name, .Value=NCookie::ToStringBuf(value)},
                .Memory=value,
                .Added=true,
            });
            return {.Info=std::string(name), .Reasons={ERequestCookieReason::Create}, .Action=ECookieAction::Add,};
        }

        TRequestCookieAction ClearReqCookie(
            TStringBuf name, TRequestCookieReasons reasons, TVector<TRequestCookie>& cookies) noexcept
        {
            ui32 cnt = DelReqCookie(cookies, name);
            return {
                .Info=std::string(name),
                .Reasons=reasons,
                .Action=ECookieAction::Drop,
                .Count=cnt,
            };
        }

        TRequestCookieAction UpdateReqCookie(
            TStringBuf name,
            TBlob value,
            TVector<TRequestCookie>& cookies
        ) noexcept {
            DelReqCookie(cookies, name);
            cookies.emplace_back(TRequestCookie{
                .NameValue={.Name=name, .Value=NCookie::ToStringBuf(value)},
                .Memory=value,
                .Added=true,
            });
            return {.Info=std::string(name), .Reasons={ERequestCookieReason::Update}, .Action=ECookieAction::Fix,};
        }

        TSetCookie GenSetCookie(TStringBuf name, TBlob value, const TPolicyArgs& args) noexcept {
            const auto sameSite = GetOrSetUATraits(args.Descr).SameSiteNoneSupport
                                  ? ESameSite::None : ESameSite::Undefined;
            const bool secure = sameSite == ESameSite::None;
            return {
                .Name=std::string(name),
                .Value=value,
                .Path=std::string(TStringBuf("/")),
                .Domain=std::string(".") + *args.DomainInfo.DomainRoot,
                .Expires=TMaxAllowedExpires::Get(args.Descr.Properties->Start),
                .SameSite=sameSite,
                .Secure=secure,
            };
        }

        template <class TVal>
        TPolicyIsGdprCookieBase::TArgs BuildCookieArgs(
            TStringBuf name, TBlob value, const TExtractedRequestCookie<TVal>& old) noexcept
        {
            return {
                .Name=name,
                .Value=value,
                .Count=old.Count,
                .BadParse=old.BadParse(),
            };
        }
    }


    TPolicyIsGdprCookieBase::TPolicyIsGdprCookieBase(TString name, EPolicyMode mode)
        : ICookiePolicy(name, mode)
    {}

    bool TPolicyIsGdprCookieBase::MatchRequest(const TPolicyArgs& args) const noexcept {
        return !!args.DomainInfo.DomainRoot && NeedUpdate(args);
    }

    TRequestCookieActions TPolicyIsGdprCookieBase::ApplyToRequestCookies(
        TVector<TRequestCookie>& cookies, const TPolicyArgs& args) const noexcept
    {
        auto cArgs = Render(args);

        if (cArgs.Count == 0) {
            return {CreateReqCookie(cArgs.Name, cArgs.Value, cookies)};
        }

        TRequestCookieReasons res;
        if (cArgs.Count > 1) {
            res.Set(ERequestCookieReason::Conflict);
        }
        if (cArgs.BadParse) {
            res.Set(ERequestCookieReason::BadValue);
        }

        if (res) {
            TRequestCookieActions acts(Reserve(2));
            acts.emplace_back(ClearReqCookie(cArgs.Name, res, cookies));
            acts.emplace_back(CreateReqCookie(cArgs.Name, cArgs.Value, cookies));
            return acts;
        }

        if (cArgs.Count == 1) {
            return {UpdateReqCookie(cArgs.Name, cArgs.Value, cookies)};
        }

        return {CreateReqCookie(cArgs.Name, cArgs.Value, cookies)};
    }

    void TPolicyIsGdprCookieBase::AddSetCookies(TSetCookiesCallback cb, const TPolicyArgs& args) const noexcept {
        auto cArgs = Render(args);
        auto newCookie = GenSetCookie(cArgs.Name, cArgs.Value, args);

        TResponseCookieReasons res;
        if (cArgs.Count > 1) {
            res.Set(EResponseCookieReason::Conflict);
        }

        if (cArgs.BadParse) {
            res.Set(EResponseCookieReason::BadValue);
        }

        res.Set(!cArgs.Count ? EResponseCookieReason::Create : EResponseCookieReason::Update);
        cb({newCookie}, res);
    }


    TPolicyIsGdprCookie::TPolicyIsGdprCookie(TString name, const TPolicyIsGdprCookieCfg& cfg)
        : TPolicyIsGdprCookieBase(name, cfg.mode())
    {}

    bool TPolicyIsGdprCookie::NeedUpdate(const TPolicyArgs& args) const noexcept {
        if (!args.IsGdprUpdate) {
            return false;
        }
        return args.IsGdprCookie.Parsed != args.IsGdprUpdate;
    }

    TPolicyIsGdprCookieBase::TArgs TPolicyIsGdprCookie::Render(const TPolicyArgs& args) const noexcept {
        const auto newValue = RenderIsGdprCookie(EIsGdpr(args.Descr.Properties->Gdpr.Value.IsGdpr));
        return BuildCookieArgs(
            IsGdprCookie,
            NCookie::FromStringBuf(newValue),
            args.IsGdprCookie
        );
    }


    TPolicyIsGdprBCookie::TPolicyIsGdprBCookie(TString name, const TPolicyIsGdprBCookieCfg& cfg)
        : TPolicyIsGdprCookieBase(name, cfg.mode())
    {}

    bool TPolicyIsGdprBCookie::NeedUpdate(const TPolicyArgs& args) const noexcept {
        if (!args.IsGdprBUpdate) {
            return false;
        }
        return !args.IsGdprBUpdate->Same(args.IsGdprBCookie.Parsed);
    }

    TPolicyIsGdprCookieBase::TArgs TPolicyIsGdprBCookie::Render(const TPolicyArgs& args) const noexcept {
        return BuildCookieArgs(
            IsGdprBCookie,
            TBlob::FromStringSingleThreaded(args.IsGdprBUpdate->Render()),
            args.IsGdprBCookie
        );
    }
}
