#include "signals.h"

#include <search/base/unistat_signals/signals.h>

#include <saas/library/behaviour/behaviour.h>
#include <saas/rtyserver/model/realm/realm.h_serialized.h>

#include <library/cpp/logger/global/global.h>
#include <util/string/vector.h>
#include <util/system/cpu_id.h>
#include <util/system/env.h>

namespace {
    const TVector<double> intervals = { 0, 1, 2, 3, 4, 5, 10, 15, 20, 25, 30,
                                        40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 225, 250, 300, 350, 400,
                                        500, 600, 700, 800, 900, 1000, 1500, 2000, 3000, 5000, 10000 };
    const TVector<double> extendedIntervals = { 0, 1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 40, 50,
                                        70, 90, 125, 175, 225, 300, 400, 600, 800,
                                        1000, 1500, 2000, 3000, 5000, 10000, 20000, 40000, 80000,
                                        120000, 200000, 400000, 600000, 800000, 1000000, 2000000};

    TString GetShardFromEnvLabel() {
        try {
            TString shardEnv = GetEnv("LABELS_shard");
            TVector<TString> shards = SplitString(shardEnv, "_");
            size_t len = shards.size();
            if (len < 2) {
                return "";
            }
            return shards[len - 2] + "_" + shards[len - 1];
        } catch (...) {
            return "";
        }
    }

    TString DoGetCpuModel() {
        ui32 value[12];
        TStringBuf brandB = CpuBrand(value);
        brandB = brandB.Before('@');
        TString brand = TString{brandB};
        const TString R = "(R)", TM = "(TM)";
        size_t pos;
        while ((pos = brand.find(R)) != TString::npos) {
            brand.replace(pos, R.size(), "");
        }
        while ((pos = brand.find(TM)) != TString::npos) {
            brand.replace(pos, TM.size(), "");
        }
        brand.to_lower();

        brand.Transform([](size_t, char s) {
            if (!(s >= 'a' && s <= 'z') && !(s >= '0' && s <= '9')) {
                return '_';
            } else {
                return s;
            }
        });
        size_t trim = brand.find_last_not_of("_");
        if (trim != TString::npos) {
            brand.resize(trim + 1);
        }
        return brand;
    }

    TString GetCpuModel() {
        try {
            return DoGetCpuModel();
        } catch (...) {
            Cerr << "Error on get cpu model: " << CurrentExceptionMessage() << Endl;
            return "";
        }
    }
}

void NRTYServer::TSearchSignals::Init(TUnistat& t) const {
    NSearch::TCommonSearchSignals::Init(t);

    // TODO: replace with generic signals from TCommonSearchSignals
    t.DrillHistogramHole("search-requests", "dhhh", Prio(), intervals);
    t.DrillHistogramHole("search-l1-requests", "dhhh", Prio(), intervals);
    t.DrillHistogramHole("search-l1-succeeded-requests", "dhhh", Prio(), intervals);
    t.DrillHistogramHole("search-l1-failed-requests", "dhhh", Prio(), intervals);
    t.DrillHistogramHole("factor-requests", "dhhh", Prio(), intervals);
    t.DrillHistogramHole("snippet-requests", "dhhh", Prio(), intervals);
    t.DrillHistogramHole("search-queue-requests", "dhhh", Prio(), intervals);
    t.DrillHistogramHole("factor-queue-requests", "dhhh", Prio(), intervals);
    t.DrillHistogramHole("snippet-queue-requests", "dhhh", Prio(), intervals);

    t.DrillFloatHole("shared_l1_int_docs_to_merge", "dmmm", Prio());
    t.DrillFloatHole("shared_l1_int_docs_merged", "dmmm", Prio());

    TBaseSearchGeneralSignals::InitExtraSignals(t, true);
    TBaseSearchGeneralSignals::InitFetchDocDataSignals(t);
}

