/*
 * 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.DocumentService;
import org.kuali.kfs.krad.util.KRADConstants;
import org.kuali.kfs.sec.businessobject.SecurityDefinition;
import org.kuali.kfs.sec.businessobject.SecurityDefinitionDocumentType;
import org.kuali.kfs.sec.service.AccessSecurityService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.KimConstants;
import org.kuali.rice.kim.api.common.template.Template;
import org.kuali.rice.kim.api.permission.Permission;
import org.kuali.rice.kim.api.role.Role;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;

import java.util.HashMap;
import java.util.Map;


/**
 * Maintainable implementation for the Security Definition maintenance document. Hooks into Post processing to create the KIM permissions from the definition records
 */
public class SecurityDefinitionMaintainableImpl extends AbstractSecurityModuleMaintainable {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SecurityDefinitionMaintainableImpl.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());
                SecurityDefinition oldSecurityDefinition = (SecurityDefinition) document.getOldMaintainableObject().getBusinessObject();
                SecurityDefinition newSecurityDefinition = (SecurityDefinition) document.getNewMaintainableObject().getBusinessObject();

                oldSecurityDefinition.refreshNonUpdateableReferences();
                newSecurityDefinition.refreshNonUpdateableReferences();

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

                createOrUpdateDefinitionRole(oldSecurityDefinition, newSecurityDefinition);

                createOrUpdateDocumentPermissions(newSecurityDefinition);
                createOrUpdateLookupPermission(newSecurityDefinition);
                createOrUpdateInquiryPermissions(newSecurityDefinition);
            } 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 definition (if the definition is new), then grants to the role any new permissions granted for the definition. Also update role active indicator
     * if indicator changed values on the definition
     *
     * @param oldSecurityDefinition SecurityDefinition record before updates
     * @param newSecurityDefinition SecurityDefinition after updates
     */
    protected void createOrUpdateDefinitionRole(SecurityDefinition oldSecurityDefinition, SecurityDefinition newSecurityDefinition) { //, List<Permission> permissionsToAssign ) {
        Role oldRole = null;
        if (StringUtils.isNotBlank(oldSecurityDefinition.getRoleId())) {
            oldRole = KimApiServiceLocator.getRoleService().getRole(oldSecurityDefinition.getRoleId());
        }

        if (oldRole == null) {
            Role.Builder newRole = Role.Builder.create();
            newRole.setNamespaceCode(KFSConstants.CoreModuleNamespaces.ACCESS_SECURITY);
            newRole.setName(newSecurityDefinition.getName());
            newRole.setDescription(newSecurityDefinition.getDescription());
            newRole.setActive(newSecurityDefinition.isActive());
            newRole.setKimTypeId(getDefaultRoleTypeId());
            Role createdRole = KimApiServiceLocator.getRoleService().createRole(newRole.build());
            newSecurityDefinition.setRoleId(createdRole.getId());
        } else {
            // update role active indicator if it has been updated on the definition
            if (oldSecurityDefinition.isActive() != newSecurityDefinition.isActive()) {
                Role.Builder updatedRole = Role.Builder.create(oldRole);
                updatedRole.setActive(newSecurityDefinition.isActive());
                KimApiServiceLocator.getRoleService().updateRole(updatedRole.build());
            }
        }
    }

    /**
     * Iterates through the document types and creates any new document permissions necessary or updates old permissions setting inactive if needed
     *
     * @param oldSecurityDefinition SecurityDefiniton record before requested changes (old side of maintenance document)
     * @param newSecurityDefinition SecurityDefinition record with requested changes (new side of maintenance document)
     * @param newMaintenanceAction  Indicates whether this is a new maintenance record (old side in empty)
     */
    protected void createOrUpdateDocumentPermissions(SecurityDefinition securityDefinition) {
        for (SecurityDefinitionDocumentType definitionDocumentType : securityDefinition.getDefinitionDocumentTypes()) {
            String documentType = definitionDocumentType.getFinancialSystemDocumentTypeCode();
            boolean documentTypePermissionActive = securityDefinition.isActive() && definitionDocumentType.isActive();

            createOrUpdateDocumentTypePermissions(documentType, documentTypePermissionActive, securityDefinition);
        }
    }

    /**
     * First tries to retrieve a lookup permission previously setup for this definition. If old permission found it will be updated with the new details and its active indicator
     * will be set based on the definition active indicator and restrict lookup indicator value. If old permission does not exist but restrict lookup indicator is true on new side
     * then a new permission will be created and will be active if definition is active on new side.
     *
     * @param oldSecurityDefinition SecurityDefiniton record before requested changes (old side of maintenance document)
     * @param newSecurityDefinition SecurityDefinition record with requested changes (new side of maintenance document)
     * @param newMaintenanceAction  Indicates whether this is a new maintenance record (old side in empty)
     */
    protected void createOrUpdateLookupPermission(SecurityDefinition securityDefinition) {
        Template lookupTemplate = getAccessSecurityService().getLookupWithFieldValueTemplate();

        String permissionName = securityDefinition.getName() + "/" + lookupTemplate.getName();

        createOrUpdatePermissionAndAssignToRole(permissionName, securityDefinition.getRoleId(), securityDefinition.getDescription(), securityDefinition.isRestrictLookup(), lookupTemplate, getLookupPermissionDetails(securityDefinition));
    }

    /**
     * First tries to find inquiry permissions for GL namespace and LD namespace. If old permissions are found they will be updated with the new details and active indicator will
     * be set based on the definition active indicator and restrict gl indicator (for gl inqury permission) and restrict ld inquiry (for ld inquiry permission). If an old
     * permission does not exist for one or both of the namespaces and the corresponding indicators are set to true on new side then new permissions will be created with active
     * indicator set to true if definition is active on new side.
     *
     * @param oldSecurityDefinition SecurityDefiniton record before requested changes (old side of maintenance document)
     * @param newSecurityDefinition SecurityDefinition record with requested changes (new side of maintenance document)
     * @param newMaintenanceAction  Indicates whether this is a new maintenance record (old side in empty)
     */
    protected void createOrUpdateInquiryPermissions(SecurityDefinition securityDefinition) {
        // find old inquiry permissions
        Template inquiryTemplate = getAccessSecurityService().getInquiryWithFieldValueTemplate();
        String glPermissionName = securityDefinition.getName() + "/" + inquiryTemplate.getName() + "/" + KFSConstants.CoreModuleNamespaces.GL;
        String ldPermissionName = securityDefinition.getName() + "/" + inquiryTemplate.getName() + "/" + KFSConstants.OptionalModuleNamespaces.LABOR_DISTRIBUTION;

        Permission glPermission = KimApiServiceLocator.getPermissionService().findPermByNamespaceCodeAndName(KFSConstants.CoreModuleNamespaces.ACCESS_SECURITY, glPermissionName);
        Permission ldPermission = KimApiServiceLocator.getPermissionService().findPermByNamespaceCodeAndName(KFSConstants.CoreModuleNamespaces.ACCESS_SECURITY, ldPermissionName);

        // need to save gl inquiry permission if new side indicator is true or already has a permission in which case we need to update details and active indicator
        createOrUpdatePermissionAndAssignToRole(glPermissionName, securityDefinition.getRoleId(), securityDefinition.getDescription(), securityDefinition.isRestrictGLInquiry(), inquiryTemplate, getInquiryPermissionDetails(KFSConstants.CoreModuleNamespaces.GL, securityDefinition));

        createOrUpdatePermissionAndAssignToRole(ldPermissionName, securityDefinition.getRoleId(), securityDefinition.getDescription(), securityDefinition.isRestrictLaborInquiry(), inquiryTemplate, getInquiryPermissionDetails(KFSConstants.OptionalModuleNamespaces.LABOR_DISTRIBUTION, securityDefinition));
    }

    /**
     * For each of the document templates ids calls helper method to create or update corresponding permission
     *
     * @param documentType          workflow document type name for permission detail
     * @param active                boolean indicating whether the permissions should be set to active (true) or non-active (false)
     * @param oldSecurityDefinition SecurityDefiniton record before requested changes (old side of maintenance document)
     * @param newSecurityDefinition SecurityDefinition record with requested changes (new side of maintenance document)
     */
    protected void createOrUpdateDocumentTypePermissions(String documentType, boolean active, SecurityDefinition securityDefinition) {
        Map<String, String> permissionDetails = populateDocumentTypePermissionDetails(documentType, securityDefinition);
        // Permission Names must be unique
        // So - Security Definition Name/template name/document type
        // view document
        Template permissionTemplate = getAccessSecurityService().getViewDocumentWithFieldValueTemplate();
        String permissionName = securityDefinition.getName() + "/" + permissionTemplate.getName() + "/" + documentType;
        createOrUpdatePermissionAndAssignToRole(permissionName, securityDefinition.getRoleId(), securityDefinition.getDescription(),
            active && securityDefinition.isRestrictViewDocument(), permissionTemplate, permissionDetails);

        // view accounting line
        permissionTemplate = getAccessSecurityService().getViewAccountingLineWithFieldValueTemplate();
        permissionName = securityDefinition.getName() + "/" + permissionTemplate.getName() + "/" + documentType;
        createOrUpdatePermissionAndAssignToRole(permissionName, securityDefinition.getRoleId(), securityDefinition.getDescription(),
            active && securityDefinition.isRestrictViewAccountingLine(), permissionTemplate, permissionDetails);

        // view notes/attachments
        permissionTemplate = getAccessSecurityService().getViewNotesAttachmentsWithFieldValueTemplate();
        permissionName = securityDefinition.getName() + "/" + permissionTemplate.getName() + "/" + documentType;
        createOrUpdatePermissionAndAssignToRole(permissionName, securityDefinition.getRoleId(), securityDefinition.getDescription(),
            active && securityDefinition.isRestrictViewNotesAndAttachments(), permissionTemplate, permissionDetails);

        // edit accounting line
        permissionTemplate = getAccessSecurityService().getEditAccountingLineWithFieldValueTemplate();
        permissionName = securityDefinition.getName() + "/" + permissionTemplate.getName() + "/" + documentType;
        createOrUpdatePermissionAndAssignToRole(permissionName, securityDefinition.getRoleId(), securityDefinition.getDescription(),
            active && securityDefinition.isRestrictEditAccountingLine(), permissionTemplate, permissionDetails);

        // edit document
        permissionTemplate = getAccessSecurityService().getEditDocumentWithFieldValueTemplate();
        permissionName = securityDefinition.getName() + "/" + permissionTemplate.getName() + "/" + documentType;
        createOrUpdatePermissionAndAssignToRole(permissionName, securityDefinition.getRoleId(), securityDefinition.getDescription(),
            active && securityDefinition.isRestrictEditDocument(), permissionTemplate, permissionDetails);
    }

    /**
     * Builds an Map<String,String> populated from the given method parameters. Details are set based on the KIM 'Security Document Permission' type.
     *
     * @param documentType       workflow document type name
     * @param securityDefinition SecurityDefiniton record
     * @return Map<String,String> populated with document type name, property name, operator, and property value details
     */
    protected Map<String, String> populateDocumentTypePermissionDetails(String documentType, SecurityDefinition securityDefinition) {
        Map<String, String> permissionDetails = new HashMap<String, String>();
        permissionDetails.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, documentType);
        permissionDetails.put(KimConstants.AttributeConstants.PROPERTY_NAME, securityDefinition.getSecurityAttribute().getName());

        return permissionDetails;
    }

    /**
     * Builds an Map<String,String> populated from the given method parameters. Details are set based on the KIM 'Security Lookup Permission' type.
     *
     * @param securityDefinition SecurityDefiniton record
     * @return Map<String,String> populated with property name, operator, and property value details
     */
    protected Map<String, String> getLookupPermissionDetails(SecurityDefinition securityDefinition) {
        Map<String, String> permissionDetails = new HashMap<String, String>();
        permissionDetails.put(KimConstants.AttributeConstants.PROPERTY_NAME, securityDefinition.getSecurityAttribute().getName());

        return permissionDetails;
    }

    /**
     * Builds an Map<String,String> populated from the given method parameters. Details are set based on the KIM 'Security Inquiry Permission' type.
     *
     * @param namespaceCode      KIM namespace code
     * @param securityDefinition SecurityDefiniton record
     * @return Map<String,String> populated with namespace, property name, operator, and property value details
     */
    protected Map<String, String> getInquiryPermissionDetails(String namespaceCode, SecurityDefinition securityDefinition) {
        Map<String, String> permissionDetails = new HashMap<String, String>();
        permissionDetails.put(KimConstants.AttributeConstants.NAMESPACE_CODE, namespaceCode);
        permissionDetails.put(KimConstants.AttributeConstants.PROPERTY_NAME, securityDefinition.getSecurityAttribute().getName());

        return permissionDetails;
    }

    /**
     * Determines whether a given document type name is included in the document type list for the given security definition
     *
     * @param documentType          KEW document type name
     * @param oldSecurityDefinition SecurityDefinition record
     * @return boolean indicating whether the document type is associated with the given security definition
     */
    protected boolean isDocumentTypeInDefinition(String documentType, SecurityDefinition oldSecurityDefinition) {
        for (SecurityDefinitionDocumentType definitionDocumentType : oldSecurityDefinition.getDefinitionDocumentTypes()) {
            String oldDocumentType = definitionDocumentType.getFinancialSystemDocumentTypeCode();
            if (StringUtils.equals(documentType, oldDocumentType)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Calls PermissionUpdateService to save a permission.
     *
     * @param securityDefinition   SecurityDefinition record
     * @param permissionId         ID for the permission being saved, or empty for new permission
     * @param permissionTemplateId KIM template ID for permission to save
     * @param active               boolean indicating whether the permission should be set to active (true) or non-active (false)
     * @param permissionDetails    Map<String,String> representing the permission details
     * @see org.kuali.rice.kim.service.PermissionUpdateService#savePermission()
     */
    protected void createOrUpdatePermissionAndAssignToRole(String permissionName, String roleId, String permissionDescription, boolean active, Template permissionTemplate, Map<String, String> permissionDetails) {
        // Get the existing permission
        Permission perm = KimApiServiceLocator.getPermissionService().findPermByNamespaceCodeAndName(KFSConstants.CoreModuleNamespaces.ACCESS_SECURITY, permissionName);

        if (perm == null) {
            if (active) {
                Permission.Builder newPerm = Permission.Builder.create(KFSConstants.CoreModuleNamespaces.ACCESS_SECURITY, permissionName);
                newPerm.setTemplate(Template.Builder.create(permissionTemplate));
                newPerm.setDescription(permissionDescription);
                newPerm.setAttributes(permissionDetails);
                newPerm.setActive(true);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("About to save new permission: " + newPerm);
                }
                perm = KimApiServiceLocator.getPermissionService().createPermission(newPerm.build());
            }
        } else {
            if (perm.isActive() != active) {
                Permission.Builder updatedPerm = Permission.Builder.create(perm);
                updatedPerm.setActive(active);
                perm = KimApiServiceLocator.getPermissionService().updatePermission(updatedPerm.build());
            }
        }

        assignPermissionToRole(perm, roleId);
    }

    protected void assignPermissionToRole(Permission perm, String roleId) {
        if (perm != null) {
            if (perm.isActive()) {
                KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), roleId);
            } else {
                KimApiServiceLocator.getRoleService().revokePermissionFromRole(perm.getId(), roleId);
            }
        }
    }

    /**
     * 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) {
        SecurityDefinition securityDefinition = (SecurityDefinition) document.getNewMaintainableObject().getBusinessObject();
        securityDefinition.setRoleId("");

        super.processAfterCopy(document, parameters);
    }

    private static AccessSecurityService accessSecurityService;

    public static AccessSecurityService getAccessSecurityService() {
        if (accessSecurityService == null) {
            accessSecurityService = SpringContext.getBean(AccessSecurityService.class);
        }
        return accessSecurityService;
    }
}
