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

/*-
 * #%L
 * thinkbig-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 com.thinkbiganalytics.metadata.api.MetadataAccess;
import com.thinkbiganalytics.metadata.api.PostMetadataConfigAction;
import com.thinkbiganalytics.metadata.modeshape.common.SecurityPaths;
import com.thinkbiganalytics.metadata.modeshape.extension.ExtensionsConstants;
import com.thinkbiganalytics.metadata.modeshape.security.AdminCredentials;
import com.thinkbiganalytics.metadata.modeshape.security.JcrAccessControlUtil;
import com.thinkbiganalytics.metadata.modeshape.security.ModeShapeAdminPrincipal;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrVersionUtil;
import com.thinkbiganalytics.security.AccessController;
import com.thinkbiganalytics.security.role.SecurityRole;

import org.modeshape.jcr.api.nodetype.NodeTypeManager;
import org.modeshape.jcr.security.SimplePrincipal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.NodeTypeTemplate;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.nodetype.PropertyDefinitionTemplate;
import javax.jcr.security.Privilege;
import javax.jcr.version.Version;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionIterator;
import javax.jcr.version.VersionManager;

/**
 *
 */
public class MetadataJcrConfigurator {

    private static final Logger log = LoggerFactory.getLogger(MetadataJcrConfigurator.class);
    private final AtomicBoolean configured = new AtomicBoolean(false);
    @Inject
    private MetadataAccess metadataAccess;



    private List<PostMetadataConfigAction> postConfigActions = new ArrayList<>();

    public MetadataJcrConfigurator(List<PostMetadataConfigAction> actions) {
        this.postConfigActions.addAll(actions);
        this.postConfigActions.sort(new AnnotationAwareOrderComparator());
    }

    public void configure() {
        this.metadataAccess.commit(() -> {
            try {
                Session session = JcrMetadataAccess.getActiveSession();
                ensureLayout(session);
                ensureTypes(session);
                ensureAccessControl(session);
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Could not create initial JCR metadata", e);
            }
        }, MetadataAccess.SERVICE);

        this.metadataAccess.commit(() -> {
            try {
                Session session = JcrMetadataAccess.getActiveSession();
                removeVersionableFeedType(session);
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Could remove versioning from feeds", e);
            }
        }, MetadataAccess.SERVICE);

        this.configured.set(true);
       firePostConfigActions();
    }

