#include "dashboard_script.h"
#include "projects_utils.h"

#include <saas/deploy_manager/modules/resources/resources.h>
#include <saas/deploy_manager/scripts/broadcast/broadcast_action.h>
#include <saas/deploy_manager/scripts/check_configs/action.h>
#include <saas/deploy_manager/scripts/cluster/cluster_task.h>
#include <saas/deploy_manager/scripts/common/deploy/deploy_builder.h>
#include <saas/deploy_manager/scripts/interface/dashboard_util/grouping.h>
#include <saas/deploy_manager/scripts/interface/dashboard_util/plainer_inserter.h>
#include <saas/deploy_manager/scripts/interface/dashboard_util/plainer_richers.h>
#include <saas/deploy_manager/scripts/interface/dashboard_util/statistics.h>
#include <saas/util/logging/number_format.h>
#include <library/cpp/charset/wide.h>
#include <util/string/split.h>

namespace NRTYDeploy {
    TScriptDashboard::TFactory::TRegistrator<TScriptDashboard> TScriptDashboard::Registrator("dashboard");

    void InsertStatValue(NJson::TJsonValue& stats, TString value, TString label) {
        NJson::TJsonValue& stat = stats.AppendValue(NJson::JSON_MAP);
        if (!IsUtf(value))
            value = WideToUTF8(CharToWide(value, csYandex));
        if (!IsUtf(label))
            label = WideToUTF8(CharToWide(label, csYandex));
        stat.InsertValue("value", value);
        stat.InsertValue("label", label);
    }

    class TActionExecutor : public IObjectInQueue {
    public:
        TActionExecutor(const ICommonData& data, NDaemonController::TAction& action)
            : Data(data)
            , Action(action)
        {}

        void Process(void* /*ThreadSpecificResource*/) override {
            THolder<TActionExecutor> suicide(this);
            try {
                NDaemonController::TControllerAgent agent(Data.GetDeployManagerBalanserHost(), Data.GetDeployManagerBalanserPort(), nullptr, Data.GetDeployManagerBalanserUriPrefix());
                agent.ExecuteAction(Action);
            } catch (...) {
                ERROR_LOG << CurrentExceptionMessage() << Endl;
            }
        }
    private:
        const ICommonData& Data;
        NDaemonController::TAction& Action;
    };

    class TNeigborsFilter : public NSearchMapParser::TSearchMapModifier {
        class TCallback : public ISearchMapScannerCallback {
        public:
            TCallback(TSet<TString>& acceptedHosts, const TString& service)
                : AcceptedHosts(acceptedHosts)
                , Service(service)
            {}

            void OnHost(const NSearchMapParser::TSearchMapHost& host, const NSearchMapParser::TSearchMapReplica& /*replica*/,
                const NSearchMapParser::TSearchMapService& service) override
            {
                if (service.Name == Service)
                    AcceptedHosts.insert(host.Name);
            }
            TSet<TString>& AcceptedHosts;
            const TString& Service;
        };
    public:
        TNeigborsFilter(const TString& service)
            : Service(service)
        {}

        void CollectHosts(const NSearchMapParser::TSearchMap& sm) {
            TCallback cb(AcceptedHosts, Service);
            sm.Scan(cb);
        }

        void Modify(NSearchMapParser::TSearchMap& sm) {
            CollectHosts(sm);
            sm = Process(sm);
        }

        bool ModifyHost(NSearchMapParser::TSearchMapHost& host, const NSearchMapParser::TSearchMapReplica& /*originalReplica*/,
            const NSearchMapParser::TSearchMapService& /*originalService*/) override
        {
            return AcceptedHosts.contains(host.Name);
        }

    private:
        TSet<TString> AcceptedHosts;
        TString Service;
    };

