#include "combined_policy.h"
#include "cookie_headers.h"

#include <util/generic/map.h>

namespace NModCookiePolicy {
    namespace {
        TString JoinLogs(const TVector<TString>& logs) noexcept {
            size_t resLogSz = 0;
            for (auto&& lg : logs) {
                resLogSz += lg.size();
            }
            TString resLog(Reserve(resLogSz));
            for (auto&& lg : logs) {
                resLog.append(lg);
            }
            return resLog;
        }

        EPolicyMode GetPolicyMode(
            const TPolicyTls& tlsPolicy,
            const TRequestArgs& args,
            const TModeControls& staticControls,
            const TModeControls* dynamicControls
        ) noexcept {
            auto mode = tlsPolicy.Policy.PolicyMode();

            mode = staticControls.PolicyMode(
                tlsPolicy.Policy.Policy().Name(),
                args.DomainInfo
            ).GetOrElse(mode);

            if (dynamicControls) {
                mode = dynamicControls->PolicyMode(
                    tlsPolicy.Policy.Policy().Name(),
                    args.DomainInfo
                ).GetOrElse(mode);
            }

            if (mode == EPolicyMode::Off || !tlsPolicy.Policy.MatchRequest(tlsPolicy, args)) {
                return EPolicyMode::Off;
            }

            return mode;
        }

        EPolicyMode GetParserMode(
            const TModuleConfig& cfg,
            const TDomainInfo& domainInfo,
            const TModeControls& staticControls,
            const TModeControls* dynamicControls
        ) noexcept {
            auto parserMode = cfg.parser_mode();
            parserMode = staticControls.ParserMode(domainInfo).GetOrElse(parserMode);
            if (dynamicControls) {
                parserMode = dynamicControls->ParserMode(domainInfo).GetOrElse(parserMode);
            }
            return parserMode;
        }
    }

    // RFC 6265bis-06:
    //      2.  The user agent SHOULD sort the cookie-list in the following
    //          order:
    //          *  Cookies with longer paths are listed before cookies with
    //             shorter paths.
    //          *  Among cookies that have equal-length path fields, cookies with
    //             earlier creation-times are listed before cookies with later
    //             creation-times.
    //
    //      NOTE: Not all user agents sort the cookie-list in this order, but
    //      this order reflects common practice when this document was
    //      written, and, historically, there have been servers that
    //      (erroneously) depended on this order.
    //
    // Impl:
    //      1.  We cannot reorder the request cookie-list because a backend might depend on it (see above).
    //          E.g. grouping by name is out of question.
    //      2.  We still need to see all the values before we are able to make any decision.
    //      3.  The task at hand requires us being able to
    //          1) add a new cookie in request AND response.
    //          2) drop some old cookies in request AND response.
    //      4.  It is ok to add only by appending to the end of request cookie-list and response header-list.
    //      5.  It is ok to represent a change in value as dropping and then adding to the end.
    //      6.  We must keep counters of our actions and log the reasons for each and every cookie modification.
    //          In dry-run as well.

    TCombinedPolicyTls::TCombinedPolicyTls(const TCombinedPolicy& pol, TSharedStatsManager& manager)
        : Policy_(pol)
        , Stats_(pol.Config.uuid(), manager)
    {
        for (auto&& p : pol.Policies) {
            Policies_.emplace_back(p, manager);
        }
    }

    TCombinedPolicyTls::TCombinedPolicyTls(const TCombinedPolicyTls& tmpl, IWorkerCtl& ctl)
        : Policy_(tmpl.Policy_)
        , Stats_(tmpl.Stats_, ctl.WorkerId())
    {
        for (auto&& p : tmpl.Policies_) {
            Policies_.emplace_back(p, ctl);
        }
    }

