#include <yandex/maps/wiki/tasks/export.h>

#include <maps/libs/introspection/include/comparison.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/profiletimer.h>

#include <pqxx/pqxx>
#include <optional>
#include <set>

namespace maps::wiki::tasks {

namespace {

using maps::introspection::operator<;

const std::string EXPORT_DATABASE_KEY = "databases/database";
const std::string EXPORT_DB_ID_KEY = "id";
const std::string EXPORT_DB_CONN_KEY = "conn-str";

struct DatabaseInfo
{
    double pingTime;
    std::string dbId;
    std::string connStr;

    auto introspect() const
    {
        return std::tie(pingTime, dbId, connStr);
    }
};

std::optional<double> pingTimeDb(const std::string& dbId, const std::string& connStr)
{
    try {
        pqxx::connection conn(connStr);
        pqxx::work work(conn);

        ProfileTimer pt;
        work.exec("SELECT 1");
        auto pingTime = pt.getElapsedTimeNumber();
        INFO() << "Ping db (" << dbId << "), time: " << pingTime;
        return pingTime;
    } catch (const std::exception& ex) {
        WARN() << "Ping db (" << dbId << "), failed: " << ex.what();
    }
    return std::nullopt;
}

std::optional<std::set<DatabaseInfo>> collectDatabases(const common::ExtendedXmlDoc& config)
{
    auto exportNode = config.node(getConfigExportXpath(), true);
    if (exportNode.isNull()) {
        return std::nullopt;
    }

    auto nodes = exportNode.nodes(EXPORT_DATABASE_KEY, true);
    if (nodes.size() == 0) {
        return std::nullopt;
    }

    std::set<DatabaseInfo> result;

    for (size_t i = 0; i < nodes.size(); ++i) {
        auto node = nodes[i];
        auto dbId = node.attr<std::string>(EXPORT_DB_ID_KEY);
        auto connStr = node.attr<std::string>(EXPORT_DB_CONN_KEY);
        auto pingTime = pingTimeDb(dbId, connStr);

        if (pingTime) {
            result.emplace(DatabaseInfo{*pingTime, std::move(dbId), std::move(connStr)});
        }
    }
    return result;
}

} // namespace

const std::string& getConfigExportXpath()
{
    static const std::string EXPORT_XPATH = "/config/services/tasks/export";
    return EXPORT_XPATH;
}

std::string defaultExportDatabase(const common::ExtendedXmlDoc& config)
{
    return config.getAttr<std::string>(getConfigExportXpath(), EXPORT_DB_CONN_KEY);
}

std::string detectNearestExportDatabase(const common::ExtendedXmlDoc& config)
{
    const auto dbInfos = collectDatabases(config);
    if (!dbInfos) {
        return {};
    }
    REQUIRE(!dbInfos->empty(), "Can not detect nearest export database");
    const auto& nearest = dbInfos->begin();
    INFO() << "Nearest export db (" << nearest->dbId << "), ping time: " << nearest->pingTime;
    return nearest->connStr;
}

std::unique_ptr<DatabaseLock> detectAndLockNearestExportDatabase(
    const common::ExtendedXmlDoc& config, int64_t lockId)
{
    const auto dbInfos = collectDatabases(config);
    if (!dbInfos) {
        return {};
    }
    for (const auto& dbInfo : *dbInfos) {
        INFO() << "Trying export db (" << dbInfo.dbId << "), ping time: " << dbInfo.pingTime;
        auto dbLock = std::make_unique<DatabaseLock>(dbInfo.connStr);
        if (dbLock->tryLock(lockId)) {
            INFO() << "Nearest export db (" << dbInfo.dbId << "), ping time: " << dbInfo.pingTime;
            return dbLock;
        }
    }
    return {};
}

} // namespace maps::wiki::tasks
