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

#include <boost/program_options.hpp>

#include <pqxx/pqxx>

#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);
}

pqxx::result
exec(pqxx::work& work, const std::string& query, const std::string& funcSignature)
{
    const auto funcName = signatureToName(funcSignature);

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

    DEBUG() << funcName << ":" << query;

    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.";

    return res;
}

enum class AdMode {Parent, Subst};

std::string
adToChildAdQuery(size_t pAdLevel, AdMode mode)
{
    std::stringstream fields;
    std::stringstream tables;
    std::stringstream predicates;

    switch (mode) {
        case AdMode::Parent:
            fields << "ad" << pAdLevel << ".ad_id, ";
            break;
        case AdMode::Subst:
            fields << "ad_subst.ad_id, ";
            tables << "ad ad_subst, ";
            predicates << "ad_subst.g_ad_id = ad" << pAdLevel << ".ad_id AND "
                << "ad_subst.p_ad_id IS NULL AND ";
            break;
    }

    fields << "ad0.isocode ";

    tables << "ad ad0";
    for (size_t i = 1; i <= pAdLevel; ++i) {
        tables << ", ad ad" << i;
    }

    predicates << "ad0.level_kind=1 "
        << "AND ad0.g_ad_id IS NULL "
        << "AND ad0.p_ad_id IS NULL";
    for (size_t i = 1; i <= pAdLevel; ++i) {
        predicates << " AND ad" << i << ".p_ad_id = ad" << i-1 << ".ad_id ";
    }

    std::stringstream query;
    query << "INSERT INTO ad_isocode"
        << " SELECT " << fields.str()
        << " FROM " << tables.str()
        << " WHERE " << predicates.str();

    return query.str();
}

void
adToChildAd(pqxx::work& work)
{
    for (size_t parentLevel = 0; ; ++parentLevel) {
        std::string taskName = "adToChildAd_" + std::to_string(parentLevel);
        auto res = exec(work, adToChildAdQuery(parentLevel, AdMode::Parent), taskName);
        if (res.affected_rows() == 0) {
            break;
        }
        taskName = "adToChildAd_" + std::to_string(parentLevel) + "_subst";
        exec(work, adToChildAdQuery(parentLevel, AdMode::Subst), taskName);
    }
}

void
adToCenter(pqxx::work& work)
{
    auto query =
        "INSERT INTO node_isocode "
        "SELECT node_id, isocode "
        "FROM ad_center "
        "JOIN ad_isocode USING(ad_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
adToFace(pqxx::work& work)
{
    auto query =
        "INSERT INTO face_isocode "
        "("
            "SELECT face_id, isocode "
            "FROM ad_face "
            "JOIN ad_isocode USING(ad_id) "
            "UNION "
            "SELECT face_id, isocode "
            "FROM ad_face_patch "
            "JOIN ad_isocode USING(ad_id)"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

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

void
edgeToNodeFrom(pqxx::work& work)
{
    auto query =
        "INSERT INTO node_isocode "
            "SELECT DISTINCT f_node_id, isocode "
            "FROM edge "
            "JOIN edge_isocode USING(edge_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
edgeToNodeTo(pqxx::work& work)
{
    auto query =
        "INSERT INTO node_isocode ("
            "SELECT t_node_id, isocode "
            "FROM edge "
            "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");

        adToChildAd(work);
        adToFace(work);
        faceToEdge(work);
        edgeToNodeFrom(work);
        edgeToNodeTo(work);
        adToCenter(work);

        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;
}