    TMaybe<TCombinedPolicyCtx> TCombinedPolicyTls::ApplyToRequest(const TConnDescr& descr) noexcept {
        Stats_.Total.Add(1);

        TCombinedPolicyCtx res;
        res.DomainInfo = GetDomainInfo(descr);

        if (GetParserMode(Policy_.Config, res.DomainInfo, Policy_.ModeControls, ModeControls_) == EPolicyMode::Off) {
            Stats_.Off.Add(1);
            return Nothing();
        }

        Stats_.Checked.Add(1);
        Stats_.GdprDomain.Add(res.DomainInfo.IsGdpr);

        res.Cookies = ParseRequestCookies(descr.Request->Headers().GetValuesRef("cookie"));
        res.IsGdprCookie=ExtractIsGdprCookie(res.Cookies);
        res.IsGdprBCookie=ExtractIsGdprBCookie(res.Cookies);

        if (res.IsGdprCookie.Count) {
            Stats_.OnIsGdpr(res.IsGdprCookie);
        }

        if (res.IsGdprBCookie.Count) {
            Stats_.OnIsGdprB(res.IsGdprBCookie);
        }

        if (res.DomainInfo.IsGdpr) {
            descr.ExtraAccessLog << " GdprDomain=1"sv;
        }

        const auto gdprCookie = ExtractGdprCookie(res.Cookies);
        if (gdprCookie.Count) {
            if (gdprCookie.Parsed) {
                descr.ExtraAccessLog << " Gdpr="sv << *gdprCookie.Parsed;
            } else {
                descr.ExtraAccessLog << " Gdpr:badValue"sv;
            }
            Stats_.OnGdpr(gdprCookie);
        }

        const auto gdprPopup = HasGdprPopupCookie(res.Cookies);
        if (gdprPopup) {
            descr.ExtraAccessLog << " GdprPopup=1"sv;
            Stats_.GdprPopup.Add(1);
            Stats_.GdprPopupNoGdpr.Add(gdprCookie.Count == 0);
        }

        const TReporting reporting = {
            .Log=*descr.ExtraAccessLog.Slave(),
            .Stats=Stats_,
        };

        const TFilterArgs filterArgs = {
            .Cfg=Policy_.GdprControls,
            .DynCfg=GdprControls_,
        };

        if (auto xYandexEURequest = FilterAndLogXYandexEURequest(
            ExtractXYandexEURequest(descr.Request->Headers()), filterArgs, reporting))
        {
            descr.Properties->Parent.GdprCache.XYandexEURequest = xYandexEURequest;
        }

        if (auto xIpProperties = FilterAndLogXIpProperties(
            ExtractXIpProperties(descr.Request->Headers()), filterArgs, reporting))
        {
            descr.Properties->Parent.GdprCache.XIpProperties = xIpProperties;
        }

        const auto isGdprB = FilterAndLogIsGdprB(res.IsGdprBCookie, descr.Properties->Start, filterArgs, reporting);
        const auto loc = UpdateGdprLocation(descr.Properties->Parent.GdprCache, isGdprB);

        res.IsGdprUpdate = UpdateIsGdpr(loc);
        res.IsGdprBUpdate = UpdateIsGdprB(loc, descr.Properties->Start);

        const auto gdprStatus = UpdateGdprStatus(gdprCookie.Parsed, loc, res.DomainInfo, gdprPopup, reporting);
        descr.Properties->Gdpr = gdprStatus.GetOrElse({});

        TCookieClassifier cookieClassifier(Policy_.GdprControls, GdprControls_);

        TRequestArgs reqArgs{
            .Descr=descr,
            .DomainInfo=res.DomainInfo,
            .IsGdprCookie=res.IsGdprCookie,
            .IsGdprUpdate=res.IsGdprUpdate,
            .IsGdprBCookie=res.IsGdprBCookie,
            .IsGdprBUpdate=res.IsGdprBUpdate,
            .CookieClassifier=cookieClassifier,
            .Cookies=res.Cookies,
        };

        for (auto&& tlsPolicy : Policies_) {
            tlsPolicy.Stats.Total.Add(1);

            const auto mode = GetPolicyMode(
                tlsPolicy,
                reqArgs,
                Policy_.ModeControls,
                ModeControls_
            );

            if (EPolicyMode::Off == mode) {
                tlsPolicy.Stats.Off.Add(1);
                continue;
            }

            res.Contexts.emplace_back(
                tlsPolicy,
                (mode == EPolicyMode::DryRun)
            );
        }

        bool modified = false;
        for (auto&& policyCtx : res.Contexts) {
            auto actions = policyCtx.Tls.Policy.ApplyToRequestCookies(policyCtx, reqArgs);
            if (!actions) {
                continue;
            }
            policyCtx.Passed = false;
            *descr.ExtraAccessLog.Slave() << LogPolicyResult(policyCtx.Tls.Policy.Uuid(), policyCtx.DryRun, actions);
            modified |= !policyCtx.DryRun;
        }

        if (modified) {
            Stats_.ModifiedReq.Add(1);
            auto res = RenderRequestCookies(reqArgs.Cookies);
            descr.Request->Headers().Replace("cookie", std::move(res));
        }

        return res;
    }

