/*
 * Copyright 2013 Red Hat, Inc.
 *
 * Red Hat 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.jbosson.plugins.fuse;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyList;
import org.rhq.core.domain.configuration.PropertyMap;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.criteria.MeasurementDataTraitCriteria;
import org.rhq.core.domain.criteria.ResourceCriteria;
import org.rhq.core.domain.criteria.ResourceGroupDefinitionCriteria;
import org.rhq.core.domain.measurement.MeasurementDataTrait;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceCategory;
import org.rhq.core.domain.resource.group.GroupDefinition;
import org.rhq.core.domain.util.PageList;
import org.rhq.enterprise.server.auth.SubjectManagerLocal;
import org.rhq.enterprise.server.measurement.MeasurementDataManagerLocal;
import org.rhq.enterprise.server.plugin.pc.ScheduledJobInvocationContext;
import org.rhq.enterprise.server.plugin.pc.ServerPluginComponent;
import org.rhq.enterprise.server.plugin.pc.ServerPluginContext;
import org.rhq.enterprise.server.resource.ResourceManagerLocal;
import org.rhq.enterprise.server.resource.group.definition.GroupDefinitionManagerLocal;
import org.rhq.enterprise.server.resource.group.definition.exception.GroupDefinitionAlreadyExistsException;
import org.rhq.enterprise.server.resource.group.definition.exception.GroupDefinitionCreateException;
import org.rhq.enterprise.server.util.LookupUtil;

/**
 * Server plugin to create and manage DynaGroups for Fabric Profiles.
 * This plugin uses metrics exposed by Fabric Containers to dynamically create DynaGroups.
 *
 * @author dbokde
 */
public class FabricProfileGroupsComponent implements ServerPluginComponent {

    private final Log log = LogFactory.getLog(FabricProfileGroupsComponent.class);

    private static final String CONTAINER_VERSION_TRAIT = "container.version";
    private static final String FABRIC_REGISTRIES_PROPERTY = "fabricRegistries";
    private static final String RHQ_USER_PROPERTY = "rhq.user";
    private static final String RHQ_PASSWORD_PROPERTY = "rhq.password";

    private static final String RECALCULATION_INTERVAL_PROPERTY = "groups.recalculationInterval";
    private static final String PROFILES_PROFILE_GROUPS = "profiles.profileGroups";
    private static final String PROFILES_PARENT_GROUPS = "profiles.parentGroups";
    private static final String REGISTRY_PROPERTIES_MAP = "registryProperties";

    private static final String FABRIC_CONTAINER_TYPE = "Fabric Container";
    private static final String JBOSS_FUSE_CONTAINER_TYPE = "JBoss Fuse Container";
    private static final String DEFAULT_REGISTRY_URL = "*";

    private static final String ZOOKEEPER_URL_PROPERTY = "zookeeper.url";
    private static final String PROFILES_TRAIT = "profiles";
    private static final String PARENT_PROFILES_TRAIT = "parentProfiles";
    private static final String MQ_CLUSTERS_TRAIT = "mqClusters";

    private static final String DEFAULT_PROFILE_INCLUDES = ".*";
    private static final String DEFAULT_PROFILE_EXCLUDES = "fabric-ensemble.* default karaf mq-base";
    private static final String PROFILES_EXCLUDE = "profiles.exclude";
    private static final String PROFILES_INCLUDE = "profiles.include";

    private static final String DEFAULT_ENABLE_PROFILE_GROUPS = "true";
    private static final String DEFAULT_DISABLE_PARENT_PROFILES = "false";

    private ServerPluginContext context;
    private String userName;
    private String userPassword;

    private final Map<String, PropertyMap> registryPropertiesMap = new HashMap<String, PropertyMap>();
    private long recalculationInterval;

