#include "plainer_richers.h"
#include "statistics.h"
#include <library/cpp/logger/global/global.h>
#include <util/generic/algorithm.h>
#include <math.h>

namespace NPrivate {

    inline bool CheckIsSearching(const NJson::TJsonValue& inserted) {
        if (inserted["result"]["controller_status"].GetString() != "Active")
            return false;
        if (inserted.Has("disable_search") && inserted["disable_search"].GetBoolean())
            return false;
        return true;
    }

    void TRicherReplicId::Rich(NJson::TJsonValue& inserted) {
        TString DC = inserted.Has("$datacenter_alias$") ? inserted["$datacenter_alias$"].GetStringRobust() : "";
        TString interval = inserted.Has("interval") ? inserted["interval"].GetStringRobust() : "";
        TString curService = inserted.Has("service") ? inserted["service"].GetStringRobust() : "";
        auto& service = Services[curService];
        auto& replicsInDC = service.ReplicasByDc[DC];
        ui32 replicId = 0;
        for (auto& replic : replicsInDC) {
            auto ins = replic.second.insert(interval);
            if (ins.second) {
                replicId = replic.first;
                break;
            }
        }
        if (!replicId) {
            replicsInDC.push_back(std::make_pair(service.CurrentReplicId++, THashSet<TString>()));
            replicsInDC.back().second.insert(interval);
            replicId = replicsInDC.back().first;
        }
        inserted.InsertValue("replic_id", replicId);
    }


    TRicherControllerStatus::TRicherControllerStatus(bool reportFlagged)
        : ReportFlagged(reportFlagged)
    {
    }

    void TRicherControllerStatus::Rich(NJson::TJsonValue& inserted) {
        if (!inserted.GetValueByPath("result.controller_status", '.'))
            inserted["result"].InsertValue("controller_status", "Down");
        else if (ReportFlagged && ((inserted.Has("disable_search") && inserted["disable_search"].GetStringRobust() == "true")
            || (inserted.Has("disable_indexing") && inserted["disable_indexing"].GetStringRobust() == "true")
            || (inserted["result"].Has("search_enabled") && inserted["result"]["search_enabled"].GetStringRobust() == "false")))
        {
            inserted["result"]["controller_status"] = "Flagged";
        }
    }


    void TRicherStatistics::Rich(NJson::TJsonValue& inserted) {
        TString service = inserted.Has("service") ? inserted["service"].GetStringRobust() : "";
        if (service == Service) {
            for (auto& stat : Statistics)
                stat->UpdateValue(inserted);
        }
    }

    TRicherStatistics::TRicherStatistics(TVector<TStatisticsField::TPtr>& statistics, const TString& service)
        : Statistics(statistics)
        , Service(service)
    {}


    void TRicherCheckConfigs::Rich(NJson::TJsonValue& inserted) {
        TString service = inserted.Has("service") ? inserted["service"].GetStringRobust() : "";
        TString slot = inserted.Has("slot") ? inserted["slot"].GetStringRobust() : "";
        NJson::TJsonValue& invalidFiles = inserted.InsertValue("invalid_config_files", NJson::JSON_NULL);
        if (CheckConfigResult.Has(service) && CheckConfigResult[service].Has(slot)) {
            for (const auto& file : CheckConfigResult[service][slot].GetMap()) {
                const TString& fromHost = file.second["from_host"].GetString();
                const TString& ld = file.second["last_deployed"].GetString();
                if (ld != fromHost) {
                    NJson::TJsonValue& val = invalidFiles.AppendValue(NJson::JSON_MAP);
                    val.InsertValue("id", file.first);
                    if (!fromHost)
                        val.InsertValue("error", "absent");
                    else if (!ld)
                        val.InsertValue("error", "extra");
                    else
                        val.InsertValue("error", "diff");
                }
            }
        }
    }

    TRicherCheckConfigs::TRicherCheckConfigs(const NJson::TJsonValue& checkConfigResult)
        : CheckConfigResult(checkConfigResult)
    {}

