#include "topology_deleter.h"

#include "db_strings.h"

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

#include <vector>
#include <map>
#include <set>
#include <string>

namespace maps {
namespace wiki {
namespace topology_fixer {

namespace {

const std::string EDGE_F_NODE_ID_FKEY = "edge_f_node_id_fkey";
const std::string EDGE_T_NODE_ID_FKEY = "edge_t_node_id_fkey";
const std::string FACE_EDGE_EDGE_ID_FKEY = "face_edge_edge_id_fkey";
const std::string FACE_EDGE_FACE_ID_FKEY = "face_edge_face_id_fkey";
const std::string FT_FACE_FACE_ID_FKEY = "ft_face_face_id_fkey";
const std::string FT_FACE_FT_ID_FKEY = "ft_face_ft_id_fkey";
const std::string FT_EDGE_EDGE_ID_FKEY = "ft_edge_edge_id_fkey";
const std::string FT_EDGE_FT_ID_FKEY = "ft_edge_ft_id_fkey";
const std::string AD_FACE_FACE_ID_FKEY = "ad_face_face_id_fkey";
const std::string AD_FACE_AD_ID_FKEY = "ad_face_ad_id_fkey";

struct ForeignKeyConstraint
{
    std::string dropStatement() const
    {
        return std::string("ALTER TABLE ") + table
            + " DROP CONSTRAINT " + name + " RESTRICT";
    }

    std::string addStatement() const
    {
        return std::string("ALTER TABLE ") + table
            + " ADD CONSTRAINT " + name
            + " FOREIGN KEY (" + field + ") REFERENCES " + refTable + "(" + refField + ")";
    }