    public void initialize(ServerPluginContext context) throws Exception {

        // remember plugin context
        this.context = context;

        // validate plugin configuration
        final Configuration pluginConfiguration = context.getPluginConfiguration();
        this.userName = pluginConfiguration.getSimpleValue(RHQ_USER_PROPERTY);
        if (userName == null) {
            throw new IllegalArgumentException(
                "Fabric profile groups server plugin is misconfigured - User Name property is not set");
        }

        this.userPassword = pluginConfiguration.getSimpleValue(RHQ_PASSWORD_PROPERTY);
        if (userPassword == null) {
            throw new IllegalArgumentException(
                "Fabric profile groups server plugin is misconfigured - User Password property is not set");
        }

        String recalculationIntervalStr = pluginConfiguration.getSimpleValue(RECALCULATION_INTERVAL_PROPERTY);
        if (recalculationIntervalStr == null) {
            throw new IllegalArgumentException(
                "Fabric profile groups server plugin is misconfigured - Recalculation Interval property is not set");
        }
        this.recalculationInterval = Long.parseLong(recalculationIntervalStr);

        // validate user by creating a transient session
        final SubjectManagerLocal subjectManager = LookupUtil.getSubjectManager();
        final Subject subject = subjectManager.login(userName, userPassword);
        subjectManager.logout(subject);

        // also validate jobs configuration
        parseFabricRegistries();

        log.info("The Fabric profile groups server plugin has been initialized!!!");
    }

    public void start() {
        log.info("The Fabric profile groups server plugin has started!!!");
    }

    public void stop() {
        log.info("The Fabric profile groups server plugin has stopped!!!");
    }

    public void shutdown() {
        registryPropertiesMap.clear();

        log.info("The Fabric profile groups server plugin has been shut down!!!");
    }

    public void createProfileGroups(ScheduledJobInvocationContext invocation) throws Exception {

        log.info("The Fabric profile groups server plugin scheduled job [createProfileGroups] has triggered!!!");

        // login, this should be done everything this job is run
        // because the user may have been removed or the password changed between invocations
        final SubjectManagerLocal subjectManager = LookupUtil.getSubjectManager();
        final Subject subject = subjectManager.login(userName, userPassword);

        try {

            // get Fabric Containers
            final List<Resource> containers = new ArrayList<Resource>();

            final ResourceCriteria criteria = new ResourceCriteria();
            criteria.addFilterResourceTypeName(FABRIC_CONTAINER_TYPE);
            criteria.addFilterResourceCategories(ResourceCategory.SERVER);
            criteria.addFilterParentResourceCategory(ResourceCategory.PLATFORM);
            criteria.fetchPluginConfiguration(true);

            final ResourceManagerLocal resourceManager = LookupUtil.getResourceManager();
            final PageList<Resource> fabricContainers = resourceManager.findResourcesByCriteria(subject, criteria);
            containers.addAll(fabricContainers);

            // now find JBoss Fuse Containers
            criteria.addFilterResourceTypeName(JBOSS_FUSE_CONTAINER_TYPE);
            final PageList<Resource> jbossFuseContainers = resourceManager.findResourcesByCriteria(subject, criteria);
            containers.addAll(jbossFuseContainers);

            // optimize profile and cluster handling by collecting metadata for all containers first
            // this ensures shared profiles, clusters, etc. are processed once
            final Set<FabricProfileInfo> profiles = new HashSet<FabricProfileInfo>();
            final Set<MQClusterInfo> mqClusters = new HashSet<MQClusterInfo>();

            // process containers
            if (!containers.isEmpty()) {

                // collect container metadata
                final MeasurementDataManagerLocal dataManager = LookupUtil.getMeasurementDataManager();
                for (Resource container : containers) {
                    // only process containers with ZK URLs
                    if (getZooKeeperUrl(container) != null) {
                        getContainerMetadata(container, subject, dataManager, profiles, mqClusters);
                    }
                }

                // process all profiles
                final GroupDefinitionManagerLocal groupDefinitionManager = LookupUtil.getGroupDefinitionManager();
                for (FabricProfileInfo profile : profiles) {
                    processContainerProfile(profile, subject, groupDefinitionManager);
                }

                // process all clusters
                for (MQClusterInfo mqCluster : mqClusters) {
                    processMqCluster(mqCluster, subject, groupDefinitionManager);
                }
            }

            log.info(String.format("Found %s Fabric Containers, %s Profiles, and %s MQ Clusters",
                containers.size(), profiles.size(), mqClusters.size()));

        } finally {
            subjectManager.logout(subject);
        }
    }

