#include "signals.h"

#include <saas/api/action.h>
#include <saas/library/daemon_base/unistat_signals/messages.h>
#include <saas/rtyserver/docfetcher/library/config.h>
#include <saas/rtyserver/docfetcher/library/types.h>

#include <saas/protos/reply.pb.h>

#include <util/string/join.h>

namespace {
    const TVector<double> AgeHistogram = {
        0,
        1,
        3,
        5,
        10,
        15,
        20,
        30,
        45,
        60,
        60 * 3,
        60 * 5,
        60 * 10,
        60 * 20,
        60 * 30,
        60 * 45,
        60 * 60,
        60 * 60 + 20 * 60,
        60 * 60 + 40 * 60,
        60 * 60 * 2,
        60 * 60 * 2 + 30 * 60,
        60 * 60 * 3,
        60 * 60 * 5,
        60 * 60 * 7,
        60 * 60 * 10,
        60 * 60 * 15,
        60 * 60 * 20,
        60 * 60 * 24
    };

    constexpr TStringBuf SignalPrefix = "backend-df";

    const TVector<TString> SignalNameModifiers = {
        "",
        "-CTYPE",
        "-SERV-CTYPE"
    };

    TString ToSuffix(const TString& streamName) {
        return "-" + streamName;
    }

    template <class... TArgs>
    TString GetFullSignalName(TArgs... args) {
        return Join("", SignalPrefix, std::forward<TArgs>(args)...);
    }
}


NFusion::TCommonDocStreamSignals::TCommonDocStreamSignals(const NFusion::TDocStreamConfig& config)
    : SignalSuffix(ToSuffix(config.Name))
    , DistributionAttributes(config.DistributionMetrics)
    , LastReceiveTimestamp(0)
    , IsExhausted(false)
{
    Build();
    RegisterGlobalMessageProcessor(this);
}

NFusion::TCommonDocStreamSignals::~TCommonDocStreamSignals() {
    UnregisterGlobalMessageProcessor(this);
}

void NFusion::TCommonDocStreamSignals::Build() {
    Init(TUnistat::Instance());
}

void NFusion::TCommonDocStreamSignals::DrillHolesReply(TUnistat &t) const {
    for (auto &&modifier : SignalNameModifiers) {
        const bool service = !modifier.empty();
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-error-update"), "dmmm", Prio("400", service, true));
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-error-400"), "dmmm", Prio("400", service, true), NUnistat::TStartValue(0), EAggregationType::Sum, true);
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-error-5xx"), "dmmm", Prio("5xx", service, true), NUnistat::TStartValue(0), EAggregationType::Sum, true);
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-deprecated"), "dmmm", Prio("", service, true), NUnistat::TStartValue(0), EAggregationType::Sum, true);
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-onstop"), "dmmm", Prio("", service, true), NUnistat::TStartValue(0), EAggregationType::Sum, true);
    }
}

void NFusion::TCommonDocStreamSignals::ProcessReply(TActionPtr action, const NRTYServer::TReply& reply) {
    TUnistat& I = TUnistat::Instance();
    for (auto&& modifier : SignalNameModifiers) {
        switch (reply.GetStatus()) {
            case NRTYServer::TReply::OK:
                AtomicSet(LastReceiveTimestamp, action->GetReceiveTimestamp().Seconds());
                break;
            case NRTYServer::TReply::INCORRECT_DOCUMENT:
                PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-error-400"), 1);
                break;
            case NRTYServer::TReply::INCORRECT_UPDATE:
                PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-error-update"), 1);
                break;
            case NRTYServer::TReply::DEPRECATED:
                PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-deprecated"), 1);
                break;
            case NRTYServer::TReply::NOTNOW_ONSTOP:
                PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-onstop"), 1);
                break;
            default:
                PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-error-5xx"), 1);
                break;
        }
    }
}

void NFusion::TCommonDocStreamSignals::Init(TUnistat& t) const {
    InitAttrs(t);
    DrillHolesReply(t); // "-error...", "-deprecated"

    for (auto&& modifier : SignalNameModifiers) {
        const bool service = !modifier.empty();
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-incoming"), "dmmm", Prio("200", service));
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-expired"), "dmmm", Prio("", service, true));
        t.DrillHistogramHole(GetFullSignalName(modifier, SignalSuffix, "-age"), "dhhh", Prio("", service, true), AgeHistogram);
        t.DrillHistogramHole(GetFullSignalName(modifier, SignalSuffix, "-receive_duration"), "dhhh", Prio("", service, true), AgeHistogram);
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-receive_lag"), "avvv", Prio("", service), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-receive_duration-avg"), "avvv", Prio("", service), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-receive_duration-max"), "axxx", Prio("", service, true), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-version_age"), "avvv", Prio("", service), NUnistat::TStartValue(0), EAggregationType::LastValue);
    }
}

void NFusion::TCommonDocStreamSignals::InitAttrs(TUnistat& t) const {
    for (auto&& modifier : SignalNameModifiers) {
        for (auto&& attribute : DistributionAttributes) {
            t.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, "-distrattr-", attribute), "dmmm", NUnistat::TPriority(55));
        }
    }
}

