#include <library/cpp/getopt/last_getopt.h>
#include <library/cpp/testing/unittest/env.h>
#include <maps/libs/local_postgres/include/instance.h>
#include <maps/libs/log8/include/log8.h>
#include <util/system/fs.h>
#include <yandex/maps/shell_cmd.h>

#include <pqxx/pqxx>

#include <set>
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>

using namespace maps;

namespace {

const auto EXIT_CODE_WRONG_CMD_LINE_ARGUMENTS = 1;


const std::string IN_SCHEMA = "export";


struct Options {
    std::string configFile;
    std::string jsonDir;
    std::string printCfg;
    std::string region;
    bool runWithoutRegionsList;
    std::string experiment;
    std::string sqlDump;
};


Options getOptions(int argc, char* argv[])
{
    Options result;

    NLastGetopt::TOpts opts = NLastGetopt::TOpts::Default();
    opts.AddLongOption("config").Required().StoreResult(&result.configFile).RequiredArgument("<json2ymapsdf.xml>")
        .Help("json2ymapsdf configuration file.");
    opts.AddLongOption("json-dir").Optional().StoreResult(&result.jsonDir).RequiredArgument("<directory>")
        .Help("Directory with JSON files to process.");
    opts.AddLongOption("print-configuration").Optional().StoreResult(&result.printCfg).RequiredArgument("<config-file>")
        .Help("Print configuration to the <config-file> and exit.");
    opts.AddLongOption("region").Optional().StoreResult(&result.region).RequiredArgument("<region>")
        .Help("Split and export the specified region only.");
    opts.AddLongOption("run-without-regions-list").Optional().StoreResult(&result.runWithoutRegionsList).NoArgument()
        .Help("Run json2ymapsdf without regions list.");
    opts.AddLongOption("experiment").Optional().StoreResult(&result.experiment).RequiredArgument("<experiment name>")
        .Help("json2ymapsdf experiment name.");
    opts.AddLongOption("sql-dump").Optional().StoreResult(&result.sqlDump).RequiredArgument("<out.sql name>")
        .Help("file name for sql dump (out.sql).").DefaultValue("out.sql");
    NLastGetopt::TOptsParseResult(&opts, argc, argv);

    if (result.jsonDir.empty() && result.printCfg.empty()) {
        std::cerr << "`--json-dir` or `--print-configuration` must be specified./n";
        exit(EXIT_CODE_WRONG_CMD_LINE_ARGUMENTS);
    }
    if (!result.jsonDir.empty() && !result.printCfg.empty()) {
        std::cerr << "`--json-dir` and `--print-configuration` can't be specified simultaneously./n";
        exit(EXIT_CODE_WRONG_CMD_LINE_ARGUMENTS);
    }

    return result;
}

std::string getRegionSchemaName(
    const std::string& connStr,
    const std::string& baseSchema)
{
    std::ostringstream query;
    query << "SELECT schema_name "
        << "FROM information_schema.schemata "
        << "WHERE schema_name LIKE '" << baseSchema << "%' "
        << "ORDER BY 1 DESC";

    pqxx::connection conn(connStr);
    pqxx::work work(conn);
    auto rows = work.exec(query.str());

    REQUIRE(!rows.empty(), "No schema with '" << baseSchema << "' prefix");
    REQUIRE(rows.size() <= 2, "More than two schemata with '" << baseSchema << "' prefix");
    return rows.front()["schema_name"].as<std::string>();
}

std::vector<std::string> getSchemaTables(
    pqxx::transaction_base& txn,
    const std::string& schema)
{
    std::vector<std::string> result;

    std::ostringstream query;
    query << "SELECT table_name "
        << "FROM information_schema.tables "
        << "WHERE table_schema='" << schema << "'";
    auto rows = txn.exec(query.str());
    result.reserve(rows.size());

    for (const auto& row: rows) {
        result.push_back(row["table_name"].as<std::string>());
    }
    return result;
}

using TypeNames = std::map<pqxx::oid, std::string>;

TypeNames getTypeNames(pqxx::transaction_base& txn)
{
    TypeNames result;

    const std::string query = "SELECT oid, typname FROM pg_type;";
    auto rows = txn.exec(query);

    for (const auto& row: rows) {
        result.emplace(row["oid"].as<int>(), row["typname"].as<std::string>());
    }
    return result;
}


std::string printField(
    pqxx::transaction_base& txn,
    const pqxx::field& field)
{
    static const auto typeNames = getTypeNames(txn);

    const std::string CHAR_TRUE = "t";
    const std::string STRING_TRUE = "true";
    const std::string STRING_FALSE = "false";

    const std::string TYPE_TEXT = "text";
    const std::string TYPE_BPCHAR = "bpchar";
    const std::string TYPE_VARCHAR = "varchar";
    const std::string TYPE_BOOL = "bool";
    const std::string TYPE_GEOMETRY = "geometry";
    const std::string TYPE_BYTEA = "bytea";

   if (field.is_null()) {
        return "NULL";
    }

    const auto type = field.type();
    REQUIRE (typeNames.count(type) != 0, "Unknown typname oid " << type);
    const auto typeName = typeNames.at(type);

    if (typeName == TYPE_TEXT
        || typeName == TYPE_BPCHAR
        || typeName == TYPE_VARCHAR
        || typeName == TYPE_GEOMETRY
        || typeName == TYPE_BYTEA)
    {
        return txn.quote(field.c_str());
    }

    if (typeName == TYPE_BOOL) {
        return field.c_str() == CHAR_TRUE ? STRING_TRUE : STRING_FALSE;
    }

    return field.c_str();
}

std::string printInsertStatement(
    pqxx::transaction_base& txn,
    const std::string& table,
    const pqxx::row& row)
{
    std::ostringstream query;
    query << "INSERT INTO " << table << " VALUES (";
    for (auto field = row.begin(); field != row.end(); ++field) {
        if (field != row.begin()) {
            query << ", ";
        }
        query << printField(txn, *field);
    }
    query << ");";

    return query.str();
}

std::vector<std::string> getInsertStatements(
    const local_postgres::Database& db,
    const std::string& schema)
{
    std::vector<std::string> result;

    pqxx::connection conn(db.connectionString());
    pqxx::work work(conn);

    const auto tables = getSchemaTables(work, schema);

    const std::set<std::string> skipTables = {
        "access",
        "ft_type",
        "lane_direction",
        "source_type",
    };
    for (const auto& table: tables) {
        if (skipTables.count(table)) {
            continue;
        }
        std::ostringstream query;
        query << "SELECT * FROM " << schema << "." << table << ";";
        auto rows = work.exec(query.str());
        for (const auto& row: rows) {
            auto line = printInsertStatement(work, table, row);
            if (line.find("schema_version") == std::string::npos) { // Skip schema version as it changes with time.
                result.push_back(line);
            }
        }
    }

    return result;
}


void dumpToFile(
    const local_postgres::Database& db,
    const std::string& schema,
    const std::string& filename)
{
    auto insertStatements = getInsertStatements(db, schema);
    std::sort(insertStatements.begin(), insertStatements.end());

    std::ofstream file(filename);
    for (const auto& statement: insertStatements) {
        file << statement << "\n";
    }
}

} // namespace


