#include <maps/wikimap/mapspro/services/acl/lib/access_checks.h>
#include <maps/wikimap/mapspro/services/acl/lib/bans.h>
#include <maps/wikimap/mapspro/services/acl/lib/common.h>
#include <maps/wikimap/mapspro/services/acl/lib/config.h>
#include <maps/wikimap/mapspro/services/acl/lib/connection_manager.h>
#include <maps/wikimap/mapspro/services/acl/lib/exception.h>
#include <maps/wikimap/mapspro/services/acl/lib/idm.h>
#include <maps/wikimap/mapspro/services/acl/lib/magic_strings.h>
#include <maps/wikimap/mapspro/services/acl/lib/moderation_status.h>
#include <maps/wikimap/mapspro/services/acl/lib/policy.h>
#include <maps/wikimap/mapspro/services/acl/lib/response_wrapper.h>
#include <maps/wikimap/mapspro/services/acl/lib/save.h>
#include <maps/wikimap/mapspro/services/acl/lib/save_group.h>
#include <maps/wikimap/mapspro/services/acl/lib/save_role.h>
#include <maps/wikimap/mapspro/services/acl/lib/user_info.h>

#include <maps/infra/yacare/include/params/tvm.h>
#include <maps/infra/yacare/include/tvm.h>

#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl/include/exception.h>
#include <yandex/maps/wiki/common/before_after.h>
#include <yandex/maps/wiki/common/compound_token.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/robot.h>

#include <maps/infra/yacare/include/yacare.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/pgpool/include/pgpool3.h>

#include <boost/lexical_cast.hpp>
#include <set>
#include <string>
#include <vector>

namespace acl = maps::wiki::acl;
namespace srv = maps::wiki::aclsrv;
namespace common = maps::wiki::common;
namespace json = maps::json;
namespace pgpool3 = maps::pgpool3;

using TvmAlias = NTvmAuth::TClientSettings::TAlias;

namespace {

const std::string CONFIG_PATH = "/etc/yandex/maps/wiki/services/services.xml";
const std::string CONFIG_SERVICE_NAME = "core";
const std::string EMPTY;
const size_t THREADS = 15;
const size_t BACKLOG = 256;
const TvmAlias TVM_ALIAS = "maps-core-acl";
const std::string NMAPS_GROUP_NAME = "nmaps";

template<typename T>
T positionalParam(const std::vector<std::string>& argv, size_t index)
{
    if (index >= argv.size()) {
        throw yacare::errors::BadRequest()
            << "Bad parameters count";
    }
    try {
        return boost::lexical_cast<T>(argv[index]);
    } catch (const boost::bad_lexical_cast&) {
        throw yacare::errors::BadRequest()
            << "Invalid parameter value";
    }
}

void requireIdmTvmId(maps::auth::TvmId srcTvmId)
{
    if (!srv::idm::isIDM(srcTvmId)) {
        throw yacare::errors::Forbidden() << "Only IDM may perform this request";
    }
}

void logIdmRequestId(const yacare::Request& request)
{
    const auto& idmRequestId = request.env("HTTP_X_IDM_REQUEST_ID");
    INFO() << "X-IDM-Request-Id: " << idmRequestId;
}

} // namespace

// Common params

YCR_QUERY_CUSTOM_PARAM(("uid"), authorUid, acl::UID)
{ return yacare::impl::parseArg(dest, request, "uid"); }

YCR_QUERY_CUSTOM_PARAM((), token, std::string)
{
    auto token = request.input()[srv::TOKEN];
    if (!common::CompoundToken::isValid(token)) {
        throw yacare::errors::BadRequest()
            << "Invalid token";
    }
    dest = token;
    return true;
}

YCR_QUERY_CUSTOM_PARAM((), startId, acl::ID)
{
    if (request.input().has("after")) {
        return yacare::impl::parseArg(dest, request, "after");
    } else if (request.input().has("before")) {
        return yacare::impl::parseArg(dest, request, "before");
    } else {
        dest = 0;
    }
    return true;
}

YCR_QUERY_CUSTOM_PARAM((), beforeAfter, common::BeforeAfter)
{
    return common::queryBeforeAfterParam(request, dest);
}

// Filter params