void NRTYServer::TSearchSignals::DoUnistatRecordContext(const TUnistatRecordContext& ctx) {
    if (ctx.IsSuccess) {
        const auto queueMs = ctx.DurationMs - ctx.ProcessMs;
        TUnistat& a = TUnistat::Instance();
        switch (ctx.RequestType) {
        case RT_Factors:
            PushSignal(a, "factor-requests", ctx.DurationMs);
            PushSignal(a, "factor-queue-requests", queueMs);
            break;
        case RT_Fetch:
            PushSignal(a, "snippet-requests", ctx.DurationMs);
            PushSignal(a, "snippet-queue-requests", queueMs);
            break;
        default:
            PushSignal(a, "search-requests", ctx.DurationMs);
            PushSignal(a, "search-queue-requests", queueMs);
            break;
        }
    }
}

/////initiation/////

void TSaasRTYServerSignals::BuildSignals(TVector<TString> realmConfigNames, bool addTags) {
    RealmConfigNames = std::move(realmConfigNames);
    AddTags = addTags;
    TSaasRTYServerSignals signals;
    DEBUG_LOG << "Init signals..." << Endl;
    signals.Init(TUnistat::Instance());
    if (addTags) {
        TString shard = GetShardFromEnvLabel();
        if (shard)
            TUnistat::Instance().AddGlobalTag("shard", shard);
    }
    DEBUG_LOG << "Init signals... ok" << Endl;
}

TVector<TString> TSaasRTYServerSignals::RealmConfigNames = {};
THashMap<TString, ui32> TSaasRTYServerSignals::LastMergeFinished = {};
bool TSaasRTYServerSignals::SearchBanned = true;
bool TSaasRTYServerSignals::AddTags = false;

/////signals drilling/////

void TSaasRTYServerSignals::InitSearchSignalsGroup(TUnistat& t, bool isService, TString cache, const TVector<double>& theIntervals) const {
    TString SERV = isService ? "-SERV" : "";
    bool isSourced = (cache != "");
    cache = isSourced ? ("-" + cache) : cache;
    t.DrillFloatHole("backend-search-CTYPE" + cache + SERV + "-count-all", "dmmm", Prio("", isService, isSourced));
    t.DrillFloatHole("backend-search-CTYPE" + cache + SERV + "-count-zero", "dmmm", Prio("", isService, isSourced));
    t.DrillFloatHole("backend-search-CTYPE" + cache + SERV + "-count-nonzero", "dmmm", Prio("", isService, isSourced));
    t.DrillFloatHole("backend-search-CTYPE" + cache + SERV + "-result-fail", "dmmm", Prio("", isService, isSourced));
    t.DrillFloatHole("backend-search-CTYPE" + cache + SERV + "-result-ok", "dmmm", Prio("", isService, isSourced));
    t.DrillFloatHole("backend-search-CTYPE" + SERV + "-fast-cacheHit", "dmmm", Prio("", isService, isSourced));
    t.DrillFloatHole("backend-search-CTYPE" + cache + SERV + "-fail-on-zero", "dmmm", Prio("", isService, isSourced));
    t.DrillFloatHole("backend-search" + cache + SERV + "-count-all", "dmmm", Prio("", true, true));

    t.DrillHistogramHole("search-times-CTYPE" + cache + SERV + "-nonzero", "dhhh", Prio("", isService, isSourced), theIntervals);
    NUnistat::IHolePtr timeHole = t.DrillHistogramHole("search-times-CTYPE" + cache + SERV + "-full", "dhhh", Prio("", isService, isSourced), theIntervals);
    t.DrillHistogramHole("search-times-CTYPE" + cache + SERV + "-work", "dhhh", Prio("", isService, isSourced), theIntervals);
    t.DrillHistogramHole("search-times-CTYPE" + cache + SERV + "-queue", "dhhh", Prio("", isService, isSourced), theIntervals);
    t.DrillHistogramHole("search-times" + cache + SERV + "-full", "dhhh", Prio("", isService, true), theIntervals);
    if (AddTags) {
        timeHole->AddTag("cpu_model", GetCpuModel());
    }
}

