#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_face_problem_common.h"

namespace NWebmaster {
namespace NAcceptance {

using namespace NJupiter;

TInputTag<::NWebmaster::NProto::TMordaSample> HostFaceProblemPrevInputTag              (1);
TInputTag<::NWebmaster::NProto::TMordaSample> HostFaceProblemNewInputTag               (2);

TOutputTag<NProto::THostFaceProblemDiff> HostFaceProblemOutputTag                      (1);

struct THostFaceProblemDiffCounter {
    struct TLastAccessGetter {
       inline double operator()(const ::NWebmaster::NProto::TMordaSample &msg) const {
            return static_cast<double>(msg.GetLastAccess());
        }
    };

    struct TFetchTimeGetter {
       inline double operator()(const ::NWebmaster::NProto::TMordaSample &msg) const {
            return static_cast<double>(msg.GetFetchTime());
        }
    };

    struct THttpCodeGetter {
       inline double operator()(const ::NWebmaster::NProto::TMordaSample &msg) const {
            return static_cast<double>(msg.GetHttpCode());
        }
    };

    template<class TGetter>
    static double GetDifference(const ::NWebmaster::NProto::TMordaSample &pr, const ::NWebmaster::NProto::TMordaSample &nr) {
        const static TGetter getter;
        const double pValue = getter(pr);
        const double nValue = getter(nr);
        if (pValue == 0) {
            if (nValue == 0) {
                return 0.0;
            }
            return 1.0;
        }
        return (nValue - pValue) / pValue;
    }

    static double GetDifferenceHttpCode(const ::NWebmaster::NProto::TMordaSample &pr,
                                        const ::NWebmaster::NProto::TMordaSample  &nr) {
        return pr.GetHttpCode() == nr.GetHttpCode() ? 0.0 : 1.0;
    }

    void Set(const ::NWebmaster::NProto::TMordaSample &pr, const ::NWebmaster::NProto::TMordaSample  &nr) {
        DiffLastAccess      = GetDifference<TLastAccessGetter>(pr, nr);
        DiffHttpCode        = GetDifferenceHttpCode(pr, nr);
        DiffFetchTime       = GetDifference<TFetchTimeGetter>(pr, nr);
    }
public:
    double DiffLastAccess   = 0;
    double DiffHttpCode     = 0;
    double DiffFetchTime    = 0;
};

struct THostFaceProblemDiffReducer : public TTaggedReducer {
    THostFaceProblemDiffReducer() = default;
    THostFaceProblemDiffReducer(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 {
        const TMaybe<::NWebmaster::NProto::TMordaSample> mbPrev = reader.GetSingleRowMaybe(HostFaceProblemPrevInputTag);
        const TMaybe<::NWebmaster::NProto::TMordaSample> mbNew  = reader.GetSingleRowMaybe(HostFaceProblemNewInputTag);

        TString host;

        THostFaceProblemDiffCounter dc;
        ::NWebmaster::NProto::TMordaSample prevRow, newRow;

        if (mbPrev.Defined()) {
            prevRow = mbPrev.GetRef();
            host = mbPrev->GetHost();
        } else {
            host = mbNew->GetHost();
        }

        if (mbNew.Defined()) {
            newRow = mbNew.GetRef();
        }

        dc.Set(prevRow, newRow);

        NProto::THostFaceProblemDiff dstMsg;
        dstMsg.SetHost(host);
        dstMsg.SetTimestamp(Timestamp);
        dstMsg.SetDiffFetchTime(dc.DiffFetchTime);
        dstMsg.SetDiffHttpCode(dc.DiffHttpCode);
        dstMsg.SetDiffLastAccess(dc.DiffLastAccess);
        writer.AddRow(dstMsg, HostFaceProblemOutputTag);
    }

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

struct THostFaceProblemArchiveLimitReducer :
        public NYT::IReducer<NYT::TTableReader<NProto::THostFaceProblemDiff>, NYT::TTableWriter<NProto::THostFaceProblemDiff>> {
    void Do(TReader *input, TWriter *output) override {
        for (; input->IsValid(); input->Next()) {
            output->AddRow(input->GetRow());
        }
    }
};
REGISTER_REDUCER(THostFaceProblemArchiveLimitReducer)

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

    bool rejected = false;

    TDeque<double> diffLastAccess;
    TDeque<double> diffHttpCode;
    TDeque<double> diffFetchTime;

    TMap<double, TString> diffLastAccessSamples;
    TMap<double, TString> diffHttpCodeSamples;
    TMap<double, TString> diffFetchTimeSamples;

    double cntOfNegatives = 0;
    double cntTotal = 0;

    auto reader = TTable<NProto::THostFaceProblemDiff>(client, cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM).GetReader();
    for (; reader->IsValid(); reader->Next()) {
        const auto &row = reader->GetRow();
        cntOfNegatives += row.GetDiffLastAccess() < 0;
        cntTotal += 1;
        AddSample(diffLastAccess,       diffLastAccessSamples,          row.GetDiffLastAccess(),        row.GetHost());
        AddSample(diffHttpCode,         diffHttpCodeSamples,            row.GetDiffHttpCode(),          row.GetHost());
        AddSample(diffFetchTime,        diffFetchTimeSamples,           row.GetDiffFetchTime(),         row.GetHost());
    }

    HostinfoLog("count of negative diffs of last accesses %s", (ToString(cntOfNegatives / cntTotal)).c_str());

    rejected |= cntOfNegatives / cntTotal >= 0.5;
    rejected |= IsThresholdBroken("ValidateHostFaceProblemDiffs diffLastAccess",
            diffLastAccess,    diffLastAccessSamples,   0.8,  0.01);
    rejected |= IsThresholdBroken("ValidateHostFaceProblemDiffs diffHttpCode",
            diffHttpCode,      diffHttpCodeSamples,     0.8,  0);
    rejected |= IsThresholdBroken("ValidateHostFaceProblemDiffs diffFetchTime",
            diffFetchTime,     diffFetchTimeSamples,    0.9,  0.01);

    if (rejected) {
        ythrow yexception() << "HostFaceProblem is rejected";
    }
}

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

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

