#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/cmdline/include/cmdline.h>

#include <maps/libs/json/include/builder.h>

#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/config/include/config.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/storage/include/yt_storage.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/storage/include/detection_results.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/storage/include/tolokers_results.h>

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

#include <random>
#include <vector>
#include <fstream>
#include <sstream>

using namespace maps;
using namespace maps::geolib3;
using namespace maps::wiki::autocart::pipeline;

namespace {

static const std::string TASKS = "tasks";

std::string getPolygonString(const Polygon2& polygon) {
    std::stringstream ss;
    ss << std::setprecision(12);
    for (size_t i = 0; i < polygon.pointsNumber(); i++) {
        Point2 point = polygon.pointAt(i);
        ss << "," << point.x() << "," << point.y();
    }
    ss << "," << polygon.pointAt(0).x() << "," << polygon.pointAt(0).y();
    return ss.str();
}

std::string getBldImageURL(const Building& bld) {
    static const std::string URL_PREFIX = "https://static-maps.yandex.ru/1.x/?l=sat&pl=c:7FFF00,w:4";
    return URL_PREFIX + getPolygonString(bld.toGeodeticGeom());
}

std::string getMapImageURL(const Building& bld) {
    static const std::string URL_PREFIX = "https://static-maps.yandex.ru/1.x/?l=sat&pl=c:7FFF00,w:0";
    return URL_PREFIX + getPolygonString(bld.toGeodeticGeom());
}

template <typename Result>
json::Value resultToTask(const Result& result) {
    static const std::string INPUT_VALUES = "inputValues";
    static const std::string BLD_IMAGE = "bld_image";
    static const std::string MAP_IMAGE = "map_image";

    std::stringstream ss;
    json::Builder builder(ss);
    builder << [&](json::ObjectBuilder b) {
        b[INPUT_VALUES] = [&](json::ObjectBuilder b) {
            result.bld.toJson(b);
            b[RESULT_ID] = result.id;
            b[BLD_IMAGE] = getBldImageURL(result.bld);
            b[MAP_IMAGE] = getMapImageURL(result.bld);
        };
    };
    return json::Value::fromString(ss.str());
}

std::vector<json::Value> loadGoldenTasks(const std::string& path) {
    json::Value values = json::Value::fromFile(path);
    return {values.begin(), values.end()};
}

void suiteToJson(
    const std::vector<json::Value>& realTasks,
    const std::vector<json::Value>& goldenTasks,
    json::ObjectBuilder& builder,
    std::mt19937& rndGen)
{
    std::vector<json::Value> tasks;
    tasks.insert(tasks.end(), realTasks.begin(), realTasks.end());
    tasks.insert(tasks.end(), goldenTasks.begin(), goldenTasks.end());
    std::shuffle(tasks.begin(), tasks.end(), rndGen);

    builder[TASKS] = [&](json::ArrayBuilder b) {
        for (const json::Value& task : tasks) {
            b << task;
        }
    };
}

std::vector<json::Value> getRandomGoldenTasks(
    const std::vector<json::Value>& goldenTasks,
    size_t tasksCount,
    std::mt19937& rndGen)
{
    std::vector<size_t> indices;
    for (size_t i = 0; i < std::max(tasksCount, goldenTasks.size()); i++) {
        indices.push_back(i % goldenTasks.size());
    }
    std::shuffle(indices.begin(), indices.end(), rndGen);

    std::vector<json::Value> tasks;
    for (size_t i = 0; i < tasksCount; i++) {
        tasks.push_back(goldenTasks[indices[i]]);
    }

    return tasks;
}

void prepareSuites(
    const std::vector<json::Value>& realTasks,
    const std::vector<json::Value>& goldenTasks,
    const AssessorsConfig& assessorsConfig,
    size_t randomSeed,
    const std::string& suitesJsonPath)
{
    std::mt19937 rndGen{randomSeed};

    std::ofstream ofs(suitesJsonPath);
    REQUIRE(ofs.is_open(), "Failed to open file: " + suitesJsonPath);
    json::Builder builder(ofs);
    builder << [&](json::ArrayBuilder b) {
        size_t suiteIndex = 0;
        auto suiteBegin = realTasks.cbegin();
        while (suiteBegin != realTasks.cend()) {
            std::vector<json::Value> goldenTasksSuite;
            if (suiteIndex % assessorsConfig.goldenSuitesFrequency() == 0) {
                goldenTasksSuite = getRandomGoldenTasks(
                    goldenTasks,
                    assessorsConfig.goldenTasksCount(),
                    rndGen
                );
            }
            auto suiteEnd = std::min(
                realTasks.cend(),
                suiteBegin + assessorsConfig.tasksCount() - goldenTasksSuite.size()
            );
            std::vector<json::Value> realTasksSuite(suiteBegin, suiteEnd);
            b << [&](json::ObjectBuilder b) {
                suiteToJson(realTasksSuite, goldenTasksSuite, b, rndGen);
            };

            suiteIndex++;
            suiteBegin = suiteEnd;
        }
    };
    ofs.close();
}

template <typename Result>
std::vector<maps::json::Value> prepareTasks(
    YTStorageClient& storage,
    const std::string& region,
    uint64_t issueId)
{
    INFO() << "Loading " << Result::getName() << " results:"
           << " region - " << region << ", issue id - " << issueId;
    std::vector<Result> results;
    storage.getResults(TString(region), issueId, &results);
    INFO() << "Loaded " << results.size() << " results";

    INFO() << "Removing rejected results";
    results.erase(
        std::remove_if(
            results.begin(), results.end(),
            [](const Result& result) {
                return result.state != TolokaState::Yes;
            }
        ),
        results.end()
    );
    REQUIRE(!results.empty(), "Failed to prepare tasks with empty input");
    INFO() << results.size() << " results left";

    INFO() << "Converting results to tasks json";
    std::vector<maps::json::Value> tasks;
    for (const Result& result : results) {
        tasks.push_back(resultToTask(result));
    }
    return tasks;
}

} // namespace