    private void removeVersionableFeedType(Session session) throws RepositoryException {
        Node feedsNode = session.getRootNode().getNode("metadata/feeds");

        NodeTypeManager typeMgr = (NodeTypeManager) session.getWorkspace().getNodeTypeManager();
        NodeType currentFeedType = typeMgr.getNodeType("tba:feed");
        List<String> currentSupertypes = Arrays.asList(currentFeedType.getDeclaredSupertypeNames());

        if (currentSupertypes.contains("mix:versionable")) {
            log.info("Removing versionable feed type {} ", currentFeedType);
            // Remove feed version history
            for (Node catNode : JcrUtil.getNodesOfType(feedsNode, "tba:category")) {
                for (Node feedNode : JcrUtil.getNodesOfType(catNode, "tba:feed")) {
                    log.debug("Removing prior versions of feed: {}.{}", catNode.getName(), feedNode.getName());
                    if (JcrUtil.isVersionable(feedNode)) {
                        VersionManager versionManager = session.getWorkspace().getVersionManager();
                        VersionHistory versionHistory = versionManager.getVersionHistory(feedNode.getPath());
                        VersionIterator vIt = versionHistory.getAllVersions();
                        int count = 0;
                        String last = "";

                        while (vIt.hasNext()) {
                            Version version = vIt.nextVersion();
                            String versionName = version.getName();
                            String baseVersion = "";
                            if (!"jcr:rootVersion".equals(versionName)) {
                                //baseVersion requires actual versionable node to get the base version name
                                baseVersion = JcrVersionUtil.getBaseVersion(feedNode).getName();
                            }
                            if (!"jcr:rootVersion".equals(versionName) && !versionName.equalsIgnoreCase(baseVersion)) {
                                last = version.getName();
                                // removeVersion writes directly to workspace, no session.save is necessary
                                versionHistory.removeVersion(version.getName());
                                count++;
                            }
                        }

                        if (count > 0) {
                            log.info("Removed {} versions through {} of feed {}", count, last, feedNode.getName());
                        } else {
                            log.debug("Feed {} had no versions", feedNode.getName());
                        }
                    }
                }
            }

            // Redefine the NodeType of tba:feed to remove versionable but retain the versionable properties with weaker constraints
            // Retaining the properties seems to override some residual properties on feed nodes that causes a failure later.
            // In particular, jcr:predecessors was accessed later but redefining all mix:versionable properties to be safe.
            NodeTypeTemplate template = typeMgr.createNodeTypeTemplate(currentFeedType);
            List<String> newSupertypes = currentSupertypes.stream().filter(type -> !type.equals("mix:versionable")).collect(Collectors.toList());

            template.setDeclaredSuperTypeNames(newSupertypes.toArray(new String[newSupertypes.size()]));

            @SuppressWarnings("unchecked")
            List<PropertyDefinitionTemplate> propTemplates = template.getPropertyDefinitionTemplates();
            PropertyDefinitionTemplate prop = typeMgr.createPropertyDefinitionTemplate();
            prop.setName("jcr:versionHistory");
            prop.setRequiredType(PropertyType.WEAKREFERENCE);
            propTemplates.add(prop);
            prop = typeMgr.createPropertyDefinitionTemplate();
            prop.setName("jcr:baseVersion");
            prop.setRequiredType(PropertyType.WEAKREFERENCE);
            propTemplates.add(prop);
            prop = typeMgr.createPropertyDefinitionTemplate();
            prop.setName("jcr:predecessors");
            prop.setRequiredType(PropertyType.WEAKREFERENCE);
            prop.setMultiple(true);
            propTemplates.add(prop);
            prop = typeMgr.createPropertyDefinitionTemplate();
            prop.setName("jcr:mergeFailed");
            prop.setRequiredType(PropertyType.WEAKREFERENCE);
            propTemplates.add(prop);
            prop = typeMgr.createPropertyDefinitionTemplate();
            prop.setName("jcr:activity");
            prop.setRequiredType(PropertyType.WEAKREFERENCE);
            propTemplates.add(prop);
            prop = typeMgr.createPropertyDefinitionTemplate();
            prop.setName("jcr:configuration");
            prop.setRequiredType(PropertyType.WEAKREFERENCE);
            propTemplates.add(prop);

            log.info("Replacing the versionable feed type '{}' with a non-versionable type", currentFeedType);
            NodeType newType = typeMgr.registerNodeType(template, true);
            log.info("Replaced with new feed type '{}' with a non-versionable type", newType);

            // This step may not be necessary.
            for (Node catNode : JcrUtil.getNodesOfType(feedsNode, "tba:category")) {
                for (Node feedNode : JcrUtil.getNodesOfType(catNode, "tba:feed")) {
                    feedNode.setPrimaryType(newType.getName());
                    // log.info("Replaced type of node {}", feedNode);

                    if (feedNode.hasProperty("jcr:predecessors")) {
                        feedNode.getProperty("jcr:predecessors").setValue(new Value[0]);
                        ;
                        feedNode.getProperty("jcr:predecessors").remove();
                    }
                }
            }
        }
    }

    private void firePostConfigActions() {

        for (PostMetadataConfigAction action : this.postConfigActions) {
            // TODO: catch exceptions and continue?  Currently propagates runtime exceptions and will fail startup.
            action.run();
        }
    }

    public boolean isConfigured() {
        return this.configured.get();
    }

    private void ensureAccessControl(Session session) throws RepositoryException {
        if (!session.getRootNode().hasNode(SecurityPaths.SECURITY.toString())) {
            session.getRootNode().addNode(SecurityPaths.SECURITY.toString(), "tba:securityFolder");
        }

        Node prototypesNode = session.getRootNode().getNode(SecurityPaths.PROTOTYPES.toString());

        // Uncommenting below will remove all access control action configuration (DEV ONLY.)
        // TODO a proper migration should be implemented to in case the action hierarchy
        // has changed and the currently permitted actions need to be updated.
//        for (Node protoNode : JcrUtil.getNodesOfType(prototypesNode, "tba:allowedActions")) {
//            for (Node actionsNode : JcrUtil.getNodesOfType(protoNode, "tba:allowableAction")) {
//                actionsNode.remove();
//            }
//            
//            String modulePath = SecurityPaths.moduleActionPath(protoNode.getName()).toString();
//            
//            if (session.getRootNode().hasNode(modulePath)) {
//                Node moduleNode = session.getRootNode().getNode(modulePath);
//                
//                for (Node actionsNode : JcrUtil.getNodesOfType(moduleNode, "tba:allowableAction")) {
//                    actionsNode.remove();
//                }
//            }
//        }

        JcrAccessControlUtil.addPermissions(prototypesNode, new ModeShapeAdminPrincipal(), Privilege.JCR_ALL);
        JcrAccessControlUtil.addPermissions(prototypesNode, AdminCredentials.getPrincipal(), Privilege.JCR_ALL);
        JcrAccessControlUtil.addPermissions(prototypesNode, SimplePrincipal.EVERYONE, Privilege.JCR_READ);
    }


