/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sec.document;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.kns.document.MaintenanceDocument;
import org.kuali.kfs.krad.bo.DocumentHeader;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.util.KRADConstants;
import org.kuali.kfs.sec.businessobject.SecurityDefinition;
import org.kuali.kfs.sec.businessobject.SecurityModel;
import org.kuali.kfs.sec.businessobject.SecurityModelDefinition;
import org.kuali.kfs.sec.businessobject.SecurityModelMember;
import org.kuali.kfs.sec.businessobject.SecurityPrincipal;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.membership.MemberType;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.role.Role;
import org.kuali.rice.kim.api.role.RoleMember;
import org.kuali.rice.kim.api.role.RoleService;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.springframework.util.ObjectUtils;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;


/**
 * Maintainable implementation for the Security Model maintenance document. Hooks into Post processing to create a KIM role from
 * Model and assigns users/permissions to role based on Model
 */
public class SecurityModelMaintainableImpl extends AbstractSecurityModuleMaintainable {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SecurityModelMaintainableImpl.class);

    /**
     * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#doRouteStatusChange(org.kuali.rice.krad.bo.DocumentHeader)
     */
    @Override
    public void doRouteStatusChange(DocumentHeader documentHeader) {
        super.doRouteStatusChange(documentHeader);

        if (documentHeader.getWorkflowDocument().isProcessed()) {
            DocumentService documentService = SpringContext.getBean(DocumentService.class);
            try {
                MaintenanceDocument document = (MaintenanceDocument) documentService.getByDocumentHeaderId(documentHeader.getDocumentNumber());
                SecurityModel oldSecurityModel = (SecurityModel) document.getOldMaintainableObject().getBusinessObject();
                SecurityModel newSecurityModel = (SecurityModel) document.getNewMaintainableObject().getBusinessObject();

                boolean newMaintenanceAction = getMaintenanceAction().equalsIgnoreCase(KRADConstants.MAINTENANCE_NEW_ACTION) || getMaintenanceAction().equalsIgnoreCase(KRADConstants.MAINTENANCE_COPY_ACTION);

                Role modelRole = createOrUpdateModelRole(newSecurityModel);
                assignOrUpdateModelMembershipToDefinitionRoles(modelRole, oldSecurityModel, newSecurityModel, newMaintenanceAction);
                assignOrUpdateModelMembers(modelRole, newSecurityModel);

                if (!newSecurityModel.isActive()) {
                    inactivateModelRole(modelRole);
                }
            } catch (WorkflowException e) {
                LOG.error("caught exception while handling handleRouteStatusChange -> documentService.getByDocumentHeaderId(" + documentHeader.getDocumentNumber() + "). ", e);
                throw new RuntimeException("caught exception while handling handleRouteStatusChange -> documentService.getByDocumentHeaderId(" + documentHeader.getDocumentNumber() + "). ", e);
            }
        }
    }

    /**
     * Creates a new role for the model (if the model is new), otherwise updates the role
     *
     * @param oldSecurityModel SecurityModel record before updates
     * @param newSecurityModel SecurityModel after updates
     */
    protected Role createOrUpdateModelRole(SecurityModel newSecurityModel) {
        RoleService roleService = KimApiServiceLocator.getRoleService();

        // the roles are created in the KFS-SEC namespace with the same name as the model
        Role modelRole = roleService.getRoleByNamespaceCodeAndName(KFSConstants.CoreModuleNamespaces.ACCESS_SECURITY, newSecurityModel.getName());

        if (modelRole != null) {
            // always set the role as active so we can add members and definitions, after processing the indicator will be updated to
            // the appropriate value
            Role.Builder updatedRole = Role.Builder.create(modelRole);
            updatedRole.setActive(true);
            updatedRole.setDescription(newSecurityModel.getDescription());
            modelRole = roleService.updateRole(updatedRole.build());
        } else {
            String roleId = KFSConstants.CoreModuleNamespaces.ACCESS_SECURITY + "-" + newSecurityModel.getId();
            Role.Builder newRole = Role.Builder.create();
            newRole.setId(roleId);
            newRole.setName(newSecurityModel.getName());
            newRole.setNamespaceCode(KFSConstants.CoreModuleNamespaces.ACCESS_SECURITY);
            newRole.setDescription(newSecurityModel.getDescription());
            newRole.setKimTypeId(getDefaultRoleTypeId());
            newRole.setActive(true);
            modelRole = roleService.createRole(newRole.build());
        }
        newSecurityModel.setRoleId(modelRole.getId());
        return modelRole;
    }

    /**
     * Saves the given security model setting the active indicator to false
     *
     * @param newSecurityModel SecurityModel to inactivate
     */
    protected void inactivateModelRole(Role modelRole) {
        RoleService roleService = KimApiServiceLocator.getRoleService();

        if (modelRole != null) {
            Role.Builder updatedRole = Role.Builder.create(modelRole);
            updatedRole.setActive(false);
            KimApiServiceLocator.getRoleService().updateRole(updatedRole.build());
        }
    }

    /**
     * Iterates through the model definition list and assigns the model role to the definition role if necessary or updates the
     * current member assignment
     *
     * @param oldSecurityModel     SecurityModel record before updates
     * @param newSecurityModel     SecurityModel whose membership should be updated
     * @param newMaintenanceAction boolean indicating whether this is a new record (old side will not contain data)
     */
    protected void assignOrUpdateModelMembershipToDefinitionRoles(Role modelRole, SecurityModel oldSecurityModel, SecurityModel newSecurityModel, boolean newMaintenanceAction) {
        RoleService roleService = KimApiServiceLocator.getRoleService();

        if (modelRole == null) {
            LOG.error("Model Role does not exist for SecurityModel: " + newSecurityModel);
            throw new RuntimeException("Model Role does not exist for SecurityModel: " + newSecurityModel);
        }

        for (SecurityModelDefinition securityModelDefinition : newSecurityModel.getModelDefinitions()) {
            SecurityDefinition securityDefinition = securityModelDefinition.getSecurityDefinition();

            Role definitionRole = roleService.getRole(securityDefinition.getRoleId());

            if (definitionRole == null) {
                LOG.error("Definition Role does not exist for SecurityModelDefinition: " + securityDefinition);
                throw new RuntimeException("Definition Role does not exist for SecurityModelDefinition: " + securityDefinition);
            }

            RoleMember modelRoleMembership = null;
            if (!newMaintenanceAction) {
                SecurityModelDefinition oldSecurityModelDefinition = null;
                for (SecurityModelDefinition modelDefinition : oldSecurityModel.getModelDefinitions()) {
                    if (ObjectUtils.nullSafeEquals(modelDefinition.getModelDefinitionId(), securityModelDefinition.getModelDefinitionId())) {
                        oldSecurityModelDefinition = modelDefinition;
                        break;
                    }
                }

                if (oldSecurityModelDefinition != null) {
                    modelRoleMembership = getRoleMembershipForMemberType(definitionRole.getId(),
                        modelRole.getId(), MemberType.ROLE.getCode(),
                        getRoleQualifiersFromSecurityModelDefinition(oldSecurityModelDefinition));
                }
            }

            // only create membership if model is active and the model definition record is active
            boolean membershipActive = newSecurityModel.isActive() && securityModelDefinition.isActive();

            // if membership already exists, need to remove if the model definition record is now inactive or the qualifications
            // need updated
            if (modelRoleMembership != null) {
                if (!membershipActive) {
                    roleService.removeRoleFromRole(modelRoleMembership.getMemberId(), definitionRole.getNamespaceCode(), definitionRole.getName(), modelRoleMembership.getAttributes());
                }
            }

            // create of update role if membership should be active
            if (membershipActive) {
                if (modelRoleMembership == null) {
                    modelRoleMembership = roleService.assignRoleToRole(modelRole.getId(), definitionRole.getNamespaceCode(), definitionRole.getName(), getRoleQualifiersFromSecurityModelDefinition(securityModelDefinition));
                } else {
                    RoleMember.Builder updatedRoleMember = RoleMember.Builder.create(modelRoleMembership);
                    updatedRoleMember.setActiveToDate(null);
                    updatedRoleMember.setAttributes(getRoleQualifiersFromSecurityModelDefinition(securityModelDefinition));
                    modelRoleMembership = roleService.updateRoleMember(updatedRoleMember.build());
                }
            }
        }
    }

    /**
     * Iterates through the model member list and assign members to the model role or updates the membership
     *
     * @param securityModel SecurityModel whose member list should be updated
     */
    protected void assignOrUpdateModelMembers(Role modelRole, SecurityModel securityModel) {
        if (modelRole == null) {
            // this should throw an elegant error if either are null
            String error = "Data problem with access security. KIM Role backing the security model is missing.  SecurityModel: " + securityModel;
            LOG.error(error);
            throw new RuntimeException(error);
        }

        for (SecurityModelMember modelMember : securityModel.getModelMembers()) {
            updateSecurityModelRoleMember(modelRole, modelMember, modelMember.getMemberTypeCode(), modelMember.getMemberId(), new HashMap<String, String>(0));

            createPrincipalSecurityRecords(modelMember.getMemberId(), modelMember.getMemberTypeCode());
        }
    }

    /**
     * Creates security principal records for model members (if necessary) so that they will appear on security principal lookup for
     * editing
     *
     * @param memberId       String member id of model role
     * @param memberTypeCode String member type code for member
     */
    protected void createPrincipalSecurityRecords(String memberId, String memberTypeCode) {
        Collection<String> principalIds = new HashSet<String>();

        if (MemberType.PRINCIPAL.getCode().equals(memberTypeCode)) {
            principalIds.add(memberId);
        } else if (MemberType.ROLE.getCode().equals(memberTypeCode)) {
            Role roleInfo = KimApiServiceLocator.getRoleService().getRole(memberId);
            Collection<String> rolePrincipalIds = KimApiServiceLocator.getRoleService().getRoleMemberPrincipalIds(roleInfo.getNamespaceCode(), roleInfo.getName(), null);
            principalIds.addAll(rolePrincipalIds);
        } else if (MemberType.GROUP.getCode().equals(memberTypeCode)) {
            List<String> groupPrincipalIds = KimApiServiceLocator.getGroupService().getMemberPrincipalIds(memberId);
            principalIds.addAll(groupPrincipalIds);
        }

        BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
        for (String principalId : principalIds) {
            SecurityPrincipal securityPrincipal = businessObjectService.findBySinglePrimaryKey(SecurityPrincipal.class, principalId);
            if (securityPrincipal == null) {
                SecurityPrincipal newSecurityPrincipal = new SecurityPrincipal();
                newSecurityPrincipal.setPrincipalId(principalId);

                businessObjectService.save(newSecurityPrincipal);
            }
        }
    }

    /**
     * Determines whether the given definition is part of the SecurityModel associated definitions
     *
     * @param definitionName name of definition to look for
     * @param securityModel  SecurityModel to check
     * @return boolean true if the definition is in the security model, false if not
     */
    protected boolean isDefinitionInModel(String definitionName, SecurityModel securityModel) {
        for (SecurityModelDefinition securityModelDefinition : securityModel.getModelDefinitions()) {
            if (StringUtils.equalsIgnoreCase(definitionName, securityModelDefinition.getSecurityDefinition().getName())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Override to clear out KIM role id on copy
     *
     * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#processAfterCopy(org.kuali.rice.kns.document.MaintenanceDocument,
     * java.util.Map)
     */
    @Override
    public void processAfterCopy(MaintenanceDocument document, Map<String, String[]> parameters) {
        SecurityModel securityModel = (SecurityModel) document.getNewMaintainableObject().getBusinessObject();
        securityModel.setRoleId("");

        super.processAfterCopy(document, parameters);
    }


}