void NFusion::TCommonDocStreamSignals::ProcessExhausted() {
    IsExhausted = true;

    TUnistat& I = TUnistat::Instance();

    for (auto&& modifier : SignalNameModifiers) {
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-receive_duration-avg"), 0);
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-receive_duration-max"), 0);
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-version_age"), 0);
    }
}

void NFusion::TCommonDocStreamSignals::ProcessExpired(TActionPtr action) {
    TUnistat& I = TUnistat::Instance();

    for (auto&& modifier : SignalNameModifiers) {
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-expired"), 1);
    }
    ProcessTimestamps(action);
}

void NFusion::TCommonDocStreamSignals::ProcessIncoming(TActionPtr action) {
    TUnistat& I = TUnistat::Instance();

    for (auto&& modifier : SignalNameModifiers) {
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-incoming"), 1);
    }
    ProcessTimestamps(action);
}

void NFusion::TCommonDocStreamSignals::ProcessIndexed(TActionPtr action, const NRTYServer::TReply& reply) {
    ProcessReply(action, reply);
}

void NFusion::TCommonDocStreamSignals::ProcessTimestamps(TActionPtr action) {
    IsExhausted = false;

    TUnistat& I = TUnistat::Instance();
    const auto& doc = action->GetDocument();

    TInstant now = Now();
    ui64 age = (now - TInstant::Seconds(doc.GetTimestamp())).Seconds();
    ui64 receiveDuration = (now - action->GetReceiveTimestamp()).Seconds();
    bool hasVersion = doc.HasVersion() && doc.GetVersion();
    ui64 versionAge = hasVersion ? (now - TInstant::Seconds(doc.GetVersion())).Seconds() : 0;
    for (auto&& modifier : SignalNameModifiers) {
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-age"), age);
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-receive_duration"), receiveDuration);
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-receive_duration-avg"), receiveDuration);
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-receive_duration-max"), receiveDuration);
        if (hasVersion) {
            PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-version_age"), versionAge);
        }
    }
}

void NFusion::TCommonDocStreamSignals::ProcessAttr(const TString& distrAttr) {
    if (DistributionAttributes.find(distrAttr) == DistributionAttributes.end()) //a little optimization
        return;

    TUnistat& I = TUnistat::Instance();
    for (auto &&modifier : SignalNameModifiers)
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-distrattr-", distrAttr), 1);
}

void NFusion::TCommonDocStreamSignals::ProcessAttrs(TActionPtr action) {
    const NRTYServer::TMessage& message = action->ToProtobuf();
    int nAttributes = message.DistributorAttributesSize();

    for (int k = 0; k < nAttributes; k++) {
        TString distrAttr(message.GetDistributorAttributes(k));
        Y_VERIFY_DEBUG(distrAttr.find(',') == TString::npos, "DistributionAttributes should contain parsed (i.e. splitted) data");

        ProcessAttr(distrAttr);
    }
}

void NFusion::TCommonDocStreamSignals::SetReceiveTimestamp(ui64 timestamp) {
    AtomicSet(LastReceiveTimestamp, timestamp);
}

TString NFusion::TCommonDocStreamSignals::Name() const {
    return "df_stream_signals" + SignalSuffix;
}

bool NFusion::TCommonDocStreamSignals::Process(IMessage* message) {
    if (dynamic_cast<TMessageUpdateUnistatSignals*>(message)) {
        ui64 receiveLag = 0;
        TInstant receiveTimestamp = TInstant::Seconds(AtomicGet(LastReceiveTimestamp));
        if (!IsExhausted && receiveTimestamp != TInstant::Zero()) {
            receiveLag = (Now() - receiveTimestamp).Seconds();
        }
        if (IsExhausted || receiveTimestamp != TInstant::Zero()) {
            TUnistat& I = TUnistat::Instance();
            for (auto&& modifier : SignalNameModifiers) {
                PushSignal(I, GetFullSignalName(modifier, SignalSuffix, "-receive_lag"), receiveLag);
            }
        }
        return true;
    }
    return false;
}

NFusion::TSyncSignals::TSyncSignals()
{
    Init(TUnistat::Instance());
}

void NFusion::TSyncSignals::Init(TUnistat &t) const {
    for (auto&& modifier : SignalNameModifiers) {
        const bool service = !modifier.empty();
        t.DrillFloatHole(
            "backend-sy" + modifier + "-synchronizing"
            , "ammx"
            , Prio("", service)
            , NUnistat::TStartValue(0)
            , EAggregationType::LastValue);
    }
}

void NFusion::TSyncSignals::UpdateSyncStatus(bool synchronizingRun) {
    TUnistat& I = TUnistat::Instance();

    for (auto&& modifier : SignalNameModifiers) {
        PushSignal(I, "backend-sy" + modifier + "-synchronizing", (synchronizingRun ? 1 : 0));
    }
}

