#include "helpers.h"
#include "data_error.h"
#include "log.h"

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

namespace maps::wiki::json2ymapsdf {

namespace {

class ConnectionHandle
{
public:
    explicit ConnectionHandle(const std::string& connString)
        : conn_(connString)
    {}

    pqxx::connection& get() { return conn_; }

private:
    pqxx::connection conn_;
};

class FakePool
{
public:
    explicit FakePool(std::string connString)
        : connString_(std::move(connString))
    {}

    ConnectionHandle getMasterConnection() const
    {
        return ConnectionHandle(connString_);
    }

private:
    std::string connString_;
};

const std::string SET_SEARCH_PATH_PUBLIC = "SET SEARCH_PATH=public;";

template <typename Pool>
void
execCommitWithRetriesInternal(
    Pool& pool,
    const std::string& name,
    const std::string& query,
    const std::function<void(pqxx::transaction_base&)>& f)
{
    size_t retryCounter = 0;
    size_t execCounter = 0;

    common::retryDuration([&] {
        if (retryCounter++) {
            INFO() << "Attempt " << retryCounter << " " << name;
        }

        try {
            auto conn = pool.getMasterConnection();
            pqxx::work work(conn.get());
            work.exec(SET_SEARCH_PATH_PUBLIC); // test connectivity to database

            ++execCounter;
            if (!query.empty()) {
                work.exec(query);
            }
            if (f) {
                f(work);
            }
            work.commit();
        } catch (const std::exception& ex) {
            ERROR() << "Attempt " << retryCounter << " " << name
                    << " failed: " << ex.what();
            if (execCounter < 2) {
                // may be fail connectivity on lost connection, exec(), commit()...
                throw;
            }
            throw common::RetryDurationCancel() << ex.what(); // data error
        }
    });
}

} // namespace

pqxx::result
readWithRetries(
    const std::string& connString,
    const std::string& query)
{
    return common::retryDuration([&] {
        pqxx::connection conn(connString);
        pqxx::nontransaction work(conn);
        return work.exec(SET_SEARCH_PATH_PUBLIC + query);
    });
}

pqxx::result
readWithRetries(
    pgpool3::Pool& pool,
    const std::string& query)
{
    return common::retryDuration([&] {
        auto conn = pool.getMasterConnection();
        pqxx::nontransaction work(conn.get());
        return work.exec(SET_SEARCH_PATH_PUBLIC + query);
    });
}

void
execCommitWithRetries(
    const std::string& connString,
    const std::string& name,
    const std::string& query,
    const std::function<void(pqxx::transaction_base&)>& f)
{
    FakePool pool(connString);
    execCommitWithRetriesInternal(pool, name, query, f);
}

void
execCommitWithRetries(
    pgpool3::Pool& pool,
    const std::string& name,
    const std::string& query,
    const std::function<void(pqxx::transaction_base&)>& f)
{
    execCommitWithRetriesInternal(pool, name, query, f);
}

std::string
printScissors(
    const std::string& description,
    const std::string& str)
{
    ASSERT(!str.empty());

    return
        (description.empty() ? "" : description + ":") + "\n"
        "-8<-----------------------------------------------------------------------------\n"
        + str + (str.back() == '\n' ? "" : "\n") +
        "----------------------------------------------------------------------------->8-";
}

void
safeRun(
    const std::string& name,
    const std::function<void()>& fail,
    const std::function<void()>& run)
{
    INFO() << name << " started";
    try {
        run();
        INFO() << name << " finished";
        return;
    } catch (const TdsDataError& ex) {
        ERROR() << name << " failed: " << ex.what();
    } catch (const LogicError& ex) {
        ERROR() << name << " failed: " << ex.what();
    } catch (const RuntimeError& ex) {
        ERROR() << name << " failed: " << ex;
    } catch (const std::exception& ex) {
        ERROR() << name << " failed: " << ex.what();
    } catch (...) {
        ERROR() << name << " failed: Unknown exception";
    }
    fail();
}

std::function<void()>
safeRunner(
    const std::string& name,
    const std::function<void()>& fail,
    const std::function<void()>& run)
{
    return std::bind(safeRun, name, fail, run);
}

} // namespace maps::wiki::json2ymapsdf