int main(int argc, const char** argv)
try {
    NYT::Initialize(argc, argv);

    maps::cmdline::Parser parser("Prepare assessors suites");

    maps::cmdline::Option<std::string> ytConfigPath = parser.string("yt_config")
        .required()
        .help("Path to YT config");

    maps::cmdline::Option<std::string> assessorsConfigPath = parser.string("assessors_config")
        .required()
        .help("Path to assessors config");

    maps::cmdline::Option<std::string> region = parser.string("region")
        .required()
        .help("MPRO region name");

    maps::cmdline::Option<size_t> issueId = parser.size_t("issue_id")
        .required()
        .help("issue id of satellite factory");

    maps::cmdline::Option<std::string> goldenTasksPath = parser.string("golden_tasks")
        .required()
        .help("Path to json with golden tasks");

    maps::cmdline::Option<bool> useTolokers = parser.flag("use_tolokers")
        .defaultValue(false)
        .help("Use tolokers results");

    maps::cmdline::Option<std::string> suitesJsonPath = parser.string("suites")
        .required()
        .help("Path to output file with assessors suites");

    maps::cmdline::Option<size_t> randomSeed = parser.size_t("seed")
        .required()
        .help("Seed for random number generator");

    parser.parse(argc, const_cast<char**>(argv));

    INFO() << "Loading YT storage config: " << ytConfigPath;
    YTConfig ytConfig(ytConfigPath);

    INFO() << "Loading assessors config: " << assessorsConfigPath;
    AssessorsConfig assessorsConfig(assessorsConfigPath);

    INFO() << "Create YT client: yt::hahn";
    NYT::IClientPtr client = NYT::CreateClient("hahn");

    INFO() << "Create YT storage client: " << ytConfig.storagePath();
    YTStorageClient storage(client, TString(ytConfig.storagePath()));

    INFO() << "Loading golden tasks: " << goldenTasksPath;
    std::vector<maps::json::Value> goldenTasks = loadGoldenTasks(goldenTasksPath);
    INFO() << "Loaded " << goldenTasks.size() << " golden tasks";

    std::vector<maps::json::Value> tasks;
    if (useTolokers) {
        INFO() << "Preparing tasks from tolokers results";
        tasks = prepareTasks<TolokersResult>(storage, region, issueId);
    } else {
        INFO() << "Preparing suites from detection results";
        tasks = prepareTasks<DetectionResult>(storage, region, issueId);
    }

    INFO() << "Preparing suites: " << suitesJsonPath;
    prepareSuites(tasks, goldenTasks, assessorsConfig, randomSeed, suitesJsonPath);

    INFO() << "Finishing";

    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    INFO() << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    INFO() << e.what();
    return EXIT_FAILURE;
}
catch (...) {
    INFO() << "Caught unknown exception";
    return EXIT_FAILURE;
}