    // collect container profiles, parent profiles, and MQ clusters
    private void getContainerMetadata(Resource container, Subject subject, MeasurementDataManagerLocal dataManager,
                                      Set<FabricProfileInfo> profiles, Set<MQClusterInfo> mqClusters) {
        final String zooKeeperUrl = getZooKeeperUrl(container);
        PropertyMap registryProperties = getRegistryProperties(zooKeeperUrl);

        // are profile groups and parents allowed for this registry?
        final boolean allowProfileGroups = getProfileGroups(registryProperties);
        final boolean allowParentGroups = getParentProfiles(registryProperties);

        // create the profile exclude and include patterns
        final Pattern excludePattern = getProfilesExcludePattern(registryProperties);
        final Pattern includePattern = getProfilesIncludePattern(registryProperties);

        // are profile groups allowed for this registry?
        if (allowProfileGroups) {

            // get and process container traits
            final MeasurementDataTraitCriteria traitCriteria = new MeasurementDataTraitCriteria();
            traitCriteria.addFilterResourceId(container.getId());
            traitCriteria.fetchSchedule(true);
            final List<MeasurementDataTrait> containerTraits =
                dataManager.findTraitsByCriteria(subject, traitCriteria);

            // traits may not have been collected yet, or it may be a unmanaged Container like vanilla ESB or MQ!!!
            if (!containerTraits.isEmpty()) {

                String profilesValue = null;
                String parentProfilesValue = null;
                String mqClusterValue = null;
                String containerVersion = null;

                for (MeasurementDataTrait trait : containerTraits) {
                    final String traitName = trait.getSchedule().getDefinition().getName();
                    if (PROFILES_TRAIT.equals(traitName)) {
                        profilesValue = trait.getValue();
                    } else if (PARENT_PROFILES_TRAIT.equals(traitName)) {
                        parentProfilesValue = trait.getValue();
                    } else if (MQ_CLUSTERS_TRAIT.equals(traitName)) {
                        mqClusterValue = trait.getValue();
                    } else if (CONTAINER_VERSION_TRAIT.equals(traitName)) {
                        containerVersion = trait.getValue();
                    }
                }

                // create parent map first, for lookup later
                Map<String, String[]> parentProfilesMap = null;
                if (allowParentGroups && parentProfilesValue != null && !parentProfilesValue.isEmpty()) {
                    // parent profile names are of the form
                    // [child1{version}]:[parent1{version}]:[parent2{version}],[child1{version}]:...
                    // to support DynaGroups clause 'groupby ...trait[parentProfiles].contains = :[parentProfile1]
                    parentProfilesMap = getParentProfiles(parentProfilesValue);
                }
                if (profilesValue != null && !profilesValue.isEmpty()) {
                    // profile names are of the form [profile1{version}],[profile2{version}],...
                    // to support DynaGroups clause 'groupby ...trait[profiles].contains = [profile1{version}]'
                    String[] profileNames = profilesValue.split(",");
                    for (String profileName : profileNames) {

                        if (profileGroupAllowed(profileName, includePattern, excludePattern)) {
                            // collect this profile for Group creation
                            profiles.add(new FabricProfileInfo(container.getResourceType().getName(),
                                container.getResourceType().getPlugin(), profileName, containerVersion,
                                false, zooKeeperUrl));

                            if (allowParentGroups && parentProfilesMap != null) {
                                processParentProfiles(container, profileName, containerVersion,
                                    parentProfilesMap, excludePattern, includePattern, profiles);
                            }
                        }

                    }
                }

                if (mqClusterValue != null && !mqClusterValue.isEmpty()) {
                    // cluster names are of the form [cluster1],[cluster2],...
                    // to support DynaGroups clause 'groupby ...trait[mqClusters].contains = [cluster1]'
                    String[] clusters = mqClusterValue.split(",");
                    for (String cluster : clusters) {
                        mqClusters.add(new MQClusterInfo(container.getResourceType().getName(),
                            container.getResourceType().getPlugin(), cluster, zooKeeperUrl));
                    }
                }

            }

        }

    }

