#include "pg_helpers.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/wiki/common/retry_duration.h>

#include <algorithm>
#include <thread>
#include <vector>

using namespace std::chrono_literals;

namespace maps::wiki::exporter {

namespace {

size_t maxConnectionsNumber() {
    return std::max(16u, std::thread::hardware_concurrency() * 3 / 4);
}

bool schemaExists(
    pqxx::transaction_base& txn,
    const std::string& schema)
{
    const std::string query =
        "SELECT 1 "
        "FROM information_schema.schemata "
        "WHERE schema_name=" + txn.quote(schema);

    return !txn.exec(query).empty();
}

} // namespace


std::unique_ptr<pgpool3::Pool> createPgPool(const std::string& connectionString)
{
    pgpool3::PoolConstants poolConstants(1, maxConnectionsNumber(), 1, maxConnectionsNumber());
    poolConstants.getTimeoutMs = 20s;
    poolConstants.pingIntervalMs = 5s;
    poolConstants.pingTimeoutMs = 60s;
    poolConstants.timeoutEarlyOnMasterUnavailable = false;

    return std::make_unique<pgpool3::Pool>(
            connectionString,
            std::move(poolConstants)
    );
}


std::vector<std::string> getSchemasByPattern(
    pgpool3::Pool& pool,
    const std::string& sqlLikePattern)
{
    return common::retryDuration([&] {
        auto txn = pool.masterReadOnlyTransaction();

        const std::string query =
            "SELECT schema_name "
            "FROM information_schema.schemata "
            "WHERE schema_name LIKE " + txn->quote(sqlLikePattern);

        auto rows = txn->exec(query);

        std::vector<std::string> schemas;
        schemas.reserve(rows.size());
        for (const auto& row: rows) {
            schemas.push_back(row["schema_name"].as<std::string>());
        }
        return schemas;
    });
}


bool dropSchema(pgpool3::Pool& pool, const std::string& schema)
{
    try {
        return common::retryDuration([&] {
            auto txn = pool.masterWriteableTransaction();
            txn->exec("DROP SCHEMA IF EXISTS " + schema + " CASCADE");
            txn->commit();
            INFO() << "Dropped schema " << schema;
            return true;
        });
    } catch (const std::exception& e) {
        WARN() << "Failed to drop schema " << schema << ": " << e.what();
        return false;
    }
}


bool renameSchema(
    pgpool3::Pool& pool,
    const std::string& schema,
    const std::string& newSchema)
{
    try {
        return common::retryDuration([&] {
            auto txn = pool.masterWriteableTransaction();

            if (!schemaExists(*txn, schema)) {
                WARN() << "Schema " << schema << " not exists";
                return false;
            }
            if (schemaExists(*txn, newSchema)) {
                WARN() << "Schema " << newSchema << " already exists";
                return false;
            }

            txn->exec("ALTER SCHEMA " + schema + " RENAME TO " + newSchema);
            txn->commit();
            INFO() << "Schema " << schema << " renamed to " << newSchema;
            return true;
        });
    } catch (const std::exception& e) {
        WARN() << "Failed to rename schema " << schema << ": " << e.what();
        return false;
    }
}

bool schemaExists(
    pgpool3::Pool& pool,
    const std::string& schema)
{
    return common::retryDuration([&] {
        auto txn = pool.masterReadOnlyTransaction();
        return schemaExists(*txn, schema);
    });
}

std::vector<std::string> getSchemaTables(
    pgpool3::Pool& pool,
    const std::string& schema)
{
    try {
        return common::retryDuration([&] {
            auto txn = pool.masterReadOnlyTransaction();

            const std::string query =
                "SELECT table_name "
                "FROM information_schema.tables "
                "WHERE table_schema=" + txn->quote(schema);

            auto rows = txn->exec(query);

            std::vector<std::string> tables;
            tables.reserve(rows.size());
            for (const auto& row: rows) {
                tables.push_back(row["table_name"].as<std::string>());
            }
            return tables;
        });
    } catch (const std::exception& e) {
        throw RuntimeError() << "Failed to get schema tables: " << e.what();
    }
}

revision::Branch loadBranchByString(
    pgpool3::Pool& pool,
    const std::string& branch)
{
    return common::retryDuration([&] {
        auto txn = pool.masterReadOnlyTransaction();
        return revision::BranchManager(*txn).loadByString(branch);
    });
}

} // namespace maps::wiki::exporter