    void TRicherReplicsConsistanse::Rich(NJson::TJsonValue& inserted) {
        TString interval;
        if (!CheckBe(inserted, interval))
            return;

        TInteravalData& intData = DocCountIntervals[interval];
        InitializeIntervalData(intData, 0.9);
        ui64 docsCount = inserted["result"]["docs_in_final_indexes"].GetUInteger();
        docsCount += inserted["result"]["docs_in_memory_indexes"].GetUInteger();
        inserted["docs_count"] = docsCount;
        if (docsCount < intData.MinNormalValue) {
            BadBackend(inserted, "shortage_docs", intData);
        } else if (docsCount > intData.MaxNormalValue) {
            BadBackend(inserted, "overage_docs", intData);
        }

        TInteravalData& intSizeData = IndexSizeIntervals[interval];
        InitializeIntervalData(intSizeData, 0.66);
        ui64 indSize = inserted["result"]["files_size"]["__SUM"].GetUInteger();
        if (indSize < intSizeData.MinNormalValue) {
            BadBackend(inserted, "shortage_size", intSizeData, "size_");
        } else if (indSize > intSizeData.MaxNormalValue) {
            BadBackend(inserted, "overage_size", intSizeData, "size_");
        }
    }

    void TRicherReplicsConsistanse::UpdateFilter(TSet<TString>& filter) {
        filter.insert("result.docs_in_final_indexes");
        filter.insert("result.docs_in_memory_indexes");
        filter.insert("result.files_size.__SUM");
        filter.insert("result.controller_status");
        filter.insert("service");
        filter.insert("interval");
        filter.insert("disable_search");
        filter.insert("replics_consistance");
        filter.insert("inconsistent_replics_ratio");
        filter.insert("size_replics_consistance");
        filter.insert("size_inconsistent_replics_ratio");
    }

namespace {
    struct TCluster {
        double Center = 0;
        ui32 Size = 0;
        ui64 Min = -1;
        ui64 Max = 0;
    };
}

    void TRicherReplicsConsistanse::InitializeIntervalData(TInteravalData& intervalData, const double minGoodRatio) {
        if (intervalData.Initialized || intervalData.InputValues.empty())
            return;
        Sort(intervalData.InputValues.begin(), intervalData.InputValues.end());
        if (intervalData.InputValues.front() >= minGoodRatio * intervalData.InputValues.back()) {
            intervalData.MinNormalValue = intervalData.InputValues.front();
            intervalData.MaxNormalValue = intervalData.InputValues.back();
            return;
        }

        //k-means;
        constexpr ui32 CL_COUNT = 3;
        const size_t count = intervalData.InputValues.size();
        TVector<TCluster> clusters(CL_COUNT);
        for (ui32 c = 0; c < CL_COUNT; ++c)
            clusters[c].Center = intervalData.InputValues.front() + c * (intervalData.InputValues.back() - intervalData.InputValues.front()) / (CL_COUNT - 1);
        TVector<ui32> beCenter(count, 0);
        clusters[0].Size = count;
        while (true) {
            bool changed = false;
            for (ui32 i = 0; i < count; ++i) {
                for (ui32 c = 0; c < CL_COUNT; ++c) {
                    if (fabs(intervalData.InputValues[i] - clusters[c].Center) < fabs(intervalData.InputValues[i] - clusters[beCenter[i]].Center)) {
                        beCenter[i] = c;
                        changed = true;
                    }
                }
            }
            if (!changed)
                break;
            clusters = TVector<TCluster>(CL_COUNT);
            for (ui32 i = 0; i < count; ++i) {
                ui32 c = beCenter[i];
                ui64 docCount = intervalData.InputValues[i];
                TCluster& cluster = clusters[c];
                cluster.Center += docCount;
                cluster.Min = Min(cluster.Min, docCount);
                cluster.Max = Max(cluster.Max, docCount);
                ++cluster.Size;
            }
            for (ui32 c = 0; c < CL_COUNT; ++c) {
                TCluster& cluster = clusters[c];
                if (cluster.Size)
                    cluster.Center /= cluster.Size;
            }
        };
        for (int c = CL_COUNT - 1; c >= 0; --c)
            if (!clusters[c].Size)
                clusters.erase(clusters.begin() + c);
        switch (clusters.size()) {
        case 1:
            intervalData.MinNormalValue = clusters.front().Min;
            intervalData.MaxNormalValue = clusters.back().Max;
            return;
        case 2:
            if (clusters[0].Size <= clusters[1].Size) {
                intervalData.MinNormalValue = clusters.back().Min;
                intervalData.MaxNormalValue = clusters.back().Max;
            } else {
                intervalData.MinNormalValue = clusters.front().Min;
                intervalData.MaxNormalValue = clusters.front().Max;
            }
            return;
        case 3:
            if (clusters[0].Size + clusters[1].Size <= clusters[2].Size) {
                intervalData.MinNormalValue = clusters[2].Min;
                intervalData.MaxNormalValue = clusters[2].Max;
            } else if (clusters[2].Size + clusters[1].Size <= clusters[0].Size) {
                intervalData.MinNormalValue = clusters[0].Min;
                intervalData.MaxNormalValue = clusters[0].Max;
            } else {
                intervalData.MinNormalValue = clusters[1].Min;
                intervalData.MaxNormalValue = clusters[1].Max;
            }
            return;
        default:
            FAIL_LOG("invalid clusters size");
        }
    }