    private String getZooKeeperUrl(Resource container) {
        return container.getPluginConfiguration().getSimpleValue(ZOOKEEPER_URL_PROPERTY);
    }

    private PropertyMap getRegistryProperties(String zookeeperUrl) {
        PropertyMap registryProperties = registryPropertiesMap.get(zookeeperUrl);
        if (registryProperties == null) {
            registryProperties = registryPropertiesMap.get(DEFAULT_REGISTRY_URL);
        }
        return registryProperties;
    }

    // recursively process parents of the given profile
    private void processParentProfiles(Resource container, String profileName, String containerVersion,
                                       Map<String, String[]> parentProfilesMap,
                                       Pattern excludePattern, Pattern includePattern,
                                       Set<FabricProfileInfo> profiles) {

        final String[] parentProfiles = parentProfilesMap.get(profileName);
        if (parentProfiles != null) {
            for (String parent : parentProfiles) {

                if (profileGroupAllowed(parent, includePattern, excludePattern)) {
                    // process parent profile like a directly referenced container profile
                    profiles.add(new FabricProfileInfo(container.getResourceType().getName(),
                        container.getResourceType().getPlugin(), parent, containerVersion,
                        true, getZooKeeperUrl(container)));

                    // recursively process parents of this parent
                    processParentProfiles(container, parent, containerVersion,
                        parentProfilesMap, excludePattern, includePattern, profiles);
                }
            }
            // TODO create Mixed Groups in the future when RHQ supports nested Groups
        }
    }

    private boolean profileGroupAllowed(String profile, Pattern includePattern, Pattern excludePattern) {
        // check if the profile name matches included profiles
        // if it does, double check to see it does NOT match excluded profiles
        final String profileName = profile.substring(1,profile.indexOf('{'));
        return includePattern.matcher(profileName).matches() && !excludePattern.matcher(profileName).matches();
    }

    private Pattern getProfilesIncludePattern(PropertyMap registryProperties) {
        return Pattern.compile("(" + (registryProperties.getSimpleValue(PROFILES_INCLUDE,
                DEFAULT_PROFILE_INCLUDES)).replace(" ", ")|(") + ")");
    }

    private Pattern getProfilesExcludePattern(PropertyMap registryProperties) {
        return Pattern.compile("(" + (registryProperties.getSimpleValue(PROFILES_EXCLUDE,
            DEFAULT_PROFILE_EXCLUDES)).replace(" ", ")|(") + ")");
    }

    private boolean getParentProfiles(PropertyMap registryProperties) {
        // parent groups are disabled by default
        return Boolean.parseBoolean(registryProperties.getSimpleValue(PROFILES_PARENT_GROUPS,
            DEFAULT_DISABLE_PARENT_PROFILES));
    }

    private boolean getProfileGroups(PropertyMap registryProperties) {
        return Boolean.parseBoolean(registryProperties.getSimpleValue(PROFILES_PROFILE_GROUPS,
            DEFAULT_ENABLE_PROFILE_GROUPS));
    }

