#include "save.h"

#include "access_checks.h"
#include "exception.h"
#include "magic_strings.h"

#include <maps/wikimap/mapspro/libs/acl/include/exception.h>
#include <yandex/maps/wiki/common/json_helpers.h>
#include <util/string/cast.h>
#include <boost/algorithm/string/trim.hpp>
#include <algorithm>

namespace maps::wiki::aclsrv {

namespace {

const std::string NMAPS_GROUP_NAME = "nmaps";

void checkCanAssignGroup(const CanAssign& canAssign, acl::ID groupId)
{
    if (canAssign.any != CanAssign::Any::True &&
        !canAssign.groupIds.count(groupId)) {
        throw acl::AccessDenied(canAssign.uid) << " can't assign group:" << groupId;
    }
}

void checkCanAssignRole(const CanAssign& canAssign, acl::ID roleId)
{
    if (canAssign.any != CanAssign::Any::True &&
        !canAssign.roleIds.count(roleId)) {
        throw acl::AccessDenied(canAssign.uid) << " can't assign role:" << roleId;
    }
}

PoliciesUpdateData
parsePolicies(const json::Value& policiesJson)
{
    ASSERT(policiesJson.isArray());

    PoliciesUpdateData data;

    for (const auto& val : policiesJson) {
        std::vector<ScheduleData> schedulesData;
        if (val.hasField(SCHEDULES)) {
            schedulesData = parseSchedules(val[SCHEDULES]);
        }

        if (val.hasField(GROUP)) {
            const auto& grp = val[GROUP];
            acl::ID id = 0;
            std::string name;
            if (grp.hasField(ID)) {
                id = common::readField<acl::ID>(grp, ID);
            } else {
                name = common::readField<std::string>(grp, NAME);
            }
            if (schedulesData.empty()) {
                data.groupsData.push_back({id, name, std::nullopt});
            } else {
                for (const auto& scheduleData : schedulesData) {
                    data.groupsData.push_back({id, name, scheduleData});
                }
            }
        } else {
            auto roleId = common::readField<acl::ID>(val[ROLE], ID);
            auto aoiId = val.hasField(AOI)
                ? common::readField<acl::ID>(val[AOI], ID)
                : 0;
            if (schedulesData.empty()) {
                data.policiesData.push_back({roleId, aoiId, std::nullopt});
            } else {
                for (const auto& scheduleData : schedulesData) {
                    data.policiesData.push_back({roleId, aoiId, scheduleData});
                }
            }
        }
    }

    return data;
}

std::vector<GroupData>
resolveGroupNames(
    std::vector<GroupData> groupsData,
    acl::ACLGateway& gw)
{
    for (auto& groupData : groupsData) {
        if (!groupData.id && !groupData.name.empty()) {
            groupData.id = gw.group(groupData.name).id();
        }
    }
    return groupsData;
}

template<typename T>
bool
contains(const std::vector<PolicyData>& policiesData, const T& policy)
{
    return
        std::find_if(
            policiesData.begin(),
            policiesData.end(),
            [&](const auto& policyData) {
                return equal(policyData, policy);
            })
        != policiesData.end();
}

template<typename T>
bool
contains(const std::vector<GroupData>& groupsData, const T& group)
{
    return
        std::find_if(
            groupsData.begin(),
            groupsData.end(),
            [&](const auto& groupData) {
                return equal(groupData, group);
            })
        != groupsData.end();
}

acl::UID
toUid(const json::Value& value)
{
    acl::UID uid = 0;
    REQUIRE(TryFromString(value.as<std::string>(), uid) && uid > 0,
            yacare::errors::BadRequest() << "Invalid uid");
    return uid;
}

} // namespace

UserUpdateData::UserUpdateData(const json::Value& jsonUserObject)
    : id(common::readOptionalField<acl::ID>(jsonUserObject, ID, 0))
{
    if (jsonUserObject.hasField(LOGIN)) {
        login = boost::trim_copy(common::readField<std::string>(jsonUserObject, LOGIN));
    } else if (!id) {
        throw yacare::errors::BadRequest()
            << "For new users login is required.";
    }

    if (jsonUserObject.hasField(STATUS)) {
        status = common::readField<acl::User::Status>(jsonUserObject, STATUS);
        if (status == acl::User::Status::Deleted) {

            if (!jsonUserObject.hasField(DELETE_REASON)) {
                deleteReason = acl::User::DeleteReason::User;
                // TODO: Don't require yet for back compatibility, uncomment later
                /* throw yacare::errors::Status::BadRequest()
                    << "delete-reason should be provided to set 'deleted' status";*/
            } else {
                deleteReason = common::readField<acl::User::DeleteReason>(jsonUserObject,
                    DELETE_REASON);
            }
        }
    }
    if (jsonUserObject.hasField(POLICIES)) {
        policies = parsePolicies(jsonUserObject[POLICIES]);
    }
    if (jsonUserObject.hasField(UID)) {
        uid = toUid(jsonUserObject[UID]);
    }
    if (!id && !uid) {
        throw yacare::errors::BadRequest()
            << "For new users UID should be passed.";
    }
    if (jsonUserObject.hasField(DISPLAY_NAME)) {
        displayName = jsonUserObject[DISPLAY_NAME].as<std::string>();
    }
}

std::vector<UserUpdateData>
UserUpdateData::parseData(const std::string& str)
{
    std::vector<UserUpdateData> usersUpdateData;
    auto jsonValue = json::Value::fromString(str);
    if (jsonValue.isArray()) {
        for (const auto& userJsonValue : jsonValue) {
            usersUpdateData.emplace_back(userJsonValue);
        }
    } else {
        usersUpdateData.emplace_back(jsonValue);
    }
    return usersUpdateData;
}

bool
UserUpdateData::isSelfRegistration(acl::UID authorUid) const
{
    if (uid != authorUid || id || !policies) {
        return false;
    }
    return std::all_of(
        policies->groupsData.begin(),
        policies->groupsData.end(),
        [&](const auto& groupData) {
            return groupData.name.starts_with(NMAPS_GROUP_NAME);
        });
}

bool
UserUpdateData::isSelfUpdate(acl::UID authorUid, acl::ACLGateway& gw) const
{
    if (!id || policies || status ||
        !(displayName || login)) {
        return false;
    }
    return gw.userById(id).uid() == authorUid;
}

namespace {

void
updateUserPolicies(
    acl::ACLGateway& gw,
    const acl::User& user,
    std::vector<PolicyData> newPoliciesData,
    const CanAssign& canAssign)
{
    const auto scheduledPolicies = gw.scheduledObjectsForAgent(user.id()).policies;
    std::vector<acl::Policy> permanentPolicies;
    for (const auto& policy : user.policies()) {
        auto it = find(scheduledPolicies, policy);
        if (it == scheduledPolicies.end()) {
            permanentPolicies.push_back(policy);
        }
    }

    for (const auto& policy : permanentPolicies) {
        auto sizeBefore = newPoliciesData.size();
        newPoliciesData = remove(std::move(newPoliciesData), policy);
        if (sizeBefore == newPoliciesData.size()) {
            checkCanAssignRole(canAssign, policy.roleId());
            user.removePolicy(policy.roleId(), policy.aoiId());
        }
    }
    for (const auto& scheduledPolicy : scheduledPolicies) {
        auto sizeBefore = newPoliciesData.size();
        newPoliciesData = remove(std::move(newPoliciesData), scheduledPolicy);
        if (sizeBefore == newPoliciesData.size()) {
            checkCanAssignRole(canAssign, scheduledPolicy.roleId());
            if (acl::isExistingPolicy(
                    user.policies(),
                    scheduledPolicy.roleId(),
                    scheduledPolicy.aoiId()))
            {
                user.removePolicy(
                    scheduledPolicy.roleId(),
                    scheduledPolicy.aoiId());
            }
            gw.dropSchedulesObjects(scheduledPolicy.schedule().id());
        }
    }

    for (const auto& policyData : newPoliciesData) {
        checkCanAssignRole(canAssign, policyData.roleId);
        if (!policyData.schedule) {
            gw.createPolicy(
                user,
                gw.role(policyData.roleId),
                gw.aoi(policyData.aoiId));
        } else {
            const auto& schedule = *policyData.schedule;
            gw.createScheduledPolicy(
                user.id(), policyData.roleId, policyData.aoiId,
                schedule.startDate, schedule.endDate,
                schedule.startTime, schedule.endTime,
                schedule.weekdays, schedule.workRestDays
            );
        }
    }
}

void
updateUserGroups(
    acl::ACLGateway& gw,
    const acl::User& user,
    const PoliciesUpdateData& newPoliciesData,
    const CanAssign& canAssign)
{
    auto newGroupsData = resolveGroupNames(std::move(newPoliciesData.groupsData), gw);
    std::set<acl::ID> newScheduledGroupdsIds;
    for (const auto& groupData : newGroupsData) {
        if (groupData.schedule) {
            newScheduledGroupdsIds.insert(groupData.id);
        }
    }
    const auto scheduledGroups = gw.scheduledObjectsForAgent(user.id()).groups;
    std::set<acl::ID> schedulesGroupIds;
    for (const auto& scheduledGroup : scheduledGroups) {
        schedulesGroupIds.insert(scheduledGroup.groupId());
    }

    for (const auto& group: user.groups()) {
        if (schedulesGroupIds.count(group.id())) {
            continue;
        }
        auto sizeBefore = newGroupsData.size();
        newGroupsData = remove(std::move(newGroupsData), group);
        if (sizeBefore == newGroupsData.size()) {
            checkCanAssignGroup(canAssign, group.id());
            group.remove(user);
        }
    }
    for (const auto& scheduledGroup : scheduledGroups) {
        auto sizeBefore = newGroupsData.size();
        newGroupsData = remove(std::move(newGroupsData), scheduledGroup);
        if (sizeBefore == newGroupsData.size()) {
            checkCanAssignGroup(canAssign, scheduledGroup.groupId());
            if (acl::isExistingGroup(user.groups(), scheduledGroup.groupId())) {
                gw.group(scheduledGroup.groupId()).remove(
                    gw.userById(scheduledGroup.userId()));
            }
            gw.dropSchedulesObjects(scheduledGroup.schedule().id());
        }
    }
    for (const auto& groupData : newGroupsData) {
        checkCanAssignGroup(canAssign, groupData.id);
        if (!groupData.schedule) {
            if (!newScheduledGroupdsIds.contains(groupData.id)) {
                gw.group(groupData.id).add(user);
            }
        } else {
            const auto& schedule = *groupData.schedule;
            gw.createScheduledGroup(
                user.id(), groupData.id,
                schedule.startDate, schedule.endDate,
                schedule.startTime, schedule.endTime,
                schedule.weekdays, schedule.workRestDays
            );
        }
    }
}

} // namespace

acl::ID
saveUser(
    acl::ACLGateway& gw,
    const UserUpdateData& userUpdateData,
    acl::UID authorUid,
    FromTest fromTest)
{
    bool userModified = false;
    bool selfUpdate = userUpdateData.isSelfRegistration(authorUid) ||
        userUpdateData.isSelfUpdate(authorUid, gw);
    acl::User user = userUpdateData.id
        ? gw.userById(userUpdateData.id)
        : gw.createUser(*userUpdateData.uid,
                *userUpdateData.login, {},
                authorUid);
    if (user.status() == acl::User::Status::Deleted &&
        userUpdateData.status != acl::User::Status::Active)
    {
        if (userUpdateData.displayName ||
            userUpdateData.login ||
            userUpdateData.policies)
        {
            throw yacare::errors::BadRequest()
                << "Deleted user updating forbidden: " << *userUpdateData.status;
        }
    }

    if (userUpdateData.status &&
        user.status() != *userUpdateData.status)
    {
        if (*userUpdateData.status == acl::User::Status::Active) {
            user.setActive(authorUid);
        } else if (*userUpdateData.status == acl::User::Status::Deleted) {
            user.setDeleted(*userUpdateData.deleteReason, authorUid);
        } else {
            throw yacare::errors::BadRequest()
                << "SaveUser, unsupported status: " << *userUpdateData.status;
        }
        userModified = true;
    }

    if (userUpdateData.displayName && userUpdateData.displayName != user.displayName()) {
        user.setDisplayName(*userUpdateData.displayName);
        userModified = true;
    }

    if (userUpdateData.login &&
        !userUpdateData.login->empty() && // check temporarily or persistently deleted user
        *userUpdateData.login != user.login())
    {
        user.setLogin(*userUpdateData.login);
        userModified = true;
    }
    if (userModified && !selfUpdate) {
        checkAclPermissions(authorUid, gw.work(), fromTest);
    }
    if (userUpdateData.policies) {
        const auto& userPermissions = *userUpdateData.policies;
        const auto canAssign =  userUpdateData.isSelfRegistration(authorUid)
            ? CanAssign { CanAssign::Any::True, {}, {}, authorUid}
            : getUserCanAssign(authorUid, gw.work());
        updateUserGroups(gw, user, userPermissions, canAssign);
        updateUserPolicies(gw, user, userPermissions.policiesData, canAssign);
    }
    return user.id();
}

namespace {

void
updateUserGroups(
    acl::ACLGateway& gw,
    acl::User& user,
    const UsersPoliciesUpdateData& usersPoliciesUpdateData,
    const CanAssign& canAssign)
{
    auto groupsDataToAdd = resolveGroupNames(
        std::move(usersPoliciesUpdateData.policiesToAdd.groupsData), gw);
    auto groupsDataToRemove = resolveGroupNames(
        std::move(usersPoliciesUpdateData.policiesToRemove.groupsData), gw);

    const auto scheduledGroups = gw.scheduledObjectsForAgent(user.id()).groups;
    std::set<acl::ID> schedulesGroupIds;
    for (const auto& scheduledGroup : scheduledGroups) {
        schedulesGroupIds.insert(scheduledGroup.groupId());
    }

    for (const auto& group: user.groups()) {
        if (schedulesGroupIds.count(group.id())) {
            continue;
        }
        groupsDataToAdd = remove(std::move(groupsDataToAdd), group);
        if (contains(groupsDataToRemove, group)) {
            checkCanAssignGroup(canAssign, group.id());
            group.remove(user);
        }
    }
    for (const auto& scheduledGroup : scheduledGroups) {
        groupsDataToAdd = remove(std::move(groupsDataToAdd), scheduledGroup);
        if (contains(groupsDataToRemove, scheduledGroup)) {
            checkCanAssignGroup(canAssign, scheduledGroup.groupId());
            if (acl::isExistingGroup(user.groups(), scheduledGroup.groupId())) {
                gw.group(scheduledGroup.groupId()).remove(
                    gw.userById(scheduledGroup.userId()));
            }
            gw.dropSchedulesObjects(scheduledGroup.schedule().id());
        }
    }

    for (const auto& groupData : groupsDataToAdd) {
        checkCanAssignGroup(canAssign, groupData.id);
        if (!groupData.schedule) {
            gw.group(groupData.id).add(user);
        } else {
            const auto& schedule = *groupData.schedule;
            gw.createScheduledGroup(
                user.id(), groupData.id,
                schedule.startDate, schedule.endDate,
                schedule.startTime, schedule.endTime,
                schedule.weekdays, schedule.workRestDays
            );
        }
    }
}

void
updateUserPolicies(
    acl::ACLGateway& gw,
    acl::User& user,
    const UsersPoliciesUpdateData& usersPoliciesUpdateData,
    const CanAssign& canAssign)
{
    auto toAdd = usersPoliciesUpdateData.policiesToAdd.policiesData;
    const auto& toRemove = usersPoliciesUpdateData.policiesToRemove.policiesData;

    const auto scheduledPolicies = gw.scheduledObjectsForAgent(user.id()).policies;
    std::vector<acl::Policy> permanentPolicies;
    for (const auto& policy : user.policies()) {
        auto it = find(scheduledPolicies, policy);
        if (it == scheduledPolicies.end()) {
            permanentPolicies.push_back(policy);
        }
    }

    for (const auto& policy : permanentPolicies) {
        toAdd = remove(std::move(toAdd), policy);
        if (contains(toRemove, policy)) {
            checkCanAssignRole(canAssign, policy.roleId());
            user.removePolicy(policy.roleId(), policy.aoiId());
        }
    }
    for (const auto& scheduledPolicy : scheduledPolicies) {
        toAdd = remove(std::move(toAdd), scheduledPolicy);
        if (contains(toRemove, scheduledPolicy)) {
            checkCanAssignRole(canAssign, scheduledPolicy.roleId());
            if (acl::isExistingPolicy(
                    user.policies(),
                    scheduledPolicy.roleId(),
                    scheduledPolicy.aoiId()))
            {
                user.removePolicy(
                    scheduledPolicy.roleId(),
                    scheduledPolicy.aoiId());
            }
            gw.dropSchedulesObjects(scheduledPolicy.schedule().id());
        }
    }
    for (const auto& policyData : toAdd) {
        checkCanAssignRole(canAssign, policyData.roleId);
        if (!policyData.schedule) {
            gw.createPolicy(
                user,
                gw.role(policyData.roleId),
                gw.aoi(policyData.aoiId));
        } else {
            const auto& schedule = *policyData.schedule;
            gw.createScheduledPolicy(
                user.id(), policyData.roleId, policyData.aoiId,
                schedule.startDate, schedule.endDate,
                schedule.startTime, schedule.endTime,
                schedule.weekdays, schedule.workRestDays
            );
        }
    }
}

} // namespace

UsersPoliciesUpdateData
UsersPoliciesUpdateData::parseData(const std::string& jsonStr)
{
    UsersPoliciesUpdateData result;
    auto rootObject = json::Value::fromString(jsonStr);
    if (!rootObject.isObject()) {
        throw yacare::errors::BadRequest() <<
            "Invalid input: " << jsonStr;
    }
    if (!rootObject.hasField(UIDS) || !rootObject[UIDS].isArray()) {
        throw yacare::errors::BadRequest() <<
            "Input should contain uids array" << jsonStr;
    } else {
        for (const auto& value : rootObject[UIDS]) {
            result.uids.insert(toUid(value));
        }
    }
    if (rootObject.hasField(ADD)) {
        result.policiesToAdd = parsePolicies(rootObject[ADD]);
    }
    if (rootObject.hasField(REMOVE)) {
        result.policiesToRemove = parsePolicies(rootObject[REMOVE]);
    }
    return result;
}

void saveUsersPolicies(
    acl::ACLGateway& gw,
    const UsersPoliciesUpdateData& usersPoliciesUpdateData,
    acl::UID authorUid)
{
    const auto& uids = usersPoliciesUpdateData.uids;
    if (uids.empty()) {
        return;
    }
    auto users = gw.users(
        std::vector<acl::UID>(uids.begin(), uids.end()));
    if (users.size() != uids.size()) {
        throw acl::UserNotExists();
    }
    const auto canAssign = getUserCanAssign(authorUid, gw.work());
    for (auto& user : users) {
        updateUserGroups(gw, user, usersPoliciesUpdateData, canAssign);
        updateUserPolicies(gw, user, usersPoliciesUpdateData, canAssign);
    }
}

} // namespace maps::wiki::aclsrv