    time_t oldModTime = 0;
    try {
        ParseDate(GetYtAttr(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM, "modification_time"), oldModTime);
    } catch (yexception ignore) {
        oldModTime = 0;
    }
    time_t newModTime = 0;
    ParseDate(GetYtAttr(tx, hicfg.TABLE_EXPORT_HOST_FACE_PROBLEM, "modification_time"), newModTime);

    if (oldModTime >= newModTime) {
        HostinfoLog(functionName + "table %s is already processed", hicfg.TABLE_EXPORT_HOST_FACE_PROBLEM);
        return;
    }

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

    HostinfoLog(functionName + "input %s", acceptedInputTable);
    HostinfoLog(functionName + "input %s", stageInputTable);
    HostinfoLog(functionName + "output %s", cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM);

    time_t timeStamp = 0;
    ParseDate(GetYtAttr(tx, hicfg.TABLE_EXPORT_HOST_FACE_PROBLEM, "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<THostFaceProblemDiffReducer>(tx, new THostFaceProblemDiffReducer(timeStamp))
        .Input(TTable<::NWebmaster::NProto::TMordaSample>(tx, acceptedInputTable), HostFaceProblemPrevInputTag)
        .Input(TTable<::NWebmaster::NProto::TMordaSample>(tx, stageInputTable), HostFaceProblemNewInputTag)
        .Output(TTable<NProto::THostFaceProblemDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM)
            .AsSortedOutput({"Host", "Timestamp"}), HostFaceProblemOutputTag
        )
        .ReduceBy({"Host"})
        .Do()
    ;

    if (!tx->Exists(cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM_ARCHIVE)) {
        tx->Copy(TTable<NProto::THostFaceProblemDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM),
                TTable<NProto::THostFaceProblemDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM_ARCHIVE));
    }

    TReduceCmd<THostFaceProblemArchiveLimitReducer>(tx)
        .Input(TTable<NProto::THostFaceProblemDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM_ARCHIVE))
        .Input(TTable<NProto::THostFaceProblemDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM))
        .Output(TTable<NProto::THostFaceProblemDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_FACE_PROBLEM_ARCHIVE)
            .AsSortedOutput({"Host", "Timestamp"})
        )
        .SortBy({"Host", "Timestamp"})
        .ReduceBy({"Host", "Timestamp"})
        .Do()
    ;

    ValidateHostFaceProblemDiffs(tx);

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

} //namespace NAcceptance
} //namespace NWebmaster