    private void processContainerProfile(FabricProfileInfo profileInfo,
                                         Subject subject, GroupDefinitionManagerLocal groupDefinitionManager)
        throws GroupDefinitionCreateException, GroupDefinitionAlreadyExistsException {

        // get container plugin and type
        final String plugin = profileInfo.plugin;
        final String containerType = profileInfo.containerType;

        // profile name is between starting '[' and '{' before version
        final String profile = profileInfo.profileName;
        final String containerVersion = profileInfo.containerVersion;

        final String profileName = profile.substring(1,profile.indexOf("{"));
        final String profileGroupPrefix = String.format("Fabric %s %s:%s (%s)",
            profileInfo.isParent ? "Parent Profile" : "Profile",
            profileName, containerVersion, profileInfo.zookeeperUrl);

        // get all DynaGroups for this profile
        Map<String, GroupDefinition> groupDefinitionMap = getProfileGroupDefinitions(subject,
            groupDefinitionManager, profileGroupPrefix);

        // which trait to use for DynaGroup expression
        final String traitName =  profileInfo.isParent ? PARENT_PROFILES_TRAIT : PROFILES_TRAIT;

        // process compatible groups first
        // 1. Compatible group of all platforms
        final String platformGroupName = profileGroupPrefix + " Platform";
        if (!groupDefinitionMap.containsKey(platformGroupName)) {
            // create the platforms group
            createGroupDefinition(subject, groupDefinitionManager, platformGroupName,
                "Platforms for Profile " + profileName + ":" + containerVersion,
                "resource.child.type.plugin = " + plugin + "\n" +
                    "resource.child.type.name = " + containerType + "\n" +
                    "resource.child.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + profileInfo.zookeeperUrl + "\n" +
                    "resource.child.trait[" + traitName + "].contains = " + profile + "\n" +
                    "resource.type.plugin = Platforms\n" +
                    "groupby resource.type.name");
        }

        // 2. Compatible group of all containers
        final String containerGroupName = profileGroupPrefix + " Containers";
        if (!groupDefinitionMap.containsKey(containerGroupName)) {
            // create the servers group
            createGroupDefinition(subject, groupDefinitionManager, containerGroupName,
                "Containers for Profile " + profileName + ":" + containerVersion,
                "resource.type.plugin = " + plugin + "\n" +
                    "resource.type.name = " + containerType + "\n" +
                    "resource.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + profileInfo.zookeeperUrl + "\n" +
                    "resource.trait[" + traitName + "].contains = " + profile + "\n");
        }

        // 3. Compatible group of child services by plugin type and service type
        final String serviceGroupName = profileGroupPrefix + " Child Service";
        if (!groupDefinitionMap.containsKey(serviceGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, serviceGroupName,
                "Child Service for Profile " + profileName + ":" + containerVersion,
                "resource.type.category = service\n" +
                    "resource.parent.type.plugin = " + plugin + "\n" +
                    "resource.parent.type.name = " + containerType + "\n" +
                    "resource.parent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + profileInfo.zookeeperUrl + "\n" +
                    "resource.parent.trait[" + traitName + "].contains = " + profile + "\n" +
                    "groupby resource.type.plugin\n" +
                    "groupby resource.type.name");
        }

        // 4. Compatible group of grandchildren services by plugin type and service type
        final String grandchildServiceGroupName = profileGroupPrefix + " Grandchild Service";
        if (!groupDefinitionMap.containsKey(grandchildServiceGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, grandchildServiceGroupName,
                "Grandchild Service for Profile " + profileName + ":" + containerVersion,
                "resource.type.category = service\n" +
                    "resource.grandparent.type.plugin = " + plugin + "\n" +
                    "resource.grandparent.type.name = " + containerType + "\n" +
                    "resource.grandparent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + profileInfo.zookeeperUrl + "\n" +
                    "resource.grandparent.trait[" + traitName + "].contains = " + profile + "\n" +
                    "groupby resource.type.plugin\n" +
                    "groupby resource.type.name");
        }

        // Process mixed groups next
        // 5. Mixed group of all child services by plugin type
        final String allServicesGroupName = profileGroupPrefix + " Children Services";
        if (!groupDefinitionMap.containsKey(allServicesGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, allServicesGroupName,
                "Children Services for Profile " + profileName + ":" + containerVersion,
                "resource.type.category = service\n" +
                    "resource.parent.type.plugin = " + plugin + "\n" +
                    "resource.parent.type.name = " + containerType + "\n" +
                    "resource.parent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + profileInfo.zookeeperUrl + "\n" +
                    "resource.parent.trait[" + traitName + "].contains = " + profile + "\n" +
                    "groupby resource.type.plugin");
        }

        // 6. Mixed group of all grandchildren services by plugin type
        final String allGrandchildrenServicesGroupName = profileGroupPrefix + " Grandchildren Services";
        if (!groupDefinitionMap.containsKey(allGrandchildrenServicesGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, allGrandchildrenServicesGroupName,
                "Grandchildren Services for Profile " + profileName + ":" + containerVersion,
                "resource.type.category = service\n" +
                    "resource.grandparent.type.plugin = " + plugin + "\n" +
                    "resource.grandparent.type.name = " + containerType + "\n" +
                    "resource.grandparent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + profileInfo.zookeeperUrl + "\n" +
                    "resource.grandparent.trait[" + traitName + "].contains = " + profile + "\n" +
                    "groupby resource.type.plugin");
        }

        // TODO create nested group for this profile of all Platforms, Containers and Services
    }

