#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 <maps/libs/chrono/include/time_point.h>
#include <yandex/maps/pgpool3utils/dynamic_pool_holder.h>
#include <maps/libs/log8/include/log8.h>

#include <iostream>
#include <fstream>
#include <set>
#include <string>

using ID = std::string;
using Txn = pqxx::transaction_base;

const std::string DEFAULT_MAPS_WIKI_SERVICES_CONFIG_PATH = "/etc/yandex/maps/wiki/services/services.xml";
const std::string DATE_FORMAT = "%Y%m%d_%H%M";

std::set<ID>
loadIds(const std::string& fileName)
{
    std::set<ID> result;
    if (!fileName.empty()) {
        std::ifstream idsfile(fileName);
        for (ID id; idsfile >> id; ) {
            result.insert(id);
        }
    }
    return result;
}

std::string
getCategoryId(ID id, Txn& coreTxn)
{
    auto rows = coreTxn.exec(
        "SELECT contents->'rel:master' "
        "FROM revision.object_revision_relation, revision.attributes "
        "WHERE id=attributes_id AND master_object_id=" + id +
        " LIMIT 1");
    return rows.empty() ? std::string() : rows[0][0].as<std::string>();
}

struct ObjectData
{
    ID id;
    std::string categoryId;
    bool operator <(const ObjectData& other) const
    {
        return std::tie(id, categoryId) < std::tie(other.id, other.categoryId);
    }
};

std::set<std::string>
collectNames(const std::set<ObjectData>& namesData, Txn& coreTxn)
{
    std::set<std::string> result;
    for (const auto& nameData : namesData) {
        auto rows = coreTxn.exec(
            "SELECT contents->'" + nameData.categoryId + ":name' "
            "FROM revision.object_revision_without_geometry, revision.attributes "
            " WHERE id = attributes_id AND object_id = " + nameData.id);
        for (const auto& row : rows) {
            result.insert(row[0].as<std::string>());
        }
    }
    return result;
}

std::string makeBackupTableName(const std::string& originalTableName)
{
    static const auto curTime = maps::chrono::TimePoint::clock::now();
    std::tm tm;
    auto epoch = maps::chrono::TimePoint::clock::to_time_t(
        std::chrono::time_point_cast<std::chrono::system_clock::duration>(curTime));
    gmtime_r(&epoch, &tm);

    char buf[sizeof("19700101_0000")];
    auto bytesWritten
        = ::strftime(buf, sizeof(buf), DATE_FORMAT.c_str(), &tm);
    REQUIRE(bytesWritten == sizeof(buf) - 1, "Failed to format timestamp");

    std::ostringstream os;
    os << originalTableName << "_" << buf;
    return os.str();
}

void backupAndDumpRows(
    const std::string& tableName,
    const std::string& fromWhereClause,
    Txn& txn)
{
    static std::set<std::string> tablesCreated;
    const auto backupTableName = makeBackupTableName(tableName);
    if (tablesCreated.count(backupTableName)) {
        txn.exec(
            "INSERT INTO oblivion_backup." + backupTableName +
            " SELECT * " + fromWhereClause);
    } else {
        tablesCreated.insert(backupTableName);
        txn.exec("SELECT * INTO oblivion_backup." + backupTableName + " " + fromWhereClause);
    }

    auto rows = txn.exec("SELECT * " + fromWhereClause);
    if (rows.empty()) {
        return;
    }
    for (const auto& row : rows) {
        std::cout << backupTableName << ",";
        for (const auto& field : row) {
            std::cout << "," << field.name() << ":" << field.c_str();
        }
        std::cout << std::endl;
    }
}

