// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.acl;

import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.user.Account;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.ListUtils;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionStatus;
import com.google.common.base.Strings;
import org.apache.cloudstack.acl.dao.RoleDao;
import org.apache.cloudstack.acl.dao.RolePermissionsDao;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.admin.acl.CreateRoleCmd;
import org.apache.cloudstack.api.command.admin.acl.CreateRolePermissionCmd;
import org.apache.cloudstack.api.command.admin.acl.DeleteRoleCmd;
import org.apache.cloudstack.api.command.admin.acl.DeleteRolePermissionCmd;
import org.apache.cloudstack.api.command.admin.acl.ListRolePermissionsCmd;
import org.apache.cloudstack.api.command.admin.acl.ListRolesCmd;
import org.apache.cloudstack.api.command.admin.acl.UpdateRoleCmd;
import org.apache.cloudstack.api.command.admin.acl.UpdateRolePermissionCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;

import javax.ejb.Local;
import javax.inject.Inject;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Local(value = {RoleService.class})
public class RoleManagerImpl extends ManagerBase implements RoleService, Configurable, PluggableService {
    @Inject
    private AccountDao accountDao;
    @Inject
    private RoleDao roleDao;
    @Inject
    private RolePermissionsDao rolePermissionsDao;

    private void checkCallerAccess() {
        if (!isEnabled()) {
            throw new PermissionDeniedException("Dynamic api checker is not enabled, aborting role operation");
        }
        Account caller = CallContext.current().getCallingAccount();
        if (caller == null || caller.getRoleId() == null) {
            throw new PermissionDeniedException("Restricted API called by an invalid user account");
        }
        Role callerRole = findRole(caller.getRoleId());
        if (callerRole == null || callerRole.getRoleType() != RoleType.Admin) {
            throw new PermissionDeniedException("Restricted API called by an user account of non-Admin role type");
        }
    }

    @Override
    public boolean isEnabled() {
        File apiCmdFile = PropertiesUtil.findConfigFile(PropertiesUtil.getDefaultApiCommandsFileName());
        return RoleService.EnableDynamicApiChecker.value() && (apiCmdFile == null || !apiCmdFile.exists());
    }

    @Override
    public Role findRole(final Long id) {
        if (id == null || id < 1L) {
            return null;
        }
        return roleDao.findById(id);
    }

    @Override
    public RolePermission findRolePermission(final Long id) {
        if (id == null) {
            return null;
        }
        return rolePermissionsDao.findById(id);
    }