    private void processMqCluster(MQClusterInfo clusterInfo,
                                  Subject subject, GroupDefinitionManagerLocal groupDefinitionManager)
        throws GroupDefinitionCreateException, GroupDefinitionAlreadyExistsException {

        // get cluster plugin and type
        final String plugin = clusterInfo.plugin;
        final String containerType = clusterInfo.containerType;

        // strip '[' and ']'
        final String clusterName = clusterInfo.name.substring(1,clusterInfo.name.length() - 1);
        final String clusterGroupPrefix = String.format("Fuse MQ Cluster %s (%s)",
            clusterName, clusterInfo.zookeeperUrl);

        final Map<String, GroupDefinition> groupDefinitionMap = getProfileGroupDefinitions(subject, 
            groupDefinitionManager, clusterGroupPrefix);
        
        // 1. Compatible group for cluster children services by type
        final String serviceGroupName = clusterGroupPrefix + " Child Service";
        if (!groupDefinitionMap.containsKey(serviceGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, serviceGroupName,
                "Child Service for Fuse MQ Cluster " + clusterName,
                // ActiveMQ services only
                "resource.type.plugin = ActiveMQ\n" +
                    "resource.parent.type.plugin = " + plugin + "\n" +
                    "resource.parent.type.name = " + containerType + "\n" +
                    "resource.parent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + clusterInfo.zookeeperUrl + "\n" +
                    "resource.parent.trait[" + MQ_CLUSTERS_TRAIT + "].contains = " + clusterInfo.name + "\n" +
                    "groupby resource.type.name");
        }

        // 2. Compatible group for cluster grandchildren services by type
        final String grandchildServiceGroupName = clusterGroupPrefix + " Grandchild Service";
        if (!groupDefinitionMap.containsKey(grandchildServiceGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, grandchildServiceGroupName,
                "Grandchild Service for Fuse MQ Cluster " + clusterName,
                // ActiveMQ services only
                "resource.type.plugin = ActiveMQ\n" +
                    "resource.grandparent.type.plugin = " + plugin + "\n" +
                    "resource.grandparent.type.name = " + containerType + "\n" +
                    "resource.grandparent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + clusterInfo.zookeeperUrl + "\n" +
                    "resource.grandparent.trait[" + MQ_CLUSTERS_TRAIT + "].contains = " + clusterInfo.name + "\n" +
                    "groupby resource.type.name");
        }

        // 3. Compatible group for Queues in cluster
        final String queueServiceGroupName = clusterGroupPrefix + " Queue Service";
        if (!groupDefinitionMap.containsKey(queueServiceGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, queueServiceGroupName,
                "Destination Service for Fuse MQ Cluster " + clusterName,
                // ActiveMQ Queue services only
                "resource.type.plugin = ActiveMQ\n" +
                    "resource.type.name = Queue\n" +
                    "resource.grandparent.type.plugin = " + plugin + "\n" +
                    "resource.grandparent.type.name = " + containerType + "\n" +
                    "resource.grandparent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + clusterInfo.zookeeperUrl + "\n" +
                    "resource.grandparent.trait[" + MQ_CLUSTERS_TRAIT + "].contains = " + clusterInfo.name + "\n" +
                    "groupby resource.name");
        }

        // 4. Compatible group for Topics in cluster
        final String topicServiceGroupName = clusterGroupPrefix + " Topic Service";
        if (!groupDefinitionMap.containsKey(topicServiceGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, topicServiceGroupName,
                "Destination Service for Fuse MQ Cluster " + clusterName,
                // ActiveMQ Topic services only
                "resource.type.plugin = ActiveMQ\n" +
                    "resource.type.name = Topic\n" +
                    "resource.grandparent.type.plugin = " + plugin + "\n" +
                    "resource.grandparent.type.name = " + containerType + "\n" +
                    "resource.grandparent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + clusterInfo.zookeeperUrl + "\n" +
                    "resource.grandparent.trait[" + MQ_CLUSTERS_TRAIT + "].contains = " + clusterInfo.name + "\n" +
                    "groupby resource.name");
        }

        // 5. Mixed group for all cluster child services
        final String allServicesGroupName = clusterGroupPrefix + " Children Services";
        if (!groupDefinitionMap.containsKey(allServicesGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, allServicesGroupName,
                "Children Services for Fuse MQ Cluster " + clusterName,
                // ActiveMQ services only
                "resource.type.plugin = ActiveMQ\n" +
                    "resource.parent.type.plugin = " + plugin + "\n" +
                    "resource.parent.type.name = " + containerType + "\n" +
                    "resource.parent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + clusterInfo.zookeeperUrl + "\n" +
                    "resource.parent.trait[" + MQ_CLUSTERS_TRAIT + "].contains = " + clusterInfo.name + "\n");
        }

        // 6. Mixed group for all cluster services
        final String allGrandchildrenServicesGroupName = clusterGroupPrefix + " Grandchildren Services";
        if (!groupDefinitionMap.containsKey(allGrandchildrenServicesGroupName)) {
            // create the service group
            createGroupDefinition(subject, groupDefinitionManager, allGrandchildrenServicesGroupName,
                "Grandchildren Services for Fuse MQ Cluster " + clusterName,
                // ActiveMQ services only
                "resource.type.plugin = ActiveMQ\n" +
                    "resource.grandparent.type.plugin = " + plugin + "\n" +
                    "resource.grandparent.type.name = " + containerType + "\n" +
                    "resource.grandparent.pluginConfiguration[" + ZOOKEEPER_URL_PROPERTY + "] = " + clusterInfo.zookeeperUrl + "\n" +
                    "resource.grandparent.trait[" + MQ_CLUSTERS_TRAIT + "].contains = " + clusterInfo.name + "\n");
        }

        // TODO create nested group for this Cluster of all Platforms, Containers and Services
    }

