#include <maps/libs/json/include/builder.h>
#include <maps/libs/xml/include/xml.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <yandex/maps/pgpool3utils/dynamic_pool_holder.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <pqxx/pqxx>

#include <iostream>

constexpr size_t DOUBLE_PRECISION = 12;

struct RevisionRecord
{
    explicit RevisionRecord(const pqxx::row& row)
        : objectId(row["object_id"].as<uint64_t>())
        , commitId(row["commit_id"].as<uint64_t>())
        , prevCommitId(row["prev_commit_id"].as<uint64_t>())
        , nextCommitId(row["next_commit_id"].as<uint64_t>())
        , deleted(row["deleted"].as<bool>())
        , geometryId(row["geometry_id"].as<uint64_t>())
        , attributesId(row["attributes_id"].as<uint64_t>())
        , descriptionId(row["description_id"].as<uint64_t>())
        , masterObjectId(row["master_object_id"].as<uint64_t>())
        , slaveObjectId(row["slave_object_Id"].as<uint64_t>())
    {}

    uint64_t objectId;
    uint64_t commitId;
    uint64_t prevCommitId;
    uint64_t nextCommitId;
    bool deleted;
    uint64_t geometryId;
    uint64_t attributesId;
    uint64_t descriptionId;
    uint64_t masterObjectId;
    uint64_t slaveObjectId;
};

struct AttributesAndPoint
{
    explicit AttributesAndPoint(const pqxx::row& row)
    {
        auto parsed = maps::json::Value::fromString(row["attributes"].as<std::string>());
        for (const auto& field : parsed.fields()) {
            idValue.emplace(field, parsed[field].as<std::string>());
        }
    }
    std::map<std::string, std::string> idValue;
    std::optional<uint64_t> geometryId;
};

struct Coord
{
    double lon;
    double lat;
    int64_t x;
    int64_t y;
};

const auto MOSCOW = Coord {
    37.627484,
    55.753457,
    0,
    0
};

std::vector<Coord>
parseCoords(const std::string& coordsPairsString)
try {

    std::vector<Coord> coords;
    const auto array = maps::json::Value::fromString(coordsPairsString);
    for (size_t i = 0; i < array.size(); ++i) {
        auto pairValue = array[i];
        coords.emplace_back(
            Coord {
                .lon = pairValue[0][0].as<double>(),
                .lat = pairValue[0][1].as<double>(),
                .x = static_cast<int64_t>(pairValue[1][0].as<double>()),
                .y = static_cast<int64_t>(pairValue[1][1].as<double>())
            }
        );
    }
    return coords;
} catch (const std::exception& ex) {
    std::cerr << "EXCEPTION " << ex.what() << " while parsing coords: " << coordsPairsString << std::endl;
    return {};
}


std::string
mercatorPoint(const Coord& coord)
{
    std::ostringstream os;
    os.precision(DOUBLE_PRECISION);
    os
        << "ST_Transform(ST_SetSRID(ST_MakePoint("
        << coord.lon
        << ", "
        << coord.lat << "), 4326),3395)";
    return os.str();
}

std::string
toString(const std::vector<Coord>& coords)
{
    maps::json::Builder builder;
    builder.setDoublePrecision(DOUBLE_PRECISION);
    builder << [&](maps::json::ArrayBuilder builder) {
        for (const auto& coord : coords) {
            builder << [&](maps::json::ArrayBuilder builder) {
                builder << [&](maps::json::ArrayBuilder builder) {
                    builder << coord.lon;
                    builder << coord.lat;
                };
                builder << [&](maps::json::ArrayBuilder builder) {
                    builder << coord.x;
                    builder << coord.y;
                };
            };
        }
    };
    return builder.str();
}

std::string
toHstoreString(pqxx::transaction_base& txn, std::map<std::string, std::string> idValue)
{
    std::string result;
    result += "hstore(ARRAY[";
    result += maps::wiki::common::join(idValue,
        [&](const auto pair) {
            return txn.quote(pair.first) + "," + txn.quote(pair.second);
        },
        ",");
    result += "])";
    return result;
}