void forget(ID id, const std::string& categoryId, Txn& coreTxn, Txn& socialTxn)
{
    // Delete name from social.commit_event
    INFO() << "Delete name from commit_event for: " << id;
    const auto wherePrimaryObjectClause = "WHERE primary_object_id=" + id;
    backupAndDumpRows(
        "commit_event",
        "FROM social.commit_event " + wherePrimaryObjectClause,
        socialTxn);
    socialTxn.exec(
        "UPDATE social.commit_event "
        "SET primary_object_label = '{{categories:" + categoryId + "}}' " +
        wherePrimaryObjectClause);

    // Collect all current and past slaves
    const std::string SLAVES_QUERY =
        "SELECT slave_object_id, contents->'rel:slave' slave_category"
        " FROM revision.object_revision_relation, revision.attributes "
        " WHERE id=attributes_id AND master_object_id";
    auto slavesRows = coreTxn.exec(
        SLAVES_QUERY + " = " + id);
    std::set<ObjectData> namesData;
    std::set<ObjectData> otherSlaves;
    std::set<ID> fcSlavesIds;
    for (const auto& row : slavesRows) {
        ObjectData data {
            row["slave_object_id"].as<std::string>(),
            row["slave_category"].as<std::string>(),
        };
        if (data.categoryId.ends_with("_nm")) {
            namesData.emplace(data);
        } else {
            otherSlaves.emplace(data);
            if (data.categoryId.ends_with("_fc")) {
                fcSlavesIds.insert(data.id);
            }
        }
    }
    // Collect names
    INFO() << "Collect names for: " << id;
    const auto names = collectNames(namesData, coreTxn);
    if (names.empty()) {
        // Object never had names.
        return;
    }
    std::cerr << "to_sync: " << id << std::endl;
    // Collect also slaves of _fc
    if (!fcSlavesIds.empty()) {
        slavesRows = coreTxn.exec(
            SLAVES_QUERY + " IN (" + maps::wiki::common::join(fcSlavesIds, ",") + ")");
        for (const auto& row : slavesRows) {
            otherSlaves.emplace(ObjectData {
                row["slave_object_id"].as<std::string>(),
                row["slave_category"].as<std::string>()});
        }
    }

    // Delete references to names
    INFO() << "Delete references to names for: " << id;
    for (const auto& nameData : namesData) {
        const auto fromWhereClause =
            "FROM revision.object_revision_relation "
            " WHERE slave_object_id = " + nameData.id +
            " AND master_object_id = " + id;
        backupAndDumpRows(
            "object_revision_relation",
            fromWhereClause,
            coreTxn);
        coreTxn.exec(
            "DELETE " + fromWhereClause);
    }
    // Delete slaves commit_event labels
    INFO() << "Delete " << id << " slaves commit_event labels";
    std::string nameCondition = "(FALSE ";
    for (const auto& name : names) {
        nameCondition +=
            " OR primary_object_label LIKE " +
            socialTxn.quote("%" + socialTxn.esc_like(name) + "%");
    }
    nameCondition += ")";
    for (const auto& otherSlave : otherSlaves) {
        std::cerr << "to_sync: " << otherSlave.id << std::endl;
    }
    for (const auto& otherSlave : otherSlaves) {
        const auto whereClause =
            "WHERE primary_object_id = " + otherSlave.id +
            " AND " + nameCondition;
        backupAndDumpRows(
            "commit_event",
            "FROM social.commit_event " + whereClause,
            socialTxn);
        // TODO? replace part of label
        socialTxn.exec(
            "UPDATE social.commit_event "
            "SET primary_object_label = '{{categories:" + categoryId + "}}' "
             + whereClause);
    }
}

int
main(int argc, char** argv) try {
    maps::cmdline::Parser argsParser;
    auto configPath = argsParser.string("config")
        .defaultValue(DEFAULT_MAPS_WIKI_SERVICES_CONFIG_PATH)
        .help("Path to config.xml with database connection settings.")
        .metavar("<path>");
    auto idsFile = argsParser.string("ids-file")
        .help("Use ids file to process.")
        .metavar("<ids>")
        .required();
    auto dryRun = argsParser.flag("dry-run")
        .help("Do not commit changes.");
    argsParser.parse(argc, argv);

    INFO() << "Config file: " << configPath;
    INFO() << "Ids file: " << idsFile;

    auto configDoc = maps::xml3::Doc::fromFile(configPath);
    maps::pgp3utils::DynamicPoolHolder poolHolder(configDoc.node("/config//database[@id='core']"), "core");
    maps::pgp3utils::DynamicPoolHolder socialPoolHolder(configDoc.node("/config//database[@id='social']"), "social");
    auto coreWork = poolHolder.pool().masterWriteableTransaction();
    auto socialWork = socialPoolHolder.pool().masterWriteableTransaction();

    const auto objectIds = loadIds(idsFile);
    for (const auto& oid : objectIds) {
        const auto categoryId = getCategoryId(oid, *coreWork);
        if (categoryId.empty()) {
            ERROR() << "Failed to get category for: " << oid;
            continue;
        }
        INFO() << "ID: " << oid << " categoryId: " << categoryId;
        forget(oid, categoryId, *coreWork, *socialWork);
    }
    if (!dryRun) {
        socialWork->commit();
        coreWork->commit();
        INFO() << "Commited.";
    } else {
        INFO() << "Dry run complete.";
    }
    return EXIT_SUCCESS;
} catch (const maps::Exception& e) {
    FATAL() << "Exception caught: " << e;
    return EXIT_FAILURE;
} catch (const std::exception& ex) {
    FATAL() << "Exception caught: " << ex.what();
    return EXIT_FAILURE;
} catch (...) {
    FATAL() << "Unknown exception caught";
    return EXIT_FAILURE;
}
