#include "metric_storage.h"
#include <algorithm>

#include <iostream>
#include <regex>
#include <string>
#include <vector>

#include <cctype>

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

#include <util/string/builder.h>


TTenantMetricStorage::TTenantMetricStorage(const TTenantConfiguration& configuration)
    : Configuration_(configuration)
{
    GenerateMetrics();
}


void TTenantMetricStorage::GenerateMetrics() {
    auto neldrlogErrorTypes = GetNeldrlogErrorTypes(Configuration_.FullErrorTypes);

    for (const auto& [asn, site, errorType] : CartesianProduct(
        Concatenate(*Configuration_.ASNs, TVector<TString>({TMetricKey::OtherASN, TMetricKey::AnyASN})), 
        Configuration_.Sites, 
        neldrlogErrorTypes
    )) {
        Metrics_.emplace(TMetricKey(asn, site, errorType, true), 0);
    }
}

void TTenantMetricStorage::StoreNELReport(const TNELReport& report) {
    THashSet<TString> matchedASNs;
    MatchReportAsns(report, matchedASNs);

    THashSet<TString> matchedSites;
    MatchReportSites(report, matchedSites);

    THashSet<TString> errorTypes;
    ExtractReportErrorTypes(report, errorTypes);

    for (const auto& [asn, site, errorType] : CartesianProduct(
        matchedASNs, matchedSites, errorTypes
    )) {
        AtomicAdd(Metrics_.at(TMetricKey(asn, site, errorType)), 1);
    }
}

void TTenantMetricStorage::WriteMetricsForYASM(NJson::TJsonWriter& jsonWriter) const {
    for (auto& [metricKey, metricValue] : Metrics_) {
        jsonWriter.OpenArray();
        jsonWriter.Write(*metricKey.YASMMetricName);
        jsonWriter.Write(AtomicGet(metricValue));
        jsonWriter.CloseArray();
    }
}

void TTenantMetricStorage::WriteMetricsForSolomonJson(NJson::TJsonWriter& jsonWriter) const {
    for (const auto& [metricKey, metricValue] : Metrics_) {
        if (!metricKey.ReportToSolomon) {
            continue;
        }
        
        jsonWriter.OpenMap();
        
        jsonWriter.WriteKey("labels");
        jsonWriter.OpenMap();
        jsonWriter.Write("asn",        metricKey.ASN);
        jsonWriter.Write("site",       metricKey.Site);
        jsonWriter.Write("error_type", metricKey.ErrorType);
        jsonWriter.CloseMap();

        jsonWriter.Write("type", "RATE");
        jsonWriter.Write("value", AtomicGet(metricValue));

        jsonWriter.CloseMap();
    }
}

void TTenantMetricStorage::WriteMetricsForSolomonSpack(NMonitoring::IMetricEncoderPtr& spackEncoder) const {
    for (const auto& [metricKey, metricValue] : Metrics_) {
        if (!metricKey.ReportToSolomon) {
            continue;
        }
        
        spackEncoder->OnMetricBegin(NMonitoring::EMetricType::RATE);

        spackEncoder->OnLabelsBegin();
        spackEncoder->OnLabel("asn",        metricKey.ASN);
        spackEncoder->OnLabel("site",       metricKey.Site);
        spackEncoder->OnLabel("error_type", metricKey.ErrorType);
        spackEncoder->OnLabelsEnd();

        spackEncoder->OnUint64(TInstant::Zero(), AtomicGet(metricValue));
        spackEncoder->OnMetricEnd();
    }
}

void TTenantMetricStorage::MatchReportAsns(const TNELReport& report, THashSet<TString>& matchedASNs) {
    for (const auto& asn : report.Sender.ASNs) {
        if (Configuration_.ASNs->contains(asn)) {
            matchedASNs.emplace(asn);
        }
    }
    if (!matchedASNs) {
        matchedASNs.emplace(TMetricKey::OtherASN);
    }
    matchedASNs.emplace(TMetricKey::AnyASN);
}

