#include <util/datetime/base.h>
#include <util/draft/date.h>
#include <util/generic/size_literals.h>
#include <util/stream/output.h>
#include <util/thread/pool.h>

#include <mapreduce/yt/interface/client.h>

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

#include <wmconsole/version3/junk/hostspeed/hostspeed.pb.h>

#include <wmconsole/version3/wmcutil/log.h>
#include <wmconsole/version3/wmcutil/yt/yt_utils.h>

const char *FORMAT = "%Y-%m-%d";

using namespace NJupiter;

namespace THostSpeed {
/*inline static*/ const ui32 SPEED_LIMIT = 100000;
/*inline static*/ const ui32 MIN_SPEED_INDEX = 3;
/*inline static*/ const ui32 MAX_SPEED_INDEX = 8;
/*inline static*/ const ui32 ACHIEVEMENT_EVENT = 3178;
}

TInputTag<NProcus::TSpeedFactor> FeaturesInputTag   (1);
TInputTag<NProcus::TTurboShare> TurboInputTag       (2);

TOutputTag<NProcus::TGrade> GradeOutputTag          (1);

namespace NWebmaster {

class TAggregateHist : public NYT::IReducer<NYT::TTableReader<NProcus::TSpeedHist>, NYT::TTableWriter<NProcus::TSpeedHist>> {
    bool FilterSpeed = false;
    float Decay = 1.0;
    using THist = TMap<ui32, ui32>;
public:
    Y_SAVELOAD_JOB(FilterSpeed);

    TAggregateHist(bool filterSpeed = false, float decay = 1.0)
        : FilterSpeed(filterSpeed)
        , Decay(decay) {}

    void Mix(NProcus::TSpeedHist& speedHist, TVector<THist>& oldSpeedHist) {
        for (size_t t = 0; t < oldSpeedHist.size(); ++t) {
            for (auto& i: oldSpeedHist[t]) {
                (*speedHist.MutableHist()->MutableValues())[i.first] += i.second * pow(Decay, t);
            }
        }
    }