YCR_QUERY_CUSTOM_PARAM((), outputFlags, srv::OutputFlagsSet)
{
    dest = srv::OutputFlags::None;
    bool buf = false;
    if (yacare::impl::parseArg(buf, request, "policies") && buf) {
        dest = dest | srv::OutputFlags::WithPolicies;
    }
    if (yacare::impl::parseArg(buf, request, "moderation-status") && buf) {
        dest = dest | srv::OutputFlags::WithModerationStatus;
    }
    if (yacare::impl::parseArg(buf, request, "banable-info") && buf) {
        dest = dest | srv::OutputFlags::WithBanableInfo;
    }
    if (yacare::impl::parseArg(buf, request, "ban-info") && buf) {
        dest = dest | srv::OutputFlags::WithBanInfo;
    }
    if (yacare::impl::parseArg(buf, request, "yandex") && buf) {
        dest = dest | srv::OutputFlags::WithYandex;
    }
    if (yacare::impl::parseArg(buf, request, "outsourcer") && buf) {
        dest = dest | srv::OutputFlags::WithOutsourcer;
    }
    if (yacare::impl::parseArg(buf, request, "pieceworker") && buf) {
        dest = dest | srv::OutputFlags::WithPieceworker;
    }
    return true;
}

YCR_QUERY_CUSTOM_PARAM((), uids, std::vector<acl::UID>, YCR_DEFAULT({}))
{ return yacare::impl::parseArg(dest, request, "uids"); }

YCR_QUERY_CUSTOM_PARAM((), groupId, acl::ID, YCR_DEFAULT(0))
{ return yacare::impl::parseArg(dest, request, "group-id"); }

YCR_QUERY_CUSTOM_PARAM((), roleId, acl::ID, YCR_DEFAULT(0))
{ return yacare::impl::parseArg(dest, request, "role-id"); }

YCR_QUERY_CUSTOM_PARAM((), aoiId, acl::ID, YCR_DEFAULT(0))
{ return yacare::impl::parseArg(dest, request, "aoi-id"); }

YCR_QUERY_CUSTOM_PARAM((), statuses, std::vector<acl::User::Status>, YCR_DEFAULT({}))
{ return yacare::impl::parseArg(dest, request, "status"); }

YCR_QUERY_CUSTOM_PARAM((), permissions, std::string, YCR_DEFAULT(""))
{ return yacare::impl::parseArg(dest, request, "permissions"); }

YCR_QUERY_CUSTOM_PARAM((), paths, std::string, YCR_DEFAULT(""))
{ return yacare::impl::parseArg(dest, request, "paths"); }

// Paging params

YCR_QUERY_PARAM(page, size_t, YCR_DEFAULT(0));

YCR_QUERY_CUSTOM_PARAM((), perPage, size_t, YCR_DEFAULT(0))
{ return yacare::impl::parseArg(dest, request, "per-page"); }

YCR_QUERY_PARAM(after, acl::ID, YCR_DEFAULT(0));
YCR_QUERY_PARAM(before, acl::ID, YCR_DEFAULT(0));

// Misc params

YCR_QUERY_PARAM(part, std::string);

YCR_QUERY_CUSTOM_PARAM((), namePart, std::string, YCR_DEFAULT(""))
{ return yacare::impl::parseArg(dest, request, "name-part"); }

YCR_QUERY_PARAM(limit, size_t, YCR_DEFAULT(0));

yacare::ThreadPool threadPool("main", THREADS, BACKLOG);

yacare::VirtualHost vhost {
    yacare::VirtualHost::SLB { "wiki-acl" }, // For monrun checks

    // For future nginx config
    yacare::VirtualHost::SLB { "acl" },
    yacare::VirtualHost::SLB { "core-nmaps-acl" },

    yacare::VirtualHost::Real { "acl.um.um-back" }, // For testing, load
};

YCR_SET_DEFAULT(vhost);

YCR_USE(vhost, threadPool) {

srv::IsYandexRequest isYandexRequest(acl::ACLGateway& /*aclGw*/, acl::UID /*authorUid*/)
{
    return srv::IsYandexRequest::Yes;
    /* https://st.yandex-team.ru/NMAPS-14477#61b8840c6bb59665fa0c6679
    if (!authorUid) {
        return srv::IsYandexRequest::No;
    }
    auto author = aclGw.user(authorUid);
    auto modStatus = srv::moderationStatus(aclGw, author);
    auto authorUserInfo = srv::UserInfo(author, modStatus);
    return authorUserInfo.isYandex()
        ? srv::IsYandexRequest::Yes
        : srv::IsYandexRequest::No; */
}

bool needLoadModerationStatuses(
    srv::OutputFlagsSet outputFlags,
    acl::ACLGateway& aclGw,
    acl::UID authorUid)
{
    return
        outputFlags.isSet(srv::OutputFlags::WithModerationStatus) ||
        outputFlags.isSet(srv::OutputFlags::WithYandex) ||
        outputFlags.isSet(srv::OutputFlags::WithOutsourcer) ||
        outputFlags.isSet(srv::OutputFlags::WithBanableInfo) ||
        isYandexRequest(aclGw, authorUid) != srv::IsYandexRequest::Yes;
}

void
respondUsersByUids(
    acl::ACLGateway& aclGw,
    const std::vector<acl::UID>& queryUids,
    acl::UID authorUid,
    srv::OutputFlagsSet outputFlags,
    yacare::Response& response)
{
    auto users = aclGw.users(queryUids);

    std::map<acl::ID, std::string> modStatuses;
    if (needLoadModerationStatuses(outputFlags, aclGw, authorUid)) {
        modStatuses = srv::moderationStatuses(aclGw, users);
    }

    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        respBuilder[srv::USERS] << [&](json::ArrayBuilder usersBuilder) {
            for (const auto& user : users) {
                srv::write(
                    usersBuilder,
                    aclGw,
                    user,
                    isYandexRequest(aclGw, authorUid),
                    outputFlags,
                    modStatuses[user.id()]);
            }
        };
    };
}

