#include "nel_report.h"

#include <library/cpp/json/json_reader.h>
#include <library/cpp/iterator/cartesian_product.h>
#include <library/cpp/resource/resource.h>

#include <metrika/core/libs/uahints/UserAgentTraits.h>

#include <util/charset/utf8.h>
#include <util/generic/string.h>
#include <util/string/split.h>
#include <util/string/subst.h>

#include <cmath>


namespace {

struct NeldrlogErrorType {
    TString GroupedErrorType;
    TString FullErrorType;
};

const THashMap<TString, struct NeldrlogErrorType> NELDRLOG_ERROR_TYPES = {
        // DNS
        {"dns.unreachable",       {"dns_error", "dns_unreachable"}},
        {"dns.name_not_resolved", {"dns_error", "dns_name_not_resolved"}},
        {"dns.failed",            {"dns_error", "dns_failed"}},
        {"dns.address_changed",   {"dns_error", "dns_address_changed"}},

        // TCP
        {"tcp.timed_out",           {"tcp_error", "tcp_timed_out"}},
        {"tcp.closed",              {"tcp_error", "tcp_closed"}},
        {"tcp.reset",               {"tcp_error", "tcp_reset"}},
        {"tcp.refused",             {"tcp_error", "tcp_refused"}},
        {"tcp.aborted",             {"tcp_error", "tcp_aborted"}},
        {"tcp.address_invalid",     {"tcp_error", "tcp_address_invalid"}},
        {"tcp.address_unreachable", {"tcp_error", "tcp_address_unreachable"}},
        {"tcp.failed",              {"tcp_error", "tcp_failed"}},

        // HTTP
        {"http.error",                  {"http_error", "http_error"}},
        {"http.protocol.error",         {"http_error", "http_protocol_error"}},
        {"http.response.invalid",       {"http_error", "http_response_invalid"}},
        {"http.response.redirect_loop", {"http_error", "http_response_redirect_loop"}},
        {"http.failed",                 {"http_error", "http_failed"}},

        // TLS
        {"tls.version_or_cipher_mismatch",        {"tls_error", "tls_version_or_cipher_mismatch"}},
        {"tls.bad_client_auth_cert",              {"tls_error", "tls_bad_client_auth_cert"}},
        {"tls.cert.name_invalid",                 {"tls_error", "tls_cert_name_invalid"}},
        {"tls.cert.date_invalid",                 {"tls_error", "tls_cert_date_invalid"}},
        {"tls.cert.authority_invalid",            {"tls_error", "tls_cert_authority_invalid"}},
        {"tls.cert.invalid",                      {"tls_error", "tls_cert_invalid"}},
        {"tls.cert.revoked",                      {"tls_error", "tls_cert_revoked"}},
        {"tls.cert.pinned_key_not_in_cert_chain", {"tls_error", "tls_cert_pinned_key_not_in_cert_chain"}},
        {"tls.protocol.error",                    {"tls_error", "tls_cert_protocol_error"}},
        {"tls.failed",                            {"tls_error", "tls_failed"}},

        // Other
        {"abandoned", {"abandoned", "abandoned"}},
        {"unknown",   {"unknown",   "unknown"}},
        {"ok",        {"ok",        "ok"}}
};

struct TNeldrlogErrorTypes {
    THashSet<TString> Values;

    explicit TNeldrlogErrorTypes(bool extendedSignals) {
        for (const auto& [nelErrorType, neldrlogErrorType] : NELDRLOG_ERROR_TYPES) {
            Values.insert(neldrlogErrorType.GroupedErrorType);
            if (extendedSignals) {
                Values.insert(neldrlogErrorType.FullErrorType);
            }
        }
    }
};

void ParseURL(NUri::TUri& url, const TString& urlString) {
    NUri::TState::EParsed urlParsedState = url.Parse(
        urlString,
        NUri::TFeature::FeaturesDefault | NUri::TFeature::FeatureSchemeKnown
    );
    if (urlParsedState != NUri::TState::ParsedOK) {
        ythrow yexception() << "Couldn't parse url: " << urlParsedState << ", " << urlString.Quote();
    }
}

} // anonymous namespace


const THashSet<TString>& GetNeldrlogErrorTypes(bool extendedSignals) {
    if (extendedSignals) {
        static const TNeldrlogErrorTypes extendedNeldrlogErrorTypes(true);
        return extendedNeldrlogErrorTypes.Values;
    } else {
        static const TNeldrlogErrorTypes groupedNeldrlogErrorTypes(false);
        return groupedNeldrlogErrorTypes.Values;
    }
}


