#include "splitter.h"

#include "exit_codes.h"
#include "helpers.h"
#include "magic_strings.h"
#include "parallel.h"
#include "query-builder.h"

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

#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>

#include <memory>

namespace maps {
namespace wiki {
namespace ymapsdf_splitter {
namespace {

const sql::Joins JOINS{
    {table::AD, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::AD_CENTER, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::AD_EXCL, sql::Join{table::AD_ISOCODE, column::E_AD_ID, column::AD_ID}.left().as("e")},
    {table::AD_EXCL, sql::Join{table::AD_ISOCODE, column::T_AD_ID, column::AD_ID}.left().as("t")},
    {table::AD_FACE, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::AD_FACE_PATCH, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::AD_NM, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::AD_RECOGNITION, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::ADDR, sql::Join{table::ADDR_ISOCODE, column::ADDR_ID}},
    {table::ADDR_ARR, sql::Join{table::ADDR_ISOCODE, column::ADDR_ID}},
    {table::ADDR_NM, sql::Join{table::ADDR_ISOCODE, column::ADDR_ID}},
    {table::ADDR_RANGE, sql::Join{table::RD_ISOCODE, column::RD_ID}},
    {table::BLD, sql::Join{table::BLD_ISOCODE, column::BLD_ID}},
    {table::BLD_ADDR, sql::Join{table::ADDR_ISOCODE, column::ADDR_ID}},
    {table::BLD_FACE, sql::Join{table::BLD_ISOCODE, column::BLD_ID}},
    {table::BOUND_JC, sql::Join{table::RD_JC_ISOCODE, column::RD_JC_ID}},
    {table::COND, sql::Join{table::COND_ISOCODE, column::COND_ID}},
    {table::COND_DT, sql::Join{table::COND_ISOCODE, column::COND_ID}},
    {table::COND_LANE, sql::Join{table::COND_ISOCODE, column::COND_ID}},
    {table::COND_RD_SEQ, sql::Join{table::COND, column::COND_SEQ_ID}.notUseInWhere()},
    {table::COND_RD_SEQ, sql::Join{table::COND_ISOCODE, column::COND_ID}},
    {table::EDGE, sql::Join{table::EDGE_ISOCODE, column::EDGE_ID}},
    {table::FACE, sql::Join{table::FACE_ISOCODE, column::FACE_ID}},
    {table::FACE_EDGE, sql::Join{table::FACE_ISOCODE, column::FACE_ID}},
    {table::FT, sql::Join{table::FT_ISOCODE, column::FT_ID}},
    {table::FT_AD, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::FT_ADDR, sql::Join{table::FT_ISOCODE, column::FT_ID}},
    {table::FT_CENTER, sql::Join{table::FT_ISOCODE, column::FT_ID}},
    {table::FT_EDGE, sql::Join{table::EDGE_ISOCODE, column::EDGE_ID}},
    {table::FT_FACE, sql::Join{table::FACE_ISOCODE, column::FACE_ID}},
    {table::FT_NM, sql::Join{table::FT_ISOCODE, column::FT_ID}},
    {table::FT_SOURCE, sql::Join{table::FT_ISOCODE, column::FT_ID}},
    {table::LOCALITY, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::MODEL3D, sql::Join{table::MODEL3D_ISOCODE, column::MODEL3D_ID}},
    {table::NODE, sql::Join{table::NODE_ISOCODE, column::NODE_ID}},
    {table::RD, sql::Join{table::RD_ISOCODE, column::RD_ID}},
    {table::RD_AD, sql::Join{table::AD_ISOCODE, column::AD_ID}},
    {table::RD_CENTER, sql::Join{table::NODE_ISOCODE, column::NODE_ID}.left().as("node")},
    {table::RD_CENTER, sql::Join{table::RD_ISOCODE, column::RD_ID}.left().as("rd")},
    {table::RD_EL, sql::Join{table::RD_EL_ISOCODE, column::RD_EL_ID}},
    {table::RD_EL_LANE, sql::Join{table::RD_EL_ISOCODE, column::RD_EL_ID}},
    {table::RD_JC, sql::Join{table::RD_JC_ISOCODE, column::RD_JC_ID}},
    {table::RD_NM, sql::Join{table::RD_ISOCODE, column::RD_ID}},
    {table::RD_RD_EL, sql::Join{table::RD_EL_ISOCODE, column::RD_EL_ID}}
};

const std::vector<std::string> TABLES_TO_BE_CREATED = {
    table::AD_GEOM, table::BLD_GEOM, table::FT_GEOM, table::RD_GEOM
};

const std::vector<std::string> TABLES_TO_BE_COPIED = {
    table::ACCESS, table::FT_TYPE, table::LANE_DIRECTION, table::SOURCE_TYPE, // Dictionaries
    table::META, table::META_PARAM                                            // Meta information
};

} // namespace


Splitter::Splitter(
    const std::string& connStr,
    const std::string& schema,
    bool dryRun)
    : connStr_{connStr}
    , schema_{schema}
    , dryRun_{dryRun}
{
    if (dryRun_) {
        WARN() << "Dry-run mode is activated, queries that change DB won't be executed";
    }

    REQUIRE(isSchemaExist(schema_),
            "Input schema: '" + schema + "' does not exist.");
}


void Splitter::split(const Region& region, const std::string& toSchema, size_t threadsNumber) const
{
    INFO() << "Splitting region: " << region.name
           << "(" << stringutils::join(region.isoCodes, ", ") << ")";

    REQUIRE(!isSchemaExist(toSchema),
            "Output schema: '" + toSchema + "' already exists, but must not be.");
    createSchema(toSchema);

    createTables(toSchema);
    copyTables(toSchema);
    splitTables(region, toSchema, threadsNumber);
    clearNonExistingParents(toSchema);
}


bool Splitter::isSchemaExist(const std::string& schema) const
{
    const std::string query =
        "SELECT 1 "
        "FROM information_schema.schemata "
        "WHERE schema_name = '" + schema + "';";

    return exec(connStr_, query).empty() ? false : true;
}


void Splitter::createSchema(const std::string& schema) const
{
    INFO() << "Creating schema: " << schema;
    exec(connStr_, "CREATE SCHEMA " + schema + ";", dryRun_);
}


void Splitter::createTables(const std::string& schema) const
{
    for (const auto& table: TABLES_TO_BE_CREATED) {
        createTable(schema, table);
    }
}


void Splitter::createTable(const std::string& schema, const std::string& table) const
{
    boost::filesystem::path path(YMAPSDF_CREATE_DIR + table + ".sql");
    REQUIRE(boost::filesystem::exists(path),
            "Table '" << table << "' can't be created, because file '" << path.string() <<
            "' is not found.");

    INFO() << "Creating table " << table;

    auto query = maps::common::readBinaryFile(path.string());
    boost::replace_all(query, "{_self}", schema + "." + table);
    boost::replace_all(query, "{_self.schema}", schema);
    boost::replace_all(query, "{_self.name}", table);

    exec(connStr_, query, dryRun_);
}


void Splitter::copyTables(const std::string& toSchema) const
{
    INFO() << "Copying tables into the schema: " << toSchema;
    for (const auto& table: TABLES_TO_BE_COPIED) {
        copyTable(table, toSchema);
    }
}


void Splitter::copyTable(const std::string& table, const std::string& toSchema) const
{
    INFO() << "Copying table: " << table;
    exec(connStr_,
         "CREATE TABLE " + toSchema + "." + table + " AS "
         "SELECT * FROM " + schema_ + "." + table,
         dryRun_);
}


void Splitter::splitTables(const Region& region, const std::string& toSchema, size_t threadsNumber) const
{
    INFO() << "Splitting tables for region: " << region.name;

    parallel::Commands commands;

    auto tables = sql::getTables(JOINS);
    for (const auto& table: tables) {
        commands.push_back({
            "Splitting table '" + table + "'",
            splitTableQuery(table, region, toSchema)
        });
    }

    auto fail = std::make_shared<std::atomic<bool>>(false);
    parallel::exec(connStr_, commands, threadsNumber, fail, dryRun_);
}


std::string Splitter::splitTableQuery(
    const std::string& table,
    const Region& region,
    const std::string& toSchema) const
{
    std::string isocodes = "'" + stringutils::join(region.isoCodes, "','") + "'";
    return sql::query(connStr_, schema_, toSchema, table, JOINS, isocodes);
}


void Splitter::clearNonExistingParents(const std::string& toSchema) const
{
    std::string query = "SET search_path = " + toSchema + ", public;\n";

    query +=
        "WITH RECURSIVE absent_parent AS (\n"
        "  SELECT " + table::FT + "." + column::FT_ID + "\n"
        "  FROM " + table::FT + "\n"
        "  LEFT JOIN " + table::FT + " parent\n"
        "    ON " + table::FT + "." + column::P_FT_ID + " = parent." + column::FT_ID + "\n"
        "  WHERE\n"
        "    " + table::FT + "." + column::P_FT_ID + " IS NOT NULL AND\n"
        "    parent." + column::FT_ID + " IS NULL\n"
        "\n"
        "  UNION\n"
        "\n"
        "  SELECT " + table::FT + ".ft_id\n"
        "  FROM " + table::FT + "\n"
        "  JOIN absent_parent\n"
        "    ON " + table::FT + "." + column::P_FT_ID + " = absent_parent." + column::FT_ID + "\n"
        ")\n"
        "UPDATE " + table::FT + "\n"
        "SET " + column::P_FT_ID + " = NULL\n"
        "WHERE " + column::FT_ID + " IN (SELECT " + column::FT_ID + " FROM absent_parent)\n"
        "RETURNING 1";

    DEBUG() << exec(connStr_, query, dryRun_, "Clearing non-existent parents").size()
            << " non-existent parents were cleared";
}


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