#include <util/draft/date.h>
#include <util/random/random.h>
#include <util/string/join.h>
#include <util/string/printf.h>

#include <mapreduce/yt/interface/protos/yamr.pb.h>

#include <robot/library/yt/static/command.h>
#include <robot/library/yt/static/table.h>
#include <robot/library/yt/static/tags.h>

#include <wmconsole/version3/library/jupiter/jupiter.h>
#include <wmconsole/version3/searchqueries-mr/conf/yt.h>
#include <wmconsole/version3/searchqueries-mr/protos/user_sessions.pb.h>
#include <wmconsole/version3/processors/acceptance/conf/config.h>
#include <wmconsole/version3/processors/acceptance/protos/acceptance.pb.h>
#include <wmconsole/version3/processors/indexing/hostinfo/conf/config.h>
#include <wmconsole/version3/processors/indexing/hostinfo/protos/hostinfo.pb.h>
#include <wmconsole/version3/wmcutil/log.h>
#include <wmconsole/version3/wmcutil/yt/triggers.h>

#include "common.h"
#include "host_problems_common.h"

namespace NWebmaster {
namespace NAcceptance {

using namespace NJupiter;

TInputTag<::NWebmaster::NProto::THostProblems> HostProblemsPrevInputTag              (1);
TInputTag<::NWebmaster::NProto::THostProblems> HostProblemsNewInputTag               (2);

TOutputTag<NProto::THostProblemsDiff> HostProblemsOutputTag                          (1);

struct THostProblemsDiffCounter {
    struct TActualSinceGetter {
       inline double operator()(const ::NWebmaster::NProto::THostProblems &msg) const {
            return static_cast<double>(msg.GetActualSince());
        }
    };

    struct TLastUpdateGetter {
       inline double operator()(const ::NWebmaster::NProto::THostProblems &msg) const {
            return static_cast<double>(msg.GetLastUpdate());
        }
    };

    template<class TGetter>
    static double GetAvg(const TDeque<::NWebmaster::NProto::THostProblems> &msgs) {
        const static TGetter getter;
        double res = 0;
        if (msgs.empty()) {
            return 0.0;
        }
        for (const auto &msg: msgs) {
            res += getter(msg);
        }
        return res / static_cast<double>(msgs.size());
    }

    static double GetDifference(double pValue, double nValue) {
        if (pValue == 0) {
            if (nValue == 0) {
                return 0.0;
            }
            return 1.0;
        }
        return (nValue - pValue) / pValue;
    }

    template<class TGetter>
    static double GetAvgDifference(const TDeque<::NWebmaster::NProto::THostProblems> &prs,
                                    const TDeque<::NWebmaster::NProto::THostProblems> &nrs) {
        double pValue = GetAvg<TGetter>(prs);
        double nValue = GetAvg<TGetter>(nrs);
        return GetDifference(pValue, nValue);
    }

    static double GetDiffType(const TDeque<::NWebmaster::NProto::THostProblems> &prs,
                                const TDeque<::NWebmaster::NProto::THostProblems> &nrs) {
        return GetDifference(static_cast<double>(prs.size()),
                                static_cast<double>(nrs.size()));
    }

    void Set(const TDeque<::NWebmaster::NProto::THostProblems> &prs,
                                const TDeque<::NWebmaster::NProto::THostProblems> &nrs) {
        DiffType            = GetDiffType(prs, nrs);
        DiffActualSince     = GetAvgDifference<TActualSinceGetter>(prs, nrs);
        DiffLastUpdate      = GetAvgDifference<TLastUpdateGetter>(prs, nrs);
    }

public:
    double DiffType         = 0;
    double DiffActualSince  = 0;
    double DiffLastUpdate   = 0;
};

struct THostProblemsDiffReducer : public TTaggedReducer {
    THostProblemsDiffReducer() = default;
    THostProblemsDiffReducer(time_t timestamp)
        : Timestamp(timestamp)
    {
    }

