#pragma once

#include <maps/wikimap/mapspro/libs/acl/include/aoi.h>
#include <maps/wikimap/mapspro/libs/acl/include/banrecord.h>
#include <maps/wikimap/mapspro/libs/acl/include/common.h>
#include <maps/wikimap/mapspro/libs/acl/include/group.h>
#include <maps/wikimap/mapspro/libs/acl/include/permission.h>
#include <maps/wikimap/mapspro/libs/acl/include/permissions.h>
#include <maps/wikimap/mapspro/libs/acl/include/policy.h>
#include <maps/wikimap/mapspro/libs/acl/include/role.h>
#include <maps/wikimap/mapspro/libs/acl/include/schedule.h>
#include <maps/wikimap/mapspro/libs/acl/include/subject_path.h>
#include <maps/wikimap/mapspro/libs/acl/include/user.h>

#include <yandex/maps/wiki/common/before_after.h>
#include <yandex/maps/wiki/common/paged_result.h>
#include <maps/libs/enum_io/include/enum_io_fwd.h>

#include <chrono>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>

namespace maps::wiki::acl {

enum class InclusionType { StartsWith, Contains };

template <typename Object>
struct ResultBeforeAfter
{
    std::vector<Object> objects;
    bool hasMore = false;
};

struct IDMRole
{
    std::string service;
    std::string role;
};

enum class UsersOrder
{
    Date,
    Login
};
DECLARE_ENUM_IO(UsersOrder);

class ACLGateway
{
public:
    explicit ACLGateway(Transaction& work);

    User createUser(UID userId, const std::string& login, const std::string& displayName, UID authorId);
    User user(UID userId);
    User userById(ID id);
    User user(const std::string& login);
    std::vector<User> users();
    std::vector<User> users(const std::string& loginPart, size_t limit = 0);
    std::vector<User> users(const std::vector<UID>& uids);
    std::vector<User> usersByIds(const std::vector<ID>& ids);
    common::PagedResult<std::vector<User>> users(
        ID groupId,
        ID roleId, ID aoiId,
        const std::optional<User::Status>& userStatus,
        size_t page, size_t perPage);

    ResultBeforeAfter<User> 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 = UsersOrder::Login);

    std::set<ID> userIds(ID groupId, ID roleId, ID aoiId);
    std::set<ID> userIdsByRoles(const std::set<ID>& roleIds);

    void addIDMRole(const std::string& staffLogin, const IDMRole& role);
    void removeIDMRole(const std::string& staffLogin, const IDMRole& role);
    std::set<std::string> userIDMRoles(const std::string& staffLogin, const std::string& service) const;
    std::map<std::string, std::set<std::string>> allUsersIDMRoles(const std::string& service) const;
    std::map<std::string, std::vector<IDMRole>> allUsersIDMRoles() const;
    void clearUidToStaff();

    std::set<ID> clusterIdsByPermissions(const std::set<ID>& permissionIds);

    BanRecord banUser(
        UID userId, UID authorId,
        const std::string& expires,
        const std::string& reason);
    BanRecord unbanUser(UID userId, UID authorId);
    common::PagedResult<std::vector<BanRecord>> banRecords(
        UID userId, size_t page, size_t perPage);
    std::vector<UID> recentlyUnbannedUsers(std::chrono::seconds afterBanInterval);
    void drop(User&& user);

    Group createGroup(const std::string& name, const std::string& description);
    Group group(ID groupId);
    Group group(const std::string& name);
    std::vector<Group> groups();
    std::vector<Group> groups(const std::vector<ID>& groupIds);
    std::vector<Group> groups(const std::set<ID>& groupIds);
    common::PagedResult<std::vector<Group>> groups(
        ID roleId, ID aoiId,
        size_t page, size_t perPage);

    ResultBeforeAfter<Group> 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);

    void drop(Group&& group);

    Role createRole(
        const std::string& name,
        const std::string& description,
        Role::Privacy privacy = Role::Privacy::Private);
    Role role(ID roleId);
    Role role(const std::string& name);
    std::vector<Role> roles();
    std::vector<Role> roles(const std::set<ID>& roleIds);
    std::vector<Role> roles(InclusionType inclusionType, const std::string& namePart, size_t limit = 0);
    common::PagedResult<std::vector<Role>> roles(
        ID groupId, size_t page, size_t perPage);
    // Returns roles providing access to all listed paths
    std::vector<Role> permittingRoles(const std::vector<SubjectPath>& paths);
    std::set<ID> permittingRoleIds(const std::vector<SubjectPath>& paths);

    ResultBeforeAfter<Role> roles(
        ID groupId,
        const std::vector<SubjectPath>& paths,
        const std::string& namePart,
        ID startID,
        common::BeforeAfter beforeAfter,
        size_t limit);

    void drop(Role&& role);

    Aoi aoi(ID aoiId);

    Policy createPolicy(const User& user, const Role& role, const Aoi& aoi);
    Policy createPolicy(const Group& group, const Role& role, const Aoi& aoi);
    void drop(Policy&& policy);

    Permission permission(ID permissionId);
    Permission permission(const SubjectPath& path);
    Permission createRootPermission(const std::string& name);
    Permission rootPermission(const std::string& name);
    Permissions allPermissions();
    void drop(Permission&& permission);

    // Returns first role from the list applicable anywhere.
    // If none of the roles apply, returns defaultValue.
    std::string firstApplicableRole(
        const User& user,
        const std::vector<std::string>& roles,
        const std::string& defaultValue) const;

    std::map<ID, std::string> firstApplicableRoles(
        const std::vector<User>& users,
        const std::vector<std::string>& roles,
        const std::string& defaultValue) const;

    void updateUserCluster(ID userId);
    void updateGroupCluster(ID groupId);
    void updateAgentsClusters(bool onlyMissing);

    void enqueueAllClusters();
    void enqueueUserCluster(ID userId);
    void enqueueGroupCluster(ID groupId);
    void enqueueGroupAffectedClusters(ID groupId);
    void enqueueRoleAffectedClusters(ID roleId);
    size_t processClustersUpdateQueue();
    size_t clustersUpdateQueueSize() const;

    ScheduledPolicy 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);
    ScheduledGroup 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);

    ScheduledObjects allScheduledObjects();
    ScheduledObjects scheduledObjectsForAgent(ID agentId);
    void dropSchedulesObjects(ID scheduleId);
    void synchronizeSchedules();

    Transaction& work() { return work_; }

private:
    std::map<ID, std::string> firstApplicableRoles(
        const std::vector<ID>& agentIds,
        const std::vector<std::string>& roles,
        const std::string& defaultValue) const;

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

    Transaction& work_;
};

} // namespace maps::wiki::acl