void
respondUsersPaged(
    acl::ACLGateway& aclGw,
    acl::ID groupId,
    acl::ID roleId,
    acl::ID aoiId,
    const std::vector<acl::User::Status>& statuses,
    acl::UID authorUid,
    srv::OutputFlagsSet outputFlags,
    yacare::Response& response,
    size_t page,
    size_t perPage)
{
    if (statuses.size() > 1) {
        throw yacare::errors::BadRequest() << "Bad user status filter";
    }
    auto result = statuses.empty()
        ? aclGw.users(groupId, roleId, aoiId, std::nullopt, page, perPage)
        : aclGw.users(groupId, roleId, aoiId, statuses.front(), page, perPage);

    std::map<acl::ID, std::string> modStatuses;
    if (needLoadModerationStatuses(outputFlags, aclGw, authorUid)) {
        modStatuses = srv::moderationStatuses(aclGw, result.value());
    }

    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        respBuilder[srv::USERS] << [&](json::ArrayBuilder usersBuilder) {
            for (const auto& user : result.value()) {
                srv::write(
                    usersBuilder,
                    aclGw,
                    user,
                    isYandexRequest(aclGw, authorUid),
                    outputFlags,
                    modStatuses[user.id()]);
            }
        };
        srv::write(respBuilder, result.pager());
    };
}

void
respondUsersHasMore(
    acl::ACLGateway& aclGw,
    acl::ID groupId,
    acl::ID roleId,
    acl::ID aoiId,
    const std::vector<acl::User::Status>& statuses,
    acl::UID authorUid,
    srv::OutputFlagsSet outputFlags,
    acl::ID startId,
    common::BeforeAfter beforeAfter,
    size_t limit,
    const std::string& namePart,
    const std::string& permissions,
    acl::UsersOrder usersOrder,
    yacare::Response& response)
{
    std::vector<acl::SubjectPath> permissionsPaths;
    std::set<acl::ID> permissionIds;
    if (!permissions.empty()) {
        const auto paths = srv::extractPermissions(json::Value::fromString(permissions));
        const auto allPermissions = aclGw.allPermissions();
        for (const auto& path : paths) {
            permissionIds.insert(allPermissions.permission(path.pathParts()).id());
        }
    }
    std::set<acl::ID> roleIds;
    if (roleId) {
        roleIds.insert(roleId);
    }
    auto result = aclGw.users(
        groupId,
        roleIds,
        aoiId,
        statuses,
        namePart,
        startId,
        beforeAfter,
        permissionIds,
        limit,
        usersOrder);

    std::map<acl::ID, std::string> modStatuses;
    if (needLoadModerationStatuses(outputFlags, aclGw, authorUid)) {
        modStatuses = srv::moderationStatuses(aclGw, result.objects);
    }

    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        respBuilder[srv::USERS] << [&](json::ArrayBuilder usersBuilder) {
            for (const auto& user : result.objects) {
                srv::write(
                    usersBuilder,
                    aclGw,
                    user,
                    isYandexRequest(aclGw, authorUid),
                    outputFlags,
                    modStatuses[user.id()]);
            }
        };
        respBuilder[srv::HAS_MORE] = result.hasMore;
    };
}

void
respondToken(yacare::Response& response, std::string_view token)
{
    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder tokenBuilder) {
        tokenBuilder[srv::TOKEN] = token;
    };
}

YCR_RESPOND_TO("GET /users/query/uids",
        token,
        uids,
        authorUid = 0)
{
    auto work = srv::ConnectionManager::slaveTransaction(token);
    acl::ACLGateway aclGw(*work);
    respondUsersByUids(
        aclGw,
        uids,
        authorUid,
        {
            srv::OutputFlags::WithModerationStatus,
            srv::OutputFlags::WithYandex,
            srv::OutputFlags::WithOutsourcer,
            srv::OutputFlags::WithPieceworker
        },
        response);
}

