#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>

#include <boost/lexical_cast.hpp>
#include <boost/program_options.hpp>
#include <boost/current_function.hpp>

#include <pqxx/pqxx>
#include <cstdint>
#include <iostream>
#include <chrono>
#include <ctime>

namespace po = boost::program_options;

namespace maps {
namespace wiki {
namespace {

const std::string OPT_HELP = "help";
const std::string OPT_CONN = "conn";
const std::string OPT_SCHEMANAME = "schemaname";

const int EXITCODE_OK = 0;
const int EXITCODE_LOGIC_ERROR = 1;
const int EXITCODE_RUNTIME_ERROR = 2;

template <typename Type>
Type
getValue(const po::variables_map& vm, const std::string& option)
{
    if (!vm.count(option)) {
        return {};
    }
    try {
        return vm[option].as<Type>();
    } catch (...) {
        ERROR() << "invalid type of option: " << option;
        throw;
    }
}

std::string
signatureToName(const std::string& funcSignature)
{
    const auto roundBracket = funcSignature.find("(");
    const auto doubleColon = funcSignature.rfind("::",roundBracket);
    size_t start = doubleColon == std::string::npos
        ? 0
        : doubleColon + 2;
    size_t length = roundBracket == std::string::npos
        ? std::string::npos
        : roundBracket - start;

    return funcSignature.substr(start, length);
}

void
exec(pqxx::work& work, const std::string& query, const std::string& funcSignature)
{
    const auto funcName = signatureToName(funcSignature);
    DEBUG() << funcName << ": " << query;

    const auto start = std::chrono::system_clock::now();


    const auto res = work.exec(query);

    const auto end = std::chrono::system_clock::now();

    const auto elapsedSeconds = std::chrono::duration_cast<std::chrono::seconds>(end - start);

    INFO() << funcName << ": "
        << elapsedSeconds.count() << " sec, "
        << res.affected_rows() << " rows.";
}

void
edgeToFace(pqxx::work& work)
{
    std::string query =
        "INSERT INTO face_isocode "
        "("
            "SELECT face_id, isocode "
            "FROM face_edge "
            "JOIN edge_isocode USING(edge_id) "
            "EXCEPT "
            "SELECT face_id, isocode "
            "FROM face_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
geomToFt(pqxx::work& work)
{
    std::string 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
faceToBld(pqxx::work& work)
{
    std::string 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::work& work)
{
    std::string 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
adRdFtToAddr(pqxx::work& work)
{
    std::string query =
        "INSERT INTO addr_isocode "
        "("
            "SELECT addr_id, ad_isocode.isocode "
            "FROM addr "
            "JOIN ad_isocode USING(ad_id) "
            "UNION "
            "SELECT addr_id, rd_isocode.isocode "
            "FROM addr "
            "JOIN rd_isocode USING(rd_id) "
            "UNION "
            "SELECT addr_id, ft_isocode.isocode "
            "FROM addr "
            "JOIN ft_isocode USING(ft_id) "
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}
void
bldToModel3d(pqxx::work& work)
{
    std::string 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
rdElToCond(pqxx::work& work)
{
    std::string 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
ftToFace(pqxx::work& work)
{
    std::string 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::work& work)
{
    std::string 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::work& work)
{
    std::string 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::work& work)
{
    std::string 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"
        ");";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
condToRdEl(pqxx::work& work)
{
    std::string 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
rdToRdEl(pqxx::work& work)
{
    std::string query =
        "INSERT INTO rd_el_isocode "
        "("
            "SELECT rd_el_id, isocode "
            "FROM rd_rd_el "
            "JOIN rd_isocode USING(rd_id) "
            "EXCEPT "
            "SELECT rd_el_id, isocode "
            "FROM rd_el_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdElToRdJc(pqxx::work& work)
{
    std::string 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"
        ");";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
faceToEdge(pqxx::work& work)
{
    std::string 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"
        ");";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
edgeToNode(pqxx::work& work)
{
    std::string 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"
        ");";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

class Runner
{
public:
    explicit Runner(const po::variables_map& vm)
        : vm(vm)
    {}

    int run() const
    {
        pqxx::connection conn(getValue<std::string>(vm, OPT_CONN));
        pqxx::work work(conn);
        work.exec("set search_path=" + getValue<std::string>(vm, OPT_SCHEMANAME) + ",public");

        edgeToFace(work);
        rdElToRd(work);

        geomToFt(work);      // depends edgeToFace
        faceToBld(work);     // depends edgeToFace
        bldToModel3d(work);  // depends faceToBld
        rdElToCond(work);    // depends rdElToRd
        adRdFtToAddr(work);  // depends rdElToRd, geomToFt

        ftToFace(work);      // depends geomToFt
        ftToEdge(work);      // depends geomToFt
        ftToCenter(work);    // depends geomToFt

        bldToFace(work);     // depends faceToBld

        condToRdEl(work);    // depends rdElToCond
        rdToRdEl(work);      // depends condToRdEl
        rdElToRdJc(work);    // depends rdToRdEl

        faceToEdge(work);    // depends ftToFace, bldToFace
        edgeToNode(work);    // depends faceToEdge, ftToEdge

        work.commit();

        return EXITCODE_OK;
    }

private:
    const po::variables_map& vm;
};

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

int main(int argc, char** argv)
{
    using namespace maps::wiki;

    po::options_description desc("Usage: <tool> <options>\nOptions");
    desc.add_options()
        (OPT_HELP.c_str(),
            "show help message")
        (OPT_CONN.c_str(), po::value<std::string>(),
            "connection string)")
        (OPT_SCHEMANAME.c_str(), po::value<std::string>(),
            "ymapsdf schema name")
    ;

    po::variables_map vm;
    try {
        po::store(po::parse_command_line(argc, argv, desc), vm);
    } catch (po::error& e) {
        std::cerr << "error parsing command line: " << e.what() << std::endl;
        return EXITCODE_LOGIC_ERROR;
    }
    po::notify(vm);

    if (argc == 1 || vm.count(OPT_HELP)) {
        std::cerr << desc << std::endl;
        return EXITCODE_LOGIC_ERROR;
    }

    try {
        return Runner(vm).run();
    } catch(maps::Exception& ex) {
        ERROR() << ex;
    } catch(std::exception& ex) {
        ERROR() << ex.what();
    }
    return EXITCODE_RUNTIME_ERROR;
}