    TCTypeMaps DoBuildSearchMap(const IDeployInfoRequest& request, const NRTYDeploy::TClusterTask::TCgiContext& cgi,
        const THashSet<TString>& slotsFilters)
    {
        TMaybe<TNeigborsFilter> neighborsFilterMaybe = Nothing();
        NRTYDeployInfo::IDeployComponentInfo::TPtr info = NRTYDeployInfo::IDeployComponentInfo::TFactory::Construct(cgi.ServiceType);
        info->SetInfo(&request, cgi.CType);
        NSearchMapParser::TSearchMap sm(info->SearchMap((slotsFilters.contains("neighbors") || slotsFilters.contains("used")) ? "*" : cgi.Service));

        if (slotsFilters.contains("neighbors") && !slotsFilters.contains("used")) {
            neighborsFilterMaybe.ConstructInPlace(cgi.Service);
            neighborsFilterMaybe.GetRef().Modify(sm);
        }

        if (slotsFilters.contains("unused")) {
            NSearchMapParser::TSearchMap unusedSm(info->SearchMap("unused_" + cgi.Service));
            sm.AddService("unused") = *unusedSm.GetService("unused");
        }

        TCTypeMaps res;
        if (!slotsFilters.contains("neighbors") || !slotsFilters.contains("all_neighbors") || neighborsFilterMaybe.Empty()) {
            res.reserve(1);
            res.emplace_back(TString(cgi.CType), std::move(sm));
            return res;
        }

        const TVector<TString>& allCtypes = request.GetResourcesManager().GetCTypes();
        TSet<TString> allServiceTypes;
        NRTYDeployInfo::IDeployComponentInfo::TFactory::GetRegisteredKeys(allServiceTypes);
        for (const TString& ctype: allCtypes) {
            NSearchMapParser::TSearchMap ctypeSm;
            for (const TString& serviceType: allServiceTypes) {
                NRTYDeployInfo::IDeployComponentInfo::TPtr info = NRTYDeployInfo::IDeployComponentInfo::TFactory::Construct(serviceType);
                info->SetInfo(&request, ctype);
                NSearchMapParser::TSearchMap stypeSm(info->SearchMap());
                stypeSm = neighborsFilterMaybe.GetRef().Process(stypeSm);
                if (stypeSm.Size() == 0) {
                    continue;
                }
                for (const NSearchMapParser::TSearchMapService& service: stypeSm.GetInternalSearchMap()) {
                    //ctypeSm.HasService() works after Compile() invocation only
                    if (ctypeSm.GetService(service.Name) == nullptr) {
                        ctypeSm.AddService(service.Name) = service;
                    }
                }
            }
            res.emplace_back(TString(ctype), std::move(ctypeSm));
        }
        return res;
    }

    std::tuple<TString, bool, TDuration> SerializeSearchMap(const TCTypeMaps& ctypeMaps) {
        if (IS_LOG_ACTIVE(TLOG_DEBUG)) {
            for (const auto& sm : ctypeMaps) {
                DEBUG_LOG << sm.first << " " << sm.second.SerializeToJson() << Endl;
            }
        }

        ui32 iters = 0;
        TString mapsStr;
        if (ctypeMaps.size() == 1) {
            iters = ctypeMaps.front().second.GetSlots().size() / 32 + 2;
            mapsStr = ctypeMaps.front().second.SerializeToJson();
        } else {
            NJson::TJsonValue maps(NJson::JSON_ARRAY);
            ui32 slotsCount = 0;
            for (const auto& sm : ctypeMaps) {
                slotsCount += sm.second.GetSlots().size();
                maps.AppendValue(sm.second.SerializeToJsonObject());
            }
            mapsStr = NJson::WriteJson(maps);
            iters = slotsCount / 32 + 2;
        }

        return {std::move(mapsStr), ctypeMaps.size() > 1, TDuration::MilliSeconds(iters * 10000)};
    }

    void TScriptDashboard::DoProcess(IDeployInfoRequest& request) {
        NRTYDeploy::TClusterTask::TCgiContext cgi = NRTYDeploy::TClusterTask::TCgiContext::Parse(request.GetRD().CgiParam);
        if (!cgi.Service) {
            if (const TString& projectName = request.GetRD().CgiParam.Get("project")) {
                NDMInterface::TProject project = GetProject(request.GetStorage(), projectName);
                for (const auto& comp : project.GetComponents()) {
                    if (comp.GetMain() || !cgi.Service && comp.GetServiceType() == RTYSERVER_SERVICE)
                        cgi.Service = comp.GetServiceName();
                }
            }
        }
        THashSet<TString> slotsFilters;
        StringSplitter(request.GetRD().CgiParam.Get("slots_filters")).Split(',').AddTo(&slotsFilters);

        const bool collectStatistics = request.GetRD().CgiParam.Has("statistics") && FromString<bool>(request.GetRD().CgiParam.Get("statistics"));
        TVector <NPrivate::TStatisticsField::TPtr> statistics;
        if (collectStatistics) {
            statistics.push_back(new NPrivate::TStatisticsFieldGroupedSet("result.docs_in_final_indexes", "Документов в индексе", "interval"));
            statistics.back()->AddValuePath("result.docs_in_disk_indexers");
            statistics.push_back(new NPrivate::TStatisticsFieldSum("result.files_size.__SUM", "Место на диске"));
            statistics.push_back(new NPrivate::TStatisticsFieldSum("result.disk_index_rps.1", "Документов в секунду"));
            statistics.back()->AddValuePath("result.memory_index_rps.1");
            statistics.push_back(new NPrivate::TStatisticsFieldSum("result.search_rps_neh.1", "Запросов в секунду"));
        }

        const TCTypeMaps sms = DoBuildSearchMap(request, cgi, slotsFilters);
        TString mapsStr;
        bool isMapMultiple;
        TDuration broadcastTimeout;
        std::tie(mapsStr, isMapMultiple, broadcastTimeout) = SerializeSearchMap(sms);

        NDaemonController::TBroadcastAction getInfoAction("get_info_server", cgi.CType, "*", cgi.ServiceType, true, mapsStr, broadcastTimeout);
        getInfoAction.SetIsMultiple(isMapMultiple);
        if (request.GetRD().CgiParam.Has("skip_eps") && FromString<bool>(request.GetRD().CgiParam.Get("skip_eps"))) {
            getInfoAction.SetWithSd(false);
        }

        for (const auto& f : SplitString(request.GetRD().CgiParam.Get("filter"), ","))
            getInfoAction.GetFilter().insert(f);
        getInfoAction.GetFilter().insert("service");
        getInfoAction.GetFilter().insert("interval");
        getInfoAction.GetFilter().insert("$datacenter$");
        getInfoAction.GetFilter().insert("$datacenter_alias$");
        getInfoAction.GetFilter().insert("$real_host$");
        getInfoAction.GetFilter().insert("is_sd");
        const bool onlyProblem = slotsFilters.contains("problem_only");
        TDuration minOkUptime;
        if (onlyProblem) {
            if (request.GetRD().CgiParam.Has("min_ok_uptime")) {
                minOkUptime = FromString<TDuration>(request.GetRD().CgiParam.Get("min_ok_uptime"));
                getInfoAction.GetFilter().insert("result.controller_uptime");
            }
            getInfoAction.GetFilter().insert("result.slot_size");
            getInfoAction.GetFilter().insert("result.slots_count");
            getInfoAction.GetFilter().insert("result.total_mem_size");
            getInfoAction.GetFilter().insert("result.load_average");
            getInfoAction.GetFilter().insert("result.cpu_count");
            getInfoAction.GetFilter().insert("result.controller_status");
        }
        TIntrusivePtr<NPrivate::TRicherReplicsConsistanse> replicsConsistance;
        if (getInfoAction.GetFilter().contains("replics_consistance") || getInfoAction.GetFilter().contains("inconsistent_replics_ratio")) {
            replicsConsistance = MakeIntrusive<NPrivate::TRicherReplicsConsistanse>();
            replicsConsistance->UpdateFilter(getInfoAction.GetFilter());
        }

        const TString& minRespReplicsTag = request.GetRD().CgiParam.Get("min_resp_replics_tag");
        TIntrusivePtr<NPrivate::TRicherFailDomainTagger> richerFailDomainTagger;
        if (!minRespReplicsTag.empty() && sms.size() == 1) {
            THashSet<TString> perDCSearchServicesNames;
            for (const NSearchMapParser::TSearchMapService& service: sms.front().second.GetInternalSearchMap()) {
                if (service.PerDcSearch) {
                    perDCSearchServicesNames.insert(service.Name);
                }
            }
            richerFailDomainTagger = MakeIntrusive<NPrivate::TRicherFailDomainTagger>(perDCSearchServicesNames, minRespReplicsTag);
            richerFailDomainTagger->UpdateFilter(getInfoAction.GetFilter());
        }

        NPrivate::TGroupReporter reporter(request.GetRD().CgiParam);
        reporter.UpdateFieldsFilter(getInfoAction.GetFilter());

        const bool checkConfigs = getInfoAction.GetFilter().contains("invalid_config_files");
        for (const auto& stat : statistics)
            getInfoAction.GetFilter().insert(stat->GetValuePath().begin(), stat->GetValuePath().end());

        TIntrusivePtr<NPrivate::TRicherControllerStatus> controllerStatus;
        if (getInfoAction.GetFilter().contains("result.controller_status")) {
            const bool reportFlagged = request.GetRD().CgiParam.Has("report_flagged") && FromString<bool>(request.GetRD().CgiParam.Get("report_flagged"));
            controllerStatus = MakeIntrusive<NPrivate::TRicherControllerStatus>(reportFlagged);
            getInfoAction.GetFilter().insert("result.search_enabled");
            getInfoAction.GetFilter().insert("disable_search");
            getInfoAction.GetFilter().insert("disable_indexing");
        }

        TRTYMtpQueue agentQueue;
        agentQueue.Start(0);
        agentQueue.SafeAdd(new TActionExecutor(request, getInfoAction));
        NDaemonController::TCheckConfigsAction checkConfigAction(cgi.CType, "*", cgi.ServiceType, true, "last_deployed", mapsStr);
        checkConfigAction.SetIsMultiple(isMapMultiple);
        if (checkConfigs)
            agentQueue.SafeAdd(new TActionExecutor(request, checkConfigAction));
        agentQueue.Stop();

        TString categories = "service.slot";
        if (request.GetRD().CgiParam.Has("categories"))
            categories = request.GetRD().CgiParam.Get("categories");
        NPrivate::TPlainer plainer(new NPrivate::TGroupInserter(Report, reporter.GetGroupings(), onlyProblem, getInfoAction.GetResult(), checkConfigAction.GetResult(), minOkUptime), categories);
        if (controllerStatus) {
            plainer.AddPredInsertRicher(controllerStatus);
        }
        if (getInfoAction.GetFilter().contains("replic_id"))
            plainer.AddPredInsertRicher(new NPrivate::TRicherReplicId());
        if (collectStatistics)
            plainer.AddPredInsertRicher(new NPrivate::TRicherStatistics(statistics, cgi.Service));
        if (checkConfigs)
            plainer.AddPostInsertRicher(new NPrivate::TRicherCheckConfigs(checkConfigAction.GetResult()));
        if (replicsConsistance) {
            replicsConsistance->InitializeByBroadcastResult(getInfoAction.GetResult());
            plainer.AddPredInsertRicher(replicsConsistance);
        }
        if (richerFailDomainTagger) {
            plainer.AddPredInsertRicher(richerFailDomainTagger);
        }

        plainer.Process(getInfoAction.GetResult());

        NJson::TJsonValue newReport;
        NJson::TJsonValue& forReport = collectStatistics ? newReport.InsertValue("slots", NJson::JSON_MAP) : newReport;
        reporter.Report(forReport, Report);
        if (collectStatistics) {
            for (const auto& stat: statistics)
                InsertStatValue(newReport["stats"], GetHumanReadableNumber(stat->GetValue()), stat->GetCaption());
        }
        Report = newReport;
    }
}