    protected void ensureTypes(Session session) throws RepositoryException {
        Node typesNode = session.getRootNode().getNode(ExtensionsConstants.TYPES);
        NodeTypeManager typeMgr = (NodeTypeManager) session.getWorkspace().getNodeTypeManager();
        NodeTypeIterator typeItr = typeMgr.getPrimaryNodeTypes();
        NodeType extensionsType = typeMgr.getNodeType(ExtensionsConstants.EXTENSIBLE_ENTITY_TYPE);

        while (typeItr.hasNext()) {
            NodeType type = (NodeType) typeItr.next();

            if (type.isNodeType(ExtensionsConstants.EXTENSIBLE_ENTITY_TYPE) &&
                !type.equals(extensionsType) &&
                !typesNode.hasNode(type.getName())) {
                Node descrNode = typesNode.addNode(type.getName(), ExtensionsConstants.TYPE_DESCRIPTOR_TYPE);

                descrNode.setProperty("jcr:title", simpleName(type.getName()));
                descrNode.setProperty("jcr:description", "");

                PropertyDefinition[] defs = type.getPropertyDefinitions();

                for (PropertyDefinition def : defs) {
                    String fieldName = def.getName();
                    String prefix = namePrefix(fieldName);

                    if (!ExtensionsConstants.STD_PREFIXES.contains(prefix) && !descrNode.hasNode(fieldName)) {
                        Node propNode = descrNode.addNode(def.getName(), ExtensionsConstants.FIELD_DESCRIPTOR_TYPE);
                        propNode.setProperty("jcr:title", def.getName().replace("^.*:", ""));
                        propNode.setProperty("jcr:description", "");
                    }
                }
            }
        }

        NodeIterator nodeItr = typesNode.getNodes();

        while (nodeItr.hasNext()) {
            Node typeNode = (Node) nodeItr.next();

            if (!typeMgr.hasNodeType(typeNode.getName())) {
                typeNode.remove();
            }
        }
    }

    protected void ensureLayout(Session session) throws RepositoryException {
        if (!session.getRootNode().hasNode("metadata")) {
            session.getRootNode().addNode("metadata", "tba:metadataFolder");
        }

        if (!session.getRootNode().hasNode("users")) {
            session.getRootNode().addNode("users", "tba:usersFolder");
        }

        if (!session.getRootNode().hasNode("groups")) {
            session.getRootNode().addNode("groups", "tba:groupsFolder");
        }

        // TODO Temporary to cleanup schemas which had the category folder auto-created.
        if (session.getRootNode().hasNode("metadata/feeds/category")) {
            session.getRootNode().getNode("metadata/feeds/category").remove();
        }

        if (!session.getRootNode().hasNode("metadata/hadoopSecurityGroups")) {
            session.getRootNode().getNode("metadata").addNode("hadoopSecurityGroups");
        }

        if (!session.getRootNode().hasNode("metadata/datasourceDefinitions")) {
            session.getRootNode().addNode("metadata", "tba:datasourceDefinitionsFolder");
        }
        if (!session.getRootNode().hasNode("metadata/datasources/derived")) {

            if (!session.getRootNode().hasNode("metadata/datasources")) {
                session.getRootNode().addNode("metadata", "datasources");
            }
            session.getRootNode().getNode("metadata/datasources").addNode("derived");
        }

        if (!session.getRootNode().hasNode("metadata/security")) {
            session.getRootNode().addNode("metadata/security", "tba:securityFolder");
        }
        if (!session.getRootNode().hasNode("metadata/security/prototypes")) {
            session.getRootNode().addNode("metadata/security/prototypes", "tba:prototypesFolder");
        }

        //ensure the datasources exist in prototypes
        if (!session.getRootNode().hasNode("metadata/security/prototypes/datasource")) {
            session.getRootNode().addNode("metadata/security/prototypes/datasource", "tba:allowedActions");
        }

        if (!session.getRootNode().hasNode("metadata/security/roles")) {
            session.getRootNode().addNode("metadata/security/roles", "tba:rolesFolder");
        }

        //ensure the role paths exist for the entities
        for(String entity : SecurityRole.ENTITIES) {
            String entityPath = "metadata/security/roles/"+entity;
            if (!session.getRootNode().hasNode(entityPath)) {
                session.getRootNode().addNode(entityPath, "tba:rolesFolder");
            }
        }
    }

    private String namePrefix(String name) {
        Matcher m = ExtensionsConstants.NAME_PATTERN.matcher(name);

        if (m.matches()) {
            return m.group(1);
        } else {
            return null;
        }
    }

    private String simpleName(String name) {
        Matcher m = ExtensionsConstants.NAME_PATTERN.matcher(name);

        if (m.matches()) {
            return m.group(2);
        } else {
            return null;
        }
    }

}