    void Save(IOutputStream& stream) const override {
        ::Save(&stream, Timestamp);
        TTaggedReducer::Save(stream);
    }

    void Load(IInputStream& stream) override {
        ::Load(&stream, Timestamp);
        TTaggedReducer::Load(stream);
    }

    void DoTagged(TTagedReader reader, TTagedWriter writer) final {
        TDeque<NWebmaster::NProto::THostProblems> prevRows;
        TDeque<NWebmaster::NProto::THostProblems> newRows;

        auto prevIterable = reader.GetRows(HostProblemsPrevInputTag);
        auto newIterable = reader.GetRows(HostProblemsNewInputTag);

        TString host;

        for (auto &prevRow: prevIterable) {
            prevRows.push_back(prevRow);
            host = prevRow.GetHost();
        }

        for (auto &newRow: newIterable) {
            newRows.push_back(newRow);
            host = newRow.GetHost();
        }

        THostProblemsDiffCounter dc;

        dc.Set(prevRows, newRows);

        NProto::THostProblemsDiff dstMsg;

        dstMsg.SetHost(host);
        dstMsg.SetDiffActualSince(dc.DiffActualSince);
        dstMsg.SetDiffType(dc.DiffType);
        dstMsg.SetDiffLastUpdate(dc.DiffLastUpdate);
        dstMsg.SetTimestamp(Timestamp);

        writer.AddRow(dstMsg, HostProblemsOutputTag);
    }

public:
    time_t Timestamp = 0;
};
REGISTER_REDUCER(THostProblemsDiffReducer)

struct THostProblemsArchiveLimitReducer :
        public NYT::IReducer<NYT::TTableReader<NProto::THostProblemsDiff>, NYT::TTableWriter<NProto::THostProblemsDiff>> {
    void Do(TReader *input, TWriter *output) override {
        TMap<time_t, NProto::THostProblemsDiff> rows;
        for (; input->IsValid(); input->Next()) {
            const auto &row = input->GetRow();
            rows[row.GetTimestamp()] = row;
            if (rows.size() > 100) {
                rows.erase(rows.begin()->first);
            }
        }
        for (const auto &obj : rows) {
            output->AddRow(obj.second);
        }
    }
};
REGISTER_REDUCER(THostProblemsArchiveLimitReducer)

void ValidateHostProblemsDiffs(NYT::IClientBasePtr client) {
    const auto &cfg = TConfig::CInstance();

    bool rejected = false;

    TDeque<double> diffType;
    TDeque<double> diffActualSince;
    TDeque<double> diffLastUpdate;

    TMap<double, TString> diffTypeSamples;
    TMap<double, TString> diffActualSinceSamples;
    TMap<double, TString> diffLastUpdateSamples;

    auto reader = TTable<NProto::THostProblemsDiff>(client, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS).GetReader();
    for (; reader->IsValid(); reader->Next()) {
        const auto &row = reader->GetRow();
        AddSample(diffType,           diffTypeSamples,          row.GetDiffType(),          row.GetHost());
        AddSample(diffActualSince,    diffActualSinceSamples,   row.GetDiffActualSince(),   row.GetHost());
        AddSample(diffLastUpdate,     diffLastUpdateSamples,    row.GetDiffLastUpdate(),    row.GetHost());
    }

    rejected |= IsThresholdBroken("ValidateHostProblemsDiffs diffType",
            diffType,          diffTypeSamples,         0.9,  0);
    rejected |= IsThresholdBroken("ValidateHostProblemsDiffs diffActualSince",
            diffActualSince,   diffActualSinceSamples,  0.9,  0);
    rejected |= IsThresholdBroken("ValidateHostProblemsDiffs diffLastUpdate",
            diffLastUpdate,    diffLastUpdateSamples,   0.9,  0);

    if (rejected) {
        ythrow yexception() << "HostProblems are rejected";
    }
}

void AcceptHostProblems(NYT::ITransactionPtr tx) {
    const static TString functionName = "AcceptHostProblems, ";

    const auto &cfg = TConfig::CInstance();
    const auto &ccfg = TCommonYTConfig::CInstance();
    const auto &hicfg = NHostInfo::TConfig::CInstance();

    TYtModificationTimeTrigger newStateTrigger(hicfg.TABLE_EXPORT_HOST_PROBLEMS);
    TYtModificationTimeTrigger oldStateTrigger(cfg.TABLE_ACCEPTANCE_HOSTINFO_PROBLEMS);

    if (!(oldStateTrigger.NeedUpdate(tx, "host_regions", hicfg.TABLE_EXPORT_HOST_PROBLEMS) ||
          oldStateTrigger.NeedUpdate(tx, "host_statistics", hicfg.TABLE_EXPORT_HOST_PROBLEMS))) {
        HostinfoLog(functionName + "table %s is already processed", hicfg.TABLE_EXPORT_HOST_PROBLEMS);
        return;
    }

    const TString stageInputTable = hicfg.TABLE_EXPORT_HOST_PROBLEMS;
    const TString acceptedInputTable = ccfg.GetAcceptedPath(hicfg.TABLE_EXPORT_HOST_PROBLEMS);

    HostinfoLog(functionName + "input %s", acceptedInputTable);
    HostinfoLog(functionName + "input %s", stageInputTable);
    HostinfoLog(functionName + "output %s", oldStateTrigger.TargetTable);

    time_t timeStamp = 0;
    ParseDate(GetYtAttr(tx, hicfg.TABLE_EXPORT_HOST_PROBLEMS, "modification_time"), timeStamp);

    if (!tx->Exists(acceptedInputTable)) {
        const auto opts = NYT::TCopyOptions().Recursive(true);
        tx->Copy(stageInputTable, acceptedInputTable, opts);
        HostinfoLog(functionName + "first launch, created %s", acceptedInputTable);
    }

    TReduceCmd<THostProblemsDiffReducer>(tx, new THostProblemsDiffReducer(timeStamp))
        .Input(TTable<::NWebmaster::NProto::THostProblems>(tx, acceptedInputTable), HostProblemsPrevInputTag)
        .Input(TTable<::NWebmaster::NProto::THostProblems>(tx, stageInputTable), HostProblemsNewInputTag)
        .Output(TTable<NProto::THostProblemsDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_PROBLEMS)
            .AsSortedOutput({"Host", "Timestamp"}), HostProblemsOutputTag
        )
        .ReduceBy({"Host"})
        .Do()
    ;