    bool TRicherReplicsConsistanse::CheckBe(const NJson::TJsonValue& inserted, TString& interval) {
        if (!CheckIsSearching(inserted))
            return false;
        interval = TString::Join(inserted["service"].GetString(), '@', inserted["interval"].GetString());
        return true;
    }

    void TRicherReplicsConsistanse::BadBackend(NJson::TJsonValue& inserted, const char* status, const TInteravalData& intData, const TString& prefix) {
        inserted[prefix + "replics_consistance"] = status;
        inserted[prefix + "inconsistent_replics_ratio"] = intData.InputValues.empty() ? 0.0 : (1.0 / intData.InputValues.size());
    }

    void TRicherReplicsConsistanse::InitializeByBroadcastResult(const NJson::TJsonValue& brcstResult) {
        for (const auto& service: brcstResult.GetMap()) {
            for (const auto& slot : service.second.GetMap()) {
                TString interval;
                if (!CheckBe(slot.second, interval))
                    continue;
                ui64 docsCount = slot.second["result"]["docs_in_final_indexes"].GetUInteger();
                docsCount += slot.second["result"]["docs_in_memory_indexes"].GetUInteger();
                DocCountIntervals[interval].InputValues.push_back(docsCount);

                ui64 indexSize = slot.second["result"]["files_size"]["__SUM"].GetUInteger();
                IndexSizeIntervals[interval].InputValues.push_back(indexSize);
            }
        }
    }

    TRicherFailDomainTagger::TRicherFailDomainTagger(const THashSet<TString>& perDCSearchServicesNames, const TString& tagPropName)
        : PerDCSearchServicesNames(perDCSearchServicesNames)
        , TagPropName(tagPropName)
    {
    }

    void TRicherFailDomainTagger::UpdateFilter(TSet<TString>& filter) const {
        filter.insert("$datacenter$");
        filter.insert("service");
        filter.insert("result.search_enabled");
    }

    void TRicherFailDomainTagger::Rich(NJson::TJsonValue& inserted) {
        bool isEnabled = CheckIsSearching(inserted);
        if (isEnabled) {
            auto& resultMap = inserted["result"];
            isEnabled = !resultMap.Has("search_enabled") || resultMap["search_enabled"].GetBooleanRobust();
        }

        TString dcName;
        if (PerDCSearchServicesNames.contains(inserted["service"].GetString())) {
            dcName = inserted["$datacenter$"].GetString();
        } else {
            dcName = "ALL";
        }

        if (isEnabled) {
            inserted[TagPropName] = dcName;
        }
        NJson::TJsonValue jsonValue;
        jsonValue[dcName] = isEnabled ? 1 : 0;
        inserted[TagPropName + "_json"] = JsonToString(jsonValue);
    }

}
