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

#include <yandex/maps/wiki/common/moderation.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/make_batches.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/enum_io/include/enum_io.h>

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

#include <boost/lexical_cast.hpp>

namespace maps::wiki::acl {

namespace {
constexpr enum_io::Representations<UsersOrder> USERS_ORDER_ENUM_REPRESENTATION {
    {UsersOrder::Date, "date"},
    {UsersOrder::Login, "login"},
};
} // namespace

DEFINE_ENUM_IO(UsersOrder, USERS_ORDER_ENUM_REPRESENTATION);

namespace {

const std::string USER_BAN_ACTIVE_PREDICATE =
    "(u.status = 'active' AND"
    " u.current_ban_id IS NOT NULL AND"
    " br.br_action = 'ban' AND"
    " (br.br_expires IS NULL OR br.br_expires >= now())) ";
const std::string USER_QUERY =
    "SELECT u.*, br.*, " + USER_BAN_ACTIVE_PREDICATE + " AS ban_active, "
        "EXTRACT(EPOCH FROM "
            "COALESCE(u.created, 'epoch'::timestamp))::bigint "
            "AS created_ts, "
        "EXTRACT(EPOCH FROM "
            "COALESCE(br.br_expires, br.br_created, 'epoch'::timestamp))::bigint "
            "AS last_br_ts "
    "FROM acl.user u "
    "LEFT JOIN acl.ban_record br ON (u.current_ban_id = br.br_id) ";
const std::string USERS_ORDER_DATE_LOGIN = " ORDER BY u.modified DESC, u.login ASC ";
const std::string USERS_ORDER_DATE_LOGIN_REVERT = " ORDER BY u.modified ASC, u.login DESC ";
const std::string USERS_ORDER_LOGIN = " ORDER BY u.login ASC ";
const std::string USERS_ORDER_LOGIN_REVERT = " ORDER BY u.login DESC ";
const std::string BAN_RECORD_QUERY = "SELECT br.* FROM acl.ban_record br ";
const std::string BAN_RECORDS_ORDER = " ORDER BY br.br_created DESC ";

const std::string GROUP_QUERY = "SELECT g.* FROM acl.group g ";
const std::string GROUPS_ORDER = " ORDER BY g.name ASC ";
const std::string GROUPS_ORDER_REVERT = " ORDER BY g.name DESC ";

const std::string ROLE_QUERY = "SELECT r.* FROM acl.role r ";
const std::string ROLES_ORDER = " ORDER BY r.name ASC ";
const std::string ROLES_ORDER_REVERT = " ORDER BY r.name DESC ";

const std::string PERMISSION_QUERY = "SELECT pm.* FROM acl.permission pm ";
const std::string PERMISSIONS_ORDER = " ORDER BY pm.id ";

const std::string USER_ID_ALIAS = "user_id";

const std::string LESS = "<";
const std::string GREATER = ">";
const std::string SKIP_NAME_SYMBOLS = "[]\\/^$|?*+(){}";
const size_t MAX_LOGIN_LENGTH = 128;
const size_t DELETE_BATCH_SIZE = 1000;

std::string
regexForSql(const std::string& str, bool exactMatch)
{
    std::string result;
    result.reserve(str.size() * 3);
    if (exactMatch) {
        result.push_back('^');
    }
    for (char ch : str) {
        if (ch == '.' || ch == '-') {
            result += "[\\.\\-]";
        } else if (std::string::npos == SKIP_NAME_SYMBOLS.find(ch)) {
            result.push_back(ch);
        }
    }
    if (exactMatch) {
        result.push_back('$');
    }

    return result;
}

std::string
countQuery(const std::string& query)
{
    return "SELECT count(*) FROM (" + query + ") q";
}

std::string
limitOffset(const common::Pager& pager)
{
    return " LIMIT " + std::to_string(pager.limit()) +
           " OFFSET " + std::to_string(pager.offset());
}

std::string
compileUserIdQuery(ID groupId, const std::set<ID>& roleIds, ID aoiId)
{
    std::string result;
    auto roleAoiWhereCondition = [&]() {
        std::string expr;
        if (!roleIds.empty()) {
            expr += " AND p.role_id IN (" + common::join(roleIds, ',') + ")";
        }
        if (aoiId) {
            expr += " AND p.aoi_id = " + std::to_string(aoiId);
        }
        return expr;
    };

    if (groupId) {
        result += "SELECT DISTINCT gu.user_id AS " + USER_ID_ALIAS + " FROM acl.group_user gu "
            " WHERE gu.group_id = " + std::to_string(groupId);
    }
    if (roleIds.empty() && !aoiId) {
        return result;
    }

    if (!result.empty()) {
        result += " INTERSECT ";
    }

    // acl.policy.agent_id = group.id
    result += "(SELECT DISTINCT gu.user_id AS " + USER_ID_ALIAS +
        " FROM acl.policy p "
            "JOIN acl.group g ON (g.id = p.agent_id) "
            "JOIN acl.group_user gu ON (g.id = gu.group_id) WHERE TRUE ";
    result += roleAoiWhereCondition();

    // acl.policy.agent_id = user.id
    result += " UNION SELECT DISTINCT p.agent_id  AS " + USER_ID_ALIAS +
        " FROM acl.policy p WHERE p.agent_id NOT IN (SELECT id from acl.group)";
    result += roleAoiWhereCondition();
    result += ")";

    return result;
}

std::string
statusesPredicate(const std::vector<User::Status>& statuses)
{
    ASSERT(!statuses.empty());
    std::vector<std::string> statusPredicates;
    for (auto status : statuses) {
        if (status == User::Status::Deleted) {
            statusPredicates.push_back("(u.status = 'deleted')");
        } else if (status == User::Status::Banned) {
            statusPredicates.push_back(USER_BAN_ACTIVE_PREDICATE);
        } else if (status == User::Status::Active) {
            statusPredicates.push_back(
                "(u.status = 'active' AND NOT " + USER_BAN_ACTIVE_PREDICATE + ")");
        }
    }
    return "(" + common::join(statusPredicates, " OR ") + ")";
}

void
updateCurrentBanId(const BanRecord& banRecord, Transaction& work)
{
    auto query =
        "UPDATE acl.user SET "
            "modified_by = " + std::to_string(banRecord.createdBy()) + ", "
            "modified = " + work.quote(banRecord.created()) + ", "
            "current_ban_id = " + std::to_string(banRecord.id()) +
        " WHERE uid = " + std::to_string(banRecord.uid());
    work.exec(query);
}

void
writePolicy(ID agent, ID role, ID aoiId, Transaction& work)
{
    auto query =
        "INSERT INTO acl.policy (agent_id, role_id, aoi_id) VALUES "
        "(" + std::to_string(agent) + ", "
            + std::to_string(role) + ", "
            + std::to_string(aoiId) + ")";
    work.exec(query);
}

Permission
makePermission(const pqxx::row& row, Transaction& work)
{
    return Factory::permission(
        row["id"].as<ID>(),
        row["name"].as<std::string>(),
        row["parent_id"].as<ID>(),
        work);
}

std::set<ID> usersByQuery(Transaction& work, const std::string& userIdQuery)
{
    std::set<ID> result;
    for (const auto& row : work.exec(userIdQuery)) {
        result.insert(row[USER_ID_ALIAS].as<ID>());
    }
    return result;
}

} // namespace

ACLGateway::ACLGateway(Transaction& work)
    : work_(work)
{}

User
ACLGateway::createUser(
    UID userId,
    const std::string& login,
    const std::string& displayName,
    UID authorId)
{
    try {
        if (login.empty()) {
            throw BadParam() << "Empty new user login";
        }
        auto authorIdStr = std::to_string(authorId);
        auto query =
            "INSERT INTO acl.user (uid, login, display_name, created_by, modified_by) VALUES "
                "(" + std::to_string(userId) + ", "
                    + work_.quote(login) + ", "
                    + work_.quote(displayName) + ", "
                    + authorIdStr + ", "
                    + authorIdStr + ")";
        work_.exec(query);
    } catch (const pqxx::unique_violation&) {
        throw DuplicateUser()
            << "User with uid " << userId
            << " and/or login " << login
            << " already exists";
    } CATCH_CONVERT();
    return user(userId);
}

User
ACLGateway::user(UID userId)
{
    auto userRows =
        work_.exec(USER_QUERY + " WHERE u.uid = " + std::to_string(userId));

    if (userRows.empty()) {
        throw UserNotExists()
            << "User with uid " << userId << " not found";
    }
    ASSERT(userRows.size() == 1);
    return Factory::user(userRows[0], work_);
}

User
ACLGateway::userById(ID id)
{
    auto userRows =
        work_.exec(USER_QUERY + " WHERE u.id = " + std::to_string(id));

    if (userRows.empty()) {
        throw UserNotExists()
            << "User with id " << id << " not found";
    }
    ASSERT(userRows.size() == 1);
    return Factory::user(userRows[0], work_);
}

User
ACLGateway::user(const std::string& login)
{
    auto userRows =
        work_.exec(
            USER_QUERY +
            " WHERE u.login ~* " +
            work_.quote(regexForSql(login, true)));

    if (userRows.empty()) {
        throw UserNotExists()
            << "User with login " << login << " not found";
    }
    REQUIRE(userRows.size() == 1,
        "Can't match unique user to login: " << login);
    return Factory::user(userRows[0], work_);
}

std::vector<User>
ACLGateway::users()
{
    return users(0, 0, 0, std::nullopt, 0, 0).value();
}

std::vector<User>
ACLGateway::users(const std::string& loginPart, size_t limit)
{
    if (loginPart.size() > MAX_LOGIN_LENGTH) {
        return {};
    }
    auto subquery =
        USER_QUERY +
        " WHERE u.login ~* " + work_.quote(regexForSql(loginPart, false)) +
        USERS_ORDER_DATE_LOGIN;
    if (limit) {
        subquery += " LIMIT " + std::to_string(limit);
    }
    auto query =
        USER_QUERY + " WHERE u.login ~* " + work_.quote(regexForSql(loginPart, true)) +
        " UNION (" + subquery + ")";
    auto userRows = work_.exec(query);
    std::vector<User> users;
    users.reserve(userRows.size());
    for (const auto& row : userRows) {
        users.push_back(Factory::user(row, work_));
    }
    return users;
}

std::vector<User>
ACLGateway::users(const std::vector<UID>& uids)
{
    std::vector<User> users;
    if (uids.empty()) {
        return users;
    }

    auto uidsStr = "ARRAY[" + common::join(uids, ',') + "]";
    auto userRows =
        work_.exec(USER_QUERY + " WHERE u.uid = ANY(" + uidsStr + ") " + USERS_ORDER_DATE_LOGIN);
    for (const auto& row : userRows) {
        users.push_back(Factory::user(row, work_));
    }
    return users;
}

std::vector<User>
ACLGateway::usersByIds(const std::vector<ID>& ids)
{
    std::vector<User> users;
    if (ids.empty()) {
        return users;
    }

    auto idsStr = "ARRAY[" + common::join(ids, ',') + "]";
    auto userRows =
        work_.exec(USER_QUERY + " WHERE u.id = ANY(" + idsStr + ") " + USERS_ORDER_DATE_LOGIN);

    for (const auto& row : userRows) {
        users.push_back(Factory::user(row, work_));
    }
    return users;
}

common::PagedResult<std::vector<User>>
ACLGateway::users(
    ID groupId,
    ID roleId, ID aoiId,
    const std::optional<User::Status>& userStatus,
    size_t page, size_t perPage)
{
    std::string userIdQuery = compileUserIdQuery(
        groupId, roleId ? std::set<ID>{roleId} : std::set<ID>{}, aoiId);

    std::string userPredicate = " WHERE TRUE ";
    if (!userIdQuery.empty()) {
        userPredicate += " AND u.id IN (" + userIdQuery + ") ";
    }
    if (userStatus) {
        userPredicate += " AND ";
        if (*userStatus == User::Status::Banned) {
            userPredicate += USER_BAN_ACTIVE_PREDICATE;
        } else {
            userPredicate += " u.status = "
                + work_.quote(boost::lexical_cast<std::string>(*userStatus));
        }
    }

    std::string query = USER_QUERY;
    query += userPredicate;

    auto totalCount = work_.exec(countQuery(query))[0][0].as<size_t>();
    common::Pager pager(totalCount, page, perPage);

    query += USERS_ORDER_DATE_LOGIN;
    query += limitOffset(pager);

    std::vector<User> users;
    for (const auto& row : work_.exec(query)) {
        users.push_back(Factory::user(row, work_));
    }
    return {std::move(pager), std::move(users)};
}

namespace {

template<typename RowIter>
std::vector<User>
readUsers(Transaction& work, RowIter begin, RowIter end, size_t limit)
{
    std::vector<User> users;
    for (auto it = begin; it != end; ++it) {
        const auto& row = *it;
        users.emplace_back(Factory::user(row, work));
        if (limit && users.size() == limit) {
            break;
        }
    }
    return users;
}

bool
couldBeUID(const std::string& loginPart)
{
    return !loginPart.empty() &&
        std::all_of(
            loginPart.begin(),
            loginPart.end(),
            [](const auto c) { return std::isdigit(c); });
}

} // namespace

ResultBeforeAfter<User>
ACLGateway::users(
    ID groupId,
    const std::set<ID>& roleIds,
    ID aoiId,
    const std::vector<User::Status>& statuses,
    const std::string& namePart,
    ID startID,
    common::BeforeAfter beforeAfter,
    const std::set<ID>& permissionIds,
    size_t limit,
    UsersOrder order)
{
    std::string userIdQuery = compileUserIdQuery(
        groupId, roleIds, aoiId);
    std::string userPredicate = " WHERE TRUE ";
    if (!userIdQuery.empty()) {
        userPredicate += " AND u.id IN (" + userIdQuery + ") ";
    }
    if (!namePart.empty()) {
        if (namePart.size() > MAX_LOGIN_LENGTH) {
            return {};
        }
        const auto regexNamePart = work_.quote(regexForSql(namePart, false));
        userPredicate +=
            " AND (u.login ~* " + regexNamePart +
            " OR u.display_name ~* " + regexNamePart +
            (couldBeUID(namePart) ? " OR u.uid = " + namePart : std::string()) +
            ")";
    }

    if (!statuses.empty()) {
        userPredicate += "AND " + statusesPredicate(statuses);
    }

    if (startID) {
        const auto startUser = userById(startID);
        const std::string startLogin = work_.quote(startUser.login());
        if (order == UsersOrder::Login) {
            userPredicate +=
                " AND u.login "
                + (beforeAfter == common::BeforeAfter::Before ? LESS : GREATER)
                + startLogin;
        } else {
            const auto startDate = work_.quote(startUser.modified());
            userPredicate +=
                " AND ("
                "modified " + (beforeAfter == common::BeforeAfter::Before ? GREATER : LESS) // Date order is desc
                + startDate + " OR (modified = " + startDate +
                " AND u.login "
                + (beforeAfter == common::BeforeAfter::Before ? LESS : GREATER)
                + startLogin + "))";
        }
    }
    std::string query = USER_QUERY;
    if (!permissionIds.empty()) {
        auto clusterIds = clusterIdsByPermissions(permissionIds);
        if (clusterIds.empty()) {
            return {};
        }
        query +=
            " INNER JOIN acl.agent_cluster ac ON u.id=ac.agent_id AND " +
            common::whereClause("ac.cluster_id", clusterIds);
    }
    query += userPredicate;

    bool reverse = startID && beforeAfter == common::BeforeAfter::Before;

    if (order == UsersOrder::Login) {
        query += reverse ? USERS_ORDER_LOGIN_REVERT : USERS_ORDER_LOGIN;
    } else {
        query += reverse ? USERS_ORDER_DATE_LOGIN_REVERT : USERS_ORDER_DATE_LOGIN;
    }

    if (limit) {
        query += " LIMIT " + std::to_string(limit + 1);
    }
    const auto rows = work_.exec(query);
    if (rows.empty()) {
        return {};
    }
    const bool hasMore = limit ? (rows.size() > limit) : false;

    return ResultBeforeAfter<User> {
        reverse
            ? readUsers(work_, limit >= rows.size() ? rows.rbegin() : rows.rbegin() + 1, rows.rend(), limit)
            : readUsers(work_, rows.begin(), rows.end(), limit),
        hasMore
    };
}

std::set<ID>
ACLGateway::userIds(ID groupId, ID roleId, ID aoiId)
{
    std::string userIdQuery = compileUserIdQuery(
        groupId, roleId ? std::set<ID>{roleId} : std::set<ID>{}, aoiId);
    if (userIdQuery.empty()) {
        userIdQuery = "SELECT id AS " + USER_ID_ALIAS + " FROM acl.user";
    }
    return usersByQuery(work_, userIdQuery);
}

std::set<ID>
ACLGateway::userIdsByRoles(const std::set<ID>& roleIds)
{
    if (roleIds.empty()) {
        return {};
    }
    std::string userIdQuery = compileUserIdQuery(0, roleIds, 0);
    return usersByQuery(work_, userIdQuery);
}

void
ACLGateway::addIDMRole(const std::string& staffLogin, const IDMRole& role)
{
    try {
        work_.exec(
            "INSERT INTO acl.idm (staff_login, service, role) VALUES (" +
            work_.quote(staffLogin) + "," +
            work_.quote(role.service) + "," +
            work_.quote(role.role) + ") ON CONFLICT DO NOTHING;");
    } CATCH_CONVERT();
}

void
ACLGateway::removeIDMRole(const std::string& staffLogin, const IDMRole& role)
{
    try {
        work_.exec(
            "DELETE FROM acl.idm WHERE "
            "staff_login = " + work_.quote(staffLogin) + " AND "
            "service = " + work_.quote(role.service) + " AND "
            "role = " + work_.quote(role.role));
    } CATCH_CONVERT();
}

std::set<std::string>
ACLGateway::userIDMRoles(const std::string& staffLogin, const std::string& service) const
{
    std::set<std::string> roles;
    try {
        const auto rows = work_.exec(
            "SELECT role FROM acl.idm WHERE "
            "staff_login = " + work_.quote(staffLogin) + " AND "
            "service = " + work_.quote(service));
        for (const auto& row : rows) {
            roles.emplace(row[0].c_str());
        }
    } CATCH_CONVERT();
    return roles;
}

std::map<std::string, std::set<std::string>>
ACLGateway::allUsersIDMRoles(const std::string& service) const
{
    std::map<std::string, std::set<std::string>> loginToRoles;
    try {
        const auto rows = work_.exec(
            "SELECT staff_login, role FROM acl.idm WHERE "
            "service = " + work_.quote(service));
        for (const auto& row : rows) {
            loginToRoles[row[0].c_str()].insert(row[1].c_str());
        }
    } CATCH_CONVERT();
    return loginToRoles;
}

std::map<std::string, std::vector<IDMRole>>
ACLGateway::allUsersIDMRoles() const
{
    std::map<std::string, std::vector<IDMRole>> loginToServiceRoles;
    try {
        const auto rows = work_.exec(
            "SELECT staff_login, service, role FROM acl.idm");
        for (const auto& row : rows) {
            loginToServiceRoles[row[0].c_str()].push_back(
            {
                .service = row[1].c_str(),
                .role = row[2].c_str()
            });
        }
    } CATCH_CONVERT();
    return loginToServiceRoles;
}

std::set<ID>
ACLGateway::clusterIdsByPermissions(const std::set<ID>& permissionIds)
{
    std::set<ID> leafPermissions;
    std::string leafsQuery =
        "SELECT leaf_ids FROM acl.permission WHERE " +
        common::whereClause("id", permissionIds);
    auto leafRows = work_.exec(leafsQuery);
    for (const auto& leafRow : leafRows) {
        REQUIRE(!leafRow[0].is_null(), "Permissions leafs need sync.");
        auto leafs = common::parseSqlArray<ID>(leafRow[0].c_str());
        leafPermissions.insert(leafs.begin(), leafs.end());
    }
    if (leafPermissions.empty()) {
        return {};
    }
    const auto arrayBody = common::join(leafPermissions, ',');
    std::string query =
        " SELECT cluster_id FROM acl.cluster WHERE"
        " permission_leaf_ids @> ARRAY[" + arrayBody + "]::bigint[]";
    auto rows = work_.exec(query);
    std::set<ID> agentIds;
    for (const auto& row : rows) {
        agentIds.insert(row[0].as<ID>());
    }
    return agentIds;
}

BanRecord
ACLGateway::banUser(
    UID userId, UID authorId,
    const std::string& expires,
    const std::string& reason)
{
    auto query =
        "INSERT INTO acl.ban_record "
            "(br_uid, br_action, br_created_by, br_expires, br_reason) VALUES "
            "(" + std::to_string(userId) + ", "
                "'ban', "
                + std::to_string(authorId) + ", "
                + (expires.empty() ? "NULL" : work_.quote(expires)) + ", "
                + (reason.empty() ? "NULL" : work_.quote(reason)) + ")"
        " RETURNING *";

    pqxx::result banRecordRows;
    try {
        banRecordRows = work_.exec(query);
    } catch (const pqxx::foreign_key_violation&) {
        throw UserNotExists()
            << "At least one of users with ids "
            << userId << " and " << authorId << " not found";
    }
    ASSERT(banRecordRows.size() == 1);
    auto banRecord = Factory::banRecord(banRecordRows[0]);
    updateCurrentBanId(banRecord, work_);
    return banRecord;
}

BanRecord
ACLGateway::unbanUser(UID userId, UID authorId)
{
    auto query =
        "INSERT INTO acl.ban_record "
            "(br_uid, br_action, br_created_by) VALUES "
            "(" + std::to_string(userId) + ", "
                "'unban', "
                + std::to_string(authorId) + ")"
            " RETURNING *";
    pqxx::result banRecordRows;
    try {
        banRecordRows = work_.exec(query);
    } catch (const pqxx::foreign_key_violation&) {
        throw UserNotExists()
            << "At least one of users with ids "
            << userId << " and " << authorId << " not found";
    }
    ASSERT(banRecordRows.size() == 1);
    auto banRecord = Factory::banRecord(banRecordRows[0]);
    updateCurrentBanId(banRecord, work_);
    return banRecord;
}

common::PagedResult<std::vector<BanRecord>>
ACLGateway::banRecords(
    UID userId, size_t page, size_t perPage)
{
    std::string banRecordPredicate = " WHERE TRUE ";
    if (userId) {
        banRecordPredicate += " AND br.br_uid = " + std::to_string(userId);
    }

    std::string query = BAN_RECORD_QUERY;
    query += banRecordPredicate;

    auto totalCount = work_.exec(countQuery(query))[0][0].as<size_t>();
    common::Pager pager(totalCount, page, perPage);

    query += BAN_RECORDS_ORDER;
    query += limitOffset(pager);

    std::vector<BanRecord> banRecords;
    for (const auto& row : work_.exec(query)) {
        banRecords.push_back(Factory::banRecord(row));
    }
    return {std::move(pager), std::move(banRecords)};
}

std::vector<UID>
ACLGateway::recentlyUnbannedUsers(std::chrono::seconds afterBanInterval)
{
    if (afterBanInterval == std::chrono::seconds(0)) {
        return {};
    }

    auto afterBanTime =
        "NOW() - '" + std::to_string(afterBanInterval.count()) + " seconds'::interval";
    auto query =
        "SELECT uid"
        " FROM acl.user"
        " JOIN acl.ban_record ON current_ban_id = br_id"
        " WHERE"
        "  (br_action = 'unban' AND br_created > " + afterBanTime + ") OR"
        "  (br_action = 'ban' AND br_expires < NOW() AND br_expires > " + afterBanTime + ")";

    std::vector<UID> result;
    try {
        const auto rows = work_.exec(query);
        result.reserve(rows.size());
        for (const auto& row : rows) {
            result.emplace_back(row[0].as<UID>());
        }
    } CATCH_CONVERT();

    return result;
}

void
ACLGateway::drop(User&& user)
{
    try {
        const auto userIdStr = std::to_string(user.id());
        auto query =
            "DELETE FROM acl.policy WHERE agent_id = " + userIdStr +
            ";DELETE FROM acl.group_user WHERE user_id = " + userIdStr +
            ";DELETE FROM acl.ban_record WHERE br_uid = " + std::to_string(user.uid()) +
            ";DELETE FROM acl.user WHERE id = " + userIdStr;
        work_.exec(query);
        ClusterManager(work_).releaseClusterByAgentId(user.id());
    } CATCH_CONVERT();
}

Group
ACLGateway::createGroup(const std::string& name, const std::string& description)
{
    ID groupId = 0;
    try {
        if (name.empty()) {
            throw BadParam() << "Empty group name";
        }
        auto query =
            "INSERT INTO acl.group (name, description) "
            "VALUES (" + work_.quote(name) +
            "," + work_.quote(description) + ")"
            " RETURNING id";
        groupId = work_.exec(query)[0][0].as<ID>();
    } catch (const pqxx::unique_violation&) {
        throw DuplicateGroup()
            << "Group with name " << name << " already exists";
    } CATCH_CONVERT();
    return Factory::group(groupId, name, description, work_);
}

Group
ACLGateway::group(ID groupId)
{
    auto groupRows =  work_.exec(
        GROUP_QUERY + " WHERE g.id = " + std::to_string(groupId));

    if (groupRows.empty()) {
        throw GroupNotExists()
            << "Group with id " << groupId << " not found";
    }
    ASSERT(groupRows.size() == 1);
    return Factory::group(groupRows[0], work_);
}

Group
ACLGateway::group(const std::string& name)
{
    auto groupRows = work_.exec(GROUP_QUERY + " WHERE g.name = " + work_.quote(name));

    if (groupRows.empty()) {
        throw GroupNotExists()
            << "Group with name " << name << " not found";
    }
    ASSERT(groupRows.size() == 1);
    return Factory::group(groupRows[0], work_);
}

namespace {

template<typename RowIter>
std::vector<Group>
readGroups(Transaction& work, RowIter begin, RowIter end, size_t limit)
{
    std::vector<Group> groups;
    for (auto it = begin; it != end; ++it) {
        const auto& row = *it;
        groups.emplace_back(Factory::group(row, work));
        if (limit && groups.size() == limit) {
            break;
        }
    }
    return groups;
}

std::vector<Group>
readGroups(const pqxx::result& groupRows, Transaction& work)
{
    return readGroups(work, groupRows.begin(), groupRows.end(), 0);
}

} // namespace

std::vector<Group>
ACLGateway::groups()
{
    auto groupRows = work_.exec(GROUP_QUERY + GROUPS_ORDER);
    return readGroups(groupRows, work_);
}

namespace {
template<typename IdsContainer>
std::vector<Group>
groupsByIds(const IdsContainer& groupIds, Transaction& work)
{
    if (groupIds.empty()) {
        return {};
    }

    auto gidsStr = "ARRAY[" + common::join(groupIds, ',') + "]";
    auto groupRows = work.exec(
        GROUP_QUERY +
        " WHERE g.id = ANY(" +  gidsStr + ") " +
        GROUPS_ORDER);

    return readGroups(groupRows, work);
}
} // namespace

std::vector<Group>
ACLGateway::groups(const std::vector<ID>& groupIds)
{
    return groupsByIds(groupIds, work_);
}

std::vector<Group>
ACLGateway::groups(const std::set<ID>& groupIds)
{
    return groupsByIds(groupIds, work_);
}

common::PagedResult<std::vector<Group>>
ACLGateway::groups(
    ID roleId, ID aoiId,
    size_t page, size_t perPage)
{
    std::string groupIdQuery;

    if (roleId || aoiId) {
        groupIdQuery +=
            "SELECT DISTINCT p.agent_id FROM acl.policy p WHERE TRUE ";
        if (roleId) {
            groupIdQuery += " AND p.role_id = " + std::to_string(roleId);
        }
        if (aoiId) {
            groupIdQuery += " AND p.aoi_id = " + std::to_string(aoiId);
        }
    }

    std::string groupPredicate = " WHERE TRUE ";
    if (!groupIdQuery.empty()) {
        groupPredicate += " AND g.id IN (" + groupIdQuery + ") ";
    }

    std::string query = GROUP_QUERY;
    query += groupPredicate;

    auto totalCount = work_.exec(countQuery(query))[0][0].as<size_t>();
    common::Pager pager(totalCount, page, perPage);

    query += GROUPS_ORDER;
    query += limitOffset(pager);
    auto groups = readGroups(work_.exec(query), work_);
    return {std::move(pager), std::move(groups)};
}

ResultBeforeAfter<Group>
ACLGateway::groups(
        const std::set<ID>& roleIds,
        ID aoiId,
        const std::string& namePart,
        ID startID,
        common::BeforeAfter beforeAfter,
        const std::set<ID>& permissionIds,
        size_t limit)
{
    std::string groupIdQuery;

    if (aoiId || !roleIds.empty()) {
        groupIdQuery +=
            "SELECT DISTINCT p.agent_id FROM acl.policy p WHERE TRUE ";
        if (!roleIds.empty()) {
            groupIdQuery += " AND p.role_id IN (" + common::join(roleIds, ',') + ")";
        }
        if (aoiId) {
            groupIdQuery += " AND p.aoi_id = " + std::to_string(aoiId);
        }
    }

    std::string groupPredicate = " WHERE TRUE ";
    if (!groupIdQuery.empty()) {
        groupPredicate += " AND g.id IN (" + groupIdQuery + ") ";
    }

    std::string query = GROUP_QUERY;
    if (!permissionIds.empty()) {
        auto clusterIds = clusterIdsByPermissions(permissionIds);
        if (clusterIds.empty()) {
            return {};
        }
        query +=
            " INNER JOIN acl.agent_cluster ac ON g.id=ac.agent_id AND " +
            common::whereClause("ac.cluster_id", clusterIds);
    }
    query += groupPredicate;
    if (!namePart.empty()) {
        query += " AND g.name ~* " + work_.quote(regexForSql(namePart, false));
    }
    if (startID) {
        query +=
            " AND g.name "
            + (beforeAfter == common::BeforeAfter::Before ? LESS : GREATER)
            + work_.quote(group(startID).name());
    }
    bool reverse = startID && beforeAfter == common::BeforeAfter::Before;

    query += reverse ? GROUPS_ORDER_REVERT : GROUPS_ORDER;

    if (limit) {
        query += " LIMIT " + std::to_string(limit + 1);
    }
    const auto rows = work_.exec(query);
    if (rows.empty()) {
        return {};
    }
    const bool hasMore = limit ? (rows.size() > limit) : false;

    return ResultBeforeAfter<Group> {
        reverse
            ? readGroups(work_, limit >= rows.size() ? rows.rbegin() : rows.rbegin() + 1, rows.rend(), limit)
            : readGroups(work_, rows.begin(), rows.end(), limit),
        hasMore
    };
}

void
ACLGateway::drop(Group&& group)
{
    try {
        ClusterManager cm(work_);
        auto affectedClusters = cm.keyGroupAffectedClusters(group.id());
        auto query =
            "DELETE FROM acl.group WHERE id = " + std::to_string(group.id());
        work_.exec(query);
        cm.updateClusters(affectedClusters);
        cm.releaseClusterByAgentId(group.id());
    } CATCH_CONVERT();
}

Role
ACLGateway::createRole(const std::string& name, const std::string& description, Role::Privacy privacy)
{
    ID roleId = 0;
    try {
        if (name.empty()) {
            throw BadParam() << "Empty role name";
        }
        auto query =
            "INSERT INTO acl.role (name, description, is_public) VALUES (" +
            work_.quote(name) + "," +
            work_.quote(description) + "," +
            (privacy == Role::Privacy::Public ? "TRUE" : "FALSE") + ")"
            " RETURNING id";
        roleId = work_.exec(query)[0][0].as<ID>();
    } catch (const pqxx::unique_violation&) {
        throw DuplicateRole()
            << "Role with name " << name << " already exists";
    } CATCH_CONVERT();
    return Factory::role(roleId, name, description, privacy, work_);
}

Role
ACLGateway::role(ID roleId)
{
    auto roleRows =
        work_.exec(ROLE_QUERY + " WHERE r.id = " + std::to_string(roleId));

    if (roleRows.empty()) {
        throw RoleNotExists()
            << "Role with id " << roleId << " not found";
    }
    ASSERT(roleRows.size() == 1);
    return Factory::role(
        roleRows[0]["id"].as<ID>(),
        roleRows[0]["name"].as<std::string>(),
        roleRows[0]["description"].as<std::string>(),
        Role::privacy(roleRows[0]["is_public"].as<bool>()),
        work_);
}

Role
ACLGateway::role(const std::string& name)
{
    auto roleRows =
        work_.exec(ROLE_QUERY + " WHERE r.name = " + work_.quote(name));

    if (roleRows.empty()) {
        throw RoleNotExists()
            << "Role with name " << name << " not found";
    }
    ASSERT(roleRows.size() == 1);
    return Factory::role(
        roleRows[0]["id"].as<ID>(),
        roleRows[0]["name"].as<std::string>(),
        roleRows[0]["description"].as<std::string>(),
        Role::privacy(roleRows[0]["is_public"].as<bool>()),
        work_);
}

namespace {

template<typename RowIter>
std::vector<Role>
readRoles(Transaction& work, RowIter begin, RowIter end, size_t limit)
{
    std::vector<Role> roles;
    for (auto it = begin; it != end; ++it) {
        const auto& row = *it;
        roles.emplace_back(Factory::role(
            row["id"].template as<ID>(),
            row["name"].template as<std::string>(),
            row["description"].template as<std::string>(),
            Role::privacy(row["is_public"].template as<bool>()),
            work));
        if (limit && roles.size() == limit) {
            break;
        }
    }
    return roles;
}

std::vector<Role>
readRoles(const pqxx::result& roleRows, Transaction& work)
{
    std::vector<Role> roles;
    for (const auto& row : roleRows) {
        roles.push_back(Factory::role(
            row["id"].as<ID>(),
            row["name"].as<std::string>(),
            row["description"].as<std::string>(),
            Role::privacy(row["is_public"].as<bool>()),
            work));
    }
    return roles;
}

} // namespace

std::vector<Role>
ACLGateway::roles()
{
    auto roleRows =
        work_.exec(ROLE_QUERY + ROLES_ORDER);
    return readRoles(roleRows, work_);
}

std::vector<Role>
ACLGateway::roles(const std::set<ID>& roleIds)
{
    if (roleIds.empty()) {
        return {};
    }
    std::string roleQuery = ROLE_QUERY + " WHERE " +
        common::whereClause("id", roleIds);
    return readRoles(work_.exec(roleQuery), work_);
}

std::vector<Role>
ACLGateway::roles(InclusionType inclusionType, const std::string& namePart, size_t limit)
{
    REQUIRE(!namePart.empty(), "Expecting non empty roles namePart.");

    std::ostringstream os;
    os << ROLE_QUERY
       << " WHERE r.name ILIKE '";

    if (inclusionType == InclusionType::Contains) {
        os << '%';
    }
    os << work_.esc_like(namePart) << "%' "
       << ROLES_ORDER;

    if (limit) {
        os << " LIMIT " << limit;
    }

    return readRoles(work_.exec(os.str()), work_);
}

common::PagedResult<std::vector<Role>>
ACLGateway::roles(
    ID groupId, size_t page, size_t perPage)
{
    std::string roleIdQuery;

    if (groupId) {
        roleIdQuery += "SELECT DISTINCT p.role_id FROM acl.policy p "
            "WHERE p.agent_id = " + std::to_string(groupId);
    }

    std::string rolePredicate = " WHERE TRUE ";
    if (!roleIdQuery.empty()) {
        rolePredicate += " AND r.id IN (" + roleIdQuery + ") ";
    }

    std::string query = ROLE_QUERY;
    query += rolePredicate;

    auto totalCount = work_.exec(countQuery(query))[0][0].as<size_t>();
    common::Pager pager(totalCount, page, perPage);

    query += ROLES_ORDER;
    query += limitOffset(pager);

    return {std::move(pager), readRoles(work_.exec(query), work_)};
}

namespace {
std::vector<ID>
rolesIdsByPath(const SubjectPath& path, Transaction& work, ACLGateway& gateway)
{
    std::set<ID> permissionsIds;
    const auto leafPermission = gateway.permission(path);
    permissionsIds.insert(leafPermission.id());
    for (auto parentId = leafPermission.parentId();
        parentId != 0 && !permissionsIds.count(parentId);
        parentId = gateway.permission(parentId).parentId())
    {
        permissionsIds.insert(parentId);
    }
    if (permissionsIds.empty()) {
        return {};
    }
    std::string query =
        "SELECT DISTINCT role_id FROM acl.role_permission WHERE "
        + common::whereClause("permission_id", permissionsIds);
    auto roleIdsRows = work.exec(query);
    if (roleIdsRows.empty()) {
        return {};
    }
    std::vector<ID> roleIdsFound;
    roleIdsFound.reserve(roleIdsRows.size());
    for (const auto& row : roleIdsRows) {
        roleIdsFound.emplace_back(row[0].as<ID>());
    }
    return roleIdsFound;
}

} // namespace

std::vector<Role>
ACLGateway::permittingRoles(const std::vector<SubjectPath>& paths)
{
    return roles(permittingRoleIds(paths));
}

std::set<ID>
ACLGateway::permittingRoleIds(const std::vector<SubjectPath>& paths)
{
    std::unordered_map<ID, size_t> roleCounts;
    for (const auto& path : paths) {
        auto roleIds = rolesIdsByPath(path, work_, *this);
        if (roleIds.empty()) {
            return {};
        }
        for (const auto roleId : roleIds) {
            ++roleCounts[roleId];
        }
    }
    std::set<ID> rolesFoundIds;
    const auto pathsCount = paths.size();
    for (const auto& [roleId, count] : roleCounts) {
        if (count == pathsCount) {
            rolesFoundIds.emplace(roleId);
        }
    }
    return rolesFoundIds;
}

ResultBeforeAfter<Role>
ACLGateway::roles(
    ID groupId,
    const std::vector<SubjectPath>& paths,
    const std::string& namePart,
    ID startID,
    common::BeforeAfter beforeAfter,
    size_t limit)
{
    auto rolesByPermissions = permittingRoles(paths);
    if (rolesByPermissions.empty() && !paths.empty()) {
        return {};
    }

    std::string query = ROLE_QUERY;
    query += "WHERE TRUE ";

    if (groupId) {
        query +=
            "AND r.id IN ("
                "SELECT DISTINCT p.role_id FROM acl.policy p "
                "WHERE p.agent_id = " + std::to_string(groupId) +
            ")";
    }
    if (!rolesByPermissions.empty()) {
        query +=
            " AND r.id IN (" +
            common::join(rolesByPermissions, [](const Role& role){ return role.id(); }, ",") +
            ")";
    }
    if (!namePart.empty()) {
        query += " AND r.name ~* " + work_.quote(regexForSql(namePart, false));
    }
    if (startID) {
        query +=
            " AND r.name "
            + (beforeAfter == common::BeforeAfter::Before ? LESS : GREATER)
            + work_.quote(role(startID).name());
    }
    bool reverse = startID && beforeAfter == common::BeforeAfter::Before;

    query += reverse ? ROLES_ORDER_REVERT : ROLES_ORDER;

    if (limit) {
        query += " LIMIT " + std::to_string(limit + 1);
    }
    const auto rows = work_.exec(query);
    if (rows.empty()) {
        return {};
    }
    const bool hasMore = limit ? (rows.size() > limit) : false;

    return ResultBeforeAfter<Role> {
        reverse
            ? readRoles(work_, limit >= rows.size() ? rows.rbegin() : rows.rbegin() + 1, rows.rend(), limit)
            : readRoles(work_, rows.begin(), rows.end(), limit),
        hasMore
    };
}

void
ACLGateway::drop(Role&& role)
{
    try {
        ClusterManager cm(work_);
        auto affectedClusters = cm.keyRoleAffectedClusters(role.id());
        auto query =
            "DELETE FROM acl.role WHERE id = " + std::to_string(role.id());
        work_.exec(query);
        cm.updateClusters(affectedClusters);
    } CATCH_CONVERT();
}

Aoi
ACLGateway::aoi(ID aoiId)
{
    if (!aoiId) {
        return Factory::aoi(0, "", "", Deleted::No);
    }
    try {
        revision::RevisionsGateway revisions(work_); // Trunk
        auto snapshot = revisions.snapshot(revisions.headCommitId());
        auto result = snapshot.objectRevision(aoiId);
        if (!result || !result->data().attributes) {
            throw AoiNotExists()
                << "AOI with id " << aoiId << " not found";
        }
        const auto& attrs = *result->data().attributes;
        if (!attrs.count("cat:aoi")) {
            throw AoiNotExists()
                << "Object with id " << aoiId << " is not an AOI";
        }
        auto nameIt = attrs.find("aoi:name");
        if (nameIt == std::end(attrs)) {
            throw ACLLogicError()
                << "AOI " << aoiId << " doesn't have a name";
        }
        if (!result->data().geometry) {
            throw ACLLogicError()
                << "AOI " << aoiId << " doesn't have a geometry";
        }
        return Factory::aoi(
            aoiId,
            nameIt->second,
            *result->data().geometry,
            static_cast<Deleted>(result->data().deleted));
    } catch (const std::exception&) {
        // Exceptions accessing AOIs are ignored,
        // such AOIs considered deleted
    }
    return Factory::aoi(aoiId, "", "", Deleted::Yes);
}

Policy
ACLGateway::createPolicy(const User& user, const Role& role, const Aoi& aoi)
{
    try {
        writePolicy(user.id(), role.id(), aoi.id(), work_);
    } catch (const pqxx::unique_violation&) {
        throw DuplicatePolicy()
            << "Policy for user " << user.login() << " [" << user.id() << "]"
            << " and role " << role.name() << " [" << role.id() << "]"
            << " and aoi " << aoi.name() << " [" << aoi.id() << "]"
            << " already exists";
    } CATCH_CONVERT();
    return Factory::policy(user.id(), role, aoi.id(), work_);
}

Policy
ACLGateway::createPolicy(const Group& group, const Role& role, const Aoi& aoi)
{
    try {
        writePolicy(group.id(), role.id(), aoi.id(), work_);
    } catch (const pqxx::unique_violation&) {
        throw DuplicatePolicy()
            << "Policy for group " << group.name() << " [" << group.id() << "]"
            << " and role " << role.name() << " [" << role.id() << "]"
            << " and aoi " << aoi.name() << " [" << aoi.id() << "]"
            << " already exists";
    } CATCH_CONVERT();
    return Factory::policy(group.id(), role, aoi.id(), work_);
}

void
ACLGateway::drop(Policy&& policy)
{
    try {
        auto query =
            "DELETE FROM acl.policy"
            " WHERE agent_id = " + std::to_string(policy.agentId()) +
            "   AND role_id = " + std::to_string(policy.roleId()) +
            "   AND aoi_id = " + std::to_string(policy.aoiId());
        work_.exec(query);
    } CATCH_CONVERT();
}

Permission
ACLGateway::permission(ID permissionId)
{
    auto permissionRows =
        work_.exec(PERMISSION_QUERY + " WHERE pm.id = " + std::to_string(permissionId));

    if (permissionRows.empty()) {
        throw PermissionNotExists()
            << "Permission with id " << permissionId << " not found";
    }
    ASSERT(permissionRows.size() == 1);
    return makePermission(permissionRows[0], work_);
}

Permission
ACLGateway::permission(const SubjectPath& path)
{
    const auto& pathParts = path.pathParts();
    if (pathParts.empty() || pathParts.front().empty()) {
        throw BadParam() << "Bad path";
    }
    auto partIt = pathParts.begin();
    auto permission = rootPermission(*partIt);
    while (++partIt != std::end(pathParts)) {
        permission = permission.childPermission(*partIt);
    }
    return permission;
}

Permission
ACLGateway::createRootPermission(const std::string& name)
{
    ID permissionId = 0;
    try {
        if (name.empty()) {
            throw BadParam() << "Empty new permission name";
        }
        auto query =
            "INSERT INTO acl.permission (name, parent_id) VALUES "
            "(" + work_.quote(name) + ", 0)"
            " RETURNING id";
        permissionId = work_.exec(query)[0][0].as<ID>();
    } catch (const pqxx::unique_violation&) {
        throw DuplicatePermission()
            << "Root permission with name " << name << " already exists";
    } CATCH_CONVERT();

    return Factory::permission(permissionId, name, 0, work_);
}

Permission
ACLGateway::rootPermission(const std::string& name)
{
    auto permissionRows =
        work_.exec(
            PERMISSION_QUERY +
            " WHERE pm.parent_id = 0 AND pm.name = " +
            work_.quote(name));

    if (permissionRows.empty()) {
        throw PermissionNotExists()
            << "Root permission " << name << " not found";
    }
    ASSERT(permissionRows.size() == 1);
    return makePermission(permissionRows[0], work_);
}

Permissions
ACLGateway::allPermissions()
{
    auto permissionRows =
        work_.exec(PERMISSION_QUERY + PERMISSIONS_ORDER);

    std::vector<Permission> permissions;
    permissions.reserve(permissionRows.size());
    for (const auto& row : permissionRows) {
        permissions.push_back(makePermission(row, work_));
    }
    return Factory::permissions(std::move(permissions));
}

void
ACLGateway::drop(Permission&& permission)
{
    try {
        auto query =
            "DELETE FROM acl.permission"
            " WHERE id = " + std::to_string(permission.id());
        work_.exec(query);
    } CATCH_CONVERT();
}

std::string
ACLGateway::firstApplicableRole(
    const User& user,
    const std::vector<std::string>& roles,
    const std::string& defaultValue) const
{
    auto rolesMap = firstApplicableRoles({user.id()}, roles, defaultValue);
    ASSERT(rolesMap.size() == 1);
    ASSERT(rolesMap.begin()->first == user.id());
    return std::move(rolesMap.begin()->second);
}

std::map<ID, std::string>
ACLGateway::firstApplicableRoles(
    const std::vector<User>& users,
    const std::vector<std::string>& roles,
    const std::string& defaultValue) const
{
    std::vector<ID> agentIds;
    agentIds.reserve(users.size());
    for (const auto& user : users) {
        agentIds.push_back(user.id());
    }
    return firstApplicableRoles(agentIds, roles, defaultValue);
}

std::map<ID, std::string>
ACLGateway::firstApplicableRoles(
    const std::vector<ID>& agentIds,
    const std::vector<std::string>& roles,
    const std::string& defaultValue) const
{
    std::map<ID, std::string> idRolesMap;
    if (agentIds.empty() || roles.empty()) {
        for (ID agentId : agentIds) {
            idRolesMap[agentId] = defaultValue;
        }
        return idRolesMap;
    }

    auto agentIdsIn = common::join(agentIds, ',');

    std::string query =
        "SELECT policy.agent_id AS id, role.name AS role "
            "FROM acl.role JOIN acl.policy ON (policy.role_id = role.id) "
            "WHERE policy.agent_id IN (" + agentIdsIn + ") "
        "UNION SELECT group_user.user_id AS id, role.name as role "
            "FROM acl.role JOIN acl.policy ON (policy.role_id = role.id) "
                "JOIN acl.group_user ON (group_user.group_id = policy.agent_id) "
                "WHERE group_user.user_id in (" + agentIdsIn + ")";

    std::map<std::string, size_t> rolePos;
    for (size_t i = 0; i < roles.size(); ++i) {
        rolePos[roles[i]] = i;
    }

    std::map<ID, size_t> minRolePos;
    for (ID agentId : agentIds) {
        minRolePos[agentId] = roles.size();
    }
    for (const auto& row : work_.exec(query)) {
        auto minRolePosIt = minRolePos.find(row["id"].as<ID>());
        ASSERT(minRolePosIt != std::end(minRolePos));

        auto role = row["role"].as<std::string>();
        role.substr(0, role.find(common::MODERATION_STATUS_DELIMITER)).swap(role);
        auto it = rolePos.find(role);
        if (it != rolePos.end()) {
            minRolePosIt->second = std::min(minRolePosIt->second, it->second);
        }
    }

    for (const auto& idMinRolePosPair : minRolePos) {
        idRolesMap[idMinRolePosPair.first] =
            idMinRolePosPair.second < roles.size()
                ? roles[idMinRolePosPair.second]
                : defaultValue;
    }
    return idRolesMap;
}

void
ACLGateway::updateUserCluster(ID userId)
{
    ClusterManager(work_).updateUserCluster(userId);
}
void
ACLGateway::updateAgentsClusters(bool onlyMissing)
{
    ClusterManager(work_).updateAgentsClusters(onlyMissing);
}

void
ACLGateway::updateGroupCluster(ID groupId)
{
    ClusterManager(work_).updateGroupCluster(groupId);
}

void
ACLGateway::enqueueGroupAffectedClusters(ID groupId)
{
    ClusterManager cm(work_);
    cm.enqueueClusters(cm.keyGroupAffectedClusters(groupId));
}

void
ACLGateway::enqueueRoleAffectedClusters(ID roleId)
{
    ClusterManager cm(work_);
    cm.enqueueClusters(cm.keyRoleAffectedClusters(roleId));
}

void
ACLGateway::enqueueAllClusters()
{
    ClusterManager(work_).enqueueAll();
}

void
ACLGateway::enqueueUserCluster(ID userId)
{
    ClusterManager(work_).enqueueUserCluster(userId);
}

void
ACLGateway::enqueueGroupCluster(ID groupId)
{
    ClusterManager(work_).enqueueGroupCluster(groupId);
}

size_t
ACLGateway::processClustersUpdateQueue()
{
    return ClusterManager(work_).processClustersUpdateQueue();
}

size_t
ACLGateway::clustersUpdateQueueSize() const
{
    return ClusterManager(work_).clustersUpdateQueueSize();
}

namespace {

enum class AgentKind { User, Group };

ID
createSchedule(
    const std::string& startDate,
    const std::optional<std::string>& endDate,
    const std::optional<std::string>& startTime,
    const std::optional<std::string>& endTime,
    const std::optional<int>& weekdays,
    const std::optional<std::string>& workRestDays,
    Transaction& work)
{
    ID scheduleId = 0;
    try {
        std::string query =
            "INSERT INTO acl.schedule "
            "(date_start, date_end, time_start, time_end, weekdays, work_rest_days_sequence_array) "
            "VALUES ("
            "to_date(" + work.quote(startDate) + ", " + work.quote("DD.MM.YYYY") + "), " +
            (endDate
                ? "to_date(" + work.quote(endDate) + ", " + work.quote("DD.MM.YYYY") + ")"
                : "NULL") + ", " +
            (startTime ? work.quote(*startTime) : "NULL") + ", " +
            (endTime ? work.quote(*endTime) : "NULL") + ", " +
            (weekdays ? std::to_string(*weekdays) : "NULL") + ", " +
            (workRestDays ? work.quote(*workRestDays) : "NULL") + ") "
            "RETURNING id";
        scheduleId = work.exec(query)[0]["id"].as<ID>();
    } CATCH_CONVERT();
    return scheduleId;
}

Schedule
schedule(ID scheduleId, Transaction& work)
{
    std::string query =
        "SELECT * FROM acl.schedule "
        "WHERE id = " + std::to_string(scheduleId);
    auto rows = work.exec(query);

    if (rows.empty()) {
        throw ScheduleNotExists()
            << "Schedule with id " << scheduleId << " not found";
    }
    ASSERT(rows.size() == 1);
    auto& row = rows[0];
    return Factory::schedule(row);
}

AgentKind
agentKind(ID agentId, Transaction& work)
{
    auto rows = work.exec(
        "SELECT 1 FROM acl.user "
        "WHERE id = " + std::to_string(agentId) +
        " UNION "
        "SELECT 2 FROM acl.group "
        "WHERE id = " + std::to_string(agentId));
    REQUIRE(!rows.empty(), "Agent with id " << agentId << " not found");
    ASSERT(rows.size() == 1);
    auto result = rows[0][0].as<int>();
    if (result == 1) {
        return AgentKind::User;
    } else {
        return AgentKind::Group;
    }
}

} // namespace

ScheduledPolicy
ACLGateway::createScheduledPolicy(
    ID agentId, ID roleId, ID aoiId,
    const std::string& startDate,
    const std::optional<std::string>& endDate,
    const std::optional<std::string>& startTime,
    const std::optional<std::string>& endTime,
    const std::optional<int>& weekdays,
    const std::optional<std::string>& workRestDays)
{
    ID scheduleId = createSchedule(
        startDate, endDate,
        startTime, endTime,
        weekdays, workRestDays,
        work_);
    ASSERT(scheduleId != 0);
    try {
        std::string query =
            "INSERT INTO acl.schedule_policy "
            "(agent_id, role_id, aoi_id, schedule_id) "
            "VALUES (" +
            std::to_string(agentId) + ", " +
            std::to_string(roleId) + ", " +
            std::to_string(aoiId) + ", " +
            std::to_string(scheduleId) + ")";
        work_.exec(query);
    } CATCH_CONVERT();
    return Factory::scheduledPolicy(
        agentId, roleId, aoiId,
        schedule(scheduleId, work_));
}

ScheduledGroup
ACLGateway::createScheduledGroup(
    ID userId, ID groupId,
    const std::string& startDate,
    const std::optional<std::string>& endDate,
    const std::optional<std::string>& startTime,
    const std::optional<std::string>& endTime,
    const std::optional<int>& weekdays,
    const std::optional<std::string>& workRestDays)
{
    ID scheduleId = createSchedule(
        startDate, endDate,
        startTime, endTime,
        weekdays, workRestDays,
        work_);
    ASSERT(scheduleId != 0);
    try {
        std::string query =
            "INSERT INTO acl.schedule_group "
            "(user_id, group_id, schedule_id) "
            "VALUES (" +
            std::to_string(userId) + ", " +
            std::to_string(groupId) + ", " +
            std::to_string(scheduleId) + ")";
        work_.exec(query);
    } CATCH_CONVERT();
    return Factory::scheduledGroup(
        userId, groupId,
        schedule(scheduleId, work_));
}

ScheduledObjects
ACLGateway::scheduledObjects(std::optional<ID> agentId)
{
    ScheduledObjects result;

    std::string queryPolicies =
        "SELECT * "
        "FROM acl.schedule_policy sp "
        "JOIN acl.schedule s ON (s.id = sp.schedule_id)";
    if (agentId) {
        queryPolicies += " WHERE agent_id = " + std::to_string(*agentId);
    }
    auto rowsPolicies = work_.exec(queryPolicies);
    for (const auto& row : rowsPolicies) {
        result.policies.push_back(Factory::scheduledPolicy(
            row["agent_id"].as<ID>(),
            row["role_id"].as<ID>(),
            row["aoi_id"].as<ID>(),
            Factory::schedule(row)));
    }

    std::string queryGroups =
        "SELECT * "
        "FROM acl.schedule_group sg "
        "JOIN acl.schedule s ON (s.id = sg.schedule_id)";
    if (agentId) {
        queryGroups += " WHERE user_id = " + std::to_string(*agentId);
    }
    auto rowsGroups = work_.exec(queryGroups);
    for (const auto& row : rowsGroups) {
        result.groups.push_back(Factory::scheduledGroup(
            row["user_id"].as<ID>(),
            row["group_id"].as<ID>(),
            Factory::schedule(row)));
    }

    return result;
}

ScheduledObjects
ACLGateway::allScheduledObjects()
{
    return scheduledObjects(std::nullopt);
}

ScheduledObjects
ACLGateway::scheduledObjectsForAgent(ID agentId)
{
    return scheduledObjects(agentId);
}

void
ACLGateway::dropSchedulesObjects(ID scheduleId)
{
    std::string query =
        "DELETE FROM acl.schedule "
        "WHERE id = " + std::to_string(scheduleId);
    auto rows = work_.exec(query);
}

void
ACLGateway::synchronizeSchedules()
{
    auto scheduledObjects = allScheduledObjects();
    auto nowLocal = std::chrono::system_clock::now() + SCHEDULE_TIMEZONE_OFFSET;

    std::set<ID> updatingGroupIds;
    std::set<ID> updatingUserIds;

    for (const auto& sp : scheduledObjects.policies) {
        const auto& schedule = sp.schedule();
        if (schedule.isActive(nowLocal)) {
            continue;
        }

        const auto agentId = sp.agentId();
        if (agentKind(agentId, work_) == AgentKind::Group) {
            const auto group_ = group(agentId);
            if (isExistingPolicy(group_.policies(), sp.roleId(), sp.aoiId())) {
                INFO() << "Group: " << group_.name() << "."
                       << " Remove policy"
                       << " role: " << role(sp.roleId()).name()
                       << (sp.aoiId() ? (", aoi: " + aoi(sp.aoiId()).name()) : "");
                group_.removePolicy(sp.roleId(), sp.aoiId());
                updatingGroupIds.insert(agentId);
            }
        } else {
            const auto user = userById(agentId);
            if (isExistingPolicy(user.policies(), sp.roleId(), sp.aoiId())) {
                INFO() << "User: " << user.login() << "."
                       << " Remove policy"
                       << " role: " << role(sp.roleId()).name()
                       << (sp.aoiId() ? (", aoi: " + aoi(sp.aoiId()).name()) : "");
                user.removePolicy(sp.roleId(), sp.aoiId());
                updatingUserIds.insert(agentId);
            }
        }
    }
    for (const auto& sp : scheduledObjects.policies) {
        const auto& schedule = sp.schedule();
        if (!schedule.isActive(nowLocal)) {
            continue;
        }

        const auto agentId = sp.agentId();
        if (agentKind(agentId, work_) == AgentKind::Group) {
            const auto group_ = group(agentId);
            if (!isExistingPolicy(group_.policies(), sp.roleId(), sp.aoiId())) {
                const auto role_ = role(sp.roleId());
                const auto aoi_ = aoi(sp.aoiId());
                INFO() << "Group: " << group_.name() << "."
                       << " Add policy"
                       << " role: " << role_.name()
                       << (sp.aoiId() ? (", aoi: " + aoi_.name()) : "");
                createPolicy(group_, role_, aoi_);
                updatingGroupIds.insert(agentId);
            }
        } else {
            const auto user = userById(agentId);
            if (!isExistingPolicy(user.policies(), sp.roleId(), sp.aoiId())) {
                const auto role_ = role(sp.roleId());
                const auto aoi_ = aoi(sp.aoiId());
                INFO() << "User: " << user.login() << "."
                       << " Add policy"
                       << " role: " << role_.name()
                       << (sp.aoiId() ? (", aoi: " + aoi_.name()) : "");
                createPolicy(user, role_, aoi_);
                updatingUserIds.insert(agentId);
            }
        }
    }

    for (const auto& sg : scheduledObjects.groups) {
        const auto& schedule = sg.schedule();
        if (!schedule.isActive(nowLocal)) {
            const auto user = userById(sg.userId());
            if (isExistingGroup(user.groups(), sg.groupId())) {
                const auto group_ = group(sg.groupId());
                INFO() << "Group: " << group_.name() << "."
                       << " Remove user: " << user.login();
                group_.remove(user);
                updatingUserIds.insert(sg.userId());
            }
        }
    }
    for (const auto& sg : scheduledObjects.groups) {
        const auto& schedule = sg.schedule();
        if (schedule.isActive(nowLocal)) {
            const auto user = userById(sg.userId());
            if (!isExistingGroup(user.groups(), sg.groupId())) {
                const auto group_ = group(sg.groupId());
                INFO() << "Group: " << group_.name() << "."
                       << " Add user: " << user.login();
                group_.add(user);
                updatingUserIds.insert(sg.userId());
            }
        }
    }

    for (const auto groupId : updatingGroupIds) {
        enqueueGroupCluster(groupId);
        enqueueGroupAffectedClusters(groupId);
    }
    for (const auto userId : updatingUserIds) {
        enqueueUserCluster(userId);
    }
}

void ACLGateway::clearUidToStaff()
{
    const auto rows = work_.exec("SELECT uid FROM acl.uid_staff_login ORDER BY uid FOR UPDATE;");
    if (rows.empty()) {
        return;
    }
    std::vector<UID> uids;
    uids.reserve(rows.size());
    for (const auto& row : rows) {
        uids.push_back(row[0].as<UID>());
    }
    const auto batches = maps::common::makeBatches(uids, DELETE_BATCH_SIZE);
    for (const auto& batch : batches) {
        work_.exec("DELETE FROM acl.uid_staff_login WHERE " +
            common::whereClause("uid", std::vector<UID>{batch.begin(), batch.end()}));
    }
}

} // namespace maps::wiki::acl
