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

namespace NWebmaster {
namespace NAcceptance {

using namespace NJupiter;

TInputTag<::NWebmaster::NProto::THostStatus> HostStatusPrevInputTag              (1);
TInputTag<::NWebmaster::NProto::THostStatus> HostStatusNewInputTag               (2);

TOutputTag<NProto::THostStatusDiff> HostStatusOutputTag                          (1);

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

    struct THostStatusGetter {
       inline double operator()(const ::NWebmaster::NProto::THostStatus &msg) const {
            return static_cast<double>(msg.GetHostStatus());
        }
    };

    struct TDiffHasSslCertErrorsGetter {
       inline double operator()(const ::NWebmaster::NProto::THostStatus &msg) const {
            return static_cast<double>(msg.GetHasSslCertErrors());
        }
    };

    template<class TGetter>
    static double GetDifference(const ::NWebmaster::NProto::THostStatus &pr, const ::NWebmaster::NProto::THostStatus &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 GetDifferenceHostStatus(const ::NWebmaster::NProto::THostStatus &pr, const ::NWebmaster::NProto::THostStatus  &nr) {
        return static_cast<double>(pr.GetHostStatus() != nr.GetHostStatus());
    }

    void Set(const ::NWebmaster::NProto::THostStatus &pr, const ::NWebmaster::NProto::THostStatus  &nr) {
        DiffLastAccess                  = GetDifference<TLastAccessGetter>(pr, nr);
        DiffHostStatus                  = GetDifferenceHostStatus(pr, nr);
        DiffHasSslCertErrors            = GetDifference<TDiffHasSslCertErrorsGetter>(pr, nr);
    }

public:
    double DiffLastAccess               = 0;
    double DiffHostStatus               = 0;
    double DiffHasSslCertErrors         = 0;
};

struct THostStatusDiffReducer : public TTaggedReducer {
    THostStatusDiffReducer() = default;
    THostStatusDiffReducer(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::THostStatus> mbPrev = reader.GetSingleRowMaybe(HostStatusPrevInputTag);
        const TMaybe<::NWebmaster::NProto::THostStatus> mbNew  = reader.GetSingleRowMaybe(HostStatusNewInputTag);

        TString host;

        THostStatusDiffCounter dc;
        ::NWebmaster::NProto::THostStatus 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::THostStatusDiff dstMsg;
        dstMsg.SetHost(host);
        dstMsg.SetTimestamp(Timestamp);
        dstMsg.SetDiffLastAccess(dc.DiffLastAccess);
        dstMsg.SetDiffHasSslCertErrors(dc.DiffHasSslCertErrors);
        dstMsg.SetDiffHostStatus(dc.DiffHostStatus);
        writer.AddRow(dstMsg, HostStatusOutputTag);
    }

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

struct THostStatusArchiveLimitReducer : public NYT::IReducer<NYT::TTableReader<NProto::THostStatusDiff>, NYT::TTableWriter<NProto::THostStatusDiff>> {
    void Do(TReader *input, TWriter *output) override {
        TMap<time_t, NProto::THostStatusDiff> 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(THostStatusArchiveLimitReducer)

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

    bool rejected = false;

    TDeque<double> diffLastAccess;
    TDeque<double> diffHostStatus;
    TDeque<double> diffHasSslCertErrors;

    TMap<double, TString> diffLastAccessSamples;
    TMap<double, TString> diffHostStatusSamples;
    TMap<double, TString> diffHasSslCertErrorsSamples;

    double cntOfNegatives = 0;
    double cntTotal = 0;

    auto reader = TTable<NProto::THostStatusDiff>(client, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS).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(diffHostStatus,       diffHostStatusSamples,          row.GetDiffHostStatus(),        row.GetHost());
        AddSample(diffHasSslCertErrors, diffHasSslCertErrorsSamples,    row.GetDiffHasSslCertErrors(),  row.GetHost());
    }

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

    rejected |= cntOfNegatives / cntTotal >= 0.1;
    rejected |= IsThresholdBroken("ValidateHostStatusDiffs diffLastAccess",
            diffLastAccess,         diffLastAccessSamples,          0.8, 0.01);
    rejected |= IsThresholdBroken("ValidateHostStatusDiffs diffHostStatus",
            diffHostStatus,         diffHostStatusSamples,          0.8, 0);
    rejected |= IsThresholdBroken("ValidateHostStatusDiffs diffHasSslCertErrors",
            diffHasSslCertErrors,   diffHasSslCertErrorsSamples,    0.9, 0);

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

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

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

    TYtSourceTrigger newStateTrigger(tx, hicfg.TABLE_EXPORT_HOST_STATUS);
    TYtSourceTrigger oldStateTrigger(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS);

    if (!oldStateTrigger.NeedUpdate(newStateTrigger.Source)) {
        HostinfoLog(functionName + "source %s is already processed", oldStateTrigger.Source);
        return;
    }

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

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

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

    TReduceCmd<THostStatusDiffReducer>(tx, new THostStatusDiffReducer(GetJupiterTsTZFromPath(newStateTrigger.Source)))
        .Input(TTable<::NWebmaster::NProto::THostStatus>(tx, acceptedInputTable), HostStatusPrevInputTag)
        .Input(TTable<::NWebmaster::NProto::THostStatus>(tx, stageInputTable), HostStatusNewInputTag)
        .Output(TTable<NProto::THostStatusDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS)
            .AsSortedOutput({"Host", "Timestamp"}), HostStatusOutputTag
        )
        .ReduceBy({"Host"})
        .Do()
    ;

    if (!tx->Exists(cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS_ARCHIVE))
        tx->Copy(TTable<NProto::THostStatusDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS), TTable<NProto::THostStatusDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS_ARCHIVE));

    TReduceCmd<THostStatusArchiveLimitReducer>(tx)
        .Input(TTable<NProto::THostStatusDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS_ARCHIVE))
        .Input(TTable<NProto::THostStatusDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS))
        .Output(TTable<NProto::THostStatusDiff>(tx, cfg.TABLE_ACCEPTANCE_HOSTINFO_STATUS_ARCHIVE)
            .AsSortedOutput({"Host", "Timestamp"})
        )
        .SortBy({"Host", "Timestamp"})
        .ReduceBy({"Host"})
        .Do()
    ;

    oldStateTrigger.Update(tx, newStateTrigger.Source);
    ValidateHostStatusDiffs(tx);

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

} //namespace NAcceptance
} //namespace NWebmaster