    private void createGroupDefinition(Subject subject, GroupDefinitionManagerLocal groupDefinitionManager,
                                       String platformGroupName, String description, String expression)
        throws GroupDefinitionAlreadyExistsException, GroupDefinitionCreateException {

        GroupDefinition platformGroup = new GroupDefinition();
        platformGroup.setName(platformGroupName);
        // make sure description doesn't exceed 100 characters
        description = "Auto-generated, " + description;
        if (description.length() > 100) {
            description = description.substring(0,97) + "...";
        }
        platformGroup.setDescription(description);
        // convert the recalculation interval to milliseconds
        platformGroup.setRecalculationInterval(TimeUnit.MINUTES.toMillis(this.recalculationInterval));
        platformGroup.setExpression(expression);

        groupDefinitionManager.createGroupDefinition(subject, platformGroup);

    }

    private Map<String, GroupDefinition> getProfileGroupDefinitions(Subject subject,
                                                                    GroupDefinitionManagerLocal groupDefinitionManager,
                                                                    String profileGroupPrefix) {

        final ResourceGroupDefinitionCriteria criteria = new ResourceGroupDefinitionCriteria();
        criteria.addFilterName(profileGroupPrefix);
        final PageList<GroupDefinition> groupDefinitions =
            groupDefinitionManager.findGroupDefinitionsByCriteria(subject, criteria);
        // lookup map
        Map<String, GroupDefinition> groupDefinitionMap = new HashMap<String, GroupDefinition>();
        for (GroupDefinition definition : groupDefinitions) {
            groupDefinitionMap.put(definition.getName(), definition);
        }
        return groupDefinitionMap;

    }