void TTenantMetricStorage::MatchReportSites(const TNELReport& report, THashSet<TString>& matchedSites) {
    const TString queryStr = TStringBuilder{} << report.Url.GetHost() << report.Url.GetField(NUri::TField::FieldPath);

    auto [acceptedIt, acceptedEnd] = Configuration_.SitesRegex.AcceptedRegexps(
        NPire::Runner(Configuration_.SitesRegex).Run(queryStr).State()
    );
    for (; acceptedIt < acceptedEnd; ++acceptedIt) {
        matchedSites.emplace(Configuration_.Sites[*acceptedIt]);
    }
}

void TTenantMetricStorage::ExtractReportErrorTypes(const TNELReport& report, THashSet<TString>& errorTypes) {
    errorTypes.emplace(report.GroupedErrorType);
    if (Configuration_.FullErrorTypes && !(report.GroupedErrorType == report.FullErrorType)) {
        errorTypes.emplace(report.FullErrorType);
    }
}


TMetricStorage::TMetricStorage(const TConfiguration& configuration)
    : Configuration_(configuration)
{
    for (const auto& [tenantName, tenantConfiguration] : configuration.Tenants) {
        Tenants_.emplace(tenantName, TTenantMetricStorage(tenantConfiguration));
    }
}

void TMetricStorage::StoreNELReport(const TString& tenant, const TNELReport& report) {
    if (report.Age > Configuration_.MaxReportAge) {
        return;
    }

    Y_ASSERT(Tenants_.contains(tenant));
    // TODO Carefully handle the situation where the site doesn't exist
    Tenants_.at(tenant).StoreNELReport(report);
}

void TMetricStorage::WriteMetrics(const EMetricWriteFormat format, const TString& tenant, IOutputStream* out) const {
    switch (format) {
        case YASM:
            WriteMetricsForYASM(tenant, out);
            break;
        case SOLOMON_JSON:
            WriteMetricsForSolomonJson(tenant, out);
            break;
        case SOLOMON_SPACK:
            WriteMetricsForSolomonSpack(tenant, out);
            break;
    }
}

void TMetricStorage::WriteMetricsForYASM(const TString& tenant, IOutputStream* out) const {
    NJson::TJsonWriter jsonWriter(out, false);
    jsonWriter.OpenArray();

    Y_ASSERT(Tenants_.contains(tenant));
    // TODO Carefully handle the situation where the site doesn't exist
    Tenants_.at(tenant).WriteMetricsForYASM(jsonWriter);

    jsonWriter.CloseArray();
    jsonWriter.Flush();
}

void TMetricStorage::WriteMetricsForSolomonJson(const TString& tenant, IOutputStream* out) const {
    NJson::TJsonWriter jsonWriter(out, false);
    jsonWriter.OpenMap();

    jsonWriter.WriteKey("metrics");
    jsonWriter.OpenArray();

    Y_ASSERT(Tenants_.contains(tenant));
    // TODO Carefully handle the situation where the site doesn't exist
    Tenants_.at(tenant).WriteMetricsForSolomonJson(jsonWriter);

    jsonWriter.CloseArray();
    jsonWriter.CloseMap();
    jsonWriter.Flush();
}

void TMetricStorage::WriteMetricsForSolomonSpack(const TString& tenant, IOutputStream* out) const {
    auto spackEncoder = NMonitoring::EncoderSpackV1(
        out,
        NMonitoring::ETimePrecision::SECONDS,
        NMonitoring::ECompression::LZ4
    );
    spackEncoder->OnStreamBegin();
    
    spackEncoder->OnLabelsBegin();
    spackEncoder->OnLabel("project", "neldrlog");
    spackEncoder->OnLabelsEnd();
    
    Y_ASSERT(Tenants_.contains(tenant));
    // TODO Carefully handle the situation where the site doesn't exist
    Tenants_.at(tenant).WriteMetricsForSolomonSpack(spackEncoder);
    
    spackEncoder->OnStreamEnd();
    spackEncoder->Close();
}

const TString TMetricKey::OtherASN = "ASother";
// Temporary solution for backward compatibility in YASM
const TString TMetricKey::AnyASN   = "https";