    if (!tx->Exists(cfg.TABLE_ACCEPTANCE_HOSTINFO_PROBLEMS_ARCHIVE)) {
        tx->Copy(TTable<NProto::THostProblemsDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_PROBLEMS), TTable<NProto::THostProblemsDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_PROBLEMS_ARCHIVE));
    }

    TReduceCmd<THostProblemsArchiveLimitReducer>(tx)
        .Input(TTable<NProto::THostProblemsDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_PROBLEMS_ARCHIVE))
        .Input(TTable<NProto::THostProblemsDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_PROBLEMS))
        .Output(TTable<NProto::THostProblemsDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_PROBLEMS_ARCHIVE)
            .AsSortedOutput({"Host", "Timestamp"})
        )
        .SortBy({"Host", "Timestamp"})
        .ReduceBy({"Host"})
        .Do()
    ;

    oldStateTrigger.Update(tx, "host_statistics", hicfg.TABLE_EXPORT_HOST_PROBLEMS);
    oldStateTrigger.Update(tx, "host_regions", hicfg.TABLE_EXPORT_HOST_PROBLEMS);
    ValidateHostProblemsDiffs(tx);

    const auto opts = NYT::TCopyOptions().Force(true);
    tx->Copy(stageInputTable, acceptedInputTable, opts);
    HostinfoLog(functionName + "success%s");
}

} //namespace NAcceptance
} //namespace NWebmaster