int main(int argc, char* argv[])
try {
    const auto options = getOptions(argc, argv);

    auto outDir = NFs::CurrentWorkingDirectory();
    NFs::SetCurrentWorkingDirectory(BuildRoot());

    local_postgres::Database db;
    db.createExtension("postgis");
    if (!options.printCfg.empty()) {
        shell::ShellCmd(
            "json2ymapsdf --threads 1 " +
            (options.experiment.empty() ? std::string() : (" --experiment " + options.experiment + " ")) +
            "--log-level=debug "
            "--conn '" + db.connectionString() + "' "
            "--schema " + IN_SCHEMA + " "
            "--transform-cfg " + options.configFile + " "
            "--print-configuration " + outDir + "/" + options.printCfg,
            std::cout, std::cerr
        ).run();
    } else {
        shell::ShellCmd(
            "json2ymapsdf --threads 1 " +
            (options.experiment.empty() ? std::string() : (" --experiment " + options.experiment + " ")) +
            "--log-level=debug "
            "--conn '" + db.connectionString() + "' "
            "--schema " + IN_SCHEMA + " "
            "--transform-cfg " + options.configFile + " "
            "--json-dir " + options.jsonDir + " " +
            (options.region.empty() ? "" : "--regions " +
                (options.runWithoutRegionsList ? "" : options.region)),
            std::cout, std::cerr
        ).run();

        const auto OUT_SCHEMA = getRegionSchemaName(db.connectionString(), IN_SCHEMA);
        dumpToFile(db, OUT_SCHEMA, outDir + "/" + options.sqlDump);
    }

    return EXIT_SUCCESS;
} catch (const maps::Exception& ex) {
    ERROR() << ex;
    return EXIT_FAILURE;
} catch (const std::exception& ex) {
    ERROR() << ex.what();
    return EXIT_FAILURE;
}