YCR_RESPOND_TO("GET /users",
        token,
        outputFlags,
        groupId,
        roleId,
        aoiId,
        statuses,
        page,
        perPage,
        startId,
        beforeAfter,
        limit,
        permissions,
        authorUid,
        namePart = "")
{
    auto work = srv::ConnectionManager::slaveTransaction(token);
    acl::ACLGateway aclGw(*work);
    if (request.input().has("group-name")) {
        const auto groupName = request.input()["group-name"];
        if (!groupName.empty()) {
            groupId = aclGw.group(groupName).id();
        }
    }
    // NMAPS-15394 srv::checkAccessToUsers(authorUid, *work);
    if (startId || limit || !namePart.empty()) {
        const auto orderBy = request.input()["order-by"];
        const acl::UsersOrder usersOrder = !orderBy.empty()
            ? maps::enum_io::fromString<acl::UsersOrder>(orderBy)
            : acl::UsersOrder::Login;
        respondUsersHasMore(aclGw,
            groupId, roleId, aoiId, statuses,
            authorUid,
            outputFlags,
            startId,
            beforeAfter,
            limit,
            namePart,
            permissions,
            usersOrder,
            response);
    } else {
        respondUsersPaged(aclGw,
            groupId, roleId, aoiId, statuses,
            authorUid, outputFlags, response,
            page, perPage);
    }
}

YCR_RESPOND_TO("GET /users/$", token, outputFlags, authorUid = 0)
{
    auto queryUid = positionalParam<acl::UID>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(token);
    // NMAPS-13898
    // if (queryUid != authorUid) {
    //    srv::checkAccessToUsers(authorUid, *work);
    //}
    acl::ACLGateway aclGw(*work);
    auto user = aclGw.user(queryUid);

    auto modStatus = needLoadModerationStatuses(outputFlags, aclGw, authorUid)
            ? srv::moderationStatus(aclGw, user)
            : EMPTY;

    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        srv::write(
            respBuilder[srv::USER],
            aclGw,
            user,
            isYandexRequest(aclGw, authorUid),
            outputFlags,
            modStatus);
    };
}

YCR_RESPOND_TO("GET /users/$/permissions", token, authorUid)
{
    auto queryUid = positionalParam<acl::UID>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(token);
    if (queryUid != authorUid) {
        srv::checkAccessToUsers(authorUid, *work);
    }
    acl::ACLGateway aclGw(*work);
    acl::User user = aclGw.user(queryUid);
    std::vector<acl::Permission> permissions;
    for (const auto& pol : user.allPolicies()) {
        auto rolePermissions = pol.role().permissions();
        permissions.insert(
            std::end(permissions),
            std::begin(rolePermissions), std::end(rolePermissions));
    }

    auto allPermissions = aclGw.allPermissions();
    srv::JsonResponseWrapper wrapper(response);
    srv::write(wrapper.jsonBuilder(), allPermissions, permissions);
}

YCR_RESPOND_TO("GET /users/$/check-permissions", paths)
{
    // TODO: Add service ticket check.
    auto queryUid = positionalParam<acl::UID>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(EMPTY);
    auto jsonPaths = json::Value::fromString(paths);
    if (!jsonPaths.isArray()) {
        throw yacare::errors::BadRequest()
            << "JSON array in 'paths' arg is required";
    }

    std::vector<std::string> allowedPaths;
    try {
        for (const auto& jsonPath : jsonPaths) {
            if (!jsonPath.isString()) {
                throw yacare::errors::BadRequest()
                    << "Strings in 'paths' array are required";
            }
            auto path = acl::SubjectPath(jsonPath.as<std::string>());
            if (path.isAllowed(acl::CheckContext(queryUid,
                    {}, *work, {acl::User::Status::Active})))
            {
                allowedPaths.push_back(path.str());
            }
        }
    } catch (const acl::ACLException & ex) {
        WARN() << "Exception while checking permissions for: " << queryUid << " ex: " << ex.what();
    }

    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        respBuilder[srv::PATHS] << [&](json::ArrayBuilder pathsBuilder) {
            for (const auto& path : allowedPaths) {
                pathsBuilder << path;
            }
        };
    };
}

YCR_RESPOND_TO("GET /users/$/can-assign", token, authorUid)
{
    auto queryUid = positionalParam<acl::UID>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(token);
    if (queryUid != authorUid) {
        srv::checkAccessToUsers(authorUid, *work);
    }
    acl::ACLGateway aclGw(*work);
    auto canAssign = srv::getUserCanAssign(queryUid, *work);
    acl::User user = aclGw.user(queryUid);
    std::vector<acl::Group> canAssignGroups;
    std::vector<acl::Role> canAssignRoles;
    if (canAssign.any != srv::CanAssign::Any::True) {
        if (!canAssign.roleIds.empty()) {
            canAssignRoles = aclGw.roles(canAssign.roleIds);
        }
        if (!canAssign.groupIds.empty()) {
            canAssignGroups = aclGw.groups(canAssign.groupIds);
        }
    }
    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder objectBuilder) {
        if (canAssign.any == srv::CanAssign::Any::True) {
            objectBuilder[srv::ANY] = true;
        } else {
            objectBuilder[srv::ROLES] << [&](json::ArrayBuilder rolesArray) {
                srv::write(rolesArray, canAssignRoles);
            };
            objectBuilder[srv::GROUPS] << [&](json::ArrayBuilder groupsArray) {
                srv::write(groupsArray, aclGw, canAssignGroups);
            };
        }
    };
}