TNELReportSender::TNELReportSender(
    const TServerRequestData& requestData,
    const std::optional<NGeobase::NImpl::TLookup>& geobaseLookup
)
    : NeldrlogPath(requestData.ScriptName())
    , IP(requestData.HeaderInOrEmpty("X-Forwarded-For-Y"))
    , UserAgent(requestData.HeaderInOrEmpty("User-Agent"))
{
    FillASNs(IP, geobaseLookup);
    FillUATraits(UserAgent);
}

void TNELReportSender::FillASNs(
    const TStringBuf clientIp,
    const std::optional<NGeobase::NImpl::TLookup>& geobaseLookup
) {
    if (!geobaseLookup || clientIp == nullptr) {
        ASNs = {};
        return;
    }
    const TString asnsString(geobaseLookup->GetTraitsByIp(std::string(clientIp)).AsnList);
    ASNs = StringSplitter(asnsString).SplitBySet(", ").SkipEmpty();
    for (TString& asn : ASNs) {
        asn.prepend("AS");
    }
}

void TNELReportSender::FillUATraits(const TStringBuf userAgent) {
    static auto chBrowsers = NResource::Find("ch_browsers.xml");
    static auto chBuilder  = NResource::Find("ch_builder.xml");
    static auto chRules    = NResource::Find("ch_rules.xml");

    static auto browsers = NResource::Find("browser.xml");
    static auto profiles = NResource::Find("profiles.xml");
    static auto extra    = NResource::Find("extra.xml");

    static auto browsersIstr = std::istringstream{std::string{browsers.data(), browsers.size()}};
    static auto profilesIstr = std::istringstream{std::string{profiles.data(), profiles.size()}};
    static auto extraIstr    = std::istringstream{std::string{extra.data(),    extra.size()}};

    static const NUATraits::TResourcesConfigs configs {
        &chBrowsers, &chRules, &chBuilder, browsersIstr, profilesIstr, extraIstr
    };

    constexpr bool buildProto       = true;
    constexpr bool buildUAFromHints = false;
    constexpr size_t restoredUserAgentAndProtoCacheSize = 0;
    constexpr size_t protoFromFullUserAgentCacheSize    = 1000;

    static const NUATraits::TUserAgentTraitsSettings settings {
        buildProto,
        buildUAFromHints,
        configs,
        restoredUserAgentAndProtoCacheSize,
        protoFromFullUserAgentCacheSize
    };
    
    struct TUATraitsProjection {
        TString OSFamily;
        TString BrowserName;
        bool    IsMobile;
    };

    static const NUATraits::IUserAgentTraitsPtr<TUATraitsProjection> detector = 
        NUATraits::CreateUserAgentTraits<TUATraitsProjection>(
            settings,
            [](const TUserAgent& ua) -> TUATraitsProjection {
                return TUATraitsProjection{
                    .OSFamily    = ua.GetOSFamily(),
                    .BrowserName = ua.GetBrowserName(),
                    .IsMobile    = ua.GetIsMobile(),
                };
            }
        );

    constexpr NUATraits::THintsHeaders hints = {"", "", "", "", "", "", "", ""};
    const TMaybe<TUATraitsProjection> result = detector->GetUserAgents(hints, userAgent).ProtoUserAgent;

    if (result) {
        OSFamily    = result->OSFamily;
        BrowserName = result->BrowserName;
        IsMobile    = result->IsMobile;
    } else {
        constexpr TStringBuf unknownUserAgentTraitValue = "Unknown";
        OSFamily    = unknownUserAgentTraitValue;
        BrowserName = unknownUserAgentTraitValue;
        IsMobile    = false;
    }
}


TNELReport::TNELReport(TNELReportSender& sender, const NJson::TJsonValue& reportJson) 
    : Sender(sender)
{
    const NJson::TJsonValue::TMapType& reportMap = reportJson.GetMapSafe();
    const NJson::TJsonValue::TMapType& bodyMap = reportMap.at("body").GetMapSafe();

    ServerIP = bodyMap.at("server_ip").GetStringSafe();
    StatusCode = bodyMap.at("status_code").GetIntegerSafe();
    ParseURL(Url, reportMap.at("url").GetStringSafe());

    Age = TDuration::MilliSeconds(reportMap.at("age").GetUIntegerSafe());

    // Error types
    NELErrorType = bodyMap.at("type").GetStringSafe();
    if (NELDRLOG_ERROR_TYPES.contains(NELErrorType)) {
        GroupedErrorType = NELDRLOG_ERROR_TYPES.at(NELErrorType).GroupedErrorType;
        FullErrorType    = NELDRLOG_ERROR_TYPES.at(NELErrorType).FullErrorType;
    } else {
        GroupedErrorType = TString("unknown");
        FullErrorType = TString("unknown");
    }
}

