#include "gdpr.h"
#include "stats.h"
#include "utils.h"

#include <balancer/modules/cookie_policy/common/enums.h_serialized.h>

#include <laas/lib/ip_properties/proto/ip_properties.pb.h>
#include <laas/lib/ip_properties/reader/reader.h>

namespace NModCookiePolicy {
    using NGdprCookie::ParseGdprCookie;
    using NGdprCookie::RenderGdprCookie;
    using NGdprCookie::MergeGdprCookie;
    using NGdprCookie::ParseIsGdprCookie;
    using NGdprCookie::RenderIsGdprCookie;
    using NGdprCookie::MergeIsGdprCookie;


    TExtractedGdpr ExtractGdprCookie(const TVector<TRequestCookie>& cookie) noexcept {
        return ExtractRequestCookie<EGdpr>(
            cookie,
            GdprCookie,
            ParseGdprCookie,
            MergeGdprCookie
        );
    }

    bool HasGdprPopupCookie(const TVector<TRequestCookie>& cookie) noexcept {
        return FindIf(cookie, [](auto&& c) {
            return c.NameValue.Name == GdprPopupCookie && c.NameValue.Value == "1"sv;
        }) != cookie.end();
    }

    TExtractedIsGdpr ExtractIsGdprCookie(const TVector<TRequestCookie>& cookie) noexcept {
        return ExtractRequestCookie<EIsGdpr>(
            cookie,
            IsGdprCookie,
            ParseIsGdprCookie,
            MergeIsGdprCookie
        );
    }

    TExtractedIsGdprB ExtractIsGdprBCookie(const TVector<TRequestCookie>& cookie) noexcept {
        return ExtractRequestCookie<TIsGdprB>(
            cookie,
            IsGdprBCookie,
            TIsGdprB::Parse,
            TIsGdprB::Merge
        );
    }

    TMaybe<bool> ExtractXYandexEURequest(const THeaders& reqHeaders) noexcept {
        if (auto val = reqHeaders.GetFirstValue("X-Yandex-EU-Request")) {
            if (auto parsed = ParseIsGdprCookie(val)) {
                return parsed == EIsGdpr::True;
            }
        }
        return Nothing();
    }

    [[nodiscard]]
    TMaybe<TXIpProperties> ExtractXIpProperties(const THeaders& reqHeaders) noexcept {
        if (auto val = reqHeaders.GetFirstValue("X-IP-Properties")) {
            NLaas::TIpProperties props;
            if (NLaas::TryParseFromBase64(val, props)) {
                return TXIpProperties{
                    .IsGdpr=props.isgdpr(),
                    .IsVpn=props.ishosting()
                           || props.istor()
                           || props.isanonymousvpn()
                           || props.ispublicproxy(),
                };
            }
        }
        return Nothing();
    }

    namespace {
        TGdprLocation MergeResults(TMaybe<TGdprLocation> lowerPrio, TGdprLocation higherPrio) {
            TGdprLocation res = lowerPrio.GetOrElse(higherPrio);
            res.IsGdpr = higherPrio.IsGdpr;
            res.Source = higherPrio.Source;
            if (higherPrio.IsGdprNoVpn) {
                res.IsGdprNoVpn = higherPrio.IsGdprNoVpn;
                res.SourceNoVpn = higherPrio.SourceNoVpn;
            }
            return res;
        }

        TGdprLocation ApplyXIpProperties(TXIpProperties ipGeo) noexcept {
            TGdprLocation res {
                .IsGdpr=ipGeo.IsGdpr,
                .Source=EGdprSource::XIpProperties,
            };
            if (!ipGeo.IsVpn) {
                res.IsGdprNoVpn = ipGeo.IsGdpr;
                res.SourceNoVpn = EGdprSource::XIpProperties;
            }
            return res;
        }

        TGdprLocation ApplyXYandexEURequest(bool value) noexcept {
            return {
                .IsGdpr=value,
                .Source=EGdprSource::XYandexEURequest,
            };
        }

        TGdprLocation ApplyIsGdprB(TIsGdprB b) noexcept {
            return {
                .IsGdpr=b.IsGdpr,
                .IsGdprNoVpn=b.IsGdprNoVpn,
                .Source=EGdprSource::IsGdprB,
                .SourceNoVpn=b.IsGdprNoVpn ? EGdprSource::IsGdprB : EGdprSource::None,
            };
        }
    }

    TMaybe<bool> FilterAndLogXYandexEURequest(
        TMaybe<bool> xYandexEURequest, TFilterArgs args, TReporting reporting) noexcept
    {
        if (!xYandexEURequest) {
            return Nothing();
        }
        reporting.Log << " XYandexEURequest"sv;
        reporting.Stats.XYandexEURequest.Add(1);

        if (Y_COOKIE_POLICY_CFG_OR_DYN_F(args.Cfg, args.DynCfg, Cfg, use_x_yandex_eu_request)) {
            reporting.Log << "="sv << xYandexEURequest;
            return xYandexEURequest;
        }
        reporting.Log << ":off"sv;
        reporting.Stats.XYandexEURequestOff.Add(1);
        return Nothing();
    }

    TMaybe<TXIpProperties> FilterAndLogXIpProperties(
        TMaybe<TXIpProperties> xIpProperties, TFilterArgs args, TReporting reporting) noexcept
    {
        if (!xIpProperties) {
            return Nothing();
        }
        reporting.Log << " XIpProperties"sv;
        reporting.Stats.XIpProperties.Add(1);

        if (Y_COOKIE_POLICY_CFG_OR_DYN_F(args.Cfg, args.DynCfg, Cfg, use_x_ip_properties)) {
            reporting.Log << "="sv << xIpProperties->IsGdpr << ","sv;
            if (xIpProperties->IsVpn) {
                reporting.Log << "?"sv;
            } else {
                reporting.Log << xIpProperties->IsGdpr;
            }
            return xIpProperties;
        }
        reporting.Log << ":off"sv;
        reporting.Stats.XIpPropertiesOff.Add(1);
        return Nothing();
    }

