#include "propagate_geometry.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 <yandex/maps/wiki/threadutils/scheduler.h>

#include <boost/current_function.hpp>

#include <pqxx/pqxx>
#include <cstdint>

namespace maps::wiki::json2ymapsdf::isocode {

namespace {

void execRecursive(
    pqxx::transaction_base& work,
    const std::string& query,
    const std::string& name)
{
    const size_t MAX_RECURSE_LEVEL = 10;
    for (size_t level = 0; level < MAX_RECURSE_LEVEL; ++level) {
        auto res = exec(work, query, name);
        if (res.affected_rows() == 0) {
            return;
        }
    }
    ERROR() << "Possible loop, recursion depth is greater than " << MAX_RECURSE_LEVEL
        << ". Query:\n" << query;
}

void
edgeToFace(pqxx::transaction_base& work)
{
    auto query =
        "CREATE TEMPORARY TABLE non_ad_faces AS"
        "("
            "SELECT face_id FROM face "
            "EXCEPT "
            "SELECT face_id FROM ad_face "
            "EXCEPT "
            "SELECT face_id FROM ad_face_patch"
        ");"
        "ALTER TABLE non_ad_faces ADD PRIMARY KEY (face_id);"

        "INSERT INTO face_isocode "
        "("
            "SELECT DISTINCT face_id, isocode "
            "FROM face_edge "
            "JOIN edge_isocode USING(edge_id) "
            "JOIN non_ad_faces USING(face_id)"
        ");"
        ""
        "DROP TABLE non_ad_faces;";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
geomToFt(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO ft_isocode "
        "("
            "SELECT ft_id, isocode "
            "FROM ft_face "
            "JOIN face_isocode USING(face_id) "
            "UNION "
            "SELECT ft_id, isocode "
            "FROM ft_edge "
            "JOIN edge_isocode USING(edge_id) "
            "UNION "
            "SELECT ft_id, isocode "
            "FROM ft_center "
            "JOIN node_isocode USING(node_id) "
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToSlaveFt(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO ft_isocode "
        "("
            "SELECT ft.ft_id, ft_isocode.isocode "
            "FROM ft "
            "JOIN ft_isocode ON (ft_isocode.ft_id = ft.p_ft_id) "
            "WHERE ft.ft_id IN "
            "("
                "SELECT ft_id FROM ft WHERE p_ft_id IS NOT NULL "
                "EXCEPT "
                "SELECT ft_id FROM ft_isocode"
            ")"
        ")";
    execRecursive(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToParentFt(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO ft_isocode "
        "("
            "SELECT ft.p_ft_id, ft_isocode.isocode "
            "FROM ft "
            "JOIN ft_isocode USING (ft_id) "
            "WHERE p_ft_id IS NOT NULL "
            "EXCEPT "
            "SELECT ft_id, isocode "
            "FROM ft_isocode"
        ")";
    execRecursive(work, query, BOOST_CURRENT_FUNCTION);
}

void
faceToBld(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO bld_isocode "
        "SELECT DISTINCT bld_id, isocode "
        "FROM bld_face "
        "JOIN face_isocode USING(face_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdElToRd(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO rd_isocode "
        "SELECT DISTINCT rd_id, rd_el_isocode.isocode "
        "FROM rd_rd_el "
        "JOIN rd_el_isocode USING(rd_el_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
adToAddr(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO addr_isocode "
        "SELECT addr_id, ad_isocode.isocode "
        "FROM addr "
        "JOIN ad_isocode USING(ad_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdToAddr(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO addr_isocode "
        "SELECT addr_id, rd_isocode.isocode "
        "FROM addr "
        "JOIN rd_isocode USING(rd_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToAddr(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO addr_isocode "
        "SELECT addr_id, ft_isocode.isocode "
        "FROM addr "
        "JOIN ft_isocode USING(ft_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
addrToNode(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO node_isocode "
        "("
            "SELECT node_id, addr_isocode.isocode "
            "FROM addr "
            "JOIN addr_isocode USING(addr_id) "
            "EXCEPT "
            "SELECT node_id, isocode "
            "FROM node_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
bldToModel3d(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO model3d_isocode "
        "SELECT DISTINCT model3d_id, bld_isocode.isocode "
        "FROM bld "
        "JOIN bld_isocode USING(bld_id) "
        "WHERE model3d_id IS NOT NULL";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
bldToPBld(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO bld_isocode "
        "("
            "SELECT p_bld_id, bld_isocode.isocode "
            "FROM bld "
            "JOIN bld_isocode USING(bld_id) "
            "WHERE p_bld_id IS NOT NULL "
            "EXCEPT "
            "SELECT bld_id, isocode "
            "FROM bld_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdElToCond(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO cond_isocode "
        "SELECT DISTINCT cond_id, isocode "
        "FROM cond "
        "JOIN cond_rd_seq USING(cond_seq_id) "
        "JOIN rd_el_isocode USING(rd_el_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdElToFt(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO ft_isocode "
        "("
            "SELECT ft_id, isocode "
            "FROM ft_rd_el "
            "JOIN rd_el_isocode USING(rd_el_id) "
            "EXCEPT "
            "SELECT ft_id, isocode "
            "FROM ft_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToFace(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO face_isocode "
        "("
            "SELECT face_id, isocode "
            "FROM ft_face "
            "JOIN ft_isocode USING(ft_id) "
            "EXCEPT "
            "SELECT face_id, isocode "
            "FROM face_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToEdge(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO edge_isocode "
        "("
            "SELECT edge_id, isocode "
            "FROM ft_edge "
            "JOIN ft_isocode USING(ft_id) "
            "EXCEPT "
            "SELECT edge_id, isocode "
            "FROM edge_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToCenter(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO node_isocode "
        "("
            "SELECT node_id, isocode "
            "FROM ft_center "
            "JOIN ft_isocode USING(ft_id) "
            "EXCEPT "
            "SELECT node_id, isocode "
            "FROM node_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
bldToFace(pqxx::transaction_base& work)
{
    auto query =
        "CREATE TEMPORARY TABLE crossborder_bld "
        "AS "
        "("
            "SELECT bld_id "
            "FROM bld_isocode "
            "GROUP BY 1 "
            "HAVING count(isocode) >= 2"
        ");"
        "CREATE INDEX ON crossborder_bld(bld_id);"
        ""
        "INSERT INTO face_isocode "
        "("
            "SELECT face_id, isocode "
            "FROM crossborder_bld "
            "JOIN bld_face USING(bld_id) "
            "JOIN bld_isocode USING(bld_id) "
            "EXCEPT "
            "SELECT face_id, isocode "
            "FROM face_isocode"
        ");"
        ""
        "DROP TABLE crossborder_bld;";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
condToRdEl(pqxx::transaction_base& work)
{
    auto query =
        "INSERT INTO rd_el_isocode "
        "("
            "SELECT rd_el_id, cond_isocode.isocode "
            "FROM cond_rd_seq "
            "JOIN cond USING(cond_seq_id) "
            "JOIN cond_isocode USING(cond_id) "
            "EXCEPT "
            "SELECT rd_el_id, isocode "
            "FROM rd_el_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdElToRdJc(pqxx::transaction_base& work)
{
    auto query =
        "CREATE TEMPORARY TABLE crossborder_rd_el "
        "AS "
        "("
            "SELECT rd_el_id "
            "FROM rd_el_isocode "
            "GROUP BY 1 "
            "HAVING count(isocode) >= 2"
        ");"
        "CREATE INDEX ON crossborder_rd_el(rd_el_id);"
        ""
        "INSERT INTO rd_jc_isocode"
        "("
            "("
                "SELECT f_rd_jc_id, rd_el_isocode.isocode "
                "FROM crossborder_rd_el "
                "JOIN rd_el USING(rd_el_id) "
                "JOIN rd_el_isocode USING (rd_el_id) "
                "UNION "
                "SELECT t_rd_jc_id, rd_el_isocode.isocode "
                "FROM crossborder_rd_el "
                "JOIN rd_el USING(rd_el_id) "
                "JOIN rd_el_isocode USING (rd_el_id)"
            ") "
            "EXCEPT "
            "SELECT rd_jc_id, isocode "
            "FROM rd_jc_isocode"
        ");"
        ""
        "INSERT INTO bound_jc"
        "("
            "("
                "SELECT f_rd_jc_id "
                "FROM crossborder_rd_el "
                "JOIN rd_el USING(rd_el_id) "
                "UNION "
                "SELECT t_rd_jc_id "
                "FROM crossborder_rd_el "
                "JOIN rd_el USING(rd_el_id) "
            ") "
            "EXCEPT "
            "SELECT rd_jc_id "
            "FROM bound_jc"
        ");"
        ""
        "DROP TABLE crossborder_rd_el;";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
faceToEdge(pqxx::transaction_base& work)
{
    auto query =
        "CREATE TEMPORARY TABLE crossborder_face "
        "AS "
        "("
            "SELECT face_id "
            "FROM face_isocode "
            "GROUP BY 1 "
            "HAVING count(isocode) >= 2"
        ");"
        "CREATE INDEX ON crossborder_face(face_id);"
        ""
        "INSERT INTO edge_isocode "
        "("
            "SELECT edge_id, isocode "
            "FROM crossborder_face "
            "JOIN face_isocode USING(face_id) "
            "JOIN face_edge USING(face_id) "
            "EXCEPT "
            "SELECT edge_id, isocode "
            "FROM edge_isocode"
        ");"
        ""
        "DROP TABLE crossborder_face;";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
edgeToNode(pqxx::transaction_base& work)
{
    auto query =
        "CREATE TEMPORARY TABLE crossborder_edge "
        "AS "
        "("
            "SELECT edge_id "
            "FROM edge_isocode "
            "GROUP BY 1 "
            "HAVING count(isocode) >= 2"
        ");"
        "CREATE INDEX ON crossborder_edge(edge_id);"
        ""
        "INSERT INTO node_isocode"
        "("
            "("
                "SELECT f_node_id, isocode "
                "FROM crossborder_edge "
                "JOIN edge USING(edge_id) "
                "JOIN edge_isocode USING (edge_id) "
                "UNION "
                "SELECT t_node_id, isocode "
                "FROM crossborder_edge "
                "JOIN edge USING(edge_id) "
                "JOIN edge_isocode USING (edge_id) "
            ")"
            "EXCEPT "
            "SELECT node_id, isocode "
            "FROM node_isocode"
        ");"
        ""
        "DROP TABLE crossborder_edge;";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

} // namespace


void
propagateGeometry(
    pgpool3::Pool& pool,
    const std::string& schemaName,
    size_t threads,
    const std::shared_ptr<std::atomic<bool>>& fail)
{
    auto query = "SET search_path=" + schemaName + ", public;";

    auto execCommit = [=, &pool](
        std::function<void(pqxx::transaction_base&)> f, const std::string& funcName)
    {
        auto name = "propagateGeometry " + funcName;
        safeRunner(
            [&] {
                execCommitWithRetries(
                    pool,
                    name,
                    query,
                    f);
            },
            name,
            fail);
    };
# define EXEC_COMMIT(f) execCommit(f, #f)

    ThreadPool threadPool(threads);
    auto executor = [&](const Scheduler::Runner& runner) { threadPool.push(runner); };

    Scheduler scheduler;

    auto rdElToCondId = scheduler.addTask(
        [&] { EXEC_COMMIT(rdElToCond); }, executor, {});

    auto condToRdElId = scheduler.addTask(
        [&] { EXEC_COMMIT(condToRdEl); }, executor, {rdElToCondId});

    auto rdElToRdId = scheduler.addTask(
        [&] { EXEC_COMMIT(rdElToRd); }, executor, {condToRdElId});

    /* auto rdElToRdJcId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(rdElToRdJc); }, executor, {condToRdElId});

    auto rdElToFtId = scheduler.addTask(
        [&] { EXEC_COMMIT(rdElToFt); }, executor, {condToRdElId});

    auto edgeToFaceId = scheduler.addTask(
        [&] { EXEC_COMMIT(edgeToFace); }, executor, {});

    auto geomToFtId = scheduler.addTask(
        [&] { EXEC_COMMIT(geomToFt); }, executor, {edgeToFaceId});

    auto ftToSlaveFtId = scheduler.addTask(
        [&] { EXEC_COMMIT(ftToSlaveFt); }, executor, {geomToFtId, rdElToFtId});

    auto ftToParentFtId = scheduler.addTask(
        [&] { EXEC_COMMIT(ftToParentFt); }, executor, {ftToSlaveFtId});

    auto ftToFaceId = scheduler.addTask(
        [&] { EXEC_COMMIT(ftToFace); }, executor, {ftToParentFtId});

    auto ftToEdgeId = scheduler.addTask(
        [&] { EXEC_COMMIT(ftToEdge); }, executor, {ftToParentFtId});

    auto faceToBldId = scheduler.addTask(
        [&] { EXEC_COMMIT(faceToBld); }, executor, {edgeToFaceId});
        // no need to wait for ftToFace, there are no shared faces

    auto adToAddrId = scheduler.addTask(
        [&] { EXEC_COMMIT(adToAddr); }, executor, {});

    auto rdToAddrId = scheduler.addTask(
        [&] { EXEC_COMMIT(rdToAddr); }, executor, {rdElToRdId});

    auto ftToAddrId = scheduler.addTask(
        [&] { EXEC_COMMIT(ftToAddr); }, executor, {ftToParentFtId});

    /* auto addrToNodeId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(addrToNode); }, executor, {adToAddrId, rdToAddrId, ftToAddrId});

    /* auto ftToCenterId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(ftToCenter); }, executor, {ftToParentFtId});

    auto bldToPBldId = scheduler.addTask(
        [&] { EXEC_COMMIT(bldToPBld); }, executor, {faceToBldId});

    auto bldToFaceId = scheduler.addTask(
        [&] { EXEC_COMMIT(bldToFace); }, executor, {bldToPBldId});

    /* auto bldToModel3dId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(bldToModel3d); }, executor, {bldToPBldId});

    auto faceToEdgeId = scheduler.addTask(
        [&] { EXEC_COMMIT(faceToEdge); }, executor, {ftToFaceId, bldToFaceId});

    /* auto edgeToNodeId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(edgeToNode); }, executor, {ftToEdgeId, faceToEdgeId});

    scheduler.executeAll();
    threadPool.shutdown();
}

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