#include "save_group.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 <boost/algorithm/string/trim.hpp>
#include <algorithm>

namespace maps::wiki::aclsrv {

namespace {

void checkGroupUpdateAccess(const CanAssign& canAssign, const acl::Group& group)
{
    if (canAssign.any == CanAssign::Any::True) {
        return;
    }
    const auto& policies = group.policies();
    if (policies.empty()) {
        throw acl::AccessDenied(canAssign.uid)
            << " can't manage groups without roles: " << group.name();
    }
    for (const auto& policy : policies) {
        if (!canAssign.roleIds.contains(policy.role().id())) {
            throw acl::AccessDenied(canAssign.uid)
                << " can't manage group " << group.name()
                << " with role " << policy.role().name();
        }
    }
}

bool
updateGroupPolicies(
    acl::ACLGateway& gw,
    const acl::Group& group,
    std::vector<PolicyData> newPoliciesData)
{
    const auto scheduledPolicies = gw.scheduledObjectsForAgent(group.id()).policies;
    std::vector<acl::Policy> permanentPolicies;
    for (const auto& policy : group.policies()) {
        auto it = find(scheduledPolicies, policy);
        if (it == scheduledPolicies.end()) {
            permanentPolicies.push_back(policy);
        }
    }

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

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

SaveGroupResult
saveGroup(acl::ACLGateway& gw, const GroupUpdateData& newGroupData, const CanAssign& canAssign)
{
    SaveGroupResult saveResult {
        newGroupData.id()
            ? gw.group(newGroupData.id())
            : gw.createGroup(*newGroupData.name(), {}),
        newGroupData.id()
            ? IsNew::True
            : IsNew::False
    };
    auto& group = saveResult.group;
    if (newGroupData.id()) {
        checkGroupUpdateAccess(canAssign, group);
        if (group.name() != newGroupData.name() &&
            !newGroupData.name()->empty())
        {
            group.setName(*newGroupData.name());
        }
    }
    if (newGroupData.description()) {
        group.setDescription(*newGroupData.description());
    }
    if (newGroupData.policies()) {
        saveResult.changedPermissions =
            updateGroupPolicies(gw, group, *newGroupData.policies())
                ? ChangedPermissions::True
                : ChangedPermissions::False;
    }
    checkGroupUpdateAccess(canAssign, group);
    return saveResult;
}

} // namespace

GroupUpdateData::GroupUpdateData(const json::Value& jsonGroupObject)
    : id_(common::readOptionalField<acl::ID>(jsonGroupObject, ID, 0))
{
    if (jsonGroupObject.hasField(NAME)) {
        name_ = boost::trim_copy(jsonGroupObject[NAME].as<std::string>());
    }
    if (jsonGroupObject.hasField(DESCRIPTION)) {
        description_ = boost::trim_copy(jsonGroupObject[DESCRIPTION].as<std::string>());
    }
    if (jsonGroupObject.hasField(POLICIES)) {
        policies_ = std::vector<PolicyData>();
        const auto& policiesArray = jsonGroupObject[POLICIES];
        ASSERT(policiesArray.isArray());
        for (const auto& policyVal : policiesArray) {
            auto roleId = common::readField<acl::ID>(policyVal[ROLE], ID);
            auto aoiId = policyVal.hasField(AOI)
                ? common::readField<acl::ID>(policyVal[AOI], ID)
                : 0;
            if (!policyVal.hasField(SCHEDULES)) {
                policies_->emplace_back(PolicyData{roleId, aoiId, std::nullopt});
            } else {
                auto schedulesData = parseSchedules(policyVal[SCHEDULES]);
                for (const auto& scheduleData : schedulesData) {
                    policies_->emplace_back(PolicyData{roleId, aoiId, scheduleData});
                }
            }
        }
    }
}

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

std::vector<SaveGroupResult>
saveGroups(
    acl::ACLGateway& gw,
    const std::string& str,
    acl::UID authorUid)
{
    std::vector<SaveGroupResult> groups;
    const auto newDatas = GroupUpdateData::parseData(str);
    groups.reserve(newDatas.size());
    const auto canAssign = getUserCanAssign(authorUid, gw.work());
    for (const auto& newData : newDatas) {
        groups.emplace_back(saveGroup(gw, newData, canAssign));
    }
    return groups;
}

void checkDeleteGroupAccess(const acl::Group& group, acl::UID authorUid, acl::Transaction& work)
{
    const auto canAssign = getUserCanAssign(authorUid, work);
    checkGroupUpdateAccess(canAssign, group);
}

} // namespace maps::wiki::aclsrv