YCR_RESPOND_TO("GET /users/$/policies", token, permissions, authorUid)
{
    auto queryUid = positionalParam<acl::UID>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(token);
    if (queryUid != authorUid) {
        srv::checkAccessToUsers(authorUid, *work);
    }

    std::vector<acl::SubjectPath> paths;
    acl::ACLGateway aclGw(*work);
    auto user = aclGw.user(queryUid);

    std::vector<srv::PolicyWithGroup> userPolicies;

    const auto combinedScheduledObjects = srv::combineScheduledObjects(
        aclGw.scheduledObjectsForAgent(user.id()));

    for (const auto& [policyKey, schedules] : combinedScheduledObjects.policies) {
        const auto roleId = policyKey.first;
        const auto aoiId = policyKey.second;
        userPolicies.push_back({
            aclGw.role(roleId),
            aoiId ? aclGw.aoi(aoiId) : std::optional<acl::Aoi>(),
            std::nullopt, /* providedByGroup */
            schedules});
    }
    for (const auto& [groupId, schedules] : combinedScheduledObjects.groups) {
        auto group = aclGw.group(groupId);
        for (const auto& policy : group.policies()) {
            userPolicies.push_back({
                policy.role(),
                policy.aoiId() ? policy.aoi() : std::optional<acl::Aoi>(),
                group,
                schedules});
        }
        if (permissions.empty() && group.policies().empty()) {
            userPolicies.push_back({
                std::nullopt,
                std::nullopt,
                group,
                schedules});
        }
    }

    for (const auto& policy : user.policies()) {
        const auto policyKey = std::make_pair(policy.roleId(), policy.aoiId());
        if (combinedScheduledObjects.policies.count(policyKey)) {
            continue;
        }
        userPolicies.push_back({
            policy.role(),
            policy.aoiId() ? policy.aoi() : std::optional<acl::Aoi>(),
            std::nullopt, /* providedByGroup */
            {} /* schedules */});
    }
    for (const auto& group : user.groups()) {
        if (combinedScheduledObjects.groups.count(group.id())) {
            continue;
        }
        for (const auto& policy : group.policies()) {
            userPolicies.push_back({
                policy.role(),
                policy.aoiId() ? policy.aoi() : std::optional<acl::Aoi>(),
                group,
                {} /* schedules */});
        }
        if (permissions.empty() && group.policies().empty()) {
            userPolicies.push_back({
                std::nullopt,
                std::nullopt,
                group,
                {}});
        }
    }

    srv::JsonResponseWrapper wrapper(response);
    if (!permissions.empty()) {
        paths = srv::extractPermissions(json::Value::fromString(permissions));
        const auto permittingPolicies = srv::calculatePermittingPolicies(
            aclGw, userPolicies, paths);
        srv::write(
            wrapper.jsonBuilder(),
            permittingPolicies.policies,
            permittingPolicies.complete
                ? srv::CompleteResult::True
                : srv::CompleteResult::False);
    } else {
        srv::write(
            wrapper.jsonBuilder(),
            userPolicies,
            srv::CompleteResult::True);
    }
}

YCR_RESPOND_TO("GET /users/$/ban-history", token, page, perPage, authorUid)
{
    auto queryUid = positionalParam<acl::UID>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(token);
    /* NMAPS-15385 if (queryUid != authorUid) {
        srv::checkAccessToUsers(authorUid, *work);
    } */

    acl::ACLGateway aclGw(*work);
    auto result = aclGw.banRecords(queryUid, page, perPage);

    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        respBuilder[srv::BAN_RECORDS] << [&](json::ArrayBuilder recsBuilder) {
            for (const auto& banRecord : result.value()) {
                srv::write(recsBuilder, banRecord);
            }
        };
        srv::write(respBuilder, result.pager());
    };
}