    TString TCombinedPolicyTls::ApplyToResponse(TCombinedPolicyCtx& ctx, const TConnDescr& descr, TResponse& resp) noexcept {
        const auto parserDryRun = (GetParserMode(
            Policy_.Config, ctx.DomainInfo, Policy_.ModeControls, ModeControls_
        ) == EPolicyMode::DryRun);

        TArrayRef<TStringStorage> prevCookies = resp.Headers().GetValuesRef("set-cookie");
        TVector<TString> respLog;

        auto [cookies, parserErrors] = ParseResponseCookies(prevCookies, parserDryRun, Stats_);

        if (parserErrors) {
            respLog.emplace_back(LogParserResult(Policy_.ParserUuid, parserDryRun, parserErrors));
        }

        const auto hardMax =
                Y_COOKIE_POLICY_CFG_OR_DYN_F(Policy_.GdprControls, GdprControls_, Cfg.deletion_limits(), hard_max);
        const auto softMax =
                Y_COOKIE_POLICY_CFG_OR_DYN_F(Policy_.GdprControls, GdprControls_, Cfg.deletion_limits(), soft_max);

        TCookieClassifier cookieClassifier(Policy_.GdprControls, GdprControls_);

        TResponseArgs respArgs{
            .Descr=descr,
            .DomainInfo=ctx.DomainInfo,
            .IsGdprCookie=ctx.IsGdprCookie,
            .IsGdprUpdate=ctx.IsGdprUpdate,
            .IsGdprBCookie=ctx.IsGdprBCookie,
            .IsGdprBUpdate=ctx.IsGdprBUpdate,
            .CookieClassifier=cookieClassifier,
            .DeletionSoftMax=std::min(softMax, hardMax),
            .DeletionHardMax=hardMax,
            .Resp=resp,
            .Cookies=cookies,
        };

        bool modified = !(parserDryRun || parserErrors.empty());
        for (auto&& policyCtx : ctx.Contexts) {
            auto actions = policyCtx.Tls.Policy.ApplyToResponseCookies(policyCtx, respArgs);
            if (policyCtx.Passed && !actions) {
                policyCtx.Tls.Stats.Pass.Add(1);
            } else {
                policyCtx.Tls.Stats.OnFail(policyCtx.DryRun);
            }
            if (actions) {
                respLog.emplace_back(LogPolicyResult(policyCtx.Tls.Policy.Uuid(), policyCtx.DryRun, actions));
                modified |= !policyCtx.DryRun;
            }
        }

        if (modified) {
            Stats_.ModifiedResp.Add(1);
            auto res = RenderResponseCookies(cookies, prevCookies);
            resp.Headers().Replace("set-cookie", std::move(res));
        }

        auto log = JoinLogs(respLog);
        MaybeReportCookieProblems(*descr.Request, resp, log);
        return log;
    }
}