    TMaybe<TIsGdprB> FilterAndLogIsGdprB(
        const TExtractedIsGdprB& isGdprB, TInstant now, TFilterArgs args, TReporting reporting) noexcept
    {
        if (!isGdprB.Parsed) {
            if (isGdprB.Count) {
                reporting.Log << " IsGdprB:badValue"sv;
            }
            return Nothing();
        }

        reporting.Log << " IsGdprB"sv;

        if (!Y_COOKIE_POLICY_CFG_OR_DYN_F(args.Cfg, args.DynCfg, Cfg, use_is_gdpr_b)) {
            reporting.Log << ":off"sv;
            reporting.Stats.IsGdprBOff.Add(1);
            return Nothing();
        }

        if (isGdprB.Parsed->Tstamp <= IsGdprBStart
            || isGdprB.Parsed->Tstamp > now + TDuration::Hours(1)
            || !isGdprB.Parsed->BalancerVerHash
            || args.Cfg.BannedIsGdprB.Match(*isGdprB.Parsed)
            || (args.DynCfg && args.DynCfg->BannedIsGdprB.Match(*isGdprB.Parsed))
        ) {
            reporting.Log << ":filtered"sv;
            reporting.Stats.IsGdprBFiltered.Add(1);
            return Nothing();
        }

        reporting.Log << "="sv << isGdprB.Parsed->IsGdpr << ","sv;

        if (isGdprB.Parsed->IsGdprNoVpn) {
            reporting.Log << isGdprB.Parsed->IsGdprNoVpn;
        } else {
            reporting.Log << "?"sv;
        }

        return isGdprB.Parsed;
    }


    TMaybe<TGdprLocation> UpdateGdprLocation(
        TGdprCache geo,
        TMaybe<TIsGdprB> isGdprB
    ) noexcept {
        TMaybe<TGdprLocation> res;
        if (isGdprB) {
            res = MergeResults(res, ApplyIsGdprB(*isGdprB));
        }
        if (geo.XYandexEURequest) {
            res = MergeResults(res, ApplyXYandexEURequest(*geo.XYandexEURequest));
        }
        if (geo.XIpProperties) {
            res = MergeResults(res, ApplyXIpProperties(*geo.XIpProperties));
        }
        return res;
    }

    TMaybe<TGdprStatus> UpdateGdprStatus(
        TMaybe<EGdpr> gdpr,
        TMaybe<TGdprLocation> loc,
        const TDomainInfo& domainInfo,
        bool gdprPopup,
        TReporting reporting
    ) noexcept {
        if (!loc) {
            reporting.Stats.IncGdpr({});
            return Nothing();
        }

        TGdprStatus res;

        res.Source = loc->Source;
        res.Value.IsGdpr = loc->IsGdpr;
        res.Value.Mode = gdpr.GetOrElse(gdprPopup ? EGdpr::KeepTech : EGdpr::KeepAll);

        if (loc->IsGdprNoVpn) {
            // Have seen the user without VPN
            if (loc->IsGdpr) {
                res.SafeSource = loc->SourceNoVpn;
                res.SafeValue.IsGdpr = *loc->IsGdprNoVpn;
            } else {
                res.SafeSource = loc->Source;
                res.SafeValue.IsGdpr = false;
            }

            // The user 1) has seen the popup, 2) is in a gdpr region, 3) has been in a gdpr region without VPN.
            res.SafeValue.Mode = gdpr.GetOrElse(
                gdprPopup && res.SafeValue.IsGdpr
                ? EGdpr::KeepTech
                : EGdpr::KeepAll
            );
        } else {
            // Have never seen the user without VPN or lacking such information.
            res.SafeSource = res.Source;
            res.SafeValue.IsGdpr = loc->IsGdpr;
            // The user 1) has seen the popup, 2) is in a gdpr region, 3) requests a gdpr-likely domain
            res.SafeValue.Mode = gdpr.GetOrElse(
                gdprPopup && res.SafeValue.IsGdpr && domainInfo.IsGdpr
                ? EGdpr::KeepTech
                : EGdpr::KeepAll
            );
        }
        reporting.Stats.IncGdpr(res);
        return res;
    }


    TMaybe<EIsGdpr> UpdateIsGdpr(TMaybe<TGdprLocation> loc) noexcept {
        if (!loc) {
            return Nothing();
        }
        return EIsGdpr(loc->IsGdpr);
    }


    TMaybe<TIsGdprB> UpdateIsGdprB(TMaybe<TGdprLocation> loc, TInstant now) noexcept {
        if (!loc) {
            return Nothing();
        }
        return TIsGdprB{
            .BalancerVerHash=BalancerVerHash(),
            .Tstamp=now,
            .IsGdpr=loc->IsGdpr,
            .IsGdprNoVpn=loc->IsGdprNoVpn,
        };
    }
}

using namespace NModCookiePolicy;

template <>
void Out<TExtractedGdpr>(IOutputStream& out, const TExtractedGdpr& v) {
    v.PrintTo(out);
}
template <>
void Out<TExtractedIsGdpr>(IOutputStream& out, const TExtractedIsGdpr& v) {
    v.PrintTo(out);
}
template <>
void Out<TExtractedIsGdprB>(IOutputStream& out, const TExtractedIsGdprB& v) {
    v.PrintTo(out);
}