YCR_RESPOND_TO("POST /users", authorUid)
{
    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    acl::ACLGateway aclGw(*work);
    auto usersUpdateData = srv::UserUpdateData::parseData(request.body());
    std::set<acl::UID> queryUids;
    std::vector<acl::ID> usersIds;
    usersIds.reserve(usersUpdateData.size());
    for (const auto& userUpdateData : usersUpdateData) {
        if (userUpdateData.uid && !queryUids.insert(*userUpdateData.uid).second) {
            throw yacare::errors::BadRequest()
                << "Duplicate uid in request";
        }
        usersIds.push_back(srv::saveUser(aclGw, userUpdateData, authorUid));
    }
    for (const auto& userId : usersIds) {
        aclGw.enqueueUserCluster(userId);
    }
    auto users = aclGw.usersByIds(usersIds);
    auto modStatuses = srv::moderationStatuses(aclGw, users);
    auto token = pgpool3::generateToken(*work);
    bool inputIsArray = usersIds.size() > 1 ||
        json::Value::fromString(request.body()).isArray();
    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        respBuilder[srv::TOKEN] = token;
        if (!inputIsArray) {
            srv::write(
                respBuilder[srv::USER],
                aclGw,
                users.front(),
                isYandexRequest(aclGw, authorUid),
                srv::OutputAll,
                modStatuses[users.front().id()]);
        } else {
            respBuilder[srv::USERS] << [&](json::ArrayBuilder usersArray) {
                for (const auto& user : users) {
                    srv::write(
                        usersArray,
                        aclGw,
                        user,
                        isYandexRequest(aclGw, authorUid),
                        srv::OutputAll,
                        modStatuses[user.id()]);
                }
            };
        }
    };
    work->commit();
}

YCR_RESPOND_TO("POST /users/policies", authorUid)
{
    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    acl::ACLGateway aclGw(*work);
    srv::checkAclAssignPermissions(authorUid, *work);
    const auto updateData = srv::UsersPoliciesUpdateData::parseData(request.body());
    saveUsersPolicies(aclGw, updateData, authorUid);
    for (const auto& userId : updateData.uids) {
        aclGw.enqueueUserCluster(aclGw.user(userId).id());
    }
    respondToken(response, pgpool3::generateToken(*work));

    work->commit();
}

YCR_RESPOND_TO("POST /users/$/ban", authorUid)
{
    auto targetUid = positionalParam<acl::UID>(argv, 0);

    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    srv::checkBanPermissions(authorUid, *work);
    acl::ACLGateway aclGw(*work);
    auto user = aclGw.user(targetUid);
    srv::checkBanable(aclGw, user);

    auto banRequest = srv::BanRequest(request.body());
    auto banRecord =
        aclGw.banUser(targetUid, authorUid, banRequest.expires, banRequest.reason);
    auto token = pgpool3::generateToken(*work);
    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        respBuilder[srv::TOKEN] = token;
        srv::write(respBuilder[srv::BAN_RECORD], banRecord);
    };
    work->commit();
}

YCR_RESPOND_TO("POST /users/$/unban", authorUid)
{
    auto targetUid = positionalParam<acl::UID>(argv, 0);

    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    srv::checkBanPermissions(authorUid, *work);
    acl::ACLGateway aclGw(*work);

    auto banRecord = aclGw.unbanUser(targetUid, authorUid);
    auto token = pgpool3::generateToken(*work);

    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
        respBuilder[srv::TOKEN] = token;
        srv::write(respBuilder[srv::BAN_RECORD], banRecord);
    };
    work->commit();
}

YCR_RESPOND_TO("DELETE /users/$", authorUid)
{
    if (!srv::cfg()->allowDeleteUser()) {
        throw yacare::errors::BadRequest()
                << "Unsupported";
    }
    auto uid = positionalParam<uint64_t>(argv, 0);
    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    srv::checkAclPermissions(authorUid, *work);
    acl::ACLGateway aclGw(*work);
    aclGw.drop(aclGw.user(uid));
    respondToken(response, pgpool3::generateToken(*work));
    work->commit();
}

YCR_RESPOND_TO("GET /permissions", token)
{
    auto work = srv::ConnectionManager::slaveTransaction(token);
    acl::ACLGateway aclGw(*work);
    srv::JsonResponseWrapper wrapper(response);
    srv::write(wrapper.jsonBuilder(), aclGw.allPermissions());
}

YCR_RESPOND_TO("GET /roles", token,
        permissions,
        groupId,
        startId,
        beforeAfter,
        limit,
        authorUid = 0,
        namePart = "")
{
    auto work = srv::ConnectionManager::slaveTransaction(token);
    acl::ACLGateway aclGw(*work);
    srv::checkAccessToRoles(authorUid, *work);
    std::vector<acl::SubjectPath> paths;
    if (!permissions.empty()) {
        paths = srv::extractPermissions(json::Value::fromString(permissions));
    }
    auto result = aclGw.roles(groupId, paths, namePart, startId, beforeAfter, limit);
    srv::JsonResponseWrapper wrapper(response);
    srv::write(wrapper.jsonBuilder(), aclGw, aclGw.allPermissions(), result);
}

