#include <maps/wikimap/mapspro/libs/acl/include/role.h>

#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl/include/exception.h>
#include <maps/wikimap/mapspro/libs/acl/include/group.h>
#include <maps/wikimap/mapspro/libs/acl/include/permission.h>
#include <maps/wikimap/mapspro/libs/acl/include/policy.h>
#include <yandex/maps/wiki/common/pg_utils.h>

#include "exception_helper.h"
#include "factory.h"

#include <set>

namespace maps::wiki::acl {

namespace {

const std::string STR_FALSE = "false";
const std::string STR_TRUE = "true";

const std::string QUERY_PERMISSIONS_BY_ROLE_ID =
    "SELECT permission.* FROM acl.permission, acl.role_permission"
    " WHERE permission.id = role_permission.permission_id"
    "   AND role_permission.role_id = ";

const std::string QUERY_POLICIES_BY_ROLE_ID =
    "SELECT agent_id, aoi_id FROM acl.policy WHERE role_id = ";

} // namespace

Role::Role(
        ID id,
        const std::string& name,
        const std::string& description,
        Privacy privacy,
        Transaction& work)
    : id_(id)
    , name_(name)
    , description_(description)
    , privacy_(privacy)
    , work_(work)
{
}

void
Role::setName(const std::string& newName)
{
    try {
        if (newName.empty()) {
            throw BadParam() << "Empty role new name";
        }
        auto query =
            "UPDATE acl.role SET name = " + work_.quote(newName) +
            " WHERE id = " + std::to_string(id_);
        work_.exec(query);
        name_ = newName;
    } catch (const pqxx::unique_violation& ex) {
        throw DuplicateRole() << "Role with name " << newName
            << " already exists";
    } CATCH_CONVERT();
}

void
Role::setDescription(const std::string& newDescription)
{
    try {
        auto query =
            "UPDATE acl.role SET description = " + work_.quote(newDescription) +
            " WHERE id = " + std::to_string(id_);
        work_.exec(query);
        description_ = newDescription;
    } CATCH_CONVERT();
}

void
Role::setPrivacy(Privacy privacy)
{
    try {
        auto query =
            "UPDATE acl.role SET is_public = " +
            (privacy == Privacy::Public ? STR_TRUE : STR_FALSE) +
            " WHERE id = " + std::to_string(id_);
        work_.exec(query);
        privacy_ = privacy;
    } CATCH_CONVERT();
}

void
Role::add(const Permission& permission) const
{
    try {
        auto query =
            "INSERT INTO acl.role_permission(role_id, permission_id) VALUES "
            "(" + std::to_string(id_) + ", " + std::to_string(permission.id()) + ")";
        work_.exec(query);
    } catch (const pqxx::unique_violation& ex) {
        throw DuplicateRelation() << "Role " << name()
            << " already has permission " << permission.name();
    } CATCH_CONVERT();
}

void
Role::remove(const Permission& permission) const
{
    try {
        auto query =
            "DELETE FROM acl.role_permission"
            " WHERE role_id = " + std::to_string(id_) +
            "   AND permission_id = " + std::to_string(permission.id());
        work_.exec(query);
    } CATCH_CONVERT();
}

void
Role::removePermissions() const
{
    try {
        auto query =
            "DELETE FROM acl.role_permission"
            " WHERE role_id = " + std::to_string(id_);
        work_.exec(query);
    } CATCH_CONVERT();
}

bool
Role::setPermissions(std::vector<Permission> newPermissions) const
{
    bool hasPermissionsChanges = false;
    const auto currentPermissions = permissions();
    for (const auto& currentPermission : currentPermissions) {
        auto it = std::find_if(newPermissions.begin(), newPermissions.end(),
            [&](const auto& newPermission) {
                return newPermission.id() == currentPermission.id();
            });
        if (it == newPermissions.end()) {
            hasPermissionsChanges = true;
            remove(currentPermission);
        } else {
            newPermissions.erase(it);
        }
    }
    if (newPermissions.empty()) {
        return hasPermissionsChanges;
    }
    std::string values;
    std::set<ID> ids;
    for (const auto& permission : newPermissions) {
        auto id = permission.id();
        if (!ids.insert(id).second) {
            throw DuplicateRelation() << "Role " << name()
                << " duplicate permission " << permission.name();
        }
        values += values.empty() ? "" : ",";
        values += "(" + std::to_string(id_) + "," + std::to_string(id) + ")";
    }
    try {
        work_.exec(
            "INSERT INTO acl.role_permission(role_id, permission_id)"
            " VALUES " + values);
        return true;
    } CATCH_CONVERT();
    return false;
}

std::vector<Permission>
Role::permissions() const
{
    auto rows =
        work_.exec(QUERY_PERMISSIONS_BY_ROLE_ID + std::to_string(id_));

    std::vector<Permission> result;
    result.reserve(rows.size());
    for (const auto& row : rows) {
        result.push_back(Factory::permission(
            row["id"].as<ID>(),
            row["name"].c_str(),
            row["parent_id"].as<ID>(), work_));
    }
    return result;
}

std::vector<Policy>
Role::policies() const
{
    auto rows =
        work_.exec(QUERY_POLICIES_BY_ROLE_ID + std::to_string(id_));

    std::vector<Policy> result;
    result.reserve(rows.size());
    for (const auto& row : rows) {
        result.push_back(Factory::policy(
            row["agent_id"].as<ID>(),
            {id_, name_, description_, privacy_, work_},
            row["aoi_id"].as<ID>(), work_));
    }
    return result;
}

std::set<ID>
Role::leafPermissionsIds() const
{
    try {
        auto rows = work_.exec(
            "SELECT leaf_ids FROM acl.permission p, acl.role_permission rp "
            "WHERE p.id = rp.permission_id AND rp.role_id = " + std::to_string(id_));
        std::set<ID> leafPermissionsIds;
        for (const auto& row : rows) {
            REQUIRE(!row[0].is_null(),
                "Permissions leaf data out of sync");
            const auto rowLeafsIds = common::parseSqlArray<ID>(row[0].c_str());
            leafPermissionsIds.insert(
                rowLeafsIds.begin(),
                rowLeafsIds.end());
        }
        return leafPermissionsIds;
    } CATCH_CONVERT();
    return {};
}


void
Role::addCanAssignRole(const Role& role)
{
    try {
        work_.exec(
            "INSERT INTO acl.role_can_assign_role (role_id, can_assign_role_id) "
            "VALUES(" + std::to_string(id_) + ", " + std::to_string(role.id()) +
            " ) ON CONFLICT DO NOTHING");
    } CATCH_CONVERT();
}

void
Role::addCanAssignGroup(const Group& group)
{
    try {
        work_.exec(
            "INSERT INTO acl.role_can_assign_group (role_id, can_assign_group_id) "
            "VALUES(" + std::to_string(id_) + ", " + std::to_string(group.id()) +
            " ) ON CONFLICT DO NOTHING");
    } CATCH_CONVERT();
}

void
Role::removeCanAssignRole(const Role& role)
{
    try {
        work_.exec(
            "DELETE FROM acl.role_can_assign_role "
            "WHERE role_id = " + std::to_string(id_) +
            " AND can_assign_role_id=" + std::to_string(role.id()));
    } CATCH_CONVERT();
}

void
Role::removeCanAssignGroup(const Group& group)
{
    try {
        work_.exec(
            "DELETE FROM acl.role_can_assign_group "
            "WHERE role_id = " + std::to_string(id_) +
            " AND can_assign_group_id=" + std::to_string(group.id()));
    } CATCH_CONVERT();
}

std::vector<Role>
Role::canAssignRoles() const
{
    try {
        auto rows = work_.exec(
            "SELECT can_assign_role_id FROM acl.role_can_assign_role "
            "WHERE role_id = " + std::to_string(id_));
        if (rows.empty()) {
            return {};
        }
        ACLGateway gateway(work_);
        std::vector<Role> roles;
        roles.reserve(rows.size());
        for (const auto& row : rows) {
            roles.emplace_back(gateway.role(row[0].as<ID>()));
        }
        return roles;
    } CATCH_CONVERT();
    return {};
}

std::vector<Group>
Role::canAssignGroups() const
{
    try {
        auto rows = work_.exec(
            "SELECT can_assign_group_id FROM acl.role_can_assign_group "
            "WHERE role_id = " + std::to_string(id_));
        if (rows.empty()) {
            return {};
        }
        ACLGateway gateway(work_);
        std::vector<Group> groups;
        groups.reserve(rows.size());
        for (const auto& row : rows) {
            groups.emplace_back(gateway.group(row[0].as<ID>()));
        }
        return groups;
    } CATCH_CONVERT();
    return {};
}

} // namespace maps::wiki::acl
