/**
 * 
 */
package com.thinkbiganalytics.metadata.modeshape.security.role;

/*-
 * #%L
 * kylo-metadata-modeshape
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * Licensed 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.
 * #L%
 */

import java.security.Principal;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.jcr.Node;

import com.thinkbiganalytics.metadata.api.security.RoleMembership;
import com.thinkbiganalytics.metadata.modeshape.common.JcrObject;
import com.thinkbiganalytics.metadata.modeshape.security.action.JcrAllowedActions;
import com.thinkbiganalytics.metadata.modeshape.support.JcrPropertyUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.security.GroupPrincipal;
import com.thinkbiganalytics.security.UsernamePrincipal;
import com.thinkbiganalytics.security.action.Action;
import com.thinkbiganalytics.security.action.AllowableAction;
import com.thinkbiganalytics.security.role.SecurityRole;

/**
 *
 */
public class JcrRoleMembership extends JcrObject implements RoleMembership {
    
    private static final UsernamePrincipal[] NO_USERS = new UsernamePrincipal[0];
    private static final GroupPrincipal[] NO_GROUPS = new GroupPrincipal[0];

    public static final String NODE_NAME = "tba:roleMemberships";
    public static final String NODE_TYPE = "tba:roleMembership";
    public static final String ROLE = "tba:role";
    public static final String GROUPS = "tba:groups";
    public static final String USERS = "tba:users";
    
    private transient JcrAllowedActions allowedActions;

    public static JcrRoleMembership create(Node parentNode, Node roleNode, JcrAllowedActions allowed) {
        // This is a no-op if the membership already exists in the SNS nodes
        return JcrUtil.getNodeList(parentNode, NODE_NAME).stream()
                        .map(node -> JcrUtil.getJcrObject(node, JcrRoleMembership.class, allowed))
                        .filter(memshp -> memshp.getRole().getSystemName().equals(JcrUtil.getName(roleNode)))
                        .findFirst()
                        .orElseGet(() -> {
                            return JcrUtil.addJcrObject(parentNode, NODE_NAME, NODE_TYPE, JcrRoleMembership.class, roleNode, allowed);
                        });
    }
    
    public static void remove(Node parentNode, Node roleNode) {
        JcrUtil.getNodeList(parentNode, NODE_NAME).forEach(node -> {
            JcrRoleMembership membership = JcrUtil.getJcrObject(node, JcrRoleMembership.class, (JcrAllowedActions) null);
            
            if (membership.getRole().getSystemName().equals(JcrUtil.getName(roleNode))) {
                JcrUtil.removeNode(node);
            }
        });
    }
    
    public static void removeAll(Node parentNode) {
        JcrUtil.getNodeList(parentNode, NODE_NAME).forEach(node -> JcrUtil.removeNode(node));
    }
    
    public static Optional<JcrRoleMembership> find(Node parentNode, String roleName, JcrAllowedActions allowed) {
        return JcrUtil.getNodeList(parentNode, NODE_NAME).stream()
                        .map(node -> JcrUtil.getJcrObject(node, JcrRoleMembership.class, allowed))
                        .filter(memshp -> memshp.getRole().getSystemName().equals(roleName))
                        .findFirst();
    }

    /**
     * Wraps a parent node containing the tba:accessControlled mixin.
     * @param node the parent entity node
     */
    public JcrRoleMembership(Node node, JcrAllowedActions allowed) {
        super(node);
        this.allowedActions = allowed;
    }
    
    /**
     * Wraps a parent node containing the tba:accessControlled mixin.
     * @param node the parent entity node
     */
    public JcrRoleMembership(Node node, Node roleNode, JcrAllowedActions allowed) {
        super(node);
        JcrPropertyUtil.setProperty(node, ROLE, roleNode);
        this.allowedActions = allowed;
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.api.security.RoleMembership#getRole()
     */
    @Override
    public SecurityRole getRole() {
        return JcrUtil.getReferencedObject(getNode(), ROLE, JcrSecurityRole.class);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.api.security.RoleMembership#getMembers()
     */
    @Override
    public Set<Principal> getMembers() {
        Stream<? extends Principal> groups = streamGroups();
        Stream<? extends Principal> users = streamUsers();
        return Stream.concat(groups, users).collect(Collectors.toSet());
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.api.security.RoleMembership#setMemebers(com.thinkbiganalytics.security.UsernamePrincipal[])
     */
    @Override
    public void setMemebers(UsernamePrincipal... principals) {
        Set<UsernamePrincipal> newMembers = Arrays.stream(principals).collect(Collectors.toSet());
        Set<UsernamePrincipal> oldMembers = streamUsers().collect(Collectors.toSet());
        
        newMembers.stream()
            .filter(u -> ! oldMembers.contains(u))
            .forEach(this::addMember);
        
        oldMembers.stream()
            .filter(u -> ! newMembers.contains(u))
            .forEach(this::removeMember);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.api.security.RoleMembership#setMemebers(com.thinkbiganalytics.security.GroupPrincipal[])
     */
    @Override
    public void setMemebers(GroupPrincipal... principals) {
        Set<GroupPrincipal> newMembers = Arrays.stream(principals).collect(Collectors.toSet());
        Set<GroupPrincipal> oldMembers = streamGroups().collect(Collectors.toSet());
        
        newMembers.stream()
            .filter(u -> ! oldMembers.contains(u))
            .forEach(this::addMember);
        
        oldMembers.stream()
            .filter(u -> ! newMembers.contains(u))
            .forEach(this::removeMember);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.api.security.RoleMembership#addMember(java.security.Principal)
     */
    @Override
    public void addMember(GroupPrincipal principal) {
        JcrPropertyUtil.addToSetProperty(getNode(), GROUPS, principal.getName());
        enable(principal);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.api.security.RoleMembership#addMember(com.thinkbiganalytics.security.UsernamePrincipal)
     */
    @Override
    public void addMember(UsernamePrincipal principal) {
        JcrPropertyUtil.addToSetProperty(getNode(), USERS, principal.getName());
        enable(principal);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.api.security.RoleMembership#removeMember(java.security.Principal)
     */
    @Override
    public void removeMember(GroupPrincipal principal) {
        disable(principal);
        JcrPropertyUtil.removeFromSetProperty(getNode(), GROUPS, principal.getName());
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.api.security.RoleMembership#removeMember(com.thinkbiganalytics.security.UsernamePrincipal)
     */
    @Override
    public void removeMember(UsernamePrincipal principal) {
        disable(principal);
        JcrPropertyUtil.removeFromSetProperty(getNode(), USERS, principal.getName());
    }

    @Override
        public void removeAllMembers() {
            setMemebers(NO_USERS);
            setMemebers(NO_GROUPS);
        }

    protected void enable(Principal principal) {
        JcrAllowedActions roleAllowed = (JcrAllowedActions) getRole().getAllowedActions();
        Set<Action> actions = roleAllowed.getAvailableActions().stream()
                .flatMap(avail -> avail.stream())
                .collect(Collectors.toSet());
        
        this.allowedActions.enable(principal, actions);
    }

    protected void disable(Principal principal) {
        SecurityRole thisRole = getRole();
        
        // Get all actions allowed by all other memberships of this principal besides this one.
        Node parentNode = JcrUtil.getParent(getNode());
        Set<AllowableAction> otherAllowables = JcrUtil.getNodeList(parentNode, NODE_NAME).stream()
                        .map(node -> JcrUtil.getJcrObject(node, JcrRoleMembership.class, (JcrAllowedActions) null))
                        .filter(membership -> membership.getMembers().contains(principal))
                        .map(membership -> membership.getRole())
                        .filter(role -> ! role.getSystemName().equals(thisRole.getSystemName()))
                        .flatMap(role -> role.getAllowedActions().getAvailableActions().stream())
                        .flatMap(avail -> avail.stream())
                        .collect(Collectors.toSet());
            
        // Disable only the actions not permitted by any other role memberships for this principal
        Set<Action> disabled = thisRole.getAllowedActions().getAvailableActions().stream()
                        .flatMap(avail -> avail.stream())
                        .filter(action -> ! otherAllowables.contains(action))
                        .collect(Collectors.toSet());
        
        this.allowedActions.disable(principal, disabled);
    }

    private Stream<UsernamePrincipal> streamUsers() {
        return JcrPropertyUtil.<String>getSetProperty(getNode(), USERS).stream().map(UsernamePrincipal::new);
    }

    private Stream<GroupPrincipal> streamGroups() {
        return JcrPropertyUtil.<String>getSetProperty(getNode(), GROUPS).stream().map(GroupPrincipal::new);
    }
}