YCR_RESPOND_TO("GET /roles/$", token, authorUid = 0)
{
    auto roleId = positionalParam<acl::ID>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(token);
    srv::checkAccessToRoles(authorUid, *work);
    acl::ACLGateway aclGw(*work);

    srv::JsonResponseWrapper wrapper(response);
    srv::write(wrapper.jsonBuilder(), aclGw, aclGw.allPermissions(), aclGw.role(roleId));
}

YCR_RESPOND_TO("POST /roles", authorUid)
{
    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    srv::checkAclPermissions(authorUid, *work);

    std::vector<srv::RoleData> rolesData;
    for (const auto& jsonRoleObject : json::Value::fromString(request.body())) {
        rolesData.emplace_back(jsonRoleObject);
    }

    std::vector<srv::RoleSaveResult> savedRoles;

    acl::ACLGateway aclGw(*work);
    auto allPermissions = aclGw.allPermissions();
    for (const auto& roleData : rolesData) {
        savedRoles.push_back(srv::saveRole(aclGw, allPermissions, roleData, authorUid));
    }
    for (const auto& savedRole : savedRoles) {
        if (savedRole.changedPermissions == srv::ChangedPermissions::True &&
            savedRole.isNew == srv::IsNew::False)
        {
            aclGw.enqueueRoleAffectedClusters(savedRole.role.id());
        }
    }
    auto token = pgpool3::generateToken(*work);
        srv::JsonResponseWrapper wrapper(response);
        wrapper.jsonBuilder() << [&](json::ObjectBuilder respBuilder) {
            respBuilder[srv::TOKEN] = token;
            respBuilder[srv::ROLES] << [&](json::ArrayBuilder rolesBuilder) {
                for (const auto& savedRole : savedRoles) {
                    srv::write(
                        rolesBuilder,
                        aclGw,
                        allPermissions,
                        savedRole.role);
                }
            };
        };

    work->commit();
}

YCR_RESPOND_TO("DELETE /roles/$", authorUid)
{
    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    srv::checkAclPermissions(authorUid, *work);

    acl::ACLGateway aclGw(*work);
    auto roleId = positionalParam<acl::ID>(argv, 0);
    aclGw.enqueueRoleAffectedClusters(roleId);
    aclGw.drop(aclGw.role(roleId));
    respondToken(response, pgpool3::generateToken(*work));

    work->commit();
}

YCR_RESPOND_TO("GET /groups", token,
        roleId,
        aoiId,
        permissions,
        startId,
        beforeAfter,
        limit,
        authorUid = 0,
        namePart = "")
{
    auto work = srv::ConnectionManager::slaveTransaction(token);
    srv::checkAccessToGroups(authorUid, *work);
    acl::ACLGateway aclGw(*work);
    srv::JsonResponseWrapper wrapper(response);
    std::set<acl::ID> permissionIds;
    if (!permissions.empty()) {
        const auto paths = srv::extractPermissions(json::Value::fromString(permissions));
        const auto allPermissions = aclGw.allPermissions();
        for (const auto& path : paths) {
            permissionIds.insert(allPermissions.permission(path.pathParts()).id());
        }
    }
    std::set<acl::ID> roleIds;
    if (roleId) {
        roleIds.insert(roleId);
    }
    auto result = aclGw.groups(
        roleIds,
        aoiId,
        namePart,
        startId,
        beforeAfter,
        permissionIds,
        limit);
    srv::write(wrapper.jsonBuilder(), aclGw, result, srv::OutputAll);
}

YCR_RESPOND_TO("GET /groups/$", token, authorUid = 0)
{
    auto groupId = positionalParam<acl::ID>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(token);
    srv::checkAccessToGroups(authorUid, *work);
    acl::ACLGateway aclGw(*work);
        srv::JsonResponseWrapper wrapper(response);
        srv::write(wrapper.jsonBuilder(), aclGw, aclGw.group(groupId), srv::OutputAll);
}

YCR_RESPOND_TO("GET /groups/$/permissions", token, authorUid = 0)
{
    auto groupName = positionalParam<std::string>(argv, 0);
    auto work = srv::ConnectionManager::slaveTransaction(token);
    if (groupName != NMAPS_GROUP_NAME) {
        srv::checkAccessToGroups(authorUid, *work);
    }
    acl::ACLGateway aclGw(*work);
    auto group = aclGw.group(groupName);
    std::vector<acl::Permission> permissions;
    for (const auto& pol : group.policies()) {
        auto rolePermissions = pol.role().permissions();
        permissions.insert(
            std::end(permissions),
            std::begin(rolePermissions), std::end(rolePermissions));
    }

    auto allPermissions = aclGw.allPermissions();
    srv::JsonResponseWrapper wrapper(response);
    srv::write(wrapper.jsonBuilder(), allPermissions, permissions);
}

