/*
 * This file is part of EspoCRM and/or AtroCore.
 *
 * EspoCRM - Open Source CRM application.
 * Copyright (C) 2014-2019 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
 * Website: http://www.espocrm.com
 *
 * AtroCore is EspoCRM-based Open Source application.
 * Copyright (C) 2020 AtroCore GmbH.
 *
 * AtroCore as well as EspoCRM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * AtroCore as well as EspoCRM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with EspoCRM. If not, see http://www.gnu.org/licenses/.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "EspoCRM" word
 * and "AtroCore" word.
 *//** * Example:
 * Lead: {
 *   edit: 'own',
 *   read: 'team',
 *   delete: 'no',
 * }
 */

Espo.define('acl-manager', ['acl', 'metadata'], function (Acl, Metadata) {

    var AclManager = function (user, implementationClassMap, metadata = null) {
        this.setEmpty();

        this.user = user || null;
        this.implementationClassMap = implementationClassMap || {};
        this.metadata = metadata || new Metadata(null);
    }

    _.extend(AclManager.prototype, {

        data: null,

        user: null,

        fieldLevelList: ['yes', 'no'],

        setEmpty: function () {
            this.data = {
                table: {},
                fieldTable:  {},
                fieldTableQuickAccess: {}
            };
            this.implementationHash = {};
            this.forbiddenFieldsCache = {};
            this.implementationClassMap = {};
            this.forbiddenAttributesCache = {};
        },

        getImplementation: function (scope) {
            if (!(scope in this.implementationHash)) {
                var implementationClass = Acl;
                if (scope in this.implementationClassMap) {
                    implementationClass = this.implementationClassMap[scope];
                }
                var obj = new implementationClass(this.getUser(), scope);
                this.implementationHash[scope] = obj;
            }
            return this.implementationHash[scope];
        },

        getUser: function () {
            return this.user;
        },

        set: function (data) {
            data = data || {};
            this.data = data;
            this.data.table = this.data.table || {};
            this.data.fieldTable = this.data.fieldTable || {};
            this.data.attributeTable = this.data.attributeTable || {};
        },

        get: function (name) {
            return this.data[name] || null;
        },

        clear: function () {
            this.setEmpty();
        },

        checkScopeHasAcl: function (scope) {
            var data = (this.data.table || {})[scope];
            if (typeof data === 'undefined') {
                return false;
            }
            return true;
        },

        checkScope: function (scope, action, precise) {
            let checkScope = (scope, action) => {
                var data = (this.data.table || {})[scope];
                if (typeof data === 'undefined') {
                    data = null;
                }
                return this.getImplementation(scope).checkScope(data, action, precise);
            };

            if(this.metadata.get(['scopes', scope, 'type']) === 'Relation') {
                let relAction = action === 'read' ? action : 'edit';
                for (const relationEntity of this.getRelationEntities(scope)) {
                    if(!checkScope(relationEntity, relAction)) {
                        return false;
                    }
                }
            }
            return checkScope(scope, action);
        },

        getRelationEntities(scope) {
            let result = [];
            if(this.metadata.get(['scopes', scope, 'type']) !== 'Relation') {
                return result;
            }
            Object.entries(this.metadata.get(['entityDefs', scope, 'fields'])).forEach(([field, defs]) => {
                if(defs['relationField']) {
                    let relationEntityName = this.metadata.get(['entityDefs', scope, 'links', field, 'entity']);
                    if(relationEntityName) {
                        result.push(relationEntityName);
                    }
                }
            });
            return result;
        },

        checkModel: function (model, action, precise) {
            if (!model) {
                return false;
            }
            var scope = model.name;

            // todo move this to custom acl
            if (action == 'edit') {
                if (!model.isEditable()) {
                    return false;
                }
            }
            if (action == 'delete') {
                if (!model.isRemovable()) {
                    return false;
                }
            }

            var data = (this.data.table || {})[scope];
            if (typeof data === 'undefined') {
                data = null;
            }

            var impl = this.getImplementation(scope);

            var methodName = 'checkModel' + Espo.Utils.upperCaseFirst(action);
            if (methodName in impl) {
                return impl[methodName](model, data, precise);
            }

            return impl.checkModel(model, data, action, precise);
        },

        check: function (subject, action, precise) {
            if (typeof subject === 'string') {
                return this.checkScope(subject, action, precise);
            } else {
                return this.checkModel(subject, action, precise);
            }
        },

        checkIsOwner: function (model) {
            return this.getImplementation(model.name).checkIsOwner(model);
        },

        checkInTeam: function (model) {
            return this.getImplementation(model.name).checkInTeam(model);
        },

        checkAssignmentPermission: function (user) {
            return this.checkPermission('assignmentPermission', user);
        },

        checkPermission: function (permission, user) {
            var result = false;

            if (this.getUser().isAdmin()) {
                result = true;
            } else {
                if (this.get(permission) === 'no') {
                    if (user.id == this.getUser().id) {
                        result = true;
                    }
                } else if (this.get(permission) === 'team') {
                    if (user.has('teamsIds')) {
                        user.get('teamsIds').forEach(function (id) {
                            if (~(this.getUser().get('teamsIds') || []).indexOf(id)) {
                                result = true;
                            }
                        }, this);
                    }
                } else {
                    result = true;
                }
            }
            return result;
        },

        getScopeForbiddenFieldList: function (scope, action, thresholdLevel) {
            action = action || 'read';
            thresholdLevel = thresholdLevel || 'no';

            var key = scope + '_' + action + '_' + thresholdLevel;
            if (key in this.forbiddenFieldsCache) {
                return this.forbiddenFieldsCache[key];
            }

            var levelList = this.fieldLevelList.slice(this.fieldLevelList.indexOf(thresholdLevel));

            var fieldTableQuickAccess = this.data.fieldTableQuickAccess || {};
            var scopeData = fieldTableQuickAccess[scope] || {};
            var fieldsData = scopeData.fields || {};
            var actionData = fieldsData[action] || {};

            var fieldList = [];
            levelList.forEach(function (level) {
                var list = actionData[level] || [];
                list.forEach(function (field) {
                    if (~fieldList.indexOf(field)) return;
                    fieldList.push(field);
                }, this);
            }, this);

            this.forbiddenFieldsCache[key] = fieldList;

            return fieldList;
        },

        getScopeForbiddenAttributeList: function (scope, action, thresholdLevel) {
            action = action || 'read';
            thresholdLevel = thresholdLevel || 'no';

            var key = scope + '_' + action + '_' + thresholdLevel;
            if (key in this.forbiddenAttributesCache) {
                return this.forbiddenAttributesCache[key];
            }

            var levelList = this.fieldLevelList.slice(this.fieldLevelList.indexOf(thresholdLevel));

            var fieldTableQuickAccess = this.data.fieldTableQuickAccess || {};
            var scopeData = fieldTableQuickAccess[scope] || {};

            var attributesData = scopeData.attributes || {};
            var actionData = attributesData[action] || {};

            var attributeList = [];
            levelList.forEach(function (level) {
                var list = actionData[level] || [];
                list.forEach(function (attribute) {
                    if (~attributeList.indexOf(attribute)) return;
                    attributeList.push(attribute);
                }, this);
            }, this);

            this.forbiddenAttributesCache[key] = attributeList;

            return attributeList;
        },

        checkTeamAssignmentPermission: function (teamId) {
            if (this.get('assignmentPermission') === 'all') return true;
            return ~this.getUser().getLinkMultipleIdList('teams').indexOf(teamId);
        }

    });

    AclManager.extend = Backbone.Router.extend;

    return AclManager;
});

