#include "builder.h"
#include "propagate_ad.h"
#include "propagate_geometry.h"
#include "ad_geometry_builder.h"
#include "coverage_builder.h"
#include "geometry_processor.h"
#include "helpers.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/wiki/threadutils/threadpool.h>

#include <pqxx/pqxx>

#include <set>
#include <future>
#include <memory>

namespace maps {
namespace wiki {
namespace isocode {

namespace {

const std::string TABLE_AD_GEOM = "ad_geom";
const size_t LEVEL_KIND_COUNTRY = 1;
const double PERIMETER_TOLERANCE = 0.9;

std::string
generateCoverageFile(
    const std::string& connString,
    const std::string& schemaName,
    const std::string& tmpDir)
{
    pqxx::connection conn(connString);

    // build ad_geom geometry
    AdGeometryBuilder adGeomBuilder(conn, schemaName, schemaName);
    adGeomBuilder.build(LEVEL_KIND_COUNTRY);
    adGeomBuilder.check(LEVEL_KIND_COUNTRY, PERIMETER_TOLERANCE);

    return buildCoverageFile(conn, schemaName, tmpDir);
}

void
parallelExec(
    const std::string& connString,
    const std::vector<std::pair<std::string, std::string>>& cmdName,
    size_t threads,
    std::shared_ptr<std::atomic<bool>> fail)
{
    ThreadPool pool(threads);
    for (auto pair : cmdName) {
        pool.push([&, pair]{
            safeRunner(
            [&]{
                pqxx::connection conn(connString);
                pqxx::work work(conn);
                work.exec(pair.first);
                work.commit();
            },
            pair.second,
            fail);
        });
    }
    pool.shutdown();
}

std::set<std::string>
getIsocodeTables(
    const std::string& connString,
    const std::string& schemaName)
{
    pqxx::connection conn(connString);
    pqxx::work work(conn);

    std::set<std::string> tables;
    for (const auto& row : work.exec(
            "SELECT tablename FROM pg_tables"
            " WHERE schemaname=" + work.quote(schemaName) +
            " AND tablename LIKE '%_isocode'")) {
        auto tableName = row[0].as<std::string>();
        tables.insert(tableName);
    }
    return tables;
}

void
truncateTables(
    const std::string& connString,
    const std::string& schemaName,
    const std::set<std::string>& tables,
    size_t threads,
    std::shared_ptr<std::atomic<bool>> fail)
{
    std::vector<std::pair<std::string, std::string>> data;
    for (const auto& tableName : tables) {
        data.push_back({
            "TRUNCATE " + schemaName + "." + tableName,
            "Truncate table " + tableName});
    }
    parallelExec(connString, data, threads, fail);
}

void
createIsocodeIndexes(
    const std::string& connString,
    const std::string& schemaName,
    const std::set<std::string>& tables,
    size_t threads,
    std::shared_ptr<std::atomic<bool>> fail)
{
    std::vector<std::pair<std::string, std::string>> data;
    for (const auto& tableName : tables) {
        data.push_back({
            "CREATE INDEX ON " + schemaName + "." + tableName +
                    " USING btree(isocode);",
            "Build isocode index for " + tableName});
    }
    parallelExec(connString, data, threads, fail);
}

void
dropIsocodeIndexes(
    const std::string& connString,
    const std::string& schemaName,
    const std::set<std::string>& tables,
    size_t threads,
    std::shared_ptr<std::atomic<bool>> fail)
{
    std::vector<std::pair<std::string, std::string>> data;
    for (const auto& tableName : tables) {
        data.push_back({
            "DROP INDEX IF EXISTS " + schemaName + "." + tableName + "_isocode_idx;",
            "Drop isocode index for " + tableName});
    }
    parallelExec(connString, data, threads, fail);
}

} // namespace

bool
build(
    const std::string& connString,
    const std::string& schemaName,
    const std::string& tmpDir,
    size_t threads)
{
    REQUIRE(threads >= 3, "Threads must be 3 or greater, threads: " << threads);

    auto isocodeTables = getIsocodeTables(connString, schemaName);

    auto fail = std::make_shared<std::atomic<bool>>(false);

    dropIsocodeIndexes(connString, schemaName, isocodeTables, threads, fail);
    {
        auto tables = isocodeTables;
        tables.insert(TABLE_AD_GEOM);
        truncateTables(connString, schemaName, tables, threads, fail);
    }

    auto fAd = std::async(
        std::launch::async,
        safeRunner,
        [&] {propagateAd(connString, schemaName); },
        "propagateAd",
        fail);

    std::string coverageFile;
    safeRunner(
        [&]{ coverageFile = generateCoverageFile(connString, schemaName, tmpDir); },
        "generateCoverageFile",
        fail);

    GeometryProcessor geomProcessor(coverageFile, connString, schemaName);

    safeRunner(
        [&]{ geomProcessor.processRdEl(threads - 1); },
        "processGeometry rd_el",
        fail);

    safeRunner(
        [&]{ geomProcessor.processRdJc(threads - 1); },
        "processGeometry rd_jc",
        fail);

    fAd.get();

    safeRunner(
        [&]{ geomProcessor.processEdge(threads); },
        "processGeometry edge",
        fail);

    safeRunner(
        [&]{ geomProcessor.processNode(threads); },
        "processGeometry node",
        fail);

    propagateGeometry(connString, schemaName, threads, fail);

    createIsocodeIndexes(connString, schemaName, isocodeTables, threads, fail);

    return !*fail;
}

} // namespace isocode
} // namespace wiki
} // namespace maps