void TSaasRTYServerSignals::Init(TUnistat& t) const {
    TSearchSignals::Init(t);
    TVector<NUnistat::IHolePtr> holesWithTags;

    t.DrillFloatHole("debug-errors", "dmmm", Prio(""));
    t.DrillFloatHole("debug-errors-CTYPE", "dmmm", Prio(""));

    t.DrillFloatHole("long-timeout-requests", "dmmm", Prio());

    InitSearchSignalsGroup(t, false, "", intervals);
    InitSearchSignalsGroup(t, false, "dfda", intervals);
    InitSearchSignalsGroup(t, false, "chno", intervals);
    InitSearchSignalsGroup(t, false, "cache", intervals);
    InitSearchSignalsGroup(t, true, "", intervals);
    InitSearchSignalsGroup(t, true, "dfda", intervals);
    InitSearchSignalsGroup(t, true, "chno", intervals);
    InitSearchSignalsGroup(t, true, "cache", intervals);

    t.DrillFloatHole("docfetcher-CTYPE-SERV-missed-deltas", "dmmm", Prio("", false, false));

    t.DrillFloatHole("backend-source-CTYPE-unanswers", "dmmm", Prio("", false, false), NUnistat::TStartValue(0), EAggregationType::Sum, true);
    holesWithTags.push_back(t.DrillFloatHole("backend-source-CTYPE-SERV-unanswers", "dmmm", Prio("", true, false)));
    t.DrillFloatHole("backend-search-CTYPE-incomplete", "dmmm", Prio("", false, false));
    holesWithTags.push_back(t.DrillFloatHole("backend-search-CTYPE-SERV-incomplete", "dmmm", Prio("", true, false)));
    t.DrillFloatHole("backend-search-CTYPE-error", "dmmm", Prio("", false, false));
    holesWithTags.push_back(t.DrillFloatHole("backend-search-CTYPE-SERV-error", "dmmm", Prio("", true, false)));
    t.DrillFloatHole("backend-search-CTYPE-unavailable", "dmmm", Prio("", false, false));
    holesWithTags.push_back(t.DrillFloatHole("backend-search-CTYPE-SERV-unavailable", "dmmm", Prio("", true, false)));
    t.DrillFloatHole("backend-search-CTYPE-timeouted", "dmmm", Prio("", false, false));
    holesWithTags.push_back(t.DrillFloatHole("backend-search-CTYPE-SERV-timeouted", "dmmm", Prio("", true, false)));

    t.DrillFloatHole("backend-search-CTYPE-SERV-doc-dssm-mismatched", "dmmm", Prio("", false, false));
    t.DrillFloatHole("backend-search-CTYPE-SERV-doc-dssm-total", "dmmm", Prio("", false, false), NUnistat::TStartValue(0), EAggregationType::Sum, true);

    t.DrillFloatHole("backend-search-CTYPE-SERV-user-factor-calc-missing", "dmmm", Prio("", false, false));
    t.DrillFloatHole("backend-search-CTYPE-SERV-user-factor-calc-error", "dmmm", Prio("", false, false));

    for (auto code : GetDispCodes()) {
        t.DrillFloatHole("backend-index-CTYPE-" + ToString(code), "dmmm", Prio("", false, false));
        t.DrillFloatHole("backend-index-CTYPE-SERV-" + ToString(code), "dmmm", Prio("", true, false));
        t.DrillFloatHole("backend-index-" + ToString(code), "dmmm", Prio("", true, true));
        TString xxCode = ToString(code).substr(0, 1) + "xx";
        t.DrillFloatHole("backend-index-CTYPE-" + xxCode, "dmmm", Prio("", false, false));
        t.DrillFloatHole("backend-index-CTYPE-SERV-" + xxCode, "dmmm", Prio("", true, false));
        t.DrillFloatHole("backend-index-" + xxCode, "dmmm", Prio("", true, true));
    }
    t.DrillHistogramHole("index-times-CTYPE", "dhhh", Prio("", false, false), intervals);
    holesWithTags.push_back(t.DrillHistogramHole("index-times-CTYPE-SERV", "dhhh", Prio("", false, false), intervals));
    t.DrillHistogramHole("index-times-CTYPE-ok", "dhhh", Prio("", false, false), intervals);
    t.DrillHistogramHole("index-times-CTYPE-SERV-ok", "dhhh", Prio("", false, false), intervals);

    t.DrillFloatHole("surplus-doc-count", "dvvm", Prio());
    t.DrillFloatHole("surplus-lifetime-stolen", "axxx", Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
    t.DrillFloatHole("deadline-doc-count", "dmmm", Prio());

    for (const TType& type : {TType{"-max", "axxx"}, {"-avg", "avvv"}}) {
        t.DrillFloatHole("index-age-CTYPE-SERV" + type.Name, type.SigOpt, Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
        holesWithTags.push_back(t.DrillFloatHole("index-searchable-age-CTYPE-SERV" + type.Name, type.SigOpt, Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue));
    }

    for (const TString& prefix : { "", "-SHARD" }) {
        t.DrillFloatHole("search-disabled" + prefix, "ammx", Prio(), NUnistat::TStartValue(1), EAggregationType::LastValue);
        t.DrillFloatHole("average-doc-size" + prefix, "avvv", Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole("old-doc-count" + prefix, "avvv", Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole("index-state" + prefix, "avvv", Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole("index-timestamp" + prefix, "avvv", Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole("index-disk-docs" + prefix, "avvv", Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
        t.DrillFloatHole("index-searchable-docs" + prefix, "avvv", Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);

        for (const TType& type : {TType{"-max", "axxx"}, {"-avg", "avvv"}}) {
            t.DrillFloatHole("index-age" + prefix + type.Name, type.SigOpt, Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
            t.DrillFloatHole("index-searchable-age" + prefix + type.Name, type.SigOpt, Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
        }
    }
    t.DrillHistogramHole("search-report-doc-count-CTYPE-SERV", "dhhh", Prio("", false, false), extendedIntervals);
    t.DrillHistogramHole("search-total-doc-count-CTYPE-SERV", "dhhh", Prio("", false, false), extendedIntervals);
    t.DrillHistogramHole("search-report-kilobyte-size-CTYPE-SERV", "dhhh", Prio("", false, false), extendedIntervals);

    if (RealmConfigNames) {
        for (TString realmConfigName : RealmConfigNames) {
            for (const TType& type : {TType{"-max", "axxx"}, {"-avg", "avvv"}}) {
                for (const TString& prefix : {"", "-SHARD"}) {
                    t.DrillFloatHole("index-age-" + ConvertRealmToString(realmConfigName) + prefix + type.Name, type.SigOpt, Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
                    t.DrillFloatHole("index-searchable-age-" + ConvertRealmToString(realmConfigName) + prefix + type.Name, type.SigOpt, Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
                }
                for (const TString& srv : {"", "-SERV"}) {
                    holesWithTags.push_back(t.DrillFloatHole("merge-times-" + ConvertRealmToString(realmConfigName) + "-CTYPE" + srv + type.Name, type.SigOpt, Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue));
                    t.DrillFloatHole("merge-sleeping-times-" + ConvertRealmToString(realmConfigName) + "-CTYPE" + srv + type.Name, type.SigOpt, Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
                }
            }
            for (const TString& srv : {"", "-SERV"}) {
                t.DrillFloatHole("merge-cannot-" + ConvertRealmToString(realmConfigName) + "-CTYPE" + srv, "axxx", Prio(), NUnistat::TStartValue(0), EAggregationType::LastValue);
            }
        }
    }
    t.DrillFloatHole("index-jupi-no-bert-embedding", "dmmm", Prio("", false, false));
    if (AddTags) {
        const TString cpuModel = GetCpuModel();
        for (auto hole : holesWithTags) {
            hole->AddTag("cpu_model", cpuModel);
        }
    }
}

/////pushing/////
void TSaasRTYServerSignals::PushCountSignals(TUnistat& inst, const TString& countSign, const TString& cacheSign) {
    PushSignal(inst, "backend-search-CTYPE-count-" + countSign, 1);
    PushSignal(inst, "backend-search-CTYPE-SERV-count-" + countSign, 1);
    PushSignal(inst, "backend-search-CTYPE" + cacheSign + "-count-" + countSign, 1);
    PushSignal(inst, "backend-search-CTYPE" + cacheSign + "-SERV-count-" + countSign, 1);
}

void TSaasRTYServerSignals::DoUnistatRequestData(const TCgiParameters& cgi) {
    ui32 timeout = 0;
    TryFromString(cgi.Get("timeout"), timeout);
    if (TDuration::MicroSeconds(timeout) > TDuration::MilliSeconds(500)) {
        PushGlobalSignal({ "long-timeout-requests" }, 1);
    }
}

bool TSaasRTYServerSignals::DoUnistatRecordSearch(const NRTYServer::TUnistatRecordContext& ctx) {
    NRTYServer::TSearchSignals::DoUnistatRecordContext(ctx);
    TUnistat& inst = TUnistat::Instance();

    TString succ = ctx.IsSuccess ? "ok" : "fail";
    PushSignal(inst, "backend-search-CTYPE-result-" + succ, 1);
    PushSignal(inst, "backend-search-CTYPE-SERV-result-" + succ, 1);

    TString cache = "ERROR";
    if (ctx.RequestType == RT_Fetch || ctx.RequestType == RT_Factors) {
        cache = "-dfda";
    } else {
        cache = (ctx.CacheHit < 0.5) ? "-chno" : "-cache";
    }
    PushSignal(inst, "backend-search-CTYPE" + cache + "-result-" + succ, 1);
    PushSignal(inst, "backend-search-CTYPE" + cache + "-SERV-result-" + succ, 1);

    {
        PushSignal(inst, "backend-search-count-all", 1);
        PushSignal(inst, "backend-search-SERV-count-all", 1);
        PushCountSignals(inst, "all", cache);

        TString countSign = (ctx.TotalDocsCount > 0) ? "nonzero" : "zero";
        PushCountSignals(inst, countSign, cache);
        if (ctx.TotalDocsCount > 0) {
            PushSignal(inst, "search-times-CTYPE-nonzero", ctx.DurationMs);
            PushSignal(inst, "search-times-CTYPE-SERV-nonzero", ctx.DurationMs);
        }
    }

    if (ctx.FastCacheHit) {
        PushSignal(inst, "backend-search-CTYPE-fast-cacheHit", 1);
        PushSignal(inst, "backend-search-CTYPE-SERV-fast-cacheHit", 1);
    }

    if (ctx.FailOnZero) {
        PushSignal(inst, "backend-search-CTYPE-fail-on-zero", 1);
        PushSignal(inst, "backend-search-CTYPE-SERV-fail-on-zero", 1);
        PushSignal(inst, "backend-search-CTYPE" + cache + "-fail-on-zero", 1);
        PushSignal(inst, "backend-search-CTYPE" + cache + "-SERV-fail-on-zero", 1);
    }

    if (!ctx.AnswerIsComplete) {
        PushSignal(inst, "backend-search-CTYPE-incomplete", 1);
        PushSignal(inst, "backend-search-CTYPE-SERV-incomplete", 1);
    }

    if (ctx.CountUnanswered > 0) {
        PushSignal(inst, "backend-source-CTYPE-unanswers", 1);
        PushSignal(inst, "backend-source-CTYPE-SERV-unanswers", 1);
    }

    PushSignal(inst, "search-times-full", ctx.DurationMs);
    PushSignal(inst, "search-times-CTYPE-full", ctx.DurationMs);
    PushSignal(inst, "search-times-CTYPE-SERV-full", ctx.DurationMs);
    PushSignal(inst, "search-times-CTYPE" + cache + "-full", ctx.DurationMs);
    PushSignal(inst, "search-times-CTYPE" + cache + "-SERV-full", ctx.DurationMs);

    PushSignal(inst, "search-times-CTYPE-work", ctx.ProcessMs);
    PushSignal(inst, "search-times-CTYPE-SERV-work", ctx.ProcessMs);

    PushSignal(inst, "search-times-CTYPE-queue", ctx.DurationMs - ctx.ProcessMs);
    PushSignal(inst, "search-times-CTYPE-SERV-queue", ctx.DurationMs - ctx.ProcessMs);

    PushSignal(inst, "search-report-doc-count-CTYPE-SERV", ctx.ReportDocsCount);
    PushSignal(inst, "search-total-doc-count-CTYPE-SERV", ctx.TotalDocsCount);
    PushSignal(inst, "search-report-kilobyte-size-CTYPE-SERV", ctx.ReportByteSize / 1024);
    return true;
}

bool TSaasRTYServerSignals::DoUnistatRecordIndex(ui16 code, ui64 duration) {
    TUnistat& inst = TUnistat::Instance();
    const TString codeStr(ToString(code));
    const TString xxCode = codeStr.substr(0, 1) + "xx";
    PushSignalWithCode(inst, "backend-index-", codeStr, xxCode);
    PushSignalWithCode(inst, "backend-index-CTYPE-", codeStr, xxCode);
    PushSignalWithCode(inst, "backend-index-CTYPE-SERV-", codeStr, xxCode);

    if (xxCode != "5xx") {
        PushSignal(inst, "index-times-CTYPE", duration);
        PushSignal(inst, "index-times-CTYPE-SERV", duration);
    }
    if (code == 200) {
        PushSignal(inst, "index-times-CTYPE-ok", duration);
        PushSignal(inst, "index-times-CTYPE-SERV-ok", duration);
    }
    return true;
}

void TSaasRTYServerSignals::DoUnistatRecordDocDssm(bool anyDssmMismatch) {
    PushGlobalSignal("backend-search-CTYPE-SERV-doc-dssm-total", 1);
    if (anyDssmMismatch) {
        PushGlobalSignal("backend-search-CTYPE-SERV-doc-dssm-mismatched", 1);
    }
}

void TSaasRTYServerSignals::DoUnistatUserFactorCalcDefault() {
    PushGlobalSignal("backend-search-CTYPE-SERV-user-factor-calc-missing", 1);
}

void TSaasRTYServerSignals::DoUnistatUserFactorCalcError() {
    PushGlobalSignal("backend-search-CTYPE-SERV-user-factor-calc-error", 1);
}

void TSaasRTYServerSignals::DoUnistatRecordMissedDeltas() {
    PushGlobalSignal("docfetcher-CTYPE-SERV-missed-deltas", 1);
}

void TSaasRTYServerSignals::DoUnistatErrorSearch() {
    PushGlobalSignal({ "backend-search-CTYPE-error", "backend-search-CTYPE-SERV-error" }, 1);
}

void TSaasRTYServerSignals::DoUnistatUnavailable() {
    PushGlobalSignal({ "backend-search-CTYPE-unavailable", "backend-search-CTYPE-SERV-unavailable" }, 1);
}

void TSaasRTYServerSignals::DoUnistatTimeouted() {
    PushGlobalSignal({ "backend-search-CTYPE-timeouted", "backend-search-CTYPE-SERV-timeouted" }, 1);
}

void TSaasRTYServerSignals::UpdateSearchDisabledStatus(bool disabled) {
    SearchBanned = disabled;
    PushGlobalSignal({ "search-disabled", "search-disabled-SHARD" }, disabled ? 1 : 0);
}

void TSaasRTYServerSignals::UpdateIndexState(ui32 value) {
    if (value) {
        PushGlobalSignal({ "index-state", "index-state-SHARD" }, value);
    }
}

void TSaasRTYServerSignals::UpdateIndexTimestamp(const TString &realmName, ui32 timestamp) {
    if (!timestamp) {
        return;
    }
    const i64 now = Seconds();
    if (!realmName) {
        PushGlobalSignal({
            "index-searchable-age-CTYPE-SERV-avg",
            "index-searchable-age-CTYPE-SERV-max",
            "index-searchable-age-avg",
            "index-searchable-age-max",
            "index-searchable-age-SHARD-avg",
            "index-searchable-age-SHARD-max"
        }, (SearchBanned ? 0 : now - timestamp));
        PushGlobalSignal({"index-searchable-age-CTYPE-SERV-avg", "index-searchable-age-CTYPE-SERV-max"}, (SearchBanned ? 0 : now - timestamp));
    } else {
        PushGlobalSignal({
            "index-searchable-age-" + ConvertRealmToString(realmName) + "-avg",
            "index-searchable-age-" + ConvertRealmToString(realmName) + "-max",
            "index-searchable-age-" + ConvertRealmToString(realmName) + "-SHARD-avg",
            "index-searchable-age-" + ConvertRealmToString(realmName) + "-SHARD-max"
        }, (SearchBanned ? 0 : now - timestamp));
    }
    if (!realmName) {
        PushGlobalSignal({
            "index-age-CTYPE-SERV-avg",
            "index-age-CTYPE-SERV-max",
            "index-age-avg",
            "index-age-max",
            "index-age-SHARD-avg",
            "index-age-SHARD-max"
        }, now - timestamp);
        PushGlobalSignal({ "index-timestamp", "index-timestamp-SHARD" }, timestamp);
    } else {
        PushGlobalSignal({
            "index-age-" + ConvertRealmToString(realmName) + "-avg",
            "index-age-" + ConvertRealmToString(realmName) + "-max",
            "index-age-" + ConvertRealmToString(realmName) + "-SHARD-avg",
            "index-age-" + ConvertRealmToString(realmName) + "-SHARD-max"
        }, now - timestamp);
    }
}

void TSaasRTYServerSignals::UpdateAverageDocSize(ui32 value) {
    PushGlobalSignal({ "average-doc-size", "average-doc-size-SHARD" }, value);
}

void TSaasRTYServerSignals::UpdateOldDocCount(ui32 value) {
    PushGlobalSignal({ "old-doc-count", "old-doc-count-SHARD" }, value);
}

void TSaasRTYServerSignals::UpdateSurplusDocCount(ui32 value, TInstant maxDeadline) {
    PushSignal(TUnistat::Instance(), "surplus-doc-count", value);
    PushSignal(TUnistat::Instance(), "surplus-lifetime-stolen", (maxDeadline - TInstant::Now()).Seconds());
}

void TSaasRTYServerSignals::UpdateIndexDiskDocuments(ui32 value) {
    PushGlobalSignal({ "index-disk-docs", "index-disk-docs-SHARD" }, value);
}

void TSaasRTYServerSignals::UpdateIndexSearchableDocuments(ui32 value) {
    PushGlobalSignal({ "index-searchable-docs", "index-searchable-docs-SHARD" }, value);
}

TString TSaasRTYServerSignals::ConvertRealmToString(const TString& realmConfigName) {
    TString ret = realmConfigName;
    ret.to_lower();
    return ret;
}

void TSaasRTYServerSignals::OnMergerTaskFinish(const TString& realmConfigName, TDuration duration) {
    LastMergeFinished[realmConfigName] = Seconds();
    const auto value = duration.Seconds();
    const auto& realmStr = ConvertRealmToString(realmConfigName);
    PushGlobalSignal({
        "merge-times-" + realmStr + "-CTYPE-max",
        "merge-times-" + realmStr + "-CTYPE-avg",
        "merge-times-" + realmStr + "-CTYPE-SERV-max",
        "merge-times-" + realmStr + "-CTYPE-SERV-avg",
    }, value);
}

void TSaasRTYServerSignals::OnMergerTaskStart(const TString& realmConfigName) {
    if (!LastMergeFinished.contains(realmConfigName)) {
        return;
    }
    const auto value = Seconds() - LastMergeFinished[realmConfigName];
    const auto& realmStr = ConvertRealmToString(realmConfigName);
    PushGlobalSignal({
        "merge-sleeping-times-" + realmStr + "-CTYPE-max",
        "merge-sleeping-times-" + realmStr + "-CTYPE-avg",
        "merge-sleeping-times-" + realmStr + "-CTYPE-SERV-max",
        "merge-sleeping-times-" + realmStr + "-CTYPE-SERV-avg",
    }, value);
}

void TSaasRTYServerSignals::OnMergerTaskFailed(const TString& realmConfigName) {
    LastMergeFinished[realmConfigName] = Seconds();
}

void TSaasRTYServerSignals::OnCannotMerge(const TString& realmConfigName, int extraSegments) {
    const auto& realmStr = ConvertRealmToString(realmConfigName);
    PushGlobalSignal({
        "merge-cannot-" + realmStr + "-CTYPE",
        "merge-cannot-" + realmStr + "-CTYPE-SERV"
    }, extraSegments);
}

void TSaasRTYServerSignals::DoUnistatNoBertEmbedding() {
    PushGlobalSignal("index-jupi-no-bert-embedding", 1);
}