int
main(int argc, char** argv)
try {
    maps::cmdline::Parser argsParser(argv[0]);
    auto commitResults = argsParser.flag("commit")
        .help("Commit results");
    auto configPath = argsParser.string("config")
        .help("Path to config.xml with database connection settings")
        .metavar("<path>");
    auto putToMoscow = argsParser.flag("moscow")
        .help("put empty coords to moscow");
    auto forceCommit = argsParser.flag("force")
        .help("Force commit invalid results. Don't use in production!!!");
    argsParser.parse(argc, argv);

    if (!configPath.defined()) {
        argsParser.printUsageAndExit(EXIT_FAILURE);
    }
    std::cout << "Config file: " << configPath << std::endl;

    auto configDoc = maps::xml3::Doc::fromFile(configPath);
    maps::pgp3utils::DynamicPoolHolder poolHolder(configDoc.node("/config//database[@id='core']"), "core");
    auto coreTxn = poolHolder.pool().masterWriteableTransaction();
    auto rows = coreTxn->exec(
        "SELECT r.*, hstore_to_json(a.contents) as attributes FROM revision.object_revision_without_geometry r, revision.attributes a "
        "WHERE id=attributes_id and contents ?| ARRAY['cat:image_overlay', 'cat:image_overlay_public'] ORDER BY commit_id, object_id");
    const auto affectedRowsCount = rows.size();
    std::cout << "Found image_overlay/public revisions: " << affectedRowsCount << std::endl;

    std::vector<RevisionRecord> revisions;
    revisions.reserve(affectedRowsCount);
    std::map<uint64_t, AttributesAndPoint> idToAttributes;
    for (const auto& row : rows) {
        revisions.emplace_back(row);
        idToAttributes.emplace(revisions.back().attributesId, row);
    }
    std::cout << "Unique attributes: " << idToAttributes.size() << std::endl;
    for (auto& [id, attributesAndPoint] : idToAttributes) {
        auto coords = parseCoords(attributesAndPoint.idValue["image_overlay:method_parameters"]);
        if (coords.empty()) {
            std::cerr << "ERROR: Empty/broken coords array id: " << id << std::endl;
            if (putToMoscow.defined()) {
                coords.emplace_back(MOSCOW);
            } else {
                return EXIT_FAILURE;
            }
        }
        const auto originCoord = coords[0];
        for (auto& coord : coords) {
            coord.lon -= originCoord.lon;
            coord.lat -= originCoord.lat;
        }
        attributesAndPoint.idValue["image_overlay:method_parameters"] = toString(coords);
        std::string createGeomQuery =
            "INSERT INTO revision.geometry (contents) VALUES (" + mercatorPoint(originCoord) + ") RETURNING id";
        std::cout << createGeomQuery << std::endl;
        auto geomRows = coreTxn->exec(createGeomQuery);
        attributesAndPoint.geometryId = geomRows[0][0].as<uint64_t>();
        std::string updateAttrsQuery = "UPDATE revision.attributes SET contents="
            + toHstoreString(*coreTxn, attributesAndPoint.idValue)
            + " WHERE id=" + std::to_string(id);
        std::cout << updateAttrsQuery << std::endl;
        coreTxn->exec(updateAttrsQuery);
    }

    for (auto& revision : revisions) {
        revision.geometryId = *idToAttributes.at(revision.attributesId).geometryId;
        if (!revision.geometryId) {
            std::cerr << "ERROR no geometry generated for attributes id: " << revision.attributesId << std::endl;
            return EXIT_FAILURE;
        }
        std::string deleteRevisionQuery;
        deleteRevisionQuery =
            "DELETE FROM revision.object_revision WHERE commit_id ="
            + std::to_string(revision.commitId)
            + " AND object_id = " + std::to_string(revision.objectId);
        std::cout << deleteRevisionQuery << std::endl;
        coreTxn->exec(deleteRevisionQuery);
        std::string insertRevisionQuery;
        insertRevisionQuery =
            "INSERT INTO revision.object_revision_with_geometry "
            "(object_id, commit_id, prev_commit_id, next_commit_id, deleted, geometry_id, attributes_id, description_id, master_object_id, slave_object_Id) "
            "VALUES ("
            + std::to_string(revision.objectId) + ","
            + std::to_string(revision.commitId) + ","
            + std::to_string(revision.prevCommitId) + ","
            + std::to_string(revision.nextCommitId) + ","
            + std::string(revision.deleted ? "true" : "false") + ","
            + std::to_string(revision.geometryId) + ","
            + std::to_string(revision.attributesId) + ","
            + std::to_string(revision.descriptionId) + ","
            + std::to_string(revision.masterObjectId) + ","
            + std::to_string(revision.slaveObjectId)
            + ")";
        std::cout << insertRevisionQuery << std::endl;
        coreTxn->exec(insertRevisionQuery);
    }
    std::string affectedRowsQuery =
        "SELECT count(*) FROM revision.object_revision_with_geometry, revision.attributes "
        "WHERE contents ?| ARRAY['cat:image_overlay', 'cat:image_overlay_public'] "
        "AND id=attributes_id";
    std::cout << "Checking result." << std::endl;
    const auto resultSize = coreTxn->exec(affectedRowsQuery)[0][0].as<uint64_t>();
    if (affectedRowsCount != resultSize) {
        std::cerr << "ERROR: Table result " << resultSize << " row count doesn't match read records count." << std::endl;
        if (!forceCommit.defined()) {
            return EXIT_FAILURE;
        }
    } else {
        std::cout << "Result is valid." << std::endl;
    }
    if (commitResults.defined()) {
        coreTxn->commit();
        std::cout << "COMMITTED" << std::endl;
    }
    return EXIT_SUCCESS;
} catch (const std::exception& ex) {
    std::cerr << "EXCEPTION: " << ex.what() << std::endl;
    return EXIT_FAILURE;
}
