#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 <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/revision/snapshot.h>
#include <pqxx/pqxx>

#include <iostream>
#include <fstream>

using namespace maps::wiki;

const revision::Attributes COMMIT_ATTRIBUTES = {
    {"action", "group-moved"},
    {"source", "long-task"}
};

struct Offset
{
    double dx;
    double dy;
};

std::string
fixString(const std::string& s)
{
    std::string ret;
    for (const auto& c : s) {
        if (c == ',') {
            ret.push_back(';');
        } else  if (c != ' ') {
            ret.push_back(c);
        }
    }
    return ret;
}

std::map<revision::DBID, Offset>
parseCSV(const std::string& path)
{
    size_t lineNum = 0;
    std::map<revision::DBID, Offset> objectIdToOffset;
    try {
        std::ifstream input(path);
        while (input.good()) {
            std::string line;
            input >> line;
            line = fixString(line);
            if (!line.empty()) {
                std::vector<std::string> parts;
                parts = common::split(line, ";");
                if (parts.size() != 3) {
                    throw maps::Exception() << "Can't parse line " << lineNum << " " << line;
                }
                objectIdToOffset.emplace(
                    std::stoll(parts[0]),
                    Offset {
                        std::stod(parts[1]),
                        std::stod(parts[2])
                    });
            }
            ++lineNum;
        }
    } catch (...) {
        std::cerr << "Exception while parsing line: " << lineNum << std::endl;
        throw;
    }
    return objectIdToOffset;
}

revision::Wkb
moveGeometry(const revision::Wkb& originalWkb, const Offset& offset)
{
    common::Geom originalGeom(originalWkb);
    auto movePoint = [&](const maps::geolib3::Point2 pt) {
        return maps::geolib3::Point2(offset.dx + pt.x(), offset.dy + pt.y());
    };
    auto movedGeom = originalGeom.transformed(movePoint);
    return movedGeom.wkb();
}

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");
    auto offsetsFile = argsParser.file("offsets")
        .help("Path to input .csv file with object_id;dx;dy structure. Offsets are in revision coord space.");
    auto uid = argsParser.string("uid")
        .help("UID of user for revision.commit");
    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);
    const auto objectIdToOffset = parseCSV(offsetsFile);
    std::set<revision::DBID> objectIds;
    for (const auto& [oid, _] : objectIdToOffset) {
        if (objectIds.contains(oid)) {
            std::cerr << "ERROR object id repeated: " << oid << std::endl;
            return EXIT_FAILURE;
        }
        objectIds.insert(oid);
    }
    std::cout << "Moving objects count:" << objectIds.size() << std::endl;

    maps::pgp3utils::DynamicPoolHolder poolHolder(configDoc.node("/config//database[@id='core']"), "core");
    auto coreTxn = poolHolder.pool().masterWriteableTransaction();
    revision::RevisionsGateway gateway(*coreTxn, revision::BranchManager(*coreTxn).loadTrunk());
    const auto headCommitId = gateway.headCommitId();
    auto snapshot = gateway.snapshot(headCommitId);
    const auto curRevisions = snapshot.objectRevisions(objectIds);
    std::vector<revision::RevisionsGateway::NewRevisionData> newDatas;
    for (const auto& [objectId, offset] : objectIdToOffset) {
        if (!curRevisions.contains(objectId)) {
            std::cerr << "ERROR object revision is not found: " << objectId << std::endl;
            return EXIT_FAILURE;
        }
        const auto& curRevision = curRevisions.at(objectId);
        if (!curRevision.data().geometry ||curRevision.data().deleted) {
            std::cerr << "ERROR object revision doesn't have geometry or is deleted: " << objectId << std::endl;
            return EXIT_FAILURE;
        }
        revision::RevisionsGateway::NewRevisionData newObjData;
        newObjData.first = curRevision.id();
        auto& newData = newObjData.second;
        const auto& curData = curRevision.data();
        newData.geometry = moveGeometry(*curData.geometry, offset);
        newData.deleted = false;
        newDatas.push_back(std::move(newObjData));
    }

    revision::Commit result = gateway.createCommit(
        newDatas,
        revision::UserID(std::stoll(uid)),
        COMMIT_ATTRIBUTES);

    auto commitId = result.id();
    if (commitResults.defined()) {
        coreTxn->commit();
        std::cout << "COMMITTED: " << commitId << std::endl;
        std::cout << "DON'T FORGET TO SYNC VIEW" << std::endl;
    } else {
        std::cout << "SUCCESS. Transaction not commited." << std::endl;
    }
    return EXIT_SUCCESS;
} catch (const maps::Exception& ex) {
    std::cerr << "EXCEPTION: " << ex << std::endl;
    return EXIT_FAILURE;
} catch (const std::exception& ex) {
    std::cerr << "EXCEPTION: " << ex.what() << std::endl;
    return EXIT_FAILURE;
}
