#include "pqxx.h"
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/config.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/data_error.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/helpers.h>

#include <yandex/maps/wiki/common/string_utils.h>

namespace maps::wiki::json2ymapsdf::ymapsdf::schema {

namespace column {
const std::string COLUMN_NAME = "column_name";
const std::string COLUMN_NUM = "column_num";
const std::string COLUMN_TYPE = "column_type";
const std::string CONSTRAINT_NAME = "constraint_name";
const std::string DEFAULT_VALUE = "default_value";
const std::string DEFINITION = "definition";
const std::string FOREIGN_TABLE_NAME = "foreign_table_name";
const std::string FOREIGN_COLUMN_NAME = "foreign_column_name";
const std::string INDEX_NAME = "index_name";
const std::string IS_PRIMARY = "is_primary";
const std::string IS_SERVICE = "is_service";
const std::string IS_UNIQUE = "is_unique";
const std::string TABLE_NAME = "table_name";
} // namespace column

namespace {

std::vector<DBID>
collectIds(
    pgpool3::Pool& pool,
    const std::string& query)
{
    auto rows = readWithRetries(pool, query);

    std::vector<DBID> result;
    result.reserve(rows.size());
    for (const auto& row : rows) {
        if (!row.at(0).is_null()) {
            result.push_back(row.at(0).as<DBID>());
        }
    }

    return result;
}

void
checkSchema(const std::string& name)
{
    REQUIRE(name.find('-') == std::string::npos,
            "invalid schema name: " << name);
}

} // namespace

pqxx::result
selectTablesData(
    const std::string& connStr,
    const std::string& schema)
{
    std::ostringstream query;
    query <<
        "SET search_path TO " << schema << ", public; "
        "SELECT "
            "table_data.relname AS " << column::TABLE_NAME << ", "
            "column_data.attnum AS " << column::COLUMN_NUM << ", "
            "column_data.attname AS " << column::COLUMN_NAME << ", "
            "type_data.typname AS " << column::COLUMN_TYPE << ", "
            "CASE "
                "WHEN default_data.adbin IS NOT NULL "
                "THEN pg_get_expr(default_data.adbin, default_data.adrelid) "
                "ELSE CASE "
                    "WHEN column_data.attnotnull IS FALSE "
                    "THEN 'NULL' "
                    "ELSE CASE "
                        "WHEN type_data.typname LIKE 'bool' "
                        "THEN 'false' "
                        "ELSE CASE "
                            "WHEN type_data.typname LIKE 'text' "
                            "OR  type_data.typname LIKE 'bpchar' "
                            "OR type_data.typname LIKE 'varchar' "
                            "THEN '''''' "
                            "ELSE NULL "
                        "END "
                    "END "
                "END "
            "END AS " << column::DEFAULT_VALUE << ", "
            "index_data.indisprimary AS " << column::IS_PRIMARY << ", "
            "(col_description(table_data.oid, column_data.attnum) LIKE '%[SERVICE]%') AS " << column::IS_SERVICE << ", "
            "fk_table_data.relname AS " << column::FOREIGN_TABLE_NAME << " "
        "FROM "
            "pg_class table_data "
            "JOIN pg_attribute column_data "
                "ON column_data.attrelid = table_data.oid "
            "JOIN pg_catalog.pg_namespace schema_data "
                "ON schema_data.oid = table_data.relnamespace "
            "LEFT JOIN pg_index index_data "
                "ON table_data.oid = index_data.indrelid "
                "AND column_data.attnum = ALL(index_data.indkey) "
                "AND index_data.indisprimary = true "
            "LEFT JOIN pg_attrdef default_data "
                "ON default_data.adrelid = table_data.oid "
                "AND default_data.adnum = column_data.attnum "
            "LEFT JOIN pg_type type_data "
                "ON type_data.oid = column_data.atttypid "
            "LEFT JOIN pg_description column_comment "
                "ON table_data.oid = column_comment.objoid "
                "AND column_comment.objsubid = column_data.attnum "
            "LEFT JOIN pg_description table_comment "
                "ON table_data.oid = table_comment.objoid "
                "AND table_comment.objsubid = 0 "
            "LEFT JOIN pg_constraint fk_constraint "
                "ON fk_constraint.conrelid = table_data.oid "
                "AND fk_constraint.contype = 'f' "
                "AND column_data.attnum = ALL(fk_constraint.conkey) "
            "LEFT JOIN pg_class fk_table_data "
                "ON fk_table_data.oid = fk_constraint.confrelid "
        "WHERE "
            "table_data.relkind = 'r' "
            "AND column_data.attnum > 0 "
            "AND schema_data.nspname = '" << schema << "' "
            "AND (obj_description(table_data.oid) NOT LIKE '%[CALCULATED]%' "
                "OR obj_description(table_data.oid) LIKE '%[MANUALLY_MODIFIED]%') "
        "ORDER BY "
            "table_data.relname, "
            "column_data.attnum";

    return readWithRetries(connStr, query.str());
}

pqxx::result
selectIndexesData(
    const std::string& connStr,
    const std::string& schema)
{
    std::ostringstream query;
    query <<
        "SET search_path TO " << schema << ", public; "
        "SELECT "
            "ipg_class.relname  AS " << column::INDEX_NAME << ", "
            "pg_class.relname AS " << column::TABLE_NAME << ", "
            "ARRAY_AGG(pg_attribute.attname) AS " << column::COLUMN_NAME << ", "
            "pg_index.indisunique AS " << column::IS_UNIQUE << ", "
            "FALSE AS " << column::IS_PRIMARY << " "
        "FROM pg_index, pg_class, pg_class ipg_class, pg_attribute, pg_namespace "
        "WHERE pg_class.oid = pg_index.indrelid "
            "AND ipg_class.oid = pg_index.indexrelid "
            "AND pg_attribute.attrelid = pg_class.oid "
            "AND pg_attribute.attnum = ANY(pg_index.indkey) "
            "AND pg_namespace.oid = pg_class.relnamespace "
            "AND pg_namespace.nspname = '" << schema << "' "
            "AND pg_index.indisprimary is FALSE "
        "GROUP BY "
            "pg_class.relname, "
            "ipg_class.relname, "
            "pg_index.indisunique "
        "UNION "
        "SELECT "
            "pg_constraint.conname AS " << column::INDEX_NAME << ", "
            "pg_class.relname AS " << column::TABLE_NAME << ", "
            "ARRAY_AGG(pg_attribute.attname) AS " << column::COLUMN_NAME << ", "
            "TRUE AS " << column::IS_UNIQUE << ", "
            "TRUE AS " << column::IS_PRIMARY << " "
        "FROM pg_constraint "
        "JOIN pg_class ON pg_constraint.conrelid = pg_class.oid "
        "JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace "
        "JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid "
            "AND pg_attribute.attnum = ALL(pg_constraint.conkey) "
        "LEFT JOIN pg_class fpg_class ON pg_constraint.confrelid = fpg_class.oid "
        "LEFT JOIN pg_attribute fpg_attribute ON fpg_attribute.attrelid = fpg_class.oid "
            "AND fpg_attribute.attnum = ALL(pg_constraint.confkey) "
        "WHERE pg_namespace.nspname = '" << schema << "' "
            "AND pg_constraint.contype = 'p' "
            "AND pg_class.relname != 'schema_doc_structure' "
        "GROUP BY "
            "pg_constraint.conname, "
            "pg_class.relname";

    return readWithRetries(connStr, query.str());
}

pqxx::result
selectIntegrityData(
    const std::string& connStr,
    const std::string& schema)
{
    std::ostringstream query;
    query <<
        "SET search_path TO " << schema << ", public; "
        "SELECT "
            "pg_constraint.conname AS " << column::CONSTRAINT_NAME << ", "
            "pg_class.relname AS " << column::TABLE_NAME << ", "
            "pg_attribute.attname AS " << column::COLUMN_NAME << ", "
            "fpg_class.relname AS " << column::FOREIGN_TABLE_NAME << ", "
            "fpg_attribute.attname AS " << column::FOREIGN_COLUMN_NAME << " "
        "FROM pg_constraint "
        "JOIN pg_class ON pg_constraint.conrelid = pg_class.oid "
        "JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace "
        "JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid "
            "AND pg_attribute.attnum = ALL(pg_constraint.conkey) "
        "LEFT JOIN pg_class fpg_class ON pg_constraint.confrelid = fpg_class.oid "
        "LEFT JOIN pg_attribute fpg_attribute ON fpg_attribute.attrelid = fpg_class.oid "
            "AND fpg_attribute.attnum = ALL(pg_constraint.confkey) "
        "WHERE pg_namespace.nspname = '" << schema << "' "
            "AND pg_constraint.contype = 'f' ";

    return readWithRetries(connStr, query.str());
}

std::vector<DBID>
collectUniqueViolations(
    pgpool3::Pool& pool,
    const std::string& schema,
    const std::string& table,
    const std::vector<std::string>& columns)
{
    std::ostringstream query;
    const auto columnsStr = common::join(columns, ",");
    query <<
        "SELECT " << columnsStr << " "
        "FROM " << schema << "." << table << " "
        "GROUP BY " << columnsStr << " "
        "HAVING COUNT(*) > 1;";

    return collectIds(pool, query.str());
}

std::vector<DBID>
collectForeignKeyViolations(
    pgpool3::Pool& pool,
    const std::string& schema,
    const std::string& table,
    const std::string& column,
    const std::string& foreignTable,
    const std::string& foreignColumn)
{
    std::ostringstream query;
    query
        << "SELECT " << column << " "
        << "FROM " << schema << "." << table << " "
        << "WHERE " << table << "." << column << " IS NOT NULL "
        << "EXCEPT "
        << "SELECT " << foreignColumn << " "
        << "FROM " << schema << "." << foreignTable;

    return collectIds(pool, query.str());
}

void
dropDBSchema(
    const std::string& connStr,
    const std::string& name)
{
    checkSchema(name);
    auto query = "DROP SCHEMA IF EXISTS " + name + " CASCADE";

    execCommitWithRetries(connStr, "drop schema " + name, query);
}

void
createDBSchema(
    const std::string& connStr,
    const std::string& name,
    const std::string& schemaSql)
{
    checkSchema(name);
    auto query =
        "CREATE SCHEMA " + name + ";\n" +
        "SET search_path TO " + name + ", public;\n" +
        schemaSql;

    execCommitWithRetries(connStr, "create schema " + name, query);
}

void
setUnloggedTable(
    const std::string& connStr,
    const std::string& schemaName,
    const std::string& tableName)
{
    checkSchema(schemaName);

    auto table = schemaName + "." + tableName;
    auto query = "ALTER TABLE " + table + " SET UNLOGGED";

    execCommitWithRetries(connStr, "set unlogged " + table, query);
}

} // namespace maps::wiki::json2ymapsdf::ymapsdf::schema