    std::string name;
    std::string table;
    std::string field;
    std::string refTable;
    std::string refField;
};

std::vector<ForeignKeyConstraint> fkeyConstraints = {
    {EDGE_F_NODE_ID_FKEY, TABLE_EDGE, FIELD_F_NODE_ID, TABLE_NODE, FIELD_NODE_ID},
    {EDGE_T_NODE_ID_FKEY, TABLE_EDGE, FIELD_T_NODE_ID, TABLE_NODE, FIELD_NODE_ID},
    {FACE_EDGE_EDGE_ID_FKEY, TABLE_FACE_EDGE, FIELD_EDGE_ID, TABLE_EDGE, FIELD_EDGE_ID},
    {FACE_EDGE_FACE_ID_FKEY, TABLE_FACE_EDGE, FIELD_FACE_ID, TABLE_FACE, FIELD_FACE_ID},
    {FT_FACE_FACE_ID_FKEY, TABLE_FT_FACE, FIELD_FACE_ID, TABLE_FACE, FIELD_FACE_ID},
    {FT_FACE_FT_ID_FKEY, TABLE_FT_FACE, FIELD_FT_ID, TABLE_FT, FIELD_FT_ID},
    {FT_EDGE_EDGE_ID_FKEY, TABLE_FT_EDGE, FIELD_EDGE_ID, TABLE_EDGE, FIELD_EDGE_ID},
    {FT_EDGE_FT_ID_FKEY, TABLE_FT_EDGE, FIELD_FT_ID, TABLE_FT, FIELD_FT_ID},
    {AD_FACE_FACE_ID_FKEY, TABLE_AD_FACE, FIELD_FACE_ID, TABLE_FACE, FIELD_FACE_ID},
    {AD_FACE_AD_ID_FKEY, TABLE_AD_FACE, FIELD_AD_ID, TABLE_AD, FIELD_AD_ID}
};

typedef std::set<std::string> StringSet;

StringSet findExistingConstraints(pqxx::work& txn, const std::string& schemaName)
{
    StringSet result;
    auto query = "SELECT conname FROM pg_constraint c  "
        "JOIN pg_namespace n ON (c.connamespace = n.oid) "
        "WHERE n.nspname=" + txn.quote(schemaName) + " AND c.contype='f'";
    for (const auto& row : txn.exec(query)) {
        result.emplace(row[0].as<std::string>());
    }
    return result;
}

StringSet constraintTables(const TopologyGroup& group)
{
    if (group.id() == TopologyGroupId::Ad) {
        return StringSet{TABLE_AD_FACE, TABLE_FACE_EDGE, TABLE_EDGE};
    }
    std::map<TopologyType, StringSet> tables = {
        {TopologyType::Contour, StringSet{TABLE_FT_FACE, TABLE_FACE_EDGE, TABLE_EDGE}},
        {TopologyType::Linear, StringSet{TABLE_FT_EDGE, TABLE_EDGE}}
    };
    return tables.at(group.topologyType());
}

void
dropConstraints(pqxx::work& txn,
    const std::vector<ForeignKeyConstraint>& constraints,
    const StringSet& existingConstraints,
    const TopologyGroup& group)
{
    std::string query;
    const StringSet& tableNames = constraintTables(group);
    for (const auto& constraint : constraints) {
        if (existingConstraints.count(constraint.name)
            && tableNames.count(constraint.table)) {
            query += constraint.dropStatement() + "; ";
        }
    }
    INFO() << "DROPPING CONSTRAINTS: " << query;
    txn.exec(query);
}

void
addConstraints(pqxx::work& txn,
    const std::vector<ForeignKeyConstraint>& constraints,
    const StringSet& existingConstraints,
    const TopologyGroup& group)
{
    std::string query;
    const StringSet& tableNames = constraintTables(group);
    for (const auto& constraint : constraints) {
        if (existingConstraints.count(constraint.name)
            && tableNames.count(constraint.table)) {
            query += constraint.addStatement() + "; ";
        }
    }
    INFO() << "ADDING CONSTRAINTS: " << query;
    txn.exec(query);
}

std::string
joinCond(const std::string& t1, const std::string& t2, const std::string& field)
{
    return t1 + "." + field + " = " + t2 + "." + field;
}

std::string
edgeNodeJoinCond(const std::string& edgeAlias, const std::string& nodeAlias)
{
    return
        "(" + edgeAlias + "." + FIELD_F_NODE_ID + " = " + nodeAlias + "." + FIELD_NODE_ID +
        " OR " + edgeAlias + "." + FIELD_T_NODE_ID + " = " + nodeAlias + "." + FIELD_NODE_ID + ")";
}

std::string
composeDeleteQuery(
    const TopologyGroup& group,
    const std::string& table,
    const std::vector<std::string>& usingTables,
    const std::vector<std::string>& joinConditions)
{
    const std::string masterTable = group.masterTable();
    const std::string faceMasterTable = masterTable + "_face";
    std::string query = "DELETE FROM " + table;
    if (!usingTables.empty()) {
        query += " USING ";
        std::string sep = "";
        for (const auto& table : usingTables) {
            query += sep + table;
            sep = ", ";
        }
    }
    const bool isFt = group.id() != TopologyGroupId::Ad;
    if (isFt) {
        ASSERT(masterTable == TABLE_FT);
        query += (usingTables.empty() ? " USING " : ", ") + masterTable;
    }
    if (!joinConditions.empty()) {
        query += " WHERE ";
        std::string sep = "";
        for (const auto& cond : joinConditions) {
            query += sep + cond;
            sep = " AND ";
        }
    }
    if (isFt) {
        auto ftTypeFilter = group.sqlFtTypeFilter(FIELD_FT_TYPE_ID);
        ASSERT(!ftTypeFilter.empty());
        query += (joinConditions.empty() ? " WHERE " : " AND ")
            + joinCond(faceMasterTable, masterTable, FIELD_FT_ID)
            + " AND " + ftTypeFilter;
    }
    return query;
}

std::string
deleteContourNodeQuery(const TopologyGroup& group)
{
    const std::string faceMasterTable = group.masterTable() + "_face";
    auto query = composeDeleteQuery(
        group,
        TABLE_NODE,
        {TABLE_EDGE, TABLE_FACE_EDGE, faceMasterTable},
        {
            edgeNodeJoinCond(TABLE_EDGE, TABLE_NODE),
            joinCond(TABLE_FACE_EDGE, TABLE_EDGE, FIELD_EDGE_ID),
            joinCond(faceMasterTable, TABLE_FACE_EDGE, FIELD_FACE_ID)
        });
    INFO() << query;
    return query;
}

std::string
deleteLinearNodeQuery(const TopologyGroup& group)
{
    ASSERT(group.id() != TopologyGroupId::Ad);
    std::string query = std::string("DELETE FROM ") + TABLE_NODE
            + " USING " + TABLE_EDGE + ", " + TABLE_FT_EDGE + ", " + TABLE_FT
            + " WHERE " + edgeNodeJoinCond(TABLE_EDGE, TABLE_NODE)
            + " AND " + joinCond(TABLE_FT_EDGE, TABLE_EDGE, FIELD_EDGE_ID)
            + " AND " + joinCond(TABLE_FT, TABLE_FT_EDGE, FIELD_FT_ID)
            + " AND " + group.sqlFtTypeFilter(FIELD_FT_TYPE_ID);
    INFO() << query;
    return query;
}

std::string
deleteContourEdgeQuery(const TopologyGroup& group)
{
    const std::string faceMasterTable = group.masterTable() + "_face";
    auto query = composeDeleteQuery(
        group,
        TABLE_EDGE,
        {TABLE_FACE_EDGE, faceMasterTable},
        {
            joinCond(TABLE_FACE_EDGE, TABLE_EDGE, FIELD_EDGE_ID),
            joinCond(faceMasterTable, TABLE_FACE_EDGE, FIELD_FACE_ID)
        });
    INFO() << query;
    return query;
}

std::string
deleteLinearEdgeQuery(const TopologyGroup& group)
{
    ASSERT(group.id() != TopologyGroupId::Ad);
    std::string query = std::string("DELETE FROM ") + TABLE_EDGE
        + " USING " + TABLE_FT_EDGE + ", " + TABLE_FT
        + " WHERE " + joinCond(TABLE_FT_EDGE, TABLE_EDGE, FIELD_EDGE_ID)
        + " AND " + joinCond(TABLE_FT, TABLE_FT_EDGE, FIELD_FT_ID)
        + " AND " + group.sqlFtTypeFilter(FIELD_FT_TYPE_ID);
    INFO() << query;
    return query;
}

std::string
deleteFaceQuery(const TopologyGroup& group, const std::string& faceTable)
{
    const std::string faceMasterTable = group.masterTable() + "_face";
    auto query = composeDeleteQuery(
        group,
        faceTable,
        {faceMasterTable},
        {joinCond(faceMasterTable, faceTable, FIELD_FACE_ID)});
    INFO() << query;
    return query;
}

std::string
deleteFaceMasterQuery(const TopologyGroup& group)
{
    const std::string faceMasterTable = group.masterTable() + "_face";
    auto query = composeDeleteQuery(group, faceMasterTable, {}, {});
    INFO() << query;
    return query;
}

std::string
deleteEdgeMasterQuery(const TopologyGroup& group)
{
    ASSERT(group.id() != TopologyGroupId::Ad);
    std::string query = std::string("DELETE FROM ") + TABLE_FT_EDGE
        + " USING " + TABLE_FT
        + " WHERE " + joinCond(TABLE_FT, TABLE_FT_EDGE, FIELD_FT_ID)
        + " AND " + group.sqlFtTypeFilter(FIELD_FT_TYPE_ID);
    INFO() << query;
    return query;
}

void
logDeleteResult(const pqxx::result& result, const std::string& tableName,
    const TopologyGroup& group)
{
    INFO() << "Cleared " << result.affected_rows() << " rows"
        << " from table " << tableName
        << ", group: " << group.name();
}

} // namespace

void
TopologyDeleter::deleteFaceMaster(pqxx::work& txn, const TopologyGroup& group)
{
    auto r = txn.exec(deleteFaceMasterQuery(group));
    logDeleteResult(r, group.masterTable() + "_face", group);
}

void
TopologyDeleter::deleteEdgeMaster(pqxx::work& txn, const TopologyGroup& group)
{
    ASSERT(group.id() != TopologyGroupId::Ad);
    auto r = txn.exec(deleteEdgeMasterQuery(group));
    logDeleteResult(r, TABLE_FT_EDGE, group);
}

void
TopologyDeleter::deleteFace(pqxx::work& txn,
    const TopologyGroup& group, const std::string& faceTable)
{
    auto r = txn.exec(deleteFaceQuery(group, faceTable));
    logDeleteResult(r, faceTable, group);
}

void
TopologyDeleter::deleteEdge(pqxx::work& txn, const TopologyGroup& group)
{
    if (group.topologyType() == TopologyType::Contour) {
        auto r = txn.exec(deleteContourEdgeQuery(group));
        logDeleteResult(r, TABLE_EDGE, group);
    }
    if (group.topologyType() == TopologyType::Linear) {
        auto r = txn.exec(deleteLinearEdgeQuery(group));
        logDeleteResult(r, TABLE_EDGE, group);
    }
}

void
TopologyDeleter::deleteNode(pqxx::work& txn, const TopologyGroup& group)
{
    if (group.topologyType() == TopologyType::Contour) {
        auto r = txn.exec(deleteContourNodeQuery(group));
        logDeleteResult(r, TABLE_NODE, group);
    }
    if (group.topologyType() == TopologyType::Linear) {
        auto r = txn.exec(deleteLinearNodeQuery(group));
        logDeleteResult(r, TABLE_NODE, group);
    }
}

void
TopologyDeleter::deleteTopologyImpl(pqxx::work& txn, const TopologyGroup& group)
{
    deleteNode(txn, group);
    deleteEdge(txn, group);
    if (group.topologyType() == TopologyType::Contour) {
        deleteFace(txn, group, TABLE_FACE_EDGE);
        deleteFace(txn, group, TABLE_FACE);
        deleteFaceMaster(txn, group);
    }
    if (group.topologyType() == TopologyType::Linear) {
        deleteEdgeMaster(txn, group);
    }
}

void
TopologyDeleter::deleteTopology(const TopologyGroup& group)
{
    pqxx::connection conn(connStr_);
    pqxx::work work(conn);
    work.exec("SET search_path TO " + schemaName_ + ", public");
    auto existingConstraints = findExistingConstraints(work, schemaName_);
    dropConstraints(work, fkeyConstraints, existingConstraints, group);
    deleteTopologyImpl(work, group);
    addConstraints(work, fkeyConstraints, existingConstraints, group);
    work.exec("SET search_path TO public");
    work.commit();
}

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