#include <util/draft/date.h>
#include <util/generic/queue.h>

#include <library/cpp/getopt/last_getopt.h>

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

#include <robot/jupiter/protos/external/host_mirror.pb.h>

#include <wmconsole/version3/library/jupiter/jupiter.h>
#include <wmconsole/version3/processors/acceptance/conf/config.h>
#include <wmconsole/version3/processors/acceptance/protos/acceptance.pb.h>
#include <wmconsole/version3/wmcutil/log.h>

#include "task_mirrors.h"
#include "task_traffic.h"

namespace NWebmaster {
namespace NAcceptance {

using namespace NJupiter;

enum EAcceptanceState {
    E_UNKNOWN,
    E_ACCEPTED,
    E_REJECTED,
};

struct TMapper : public NYT::IMapper<NYT::TTableReader<NJupiter::THostMirror>, NYT::TTableWriter<NProto::THostMirror>> {
    Y_SAVELOAD_JOB(TopHosts)

    TMapper() = default;
    TMapper(const THashSet<TString> &topHosts)
        : TopHosts(topHosts)
    {
    }

    void Do(TReader *input, TWriter *output) override {
        NProto::THostMirror dstRow;
        for (; input->IsValid(); input->Next()) {
            const NJupiter::THostMirror &row = input->GetRow();
            if (TopHosts.contains(row.GetHost())) {
                dstRow.SetHost(row.GetHost());
                dstRow.SetMainHost(row.GetMainHost());
                output->AddRow(dstRow);
            }
        }
    }

public:
    THashSet<TString> TopHosts;
};

REGISTER_MAPPER(TMapper)

EAcceptanceState GetAcceptanceState(NYT::IClientBasePtr client, const TString &root, const TString &state) try {
    const TString table = NYTUtils::JoinPath(root, state);
    if (!client->Exists(table)) {
        return E_UNKNOWN;
    }

    const bool accepted = NYTUtils::GetAttr(client, table, TConfig::CInstance().ATTR_ACCEPTED).AsBool();

    return accepted ? E_ACCEPTED : E_REJECTED;
} catch (yexception&) {
    return E_UNKNOWN;
}

TString GetMirrorsTable(const TString &state) {
    return NYTUtils::JoinPath("//home/jupiter/export", state, "mirrors/mirrors");
}

TString GetAcceptanceTable(const TString &state) {
    return NYTUtils::JoinPath(TConfig::CInstance().TABLE_ACCEPTANCE_MIRRORS_ROOT, state);
}

void UpdateMirrorsState(NYT::IClientBasePtr tx, const TString &jupiterState) {
    const TString inputTable = GetMirrorsTable(jupiterState);
    const TString outputTable = GetAcceptanceTable(jupiterState);

    THashSet<TString> topHosts;
    LoadTopHosts(tx, topHosts);

    TMapCmd<TMapper>(tx, new TMapper(topHosts))
        .Input(TTable<NJupiter::THostMirror>(tx, inputTable))
        .Output(TTable<NProto::THostMirror>(tx, outputTable))
        .OperationWeight(TConfig::CInstance().OPERATION_WEIGHT)
        .Do()
    ;

    TMap<TString, TString> mirrors;
    auto reader = TTable<NProto::THostMirror>(tx, outputTable).GetReader();
    for (; reader->IsValid(); reader->Next()) {
        mirrors[reader->GetRow().GetHost()] = reader->GetRow().GetMainHost();
    }

    for (const TString &host : topHosts) {
        if (!mirrors.contains(host)) {
            mirrors[host] = host;
        }
    }

    NProto::THostMirror dstMsg;
    auto writer = TTable<NProto::THostMirror>(tx, outputTable).AsSortedOutput({"Host"}).GetWriter();
    for (const auto &obj : mirrors) {
        dstMsg.SetHost(obj.first);
        dstMsg.SetMainHost(obj.second);
        writer->AddRow(dstMsg);
    }
    writer->Finish();
}

void LoadMirrors(NYT::IClientBasePtr client, const TString &table, THashMap<TString, TString> &mirrors) {
    auto reader = TTable<NProto::THostMirror>(client, table).GetReader();
    for (; reader->IsValid(); reader->Next()) {
        mirrors[reader->GetRow().GetHost()] = reader->GetRow().GetMainHost();
    }
}

bool AcceptMirrorsState(NYT::IClientBasePtr client, const TString &jupiterState) {
    const TString prevAcceptedState = NYTUtils::GetAttr(client, TConfig::CInstance().TABLE_ACCEPTANCE_MIRRORS_ROOT, TConfig::CInstance().ATTR_ACCEPTED).AsString();
    const TString prevMirrorsTable = GetAcceptanceTable(prevAcceptedState);
    const TString newMirrorsTable = GetAcceptanceTable(jupiterState);

    if (jupiterState <= prevAcceptedState) {
        ythrow yexception() << "acceptance.mirrors, new state " << jupiterState <<" must be greater than previous " << prevAcceptedState;
    }

    THashMap<TString, TString> prevMirrors, newMirrors;
    LoadMirrors(client, prevMirrorsTable, prevMirrors);
    LoadMirrors(client, newMirrorsTable, newMirrors);
    LOG_INFO("acceptance.mirrors, state %s, previous accepted state %s", jupiterState.c_str(), prevAcceptedState.c_str());

    bool verdict = true;
    for (const auto &prevObj : prevMirrors) {
        const TString &host = prevObj.first;
        const TString &prevMainHost = prevObj.second;
        if (newMirrors.contains(host)) {
            const TString newMainHost = newMirrors.at(host);
            if (newMainHost != prevMainHost) {
                LOG_ERROR("acceptance.mirrors, state %s, main host %s was changed %s -> %s",
                    jupiterState.c_str(), host.c_str(), prevMainHost.c_str(), newMainHost.c_str()
                );
                verdict = false;
            }
        } else {
            LOG_ERROR("acceptance.mirrors, state %s, no host %s from previous state",
                jupiterState.c_str(), host.c_str()
            );
            verdict = false;
        }
    }

    if (prevMirrors.size() != newMirrors.size() || prevMirrors.size() < 1000) {
        LOG_ERROR("acceptance.mirrors, state %s, mirrors set with incorrect size", jupiterState.c_str());
        verdict = false;
    }

    if (verdict) {
        NYTUtils::SetAttr(client, TConfig::CInstance().TABLE_ACCEPTANCE_MIRRORS_ROOT, TConfig::CInstance().ATTR_ACCEPTED, jupiterState);
    }

    NYTUtils::SetAttr(client, newMirrorsTable, TConfig::CInstance().ATTR_ACCEPTED, verdict);
    return verdict;
}

int AcceptanceMirrors(int argc, const char **argv) {
    TString jupiterState;

    NLastGetopt::TOpts opts;

    opts
        .AddLongOption("state", "Jupiter state")
        .StoreResult(&jupiterState)
        .Required()
    ;

    opts
        .AddLongOption("forced-acceptance", "Ignore a verdict")
        .NoArgument()
    ;

    const auto parseResult = NLastGetopt::TOptsParseResult(&opts, argc, argv);
    const bool forcedAcceptance = parseResult.Has("forced-acceptance");

    NYT::IClientPtr client = NYT::CreateClient(TConfig::CInstance().MR_SERVER_HOST);
    NYT::ITransactionPtr tx = client->StartTransaction();

    if (forcedAcceptance) {
        const TString &attr = TConfig::CInstance().ATTR_ACCEPTED;
        NYTUtils::SetAttr(tx, TConfig::CInstance().TABLE_ACCEPTANCE_MIRRORS_ROOT, attr, jupiterState);
        NYTUtils::SetAttr(tx, GetAcceptanceTable(jupiterState), attr, true);
        tx->Commit();
        LOG_INFO("acceptance.mirrors, state %s is accepted", jupiterState.c_str());
        return 0;
    }

    const EAcceptanceState as = GetAcceptanceState(tx, TConfig::CInstance().TABLE_ACCEPTANCE_MIRRORS_ROOT, jupiterState);
    if (as == E_ACCEPTED) {
        LOG_INFO("acceptance.mirrors, state %s is already processed", jupiterState.c_str());
        return 0;
    } else if (as == E_REJECTED) {
        ythrow yexception() << "acceptance.mirrors, state " << jupiterState << " is rejected";
    }

    UpdateMirrorsState(tx, jupiterState);
    const bool verdict = AcceptMirrorsState(tx, jupiterState);
    tx->Commit();

    if (!verdict) {
        ythrow yexception() << "acceptance.mirrors, state " << jupiterState << " is rejected";
    }

    LOG_INFO("acceptance.mirrors, state %s is accepted", jupiterState.c_str());
    return 0;
}

}
} //namespace NWebmaster