    private void parseFabricRegistries() throws Exception {

        final PropertyList propertyList = context.getPluginConfiguration().getList(FABRIC_REGISTRIES_PROPERTY);
        for (Property property : propertyList.getList()) {
            PropertyMap registryMap = (PropertyMap) property;
            final PropertySimple zookeeperUrl = registryMap.getSimple(ZOOKEEPER_URL_PROPERTY);
            if (zookeeperUrl == null || zookeeperUrl.getStringValue() == null ||
                zookeeperUrl.getStringValue().isEmpty()) {
                throw new IllegalArgumentException("Property Registry URL MUST have a non-empty value");
            }
            registryPropertiesMap.put(zookeeperUrl.getStringValue(), registryMap);
        }

        // TODO Apparently the properties are not validated correctly in the server,
        // for example the min attribute is not being checked, so the Map may be empty
        if (!registryPropertiesMap.containsKey(DEFAULT_REGISTRY_URL)) {
            PropertyMap defaults = new PropertyMap(REGISTRY_PROPERTIES_MAP);
            defaults.put(new PropertySimple(ZOOKEEPER_URL_PROPERTY, DEFAULT_REGISTRY_URL));
            defaults.put(new PropertySimple(PROFILES_PROFILE_GROUPS, DEFAULT_ENABLE_PROFILE_GROUPS));
            // parent groups are disabled to limit the number of DynaGroups
            defaults.put(new PropertySimple(PROFILES_PARENT_GROUPS, DEFAULT_DISABLE_PARENT_PROFILES));
            // add the defaults to the plugin configuration
            propertyList.add(defaults);

            // add entry to map
            registryPropertiesMap.put(DEFAULT_REGISTRY_URL, defaults);

            log.warn(
                "No default property with Registry URL * specified, using defaults: " + defaults);
        }
    }

    private Map<String, String[]> getParentProfiles(String parentProfilesValue) {

        final Map<String, String[]> parentMap = new HashMap<String, String[]>();
        // split into child:parent1:parent2:... lists
        for (String parentList : parentProfilesValue.split(",")) {
            // split child and parents
            final String[] profileParents = parentList.split(":");
            // add to parent map
            parentMap.put(profileParents[0], Arrays.copyOfRange(profileParents, 1, profileParents.length));
        }

        return Collections.unmodifiableMap(parentMap);
    }

    // represents container metadata for a Fabric Profile
    private class FabricProfileInfo {

        final String containerType;
        final String plugin;
        final String profileName;
        final String containerVersion;
        final boolean isParent;
        final String zookeeperUrl;

        public FabricProfileInfo(String containerType, String plugin, String profileName, String containerVersion,
                                 boolean isParent, String zookeeperUrl) {
            this.containerType = containerType;
            this.plugin = plugin;
            this.profileName = profileName;
            this.containerVersion = containerVersion;
            this.isParent = isParent;
            this.zookeeperUrl = zookeeperUrl;
        }

        @Override
        public int hashCode() {
            return (containerType + profileName + containerVersion).hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if(obj != null && (obj instanceof FabricProfileInfo)) {
                FabricProfileInfo info = (FabricProfileInfo) obj;
                return containerType.equals(info.containerType) &&
                    profileName.equals(info.profileName) &&
                    containerVersion.equals(info.containerVersion);
            }
            return false;
        }

    }

    // represents MQ Cluster used by a container
    private class MQClusterInfo {
        final String containerType;
        final String plugin;
        final String name;
        final String zookeeperUrl;

        public MQClusterInfo(String containerType, String plugin, String cluster, String zookeeperUrl) {
            this.containerType = containerType;
            this.plugin = plugin;
            this.name = cluster;
            this.zookeeperUrl = zookeeperUrl;
        }
    }

}
