#include "yt_operations.h"
#include "common.h"
#include "column_names.h"
#include "table_names.h"
#include "lost_mapper.h"
#include "lost_reducer.h"
#include "lost_track_reducer.h"
#include "score_mapper.h"
#include "track_stat_mapper_reducer.h"

#include <maps/libs/log8/include/log8.h>
#include <util/generic/size_literals.h>

#include <future>

namespace maps::wiki::route_lost_feedback {

namespace {

struct ScopedLogMessage
{
    ScopedLogMessage(const char* message) : message_(message) {
        INFO() << message_ << " started";
    }

    ~ScopedLogMessage() {
        INFO() << message_ << " finished";
    }

    const char* message_;
};

NYT::TNode createOperationSpec()
{
    return NYT::TNode::CreateMap()
        ("pool", "maps-nmaps-mapreduce");
}

} // unnamed namespace


YtOperations::YtOperations(
    NYT::IClientPtr client,
    geolib3::Polygon2 polygonGeo,
    YtInputTableNames inputTables,
    YtTmpTableNames tmpTables) :
        client_(std::move(client)),
        polygonGeo_(std::move(polygonGeo)),
        inputTables_(std::move(inputTables)),
        tmpTables_(std::move(tmpTables))
{}

void YtOperations::clusterRouteLosts()
{
    ScopedLogMessage log("Clustering route-losts");

    NYT::TMapReduceOperationSpec spec;

    for (const auto& input : inputTables_.routeLost) {
        spec.AddInput<NYT::TNode>(TString(input));
    }
    spec.AddOutput<NYT::TNode>(TString(tmpTables_.routeLostClusters));
    spec.SortBy({column_names::KEY});
    spec.ReduceBy({column_names::KEY});
    spec.DataSizePerMapJob(1_GB);

    auto operation = client_->MapReduce(
        spec,
        new LostMapper(polygonGeo_),
        new LostReducer(),
        NYT::TOperationOptions()
            .Spec(createOperationSpec())
            .Wait(false)
    );

    INFO() << "RouteLost operation id: "
           << GetGuidAsString(operation->GetId());

    auto operationComplete = operation->Watch();
    operationComplete.Wait();
    operationComplete.GetValue();
}

void YtOperations::calcTrackStat()
{
    ScopedLogMessage log("Calculating tracks stat");

    NYT::TMapReduceOperationSpec spec;
    for (const auto& input: inputTables_.tracks) {
        spec.AddInput<NYT::TNode>(TString(input));
    }
    spec.AddOutput<NYT::TNode>(TString(tmpTables_.trackStat));
    spec.SortBy({column_names::PERSISTENT_ID, column_names::SEGMENT_INDEX});
    spec.ReduceBy({column_names::PERSISTENT_ID, column_names::SEGMENT_INDEX});

    auto operation = client_->MapReduce(
        spec,
        new TracksStatMapper(polygonGeo_),
        new TracksStatReducer(),
        NYT::TOperationOptions()
            .Spec(createOperationSpec())
            .Wait(false)
    );

    INFO() << "TrackStat operation id: "
           << GetGuidAsString(operation->GetId());

    auto operationComplete = operation->Watch();
    operationComplete.Wait();
    operationComplete.GetValue();
}

void YtOperations::sortRouteLostClusters()
{
    ScopedLogMessage log("Sorting route-lost clusters");

    sortByGivenСolumns(
        *client_,
        tmpTables_.routeLostClusters,
        tmpTables_.routeLostClustersSorted,
        {column_names::PERSISTENT_ID, column_names::SEGMENT_INDEX}
    );
}

void YtOperations::sortTrackStat()
{
    ScopedLogMessage log("Sorting track stat");

    sortByGivenСolumns(
        *client_,
        tmpTables_.trackStat,
        tmpTables_.trackStatSorted,
        {column_names::PERSISTENT_ID, column_names::SEGMENT_INDEX}
    );
}

void YtOperations::sortRouteLostScored()
{
    ScopedLogMessage log("Sorting route-lost scored");

    sortByGivenСolumns(
        *client_,
        tmpTables_.routeLostScored,
        tmpTables_.routeLostScoredSorted,
        {column_names::INVERTED_SCORE}
    );
}

void YtOperations::joinRouteLostAndTracks()
{
    ScopedLogMessage log("Joining route-lost and tracks stat");

    NYT::TReduceOperationSpec spec;

    spec.AddInput<NYT::TNode>(TString(tmpTables_.trackStatSorted));
    spec.AddInput<NYT::TNode>(TString(tmpTables_.routeLostClustersSorted));
    spec.AddOutput<NYT::TNode>(TString(tmpTables_.routeLostJoinTrackStat));

    spec.ReduceBy({column_names::PERSISTENT_ID, column_names::SEGMENT_INDEX});

    client_->Reduce(
        spec,
        new LostTrackReducer(),
        NYT::TOperationOptions()
            .Spec(createOperationSpec())
    );
}

void YtOperations::evalRouteLostScored()
{
    ScopedLogMessage log("Evauluating route-lost score");

    uint64_t countDays = inputTables_.routeLost.size();

    NYT::TMapOperationSpec spec;
    spec.AddInput<NYT::TNode>(TString(tmpTables_.routeLostJoinTrackStat));
    spec.AddOutput<NYT::TNode>(TString(tmpTables_.routeLostScored));

    client_->Map(
        spec,
        new ScoreMapper(countDays),
        NYT::TOperationOptions()
            .Spec(createOperationSpec())
    );
}

void YtOperations::runAll()
{
    // Clustering route losts and calculating tracks statistics
    // are the most heavy operations. Let's perform them in parallel
    //
    auto clusterRouteFut = std::async(
        std::launch::async, &YtOperations::clusterRouteLosts, this
    );

    auto calcTrackFut = std::async(
        std::launch::async, &YtOperations::calcTrackStat, this
    );

    clusterRouteFut.get();
    calcTrackFut.get();

    sortRouteLostClusters();
    sortTrackStat();
    joinRouteLostAndTracks();
    evalRouteLostScored();
    sortRouteLostScored();
}

void sortByGivenСolumns(
    NYT::IClient& client,
    const std::string& inputTable,
    const std::string& outputTable,
    const NYT::TSortColumns& columns)
{
    NYT::TSortOperationSpec spec;
    spec.AddInput(TString(inputTable));
    spec.Output(TString(outputTable));
    spec.SortBy(columns);

    client.Sort(
        spec,
        NYT::TOperationOptions()
            .Spec(createOperationSpec())
    );
}

} // namespace maps::wiki::route_lost_feedback
