#include "builder.h"
#include "region_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/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/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::wiki::json2ymapsdf::isocode {

namespace {

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

void
updateAdNeutralIsocode(
    pgpool3::Pool& pool,
    const std::string& schemaName)
{
    auto query =
        "SET search_path TO " + schemaName + ", public;"

        "CREATE TEMPORARY SEQUENCE ad_neutral_isocode "
        "START 500 MAXVALUE 999 NO CYCLE; "

        "UPDATE ad "
        "SET isocode = nextval('ad_neutral_isocode') "
        "WHERE isocode = 'XZ'; "

        "DROP SEQUENCE IF EXISTS ad_neutral_isocode;";

    execCommitWithRetries(pool, "updateAdNeutralIsocode", query);
}

std::string
generateCoverageFile(
    pgpool3::Pool& pool,
    const std::string& schemaName,
    const std::string& tmpDir)
{
    // build ad_geom geometry
    AdGeometryBuilder adGeomBuilder(pool, schemaName, schemaName);
    adGeomBuilder.build(LEVEL_KIND_COUNTRY);
    adGeomBuilder.check(LEVEL_KIND_COUNTRY, PERIMETER_TOLERANCE);

    return buildCoverageFile(pool, schemaName, tmpDir);
}

void
parallelExec(
    pgpool3::Pool& pool,
    const std::vector<std::pair<std::string, std::string>>& cmdName,
    size_t threads,
    std::shared_ptr<std::atomic<bool>> fail)
{
    ThreadPool threadPool(threads);
    for (const auto& pair : cmdName) {
        threadPool.push([&, query=pair.first, name=pair.second] {
            safeRunner(
                [&]{
                    execCommitWithRetries(pool, name, query);
                },
                name,
                fail);
        });
    }
    threadPool.shutdown();
}

std::set<std::string>
getIsocodeTables(
    pgpool3::Pool& pool,
    const ymapsdf::schema::Schema& schema)
{
    auto query =
        "SELECT tablename FROM pg_tables"
        " WHERE schemaname= '" + schema.name() + "'"
        " AND tablename LIKE '%_isocode'";

    auto rows = readWithRetries(pool, query);

    std::set<std::string> tables;
    for (const auto& row : rows) {
        auto tableName = row[0].as<std::string>();
        tables.insert(tableName);
    }
    return tables;
}

void
truncateTables(
    pgpool3::Pool& pool,
    const ymapsdf::schema::Schema& schema,
    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.emplace_back(
            "TRUNCATE " + schema.name() + "." + tableName,
            "Truncate table " + tableName);
    }
    parallelExec(pool, data, threads, std::move(fail));
}

void
createIsocodeIndexes(
    pgpool3::Pool& pool,
    const ymapsdf::schema::Schema& schema,
    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.emplace_back(
            "CREATE INDEX ON " + schema.name() + "." + tableName +
                    " USING btree(isocode);",
            "Build isocode index for " + tableName);
    }
    parallelExec(pool, data, threads, std::move(fail));
}

void
dropIsocodeIndexes(
    pgpool3::Pool& pool,
    const ymapsdf::schema::Schema& schema,
    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.emplace_back(
            "DROP INDEX IF EXISTS " + schema.name() + "." + tableName + "_isocode_idx;",
            "Drop isocode index for " + tableName);
    }
    parallelExec(pool, data, threads, std::move(fail));
}

void
addAdIndexes(
    pgpool3::Pool& pool,
    const ymapsdf::schema::Schema& schema,
    size_t threads,
    std::shared_ptr<std::atomic<bool>> fail)
{
    std::vector<std::pair<std::string, std::string>> data;
    for (const std::string& column : {"g_ad_id", "level_kind"}) {
        data.emplace_back(
            "CREATE INDEX ON " + schema.name() + ".ad USING btree (" + column + ");",
            "Add index on ad by " + column
        );
    }
    parallelExec(pool, data, threads, std::move(fail));
}

} // namespace

void
build(
    pgpool3::Pool& pool,
    const ymapsdf::schema::Schema& schema,
    const Params& params)
{
    const size_t MIN_ISOCODE_THREADS = 3;
    const auto threads = std::max(params.threadCount, MIN_ISOCODE_THREADS);

    auto isocodeTables = getIsocodeTables(pool, schema);

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

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

    addAdIndexes(pool, schema, threads, fail);
    updateAdNeutralIsocode(pool, schema.name());

    auto fAd = std::async(
        std::launch::async,
        safeRunner,
        [&]{ propagateAd(pool, schema.name()); },
        "propagateAd",
        fail);

    std::string coverageFile;
    safeRunner(
        [&]{ coverageFile = generateCoverageFile(pool, schema.name(), params.tmpDir); },
        "generateCoverageFile",
        fail);

    REQUIRE(!coverageFile.empty(), "Failed generating coverage file");
    GeometryProcessor geomProcessor(pool, coverageFile, schema.name(), fail);

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

    fAd.get();

    auto fRegions = std::async(
        std::launch::async,
        safeRunner,
        [&]{
            buildRegions(pool, schema.name());
            checkRegions(pool, schema.name());
        },
        "buildingDatasetRegions",
        fail);

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

    fRegions.get();


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

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

    propagateGeometry(pool, schema.name(), threads, fail);

    createIsocodeIndexes(pool, schema, isocodeTables, threads, fail);

    truncateTables(pool, schema, {TABLE_AD_GEOM}, threads, fail);

    REQUIRE(!*fail, "Failed to create isocodes");
}

} // namespace maps::wiki::json2ymapsdf::isocode