NFusion::TDocFetcherSignals::TDocFetcherSignals(const TDocFetcherConfig &FetcherConfig) {
    for (auto&& streamConfig : FetcherConfig.PersQueueStreams) {
        DocStreamSignalSuffixes.emplace_back(ToSuffix(streamConfig.Name));
    }
    for (auto&& streamConfig : FetcherConfig.MapReduceStreams) {
        DocStreamSignalSuffixes.emplace_back(ToSuffix(streamConfig.Name));
    }
    for (auto&& streamConfig : FetcherConfig.DistributorStreams) {
        DocStreamSignalSuffixes.emplace_back(ToSuffix(streamConfig.Name));
    }

    DocSignalsToAggregate.emplace_back("-error-update", EAggregationType::Sum);
    DocSignalsToAggregate.emplace_back("-error-400", EAggregationType::Sum);
    DocSignalsToAggregate.emplace_back("-error-5xx", EAggregationType::Sum);
    DocSignalsToAggregate.emplace_back("-deprecated", EAggregationType::Sum);
    DocSignalsToAggregate.emplace_back("-onstop", EAggregationType::Sum);

    DocSignalsToAggregate.emplace_back("-incoming", EAggregationType::Sum);
    DocSignalsToAggregate.emplace_back("-expired", EAggregationType::Sum);
    DocSignalsToAggregate.emplace_back("-receive_lag", EAggregationType::Max);

    Init(TUnistat::Instance());
    RegisterGlobalMessageProcessor(this);
}

NFusion::TDocFetcherSignals::~TDocFetcherSignals() {
    UnregisterGlobalMessageProcessor(this);
}

void NFusion::TDocFetcherSignals::Init(TUnistat &t) const {
    for (auto&& modifier : SignalNameModifiers) {
        const bool service = !modifier.empty();
        t.DrillFloatHole(GetFullSignalName(modifier, "-error-update"), "dmmm", Prio("400", service, true), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole(GetFullSignalName(modifier, "-error-400"), "dmmm", Prio("400", service, true), NUnistat::TStartValue(0), EAggregationType::LastValue, true);
        t.DrillFloatHole(GetFullSignalName(modifier, "-error-5xx"), "dmmm", Prio("5xx", service, true), NUnistat::TStartValue(0), EAggregationType::LastValue, true);
        t.DrillFloatHole(GetFullSignalName(modifier, "-deprecated"), "dmmm", Prio("", service, true), NUnistat::TStartValue(0), EAggregationType::LastValue, true);
        t.DrillFloatHole(GetFullSignalName(modifier, "-onstop"), "dmmm", Prio("", service, true), NUnistat::TStartValue(0), EAggregationType::LastValue, true);

        t.DrillFloatHole(GetFullSignalName(modifier, "-incoming"), "dmmm", Prio("200", service), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole(GetFullSignalName(modifier, "-expired"), "dmmm", Prio("", service, true), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole(GetFullSignalName(modifier, "-receive_lag"), "avvv", Prio("", service), NUnistat::TStartValue(0), EAggregationType::LastValue);
    }
}

TString NFusion::TDocFetcherSignals::Name() const {
    return "df_signals";
}

bool NFusion::TDocFetcherSignals::Process(IMessage* message) {
    if (dynamic_cast<TMessageUpdateUnistatSignals*>(message)) {
        TUnistat& I = TUnistat::Instance();
        for (auto& signal : DocSignalsToAggregate) {
            for (auto&& modifier : SignalNameModifiers) {
                double result;
                bool hasData = false;
                for (auto& signalSuffix : DocStreamSignalSuffixes) {
                    auto value = GetFloatSignalValue(I, GetFullSignalName(modifier, signalSuffix, signal.Name));
                    if (!value) {
                        continue;
                    }
                    if (!hasData) {
                        result = *value;
                        hasData = true;
                        continue;
                    }
                    switch (signal.AggregationType) {
                        case EAggregationType::Sum: {
                            result += *value;
                            break;
                        }
                        case EAggregationType::Max: {
                            result = Max(result, *value);
                            break;
                        }
                        default: {
                             Y_ASSERT(false);
                        }
                    }
                }
                if (hasData) {
                    PushSignal(I, GetFullSignalName(modifier, signal.Name), result);
                }
            }
        }
        return true;
    }
    return false;
}

NFusion::TPersQueueStreamSignals::TPersQueueStreamSignals(const TDocStreamConfig& config)
    : TCommonDocStreamSignals(config)
{
    TUnistat& I = TUnistat::Instance();
    for (const auto& modifier : SignalNameModifiers) {
        const bool service = !modifier.empty();
        I.DrillFloatHole(GetFullSignalName(modifier, SignalSuffix, ConsumerNotLockedSignalName), "ammx", Prio("", service));
    }
}

void NFusion::TPersQueueStreamSignals::ConsumerLocked(bool isLocked) {
    const double signalValue = isLocked ? 0.0 : 1.0;

    TUnistat& I = TUnistat::Instance();
    for (const auto& modifier : SignalNameModifiers) {
        PushSignal(I, GetFullSignalName(modifier, SignalSuffix, ConsumerNotLockedSignalName), signalValue);
    }
}
