#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/json/include/value.h>
#include <library/cpp/yson/node/node_io.h>
#include <mapreduce/yt/interface/client.h>

#include <string>
#include <vector>


static const TString TABLE_ATTR_NAME_COMMIT_ID = "mapCommitID";

static const TString COLUMN_NAME_FEATURE_ID = "feature_id";
static const TString COLUMN_NAME_IMAGE      = "image";
static const TString COLUMN_NAME_LAT        = "lat";
static const TString COLUMN_NAME_LON        = "lon";
static const TString COLUMN_NAME_OBJECTS    = "objects";

static const TString JSON_NAME_COMMIT_ID    = "commit_id";
static const TString JSON_NAME_HYPOTHESES   = "hypotheses";
static const TString JSON_NAME_FEATURE_ID   = "feature_id";
static const TString JSON_NAME_IMAGE        = "image";
static const TString JSON_NAME_LAT          = "lat";
static const TString JSON_NAME_LON          = "lon";
static const TString JSON_NAME_OBJECTS      = "objects";


NYT::TNode itemToTNode(const maps::json::Value& itemJson)
{
    std::string image = itemJson[JSON_NAME_IMAGE].as<std::string>();

    std::stringstream ss;
    {
        maps::json::Builder builderObjects(ss);
        builderObjects << itemJson[JSON_NAME_OBJECTS];
    }

    NYT::TNode result;
    result(COLUMN_NAME_FEATURE_ID, itemJson[JSON_NAME_FEATURE_ID].as<int64_t>())
          (COLUMN_NAME_IMAGE, NYT::TNode(image))
          (COLUMN_NAME_LAT, itemJson[JSON_NAME_LAT].as<double>())
          (COLUMN_NAME_LON, itemJson[JSON_NAME_LON].as<double>())
          (COLUMN_NAME_OBJECTS, NYT::NodeFromJsonString(ss.str()));
    return result;
}

void writeJsonDataset(const maps::json::Value& fileJson, NYT::TTableWriterPtr<NYT::TNode>& writer, int limit)
{
    int processedItems = 0;
    for(const auto& item : fileJson[JSON_NAME_HYPOTHESES])
    try {
        if (limit && processedItems >= limit) {
            break;
        }
        writer->AddRow(itemToTNode(item));
        ++processedItems;
        if (processedItems % 1000 == 0) {
            INFO() << "Uploaded " << processedItems << " items";
        }
    } catch (const maps::Exception& ex) {
        WARN() << ex;
    }
}

NYT::TNode createSchema() {
    return NYT::TNode::CreateList()
                .Add(NYT::TNode()("name", COLUMN_NAME_FEATURE_ID)("type", "int64"))
                .Add(NYT::TNode()("name", COLUMN_NAME_LAT)("type", "double"))
                .Add(NYT::TNode()("name", COLUMN_NAME_LON)("type", "double"))
                .Add(NYT::TNode()("name", COLUMN_NAME_IMAGE)("type", "string"))
                .Add(NYT::TNode()("name", COLUMN_NAME_OBJECTS)("type", "any"));
}


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

    maps::cmdline::Parser parser("Uploads ground truth dataset with images linked to address points to YT");

    auto intputFileName = parser.string("input")
        .required()
        .help("Input json file path");

    auto outputTableName = parser.string("output")
        .required()
        .help("Output YT table name");

    auto append = parser.flag("append")
        .help("append data to table (by default table rewrited)");

    auto limit = parser.num("limit");

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

    INFO() << "Connecting to yt::hahn";
    auto client = NYT::CreateClient("hahn");

    INFO() << "Reading file " << intputFileName;
    auto fileJson = maps::json::Value::fromFile(intputFileName);

    int64_t commitId = fileJson[JSON_NAME_COMMIT_ID].as<int64_t>();
    INFO() << "Commit ID: " << commitId;

    INFO() << "Opening table " << outputTableName;
    if (append) {
        TString attr = outputTableName.c_str();
        attr += "/@" + TABLE_ATTR_NAME_COMMIT_ID;
        INFO() << "YT table commitID: " << client->Get(attr).AsInt64();
        REQUIRE(commitId == client->Get(attr).AsInt64(),
            "Unable to append data to the YT table with difference maps commit ID");
    } else {
        client->Create(
            outputTableName.c_str(),
            NYT::NT_TABLE,
            NYT::TCreateOptions()
                .Recursive(true)
                .Attributes(
                    NYT::TNode()
                    ("schema", createSchema())
                    (TABLE_ATTR_NAME_COMMIT_ID, commitId)
                )
        );
    }

    NYT::TRichYPath outRPath = NYT::TRichYPath(outputTableName.c_str()).Append(append);
    auto writer = client->CreateTableWriter<NYT::TNode>(outRPath);
    writeJsonDataset(fileJson, writer, limit);
    writer->Finish();

    client->Sort(
        NYT::TSortOperationSpec()
        .AddInput(outputTableName.c_str())
        .Output(outputTableName.c_str())
        .SortBy({COLUMN_NAME_FEATURE_ID}));
}
catch (const maps::Exception& e) {
    FATAL() << "Worker failed: " << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    FATAL() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