    void Do(TReader* reader, TWriter* writer) override
    {
        try {
            NProcus::TSpeedHist speedHist;
            TVector<THist> oldSpeedHist;

            bool first = true;
            for (; reader->IsValid(); reader->Next()) {
                auto row = reader->GetRow();
                if (FilterSpeed && (row.GetIndex() < THostSpeed::MIN_SPEED_INDEX || row.GetIndex() > THostSpeed::MAX_SPEED_INDEX))
                    continue;
                if (first) {
                    speedHist = row;
                    first = false;
                    continue;
                }
                size_t tab = reader->GetTableIndex();
                for (auto value: row.GetHist().GetValues()) {
                    if (oldSpeedHist.size() <= tab)
                        oldSpeedHist.resize(tab + 1);
                    oldSpeedHist[tab][value.first] += value.second;
                }
            }
            Mix(speedHist, oldSpeedHist);
            if (!first)
                writer->AddRow(speedHist);
        } catch(yexception& ex) {
            Cerr << ex.what() << Endl;
        }
    }
};
REGISTER_REDUCER(TAggregateHist);

class TCalcTurbo : public NYT::IReducer<NYT::TTableReader<NProcus::TSpeedHist>, NYT::TTableWriter<NProcus::TTurboShare>> {
public:
    void Do(TReader* reader, TWriter* writer) override
    {
        try {
            NProcus::TTurboShare turboShare;
            bool first = true;
            ui32 turbo = 0;
            ui32 notTurbo = 0;
            for (; reader->IsValid(); reader->Next()) {
                auto row = reader->GetRow();
                if (first) {
                    turboShare.SetName(row.GetName());
                    turboShare.SetEventType(row.GetEventType());
                    turboShare.SetDeviceType(row.GetDeviceType());
                    turboShare.SetSourceType(row.GetSourceType());
                    first = false;
                }
                for (auto value: row.GetHist().GetValues()) {
                    if (row.GetTurbo())
                        turbo += value.second;
                    else notTurbo += value.second;
                }
            }
            if (!(turbo + notTurbo))
                return;
            turboShare.SetShare(100 * turbo / (turbo + notTurbo));
            turboShare.SetClicks(turbo + notTurbo);
            writer->AddRow(turboShare);
        } catch(yexception& ex) {
            Cerr << ex.what() << Endl;
        }
    }
};
REGISTER_REDUCER(TCalcTurbo);


//ReduceBy Name
struct TSpeedReducer : public NYT::IReducer<NYT::TTableReader<NProcus::TSpeedHist>, NYT::TTableWriter<NProcus::TSpeedFactor>> {
    void Do(TReader *reader, TWriter *writer) override {
        ui32 eventsCount = 0;
        TMap<ui32, ui32> hist;

        NProcus::TSpeedFactor msg;
        msg.SetName(reader->GetRow().GetName());

        for (; reader->IsValid(); reader->Next()) {
            const auto &row = reader->GetRow();
            if (row.GetSourceType() != NProcus::YANDEX_SEARCH) {
                continue;
            }

            if (row.GetDeviceType() != NProcus::MOBILE) {
                continue;
            }

            if (row.GetEventType() != THostSpeed::ACHIEVEMENT_EVENT) {
                continue;
            }

            if (row.GetIndex() < THostSpeed::MIN_SPEED_INDEX || row.GetIndex() > THostSpeed::MAX_SPEED_INDEX) {
                continue;
            }

            for (const auto &obj : row.GetHist().GetValues()) {
                if (obj.first > THostSpeed::SPEED_LIMIT) {
                    continue;
                }
                hist[obj.first] += obj.second;
                eventsCount += obj.second;
            }
        }

        ui32 currEvents = 0;
        ui32 accountedTime = 0;
        ui32 accountedEvents = 0;
        const ui32 lowerBound = eventsCount * 0.10;
        const ui32 upperBound = eventsCount * 0.90;
        for (const auto &obj : hist) {
            currEvents += obj.second;
            const ui32 ms = obj.first * 100;

            if (currEvents >= lowerBound && currEvents <= upperBound) {
                accountedTime += obj.second * ms;
                accountedEvents += obj.second;
            }
        }

        msg.SetClicks(eventsCount);
        if (accountedEvents > 0) {
            msg.SetValue(accountedTime / accountedEvents);
            writer->AddRow(msg);
        }
    }
};

REGISTER_REDUCER(TSpeedReducer);

inline ui32 GetGrade(float value) {
    if (value < 1100) {
        return 5;
    } else if (value < 1500) {
        return 4;
    } else if (value < 1850) {
        return 3;
    } else if (value < 2200) {
        return 2;
    }
    return 1;
}

//ReduceBy Name
struct TGradeReducer : public TTaggedReducer {
public:
    void DoTagged(TTagedReader reader, TTagedWriter writer) override {
        TMaybe<NProcus::TSpeedFactor> featuresRow = reader.GetRowMaybe(FeaturesInputTag);
        if (!featuresRow.Defined()) {
            return;
        }
        reader.SkipRows(FeaturesInputTag);

        const ui32 grade = GetGrade(featuresRow.GetRef().GetValue());
        if (featuresRow.GetRef().GetClicks() < 21) {
            return;
        }
        if (featuresRow.GetRef().GetValue() > 6000) {
            return;
        }

        TMaybe<NProcus::TTurboShare> turboRow;
        for (const auto &row : reader.GetRows(TurboInputTag)) {
            if (row.GetSourceType() != NProcus::YANDEX_SEARCH) {
                continue;
            }

            if (row.GetDeviceType() != NProcus::MOBILE) {
                continue;
            }

            if (row.GetEventType() != THostSpeed::ACHIEVEMENT_EVENT) {
                continue;
            }

            turboRow = row;
            break;
        }

        if (!turboRow.Defined()) {
            return;
        }

        NProcus::TGrade dstMsg;
        dstMsg.SetName(featuresRow.GetRef().GetName());
        dstMsg.SetClicks(featuresRow.GetRef().GetClicks());
        dstMsg.SetValue(featuresRow.GetRef().GetValue());
        dstMsg.SetGrade(grade);
        dstMsg.SetTurboClicks(turboRow.GetRef().GetClicks());
        dstMsg.SetTurboShare(turboRow.GetRef().GetShare());
        writer.AddRow(dstMsg, GradeOutputTag);
    }

}; //TGradeReducer

REGISTER_REDUCER(TGradeReducer)

NYT::TRichYPath DebugPath(const TString &table) {
    NYT::TRichYPath path(table);
    //path.AddRange(NYT::TReadRange().Exact(NYT::TReadLimit().Key(NYT::TKey("uu/d56438c56cfc44a7846152325fb4cdd1"))));
    //path.AddRange(NYT::TReadRange().Exact(NYT::TReadLimit().Key(NYT::TKey("kakprosto.ru"))));
    //path.AddRange(NYT::TReadRange().Exact(NYT::TReadLimit().Key(NYT::TKey("manyevents.ru"))));
    return path;
}

void GetTables(NYT::IClientBasePtr tx, const TDate &date, int period, TVector<TTable<NProcus::TSpeedHist>> &inputs) {
    const TString root = "//home/antispam/procus/daily/hostspeed_clicks";
    const TDate startDate = date - (28 * period);
    const TDate endDate = startDate - 28;
    for (TDate curDate = startDate; curDate > endDate; --curDate) {
        const TString dateStr = curDate.ToStroka(FORMAT);
        const TString tablePath = NYTUtils::JoinPath(root, dateStr + ".owners");
        TTable<NProcus::TSpeedHist> table(tx, DebugPath(tablePath));
        inputs.push_back(table);
        LOG_INFO("hostspeed, period %d, input %s", period, tablePath.c_str());
    }
}

void Calc(NYT::IClientBasePtr client, const TDate &date) {
    const TString tableRoot     = "//home/webmaster/users/lester/procus/v1.1/acceptance";
    const TString nameStat0     = "stat0";
    const TString nameStat1     = "stat1";
    const TString nameStatMix   = "statmix";
    const TString nameFeatures  = "features";
    const TString nameShare     = "share";
    const TString nameGrade     = "grade";

    NYT::ITransactionPtr tx = client->StartTransaction();
    TVector<TTable<NProcus::TSpeedHist>> inputs0, inputs1;
    GetTables(tx, date, 0, inputs0);
    GetTables(tx, date, 1, inputs1);

    const TString dateRoot          = NYTUtils::JoinPath(tableRoot, date.ToStroka(FORMAT));
    const TString tableStat0        = NYTUtils::JoinPath(dateRoot, nameStat0);
    const TString tableStat1        = NYTUtils::JoinPath(dateRoot, nameStat1);
    const TString tableStatMix      = NYTUtils::JoinPath(dateRoot, nameStatMix);
    const TString tableShare        = NYTUtils::JoinPath(dateRoot, nameShare);
    const TString tableFeatures     = NYTUtils::JoinPath(dateRoot, nameFeatures);
    const TString tableGrade        = NYTUtils::JoinPath(dateRoot, nameGrade);

    LOG_INFO("hostspeed, output %s", tableStat0.c_str());
    LOG_INFO("hostspeed, output %s", tableStat1.c_str());
    LOG_INFO("hostspeed, output %s", tableStatMix.c_str());
    LOG_INFO("hostspeed, output %s", tableShare.c_str());
    LOG_INFO("hostspeed, output %s", tableFeatures.c_str());
    LOG_INFO("hostspeed, output %s", tableGrade.c_str());

    NYTUtils::CreatePath(tx, dateRoot);

    const auto KEYS = {"Name", "SourceType", "DeviceType", "EventType"};

    DoParallel(
        TSortCmd<NProcus::TSpeedHist>(tx)
            .Inputs(inputs0)
            .Output(TTable<NProcus::TSpeedHist>(tx, tableStat0))
            .By(KEYS),
        TSortCmd<NProcus::TSpeedHist>(tx)
            .Inputs(inputs1)
            .Output(TTable<NProcus::TSpeedHist>(tx, tableStat1))
            .By(KEYS)
    );

    DoParallel(
        TReduceCmd<TAggregateHist>(tx, new TAggregateHist(true, 0.8))
            .Input(TTable<NProcus::TSpeedHist>(tx, tableStat0))
            .Input(TTable<NProcus::TSpeedHist>(tx, tableStat1))
            .Output(TTable<NProcus::TSpeedHist>(tx, tableStatMix).AsSortedOutput(KEYS))
            .MemoryLimit(2_GBs)
            .MaxRowWeight(128_MBs)
            .ReduceBy(KEYS),
        TReduceCmd<TCalcTurbo>(tx)
            .Input(TTable<NProcus::TSpeedHist>(tx, tableStat0))
            .Output(TTable<NProcus::TTurboShare>(tx, tableShare).AsSortedOutput(KEYS))
            .MemoryLimit(2_GBs)
            .MaxRowWeight(128_MBs)
            .ReduceBy(KEYS)
    );

    TReduceCmd<TSpeedReducer>(tx)
        .Input(TTable<NProcus::TSpeedHist>(tx, tableStatMix))
        .Output(TTable<NProcus::TSpeedFactor>(tx, tableFeatures).AsSortedOutput({"Name"}))
        .ReduceBy({"Name"})
        .MemoryLimit(2_GBs)
        .Do()
    ;

    TReduceCmd<TGradeReducer>(tx)
        .Input(TTable<NProcus::TSpeedFactor>(tx, tableFeatures), FeaturesInputTag)
        .Input(TTable<NProcus::TTurboShare>(tx, tableShare), TurboInputTag)
        .Output(TTable<NProcus::TGrade>(tx, tableGrade).AsSortedOutput({"Name"}), GradeOutputTag)
        .ReduceBy({"Name"})
        .Do()
    ;

    tx->Commit();
}

} //namespace NWebmaster

int main(int argc, const char **argv) {
    NYT::Initialize(argc, argv);
    using namespace NWebmaster;

    NYT::IClientBasePtr client = NYT::CreateClient("arnold.yt.yandex.net");

    THolder<IThreadPool> processQueue(CreateThreadPool(4));
    const TDate startDate = TDate("2020-04-06", FORMAT);
    const TDate endDate = startDate - 183;
    for (TDate curDate = startDate; curDate > endDate; curDate -= 7) {
        processQueue->SafeAddFunc([=]() {
            try {
                Calc(client, curDate);
            } catch (yexception &e) {
                LOG_ERROR("hostspeed, %s error: %s", curDate.ToStroka(FORMAT).c_str(), e.what());
            }
        });
    }
    processQueue->Stop();
}