    @Override
    public RolePermission findRolePermissionByUuid(final String uuid) {
        if (Strings.isNullOrEmpty(uuid)) {
            return null;
        }
        return rolePermissionsDao.findByUuid(uuid);
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_ROLE_CREATE, eventDescription = "creating Role")
    public Role createRole(final String name, final RoleType roleType, final String description) {
        checkCallerAccess();
        if (roleType == null || roleType == RoleType.Unknown) {
            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
        }
        return Transaction.execute(new TransactionCallback<RoleVO>() {
            @Override
            public RoleVO doInTransaction(TransactionStatus status) {
                return roleDao.persist(new RoleVO(name, roleType, description));
            }
        });
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_ROLE_UPDATE, eventDescription = "updating Role")
    public boolean updateRole(final Role role, final String name, final RoleType roleType, final String description) {
        checkCallerAccess();
        if (role == null) {
            return false;
        }
        if (roleType != null && roleType == RoleType.Unknown) {
            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unknown is not a valid role type");
        }
        RoleVO roleVO = (RoleVO) role;
        if (!Strings.isNullOrEmpty(name)) {
            roleVO.setName(name);
        }
        if (roleType != null) {
            if (role.getId() <= RoleType.User.getId()) {
                throw new PermissionDeniedException("The role type of default roles cannot be changed");
            }
            List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
            if (accounts == null || accounts.isEmpty()) {
                roleVO.setRoleType(roleType);
            } else {
                throw new PermissionDeniedException("Found accounts that have role in use, won't allow to change role type");
            }
        }
        if (!Strings.isNullOrEmpty(description)) {
            roleVO.setDescription(description);
        }
        return roleDao.update(role.getId(), roleVO);
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_ROLE_DELETE, eventDescription = "deleting Role")
    public boolean deleteRole(final Role role) {
        checkCallerAccess();
        if (role == null) {
            return false;
        }
        if (role.getId() <= RoleType.User.getId()) {
            throw new PermissionDeniedException("Default roles cannot be deleted");
        }
        List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
        if (accounts == null || accounts.size() == 0) {
            return Transaction.execute(new TransactionCallback<Boolean>() {
                @Override
                public Boolean doInTransaction(TransactionStatus status) {
                    List<? extends RolePermission> rolePermissions = rolePermissionsDao.findAllByRoleIdSorted(role.getId());
                    if (rolePermissions != null && !rolePermissions.isEmpty()) {
                        for (RolePermission rolePermission : rolePermissions) {
                            rolePermissionsDao.remove(rolePermission.getId());
                        }
                    }
                    return roleDao.remove(role.getId());
                }
            });
        }
        throw new PermissionDeniedException("Found accounts that have role in use, won't allow to delete role");
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_CREATE, eventDescription = "creating Role Permission")
    public RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description) {
        checkCallerAccess();
        return Transaction.execute(new TransactionCallback<RolePermissionVO>() {
            @Override
            public RolePermissionVO doInTransaction(TransactionStatus status) {
                return rolePermissionsDao.persist(new RolePermissionVO(role.getId(), rule.toString(), permission, description));
            }
        });
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_UPDATE, eventDescription = "updating Role Permission order")
    public boolean updateRolePermission(final Role role, final List<RolePermission> newOrder) {
        checkCallerAccess();
        return role != null && newOrder != null && rolePermissionsDao.update(role, newOrder);
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_DELETE, eventDescription = "deleting Role Permission")
    public boolean deleteRolePermission(final RolePermission rolePermission) {
        checkCallerAccess();
        return rolePermission != null && rolePermissionsDao.remove(rolePermission.getId());
    }

    @Override
    public List<Role> findRolesByName(final String name) {
        List<? extends Role> roles = null;
        if (!Strings.isNullOrEmpty(name)) {
            roles = roleDao.findAllByName(name);
        }
        return ListUtils.toListOfInterface(roles);
    }

    @Override
    public List<Role> findRolesByType(final RoleType roleType) {
        List<? extends Role> roles = null;
        if (roleType != null) {
            roles = roleDao.findAllByRoleType(roleType);
        }
        return ListUtils.toListOfInterface(roles);
    }

    @Override
    public List<Role> listRoles() {
        List<? extends Role> roles = roleDao.listAll();
        return ListUtils.toListOfInterface(roles);
    }

    @Override
    public List<RolePermission> findAllPermissionsBy(final Long roleId) {
        List<? extends RolePermission> permissions = rolePermissionsDao.findAllByRoleIdSorted(roleId);
        if (permissions != null) {
            return new ArrayList<>(permissions);
        }
        return Collections.emptyList();
    }

    @Override
    public String getConfigComponentName() {
        return RoleService.class.getSimpleName();
    }

    @Override
    public ConfigKey<?>[] getConfigKeys() {
        return new ConfigKey<?>[]{RoleService.EnableDynamicApiChecker};
    }

    @Override
    public List<Class<?>> getCommands() {
        final List<Class<?>> cmdList = new ArrayList<>();
        cmdList.add(CreateRoleCmd.class);
        cmdList.add(ListRolesCmd.class);
        cmdList.add(UpdateRoleCmd.class);
        cmdList.add(DeleteRoleCmd.class);
        cmdList.add(CreateRolePermissionCmd.class);
        cmdList.add(ListRolePermissionsCmd.class);
        cmdList.add(UpdateRolePermissionCmd.class);
        cmdList.add(DeleteRolePermissionCmd.class);
        return cmdList;
    }
 }
