#include <mapreduce/yt/interface/client.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/common/include/base64.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/http/include/http.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/retry.h>

static const TString COLUMN_NAME_DATE = "date";
static const TString COLUMN_NAME_FEATURE_ID = "feature_id";
static const TString COLUMN_NAME_HEADING = "heading";
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_BBOX = "bbox";
static const TString JSON_NAME_IMAGES_SET = "images_with_objects";
static const TString JSON_NAME_NUM = "num";
static const TString JSON_NAME_OBJECTS = "objects";
static const TString JSON_NAME_SOURCE = "source";
static const TString JSON_NAME_TYPE = "type";

auto loadBase64Data(const maps::http::URL& url)
{
    static maps::http::Client httpClient;
    maps::http::Request request{httpClient, maps::http::GET, url};
    auto response = request.perform();
    if (response.status() == 200) {
        std::string blob = response.readBody();
        return maps::base64Encode(blob);
    } else {
        throw maps::Exception{} << "Unexpected status: " << response.status()
                                << " from: " << url;
    }
}

template <typename Functor>
auto retry(Functor&& f) -> decltype(f())
{
    return maps::mrc::common::retryOnException<std::exception>(
        maps::mrc::common::RetryPolicy()
            .setInitialTimeout(std::chrono::minutes(1))
            .setMaxAttempts(20)
            .setTimeoutBackoff(1),
        std::forward<Functor>(f));
}

NYT::TNode bboxToTNode(const maps::json::Value& bboxJson)
{
    return NYT::TNode::CreateList()
        .Add(NYT::TNode::CreateList()
                 .Add(bboxJson[0][0].as<int64_t>())
                 .Add(bboxJson[0][1].as<int64_t>()))
        .Add(NYT::TNode::CreateList()
                 .Add(bboxJson[1][0].as<int64_t>())
                 .Add(bboxJson[1][1].as<int64_t>()));
}

NYT::TNode objectToTNode(const maps::json::Value& objJson)
{
    // clang-format off
    return NYT::TNode()
        (JSON_NAME_NUM, NYT::TNode(objJson[JSON_NAME_NUM].as<std::string>()))
        (JSON_NAME_TYPE, NYT::TNode(objJson[JSON_NAME_TYPE].as<std::string>()))
        (JSON_NAME_BBOX, bboxToTNode(objJson[JSON_NAME_BBOX]));
    // clang-format on
}

NYT::TNode objectsToTNode(const maps::json::Value& objsJson)
{
    NYT::TNode result = NYT::TNode::CreateList();
    for (const maps::json::Value& objJson : objsJson) {
        result.Add(objectToTNode(objJson));
    }
    return result;
}

NYT::TNode itemToTNode(const maps::json::Value& itemJson)
{
    auto url = itemJson[JSON_NAME_SOURCE].as<std::string>();
    auto base64Data = retry([&] { return loadBase64Data(url); });
    // clang-format off
    return NYT::TNode()
        (COLUMN_NAME_FEATURE_ID, 0)
        (COLUMN_NAME_DATE, "")
        (COLUMN_NAME_HEADING, 0.)
        (COLUMN_NAME_IMAGE, NYT::TNode(base64Data))
        (COLUMN_NAME_LAT, 0.)
        (COLUMN_NAME_LON, 0.)
        (COLUMN_NAME_OBJECTS, objectsToTNode(itemJson[JSON_NAME_OBJECTS]));
    // clang-format on
}

void writeJsonDataset(
    NYT::TTableWriterPtr<NYT::TNode>& writer, const std::string& path)
{
    INFO() << "Reading file " << path;
    auto fileJson = maps::json::Value::fromFile(path);
    size_t processedItems = 0;
    for (const auto& item : fileJson[JSON_NAME_IMAGES_SET]) {
        writer->AddRow(itemToTNode(item));
        ++processedItems;
        if (processedItems % 1000 == 0) {
            INFO() << "Uploaded " << processedItems << " items";
        }
    }
    INFO() << "Uploaded " << processedItems << " items";
}

NYT::TNode createSchema()
{
    return NYT::TNode::CreateList()
        .Add(NYT::TNode()("name", COLUMN_NAME_FEATURE_ID)("type", "int64"))
        .Add(NYT::TNode()("name", COLUMN_NAME_DATE)("type", "string"))
        .Add(NYT::TNode()("name", COLUMN_NAME_HEADING)("type", "double"))
        .Add(NYT::TNode()("name", COLUMN_NAME_IMAGE)("type", "string"))
        .Add(NYT::TNode()("name", COLUMN_NAME_LAT)("type", "double"))
        .Add(NYT::TNode()("name", COLUMN_NAME_LON)("type", "double"))
        .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 house numbers dataset from file to YT table");
    auto intputFileName
        = parser.string("input").required().help("Input file path");
    auto outputTableName
        = parser.string("output").required().help("Output YT table name");
    parser.parse(argc, const_cast<char**>(argv));
    INFO() << "Connecting to yt::hahn";
    auto client = NYT::CreateClient("hahn");
    INFO() << "Create table " << outputTableName;
    auto outRPath = NYT::TRichYPath(outputTableName.c_str());
    client->Create(outRPath.Path_, NYT::NT_TABLE,
        NYT::TCreateOptions().IgnoreExisting(false).Attributes(
            NYT::TNode()("schema", createSchema())));
    auto writer = client->CreateTableWriter<NYT::TNode>(outRPath);
    writeJsonDataset(writer, intputFileName);
    writer->Finish();
    INFO() << "Done";
    return EXIT_SUCCESS;
} 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;
}
