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

#include <maps/wikimap/mapspro/services/autocart/libs/geometry/include/hex_wkb.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/toloka/include/utils.h>

#include <maps/wikimap/mapspro/services/mrc/libs/config/include/config.h>

#include <maps/libs/chrono/include/time_point.h>

#include <util/string/split.h>
#include <util/string/join.h>
#include <util/string/cast.h>
#include <util/string/strip.h>

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

#include <map>
#include <regex>
#include <vector>
#include <fstream>

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

namespace {

constexpr const char* DATE_FORMAT = "%Y-%m-%d_%H-%M-%S";

// Записывает координаты полигона в строку в следующем формате:
// x0,y0,x1,y1,...,xn,yn,x0,y0
std::string getPolygonStr(const geolib3::Polygon2& polygon) {
    std::vector<std::string> coords;
    for (size_t i = 0; i < polygon.pointsNumber(); i++) {
        const geolib3::Point2& point = polygon.pointAt(i);
        coords.push_back(std::to_string(point.x()));
        coords.push_back(std::to_string(point.y()));
    }
    const geolib3::Point2& firstPoint = polygon.pointAt(0);
    coords.push_back(std::to_string(firstPoint.x()));
    coords.push_back(std::to_string(firstPoint.y()));

    return JoinRange(",", coords.begin(), coords.end());
}

// загружает данные по указанному url
std::string loadData(http::Client& client, const std::string& url)
{
    for (size_t tryN = 0; tryN < 5; ++tryN) try {
        http::Request request(client, http::GET, http::URL(url));
        auto response = request.perform();

        if (response.status() == 200) {
            return response.readBody();
        }
    } catch (const maps::Exception& error) {
        ERROR() << error;
    }

    throw RuntimeError() << "Can't load image " << url;
}

// Загружает картинку из Static API. На картинке зеленым цветом (7FFF00)
// обведен полигон здания (w:4)
std::string loadBldImage(http::Client& client, const geolib3::Polygon2& polygon) {
    static const std::string prefix = "https://static-maps.yandex.ru/1.x/?l=sat&pl=c:7FFF00,w:4,";
    return loadData(client, prefix + getPolygonStr(polygon));
}

// Загружает картинку из Static API. На картинке нет полигона (w:0)
std::string loadMapImage(http::Client& client, const geolib3::Polygon2& polygon) {
    static const std::string prefix = "https://static-maps.yandex.ru/1.x/?l=sat&pl=c:7FFF00,w:0,";
    return loadData(client, prefix + getPolygonStr(polygon));
}

// Скачивает картинку с полигоном и загружает ее в публичный MDS
std::string uploadBldImage(
    mds::Mds& mds, const std::string& date, http::Client& httpClient,
    int64_t bldId, const geolib3::Polygon2& polygon)
{
    std::string mdsPath = "domovoi_golden_task_" + date + "/" + std::to_string(bldId) + "_bld.jpg";
    return mds.makeReadUrl(mds.post(mdsPath, loadBldImage(httpClient, polygon)).key());
}

// Скачивает картинку без полигона и загружает ее в публичный MDS
std::string uploadMapImage(
    mds::Mds& mds, const std::string& date, http::Client& httpClient,
    int64_t bldId, const geolib3::Polygon2& polygon)
{
    std::string mdsPath = "domovoi_golden_task_" + date + "/" + std::to_string(bldId) + "_map.jpg";
    return mds.makeReadUrl(mds.post(mdsPath, loadMapImage(httpClient, polygon)).key());
}

using FTTypeDict = std::map<std::string, int64_t>;

// Загружает словарь соответствий между названием типа здания
// и его числовым значением. Название и число разделены TAB'ом
FTTypeDict loadFTTypeDict(const std::string& dictPath) {
    std::ifstream ifs(dictPath);
    REQUIRE(ifs.is_open(), "Failed to open file: " + dictPath);

    FTTypeDict ftTypeDict;

    while (!ifs.eof()) {
        std::string line;
        std::getline(ifs, line);

        if (line.empty()) {
            continue;
        }

        std::vector<std::string> items;
        StringSplitter(line).Split('\t').Collect(&items);
        REQUIRE(items.size() == 2u, "Invalid dictionary format: " + line);

        ftTypeDict[items[0]] = FromString<int64_t>(items[1]);
    }

    return ftTypeDict;
}

struct Values {
    // ограничения по типу: 101,104,222
    std::string ftTypeIdsCond;
    // ограничения по высоте: 3-6
    std::string heightCond;
};

// Извлекаем ограничея по типу зданий
// Пример:
//   Вход: жилое,сооружение
//   Выход: 101,106
std::string extractFTTypeIdsCond(
    const std::string& condLine,
    const FTTypeDict& ftTypeDict)
{
    std::vector<std::string> items;
    StringSplitter(condLine).Split(',').Collect(&items);
    REQUIRE(!items.empty(), "Ft type condition can not be empty");

    std::set<int64_t> ftTypeIds;

    for (std::string item : items) {
        item = StripString(item);

        auto it = ftTypeDict.find(item);
        REQUIRE(it != ftTypeDict.end(), "Unknown ft type: " + item);

        ftTypeIds.insert(it->second);
    }

    return JoinRange(",", ftTypeIds.begin(), ftTypeIds.end());
}

// Извлекаем ограничения на высоту
// Пример:
//   Вход: 3-6м
//   Выход: 3-6
std::string extractHeightCond(const std::string& heightCond) {
    std::string cond;

    for (char c : heightCond) {
        if (std::isdigit(c) || c == '-') {
            cond += c;
        }
    }

    REQUIRE(
        std::regex_match(cond, std::regex("[0-9]+")) ||
        std::regex_match(cond, std::regex("[0-9]+\\-[0-9]+")),
        "Incorrect height cond: " + heightCond
    );

    return cond;
}

// Загружаем ограничения из tsv файла
std::map<int64_t, Values> loadValues(
    const std::string& tsvPath,
    const FTTypeDict& ftTypeDict)
{
    std::ifstream ifs(tsvPath);
    REQUIRE(ifs.is_open(), "Failed to open file: " + tsvPath);

    std::map<int64_t, Values> bldIdToValues;

    while (!ifs.eof()) {
        std::string line;
        std::getline(ifs, line);

        if (line.empty()) {
            continue;
        }

        std::vector<std::string> items;
        StringSplitter(line).Split('\t').Collect(&items);
        REQUIRE(items.size() == 3u, "Invalid dataset format: " + line);

        int64_t bldId = extractBldId(http::URL(items[0]));

        Values values;
        values.ftTypeIdsCond = extractFTTypeIdsCond(items[1], ftTypeDict);
        values.heightCond = extractHeightCond(items[2]);

        bldIdToValues[bldId] = values;
    }

    return bldIdToValues;
}

// Выгружаем полигоны зданий из выгрузки на YT
std::map<int64_t, geolib3::Polygon2> loadPolygons(
    const std::map<int64_t, Values>& bldIdToValues)
{
    static const TString LATEST_PATH = "//home/maps/core/garden/stable/ymapsdf/latest";

    NYT::IClientPtr client = NYT::CreateClient("hahn");
    NYT::ITransactionPtr txn = client->StartTransaction();

    NYT::TTempTable bldGeomTable(txn);

    INFO() << "Sorting bld geom table";
    NYT::TSortOperationSpec sortSpec;
    for (const NYT::TNode& regionNode : client->List(LATEST_PATH)) {
        TString region = regionNode.AsString();
        TString tablePath = LATEST_PATH + "/" + region + "/bld_geom";
        sortSpec.AddInput(tablePath);
    }
    NYT::TTableSchema sortSchema = NYT::TTableSchema().Strict(false).UniqueKeys(false);
    sortSpec.Output(NYT::TRichYPath(bldGeomTable.Name()).Schema(sortSchema))
            .SortBy({"bld_id"});
    txn->Sort(sortSpec);
    INFO() << "Table has been sorted";

    std::map<int64_t, geolib3::Polygon2> bldIdToPolygon;

    NYT::TTableReaderPtr<NYT::TNode> reader
        = txn->CreateTableReader<NYT::TNode>(bldGeomTable.Name());

    for (const auto& [bldId, values] : bldIdToValues) {
        NYT::TRichYPath range(bldGeomTable.Name());
        range.AddRange(
            NYT::TReadRange().Exact(NYT::TReadLimit().Key({bldId}))
        );

        NYT::TTableReaderPtr<NYT::TNode> reader
            = txn->CreateTableReader<NYT::TNode>(range);

        REQUIRE(reader->IsValid(), "Failed load bld: " + std::to_string(bldId));
        const NYT::TNode row = reader->GetRow();

        std::string hexWKB = row["shape"].AsString();
        geolib3::Polygon2 polygon = hexWKBToPolygon(hexWKB);
        bldIdToPolygon[bldId] = polygon;
    }

    return bldIdToPolygon;
}

// Формируем json файл, который можно загружать в толоку.
// Все необходимые картинки загружаются в публичный mds
void saveTasks(
    const std::map<int64_t, Values>& bldIdToValues,
    const std::map<int64_t, geolib3::Polygon2>& bldIdToPolygon,
    mds::Mds& publicMds,
    const std::string outputJsonPath)
{
    chrono::TimePoint now = chrono::TimePoint::clock::now();
    std::string date = chrono::formatIntegralDateTime(now, DATE_FORMAT);

    http::Client httpClient;

    std::ofstream ofs(outputJsonPath);
    REQUIRE(ofs.is_open(), "Failed to create file: " + outputJsonPath);

    json::Builder builder(ofs);

    builder << [&](json::ArrayBuilder b) {
        for (const auto& bldIdAndValues : bldIdToValues) {
            int64_t bldId = bldIdAndValues.first;
            const Values& values = bldIdAndValues.second;
            const geolib3::Polygon2 polygon = bldIdToPolygon.at(bldId);

            b << [&](json::ObjectBuilder b) {
                b["inputValues"] = [&](json::ObjectBuilder b) {
                    b["result_id"] = bldId;
                    b["shape"] << [&](json::ArrayBuilder b) {
                        for (size_t i = 0; i < polygon.pointsNumber(); i++) {
                            const geolib3::Point2& point = polygon.pointAt(i);
                            b << [&](json::ArrayBuilder b) {
                                b << point.x() << point.y();
                            };
                        }
                    };
                    b["bld_image"] = uploadBldImage(publicMds, date, httpClient, bldId, polygon);
                    b["map_image"] = uploadMapImage(publicMds, date, httpClient, bldId, polygon);
                };

                b["knownSolutions"] = [&](json::ArrayBuilder b) {
                    b << [&](json::ObjectBuilder b) {
                        b["outputValues"] = [&](json::ObjectBuilder b) {
                            b["state"] = "yes";
                            b["ft_type_id"] = values.ftTypeIdsCond;
                            b["height"] = values.heightCond;
                        };
                        b["weight"] = 1;
                    };
                };
            };
        }
    };

    ofs.close();
}

} // namespace

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

    maps::cmdline::Parser parser("Create JSON file with golden tasks");

    maps::cmdline::Option<std::string> inputTSVPath = parser.string("input")
        .required()
        .help("input tsv file with tasks");

    maps::cmdline::Option<std::string> ftTypeDictPath = parser.string("ft_type_dict")
        .required()
        .help("input tsv file with ft type dictionary");

    cmdline::Option<std::string> mrcConfigPath = parser.string("mrc_config")
        .required()
        .help("Path to mrc config");

    cmdline::Option<std::string> secretVersion = parser.string("secret_version")
        .required()
        .help("version for secrets from yav.yandex-team.ru");

    maps::cmdline::Option<std::string> outputJsonPath = parser.string("output")
        .required()
        .help("output json file with tasks");

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

    INFO() << "Loading mrc config";
    mrc::common::Config mrcConfig
        = mrc::common::Config(vault_boy::loadContextWithYaVault(secretVersion), mrcConfigPath);

    INFO() << "Loading ft type dictionary: " << ftTypeDictPath;
    FTTypeDict ftTypeDict = loadFTTypeDict(ftTypeDictPath);
    INFO() << "Dictionary size: " << ftTypeDict.size();

    INFO() << "Loading dataset: " << inputTSVPath;
    std::map<int64_t, Values> bldIdToValues = loadValues(inputTSVPath, ftTypeDict);
    INFO() << "Dataset size: " << bldIdToValues.size();

    INFO() << "Loading polygons";
    std::map<int64_t, geolib3::Polygon2> bldIdToGeoPolygon = loadPolygons(bldIdToValues);
    INFO() << "Loaded " << bldIdToGeoPolygon.size() << " polygons";

    INFO() << "Create public mds client";
    mds::Mds publicMds = mrcConfig.makePublicMdsClient();

    INFO() << "Saving tasks: " << outputJsonPath;
    saveTasks(bldIdToValues, bldIdToGeoPolygon, publicMds, outputJsonPath);

    INFO() << "Done!";

    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;
}