YCR_RESPOND_TO("POST /groups", authorUid)
{
    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    acl::ACLGateway aclGw(*work);
    srv::JsonResponseWrapper wrapper(response);
    auto savedGroups = srv::saveGroups(aclGw, request.body(), authorUid);
    for (const auto& savedGroup : savedGroups) {
        if (savedGroup.changedPermissions == srv::ChangedPermissions::True ||
            savedGroup.isNew == srv::IsNew::True)
        {
            aclGw.enqueueGroupCluster(savedGroup.group.id());
        }
    }
    for (const auto& savedGroup : savedGroups) {
        if (savedGroup.changedPermissions == srv::ChangedPermissions::True &&
            savedGroup.isNew == srv::IsNew::False)
        {
            aclGw.enqueueGroupAffectedClusters(savedGroup.group.id());
        }
    }
    auto token = pgpool3::generateToken(*work);
    wrapper.jsonBuilder() << [&](json::ObjectBuilder responseBuilder) {
        responseBuilder[srv::TOKEN] = token;
        responseBuilder[srv::GROUPS] << [&](json::ArrayBuilder arrayBuilder) {
            for (const auto& savedGroup : savedGroups) {
                srv::write(arrayBuilder, aclGw, savedGroup.group, srv::OutputAll);
            }
        };
    };

    work->commit();
}

YCR_RESPOND_TO("DELETE /groups/$", authorUid)
{
    auto groupId = positionalParam<acl::ID>(argv, 0);
    auto work = srv::ConnectionManager::masterWriteableTransaction(authorUid);
    acl::ACLGateway aclGw(*work);
    auto group = aclGw.group(groupId);
    srv::checkDeleteGroupAccess(group, authorUid, *work);
    aclGw.enqueueGroupCluster(groupId);
    aclGw.enqueueGroupAffectedClusters(groupId);
    group.removePolicies();
    aclGw.drop(std::move(group));
    respondToken(response, pgpool3::generateToken(*work));
    work->commit();
}

YCR_USE(yacare::Tvm2ServiceRequire(TVM_ALIAS))
{
YCR_RESPOND_TO("GET /idm/info/", tvmId) {
    requireIdmTvmId(tvmId);
    logIdmRequestId(request);
    srv::JsonResponseWrapper wrapper(response);
    wrapper.jsonBuilder()
        << json::Verbatim(srv::idm::info());
}

YCR_RESPOND_TO("GET /idm/get-all-roles/", tvmId) {
    requireIdmTvmId(tvmId);
    logIdmRequestId(request);
    srv::JsonResponseWrapper wrapper(response);
    auto work = srv::ConnectionManager::slaveTransaction({});
    wrapper.jsonBuilder()
        << json::Verbatim(srv::idm::getAllRoles(acl::ACLGateway(*work)));
}

YCR_RESPOND_TO("POST /idm/add-role/", tvmId) {
    requireIdmTvmId(tvmId);
    logIdmRequestId(request);
    srv::JsonResponseWrapper wrapper(response);
    auto work = srv::ConnectionManager::masterWriteableTransaction(common::ROBOT_UID);
    wrapper.jsonBuilder()
        << json::Verbatim(srv::idm::addRole(request.body(), acl::ACLGateway(*work)));
    work->commit();
}

YCR_RESPOND_TO("POST /idm/remove-role/", tvmId)
{
    requireIdmTvmId(tvmId);
    logIdmRequestId(request);
    srv::JsonResponseWrapper wrapper(response);
    auto work = srv::ConnectionManager::masterWriteableTransaction(common::ROBOT_UID);
    wrapper.jsonBuilder()
        << json::Verbatim(srv::idm::removeRole(request.body(), acl::ACLGateway(*work)));
    work->commit();
}
} // YCR_USE(yacare::Tvm2ServiceRequire(TVM_ALIAS))
} // YCR_USE

// The number of seconds before the shutdown to serve requests while not
// responding to /ping
YCR_OPTIONS.shutdown().grace_period() = 10;

YCR_MAIN(argc, argv) try
{
    const std::string cfgPath = (argc >= 2)
        ? argv[1]
        : CONFIG_PATH;
    INFO() << "USING CONFIG: " << cfgPath;
    srv::ConfigScope cfgScope(cfgPath);
    maps::log8::setLevel(srv::cfg()->logLevel());
    srv::ConnectionManager connectionManager(cfgPath, CONFIG_SERVICE_NAME);
    yacare::setErrorReporter(srv::errorReporter);

    yacare::run(yacare::RunSettings{.useSystemDefaultLocale = true});
    return EXIT_SUCCESS;
} catch (maps::Exception& e) {
    std::cerr << e << std::endl;
    return EXIT_FAILURE;
} catch (std::exception& e) {
    std::cerr << e.what() << std::endl;
    return EXIT_FAILURE;
}
