/*
 * Copyright 2016 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.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mc4j.ems.connection.EmsConnection;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.attribute.EmsAttribute;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.configuration.definition.PropertyDefinition;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.inventory.ResourceDiscoveryContext;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.plugins.jmx.JMXComponent;
import org.rhq.plugins.jmx.MBeanResourceDiscoveryComponent;
import org.rhq.plugins.jmx.util.ObjectNameQueryUtility;

/**
 * Adds support to {@link org.rhq.plugins.jmx.MBeanResourceDiscoveryComponent} to allow mapping
 * bean attributes to resource properties.
 * <p/>
 * Also adds support for using parent resource properties in nameTemplate and descriptionTemplate.
 * <p/>
 * Also allows skipping unknown properties in objectName, specifically added for CXF.
 *
 * @author dbokde
 */
public class FuseMBeanDiscoveryComponent<T extends JMXComponent<?>> extends MBeanResourceDiscoveryComponent<T> {

    private final Log log = LogFactory.getLog(getClass());

    private static final Pattern PROPERTY_NAME_PATTERN = Pattern.compile("\\{([^\\{\\},=:\\*\\?]+)\\}");
    private static final Pattern OBJECT_NAME_PROPERTY_PATTERN = Pattern.compile("\\%([^\\%,=:\\*\\?]+)\\%");
    private static final String OPTIONAL_PROPERTY_DEFAULT_VALUE = "-";

    protected static final int MAX_LENGTH = 500;

    private static final String BEAN_PROPERTIES = "beanProperties";
    private static final String SKIP_UNKNOWN_PROPS_PROPERTY = "skipUnknownProps";
    public static final String STATS_OBJECT_NAME_PROPERTY = "statsObjectName";
    protected ResourceDiscoveryContext<T> discoveryContext;

    @Override
    public Set<DiscoveredResourceDetails> discoverResources(ResourceDiscoveryContext<T> context) {

        this.discoveryContext = context;

        // call super to get resources, skip unknown props by default if not set in resource plugin config
        Set<DiscoveredResourceDetails> detailsSet = super.discoverResources(context,
            Boolean.parseBoolean(context.getDefaultPluginConfiguration().getSimpleValue(SKIP_UNKNOWN_PROPS_PROPERTY, "true")));

        // get the beanProperties property group
        final List<PropertyDefinition> beanProperties = context.getResourceType().getPluginConfigurationDefinition().
            getPropertiesInGroup(BEAN_PROPERTIES);

        // now substitute any parent resource plugin properties and MBean properties in name and description
        final Set<DiscoveredResourceDetails> resultSet = new HashSet<DiscoveredResourceDetails>();
        for (DiscoveredResourceDetails details : detailsSet) {

            // check if there are any bean beanProperties to set in the first place
            if (!beanProperties.isEmpty()) {
                if (!setBeanProperties(context, details, beanProperties)) {
                    // bean is ignored if required bean beanProperties are not found
                    // so this acts like a second filter, in addition to the objectName args
                    continue;
                }
            }

            final ResourceContext<?> parentContext = context.getParentResourceContext();
            // if it has a parent and we need to replace something in name or description
            if (parentContext != null &&
                (details.getResourceName().contains("{") || details.getResourceDescription().contains("}"))) {

                final Configuration parentConfiguration = parentContext.getPluginConfiguration();
                final Map<String, PropertySimple> simpleProperties = parentConfiguration.getSimpleProperties();

                setResourceNameAndDescription(details, simpleProperties.values());
            }

            processStatsObjectName(context, details);

            // pass along these details
            resultSet.add(details);
        }

        return resultSet;
    }

    // Substitutes parent and plugin properties in statsObjectName if present
    private void processStatsObjectName(ResourceDiscoveryContext discoveryContext, DiscoveredResourceDetails details) {
        final Configuration pluginConfiguration = details.getPluginConfiguration();
        final Configuration parentConfiguration = (discoveryContext.getParentResourceContext() != null) ?
            discoveryContext.getParentResourceContext().getPluginConfiguration() : null;
        String statsObjectName = pluginConfiguration.getSimpleValue(STATS_OBJECT_NAME_PROPERTY);
        if (statsObjectName != null) {
            // get stats object name and substitute resource and parent properties
            statsObjectName = substituteConfigProperties(statsObjectName, pluginConfiguration, false);
            if (parentConfiguration != null) {
                statsObjectName = substituteConfigProperties(statsObjectName, parentConfiguration, true);
            }
            pluginConfiguration.setSimpleValue(STATS_OBJECT_NAME_PROPERTY, statsObjectName);
        }
    }

    private String substituteConfigProperties(String objectName, Configuration configuration, boolean isParent) {

        final Pattern p;
        if (isParent) {
            p = PROPERTY_NAME_PATTERN;
        } else {
            p = OBJECT_NAME_PROPERTY_PATTERN;
        }

        StringBuffer buffer = new StringBuffer();
        final Matcher m = p.matcher(objectName);
        while (m.find()) {
            String name = m.group(1);
            m.appendReplacement(buffer, configuration.getSimpleValue(name));
        }
        m.appendTail(buffer);
        return buffer.toString();
    }

    private boolean setBeanProperties(ResourceDiscoveryContext<T> context, DiscoveredResourceDetails details, List<PropertyDefinition> beanProperties) {
        // get all MBean properties from details, would this cause a reconnect???
        String beanName = details.getPluginConfiguration().getSimpleValue(PROPERTY_OBJECT_NAME);
        EmsBean bean = context.getParentResourceComponent().getEmsConnection().getBean(beanName);

        final List<PropertySimple> newProperties = new ArrayList<PropertySimple>(beanProperties.size());
        for (PropertyDefinition property : beanProperties) {
            String name = property.getName();
            EmsAttribute attribute = bean.getAttribute(name);

            if (attribute == null) {
                if (property.isRequired()) {
                    // missing property, ignore this bean resource
                    log.warn(String.format("Required Property %s is missing in Bean %s, ignoring Bean for resource type %s",
                        name, beanName, details.getResourceType().getName()));
                    return false;
                }
                // set default value for optional property
                newProperties.add(new PropertySimple(name, OPTIONAL_PROPERTY_DEFAULT_VALUE));
            } else {
                final String value = attribute.getValue().toString();
                // set the property as a simple String value in plugin configuration
                details.getPluginConfiguration().setSimpleValue(name, value);

                // collect properties for updating resource name and description
                newProperties.add(new PropertySimple(name, value));
            }
        }

        // update name and description using discovered properties
        setResourceNameAndDescription(details, newProperties);

        return true;
    }

    private void setResourceNameAndDescription(DiscoveredResourceDetails details,
                                               Collection<PropertySimple> simpleProperties) {

        String resourceName = details.getResourceName();
        String description = details.getResourceDescription();

        final boolean nameHasTemplate = resourceName.contains("{");
        final boolean descriptionHasTemplate = description.contains("}");
        if (nameHasTemplate || descriptionHasTemplate) {
            Map<String, String> variableValues = convertToMap(simpleProperties);
            if (nameHasTemplate) {
                resourceName = formatMessage(resourceName, variableValues);
            }
            if (descriptionHasTemplate) {
                description = formatMessage(description, variableValues);
            }

            details.setResourceName(resourceName);
            details.setResourceDescription(description);
        }
    }

    private Map<String, String> convertToMap(Collection<PropertySimple> simpleProperties) {
        final HashMap<String, String> result = new HashMap<String, String>();
        for (PropertySimple property : simpleProperties) {
            result.put(property.getName(), property.getStringValue());
        }
        return result;
    }

    @Override
    public Set<DiscoveredResourceDetails> performDiscovery(Configuration pluginConfiguration,
                                                           JMXComponent parentResourceComponent,
                                                           ResourceType resourceType, boolean skipUnknownProps) {

        String objectNameQueryTemplateOrig = pluginConfiguration.getSimple(PROPERTY_OBJECT_NAME).getStringValue();

        log.debug("Discovering MBean resources with object name query template: " + objectNameQueryTemplateOrig);

        EmsConnection connection = parentResourceComponent.getEmsConnection();

        if (connection == null) {
            throw new NullPointerException("The parent resource component [" + parentResourceComponent
                + "] returned a null connection - cannot discover MBeans without a connection");
        }

        Set<DiscoveredResourceDetails> services = new HashSet<DiscoveredResourceDetails>();
        String templates[] = objectNameQueryTemplateOrig.split("\\|");
        for (String objectNameQueryTemplate : templates) {
            // Get the query template, replacing the parent key variables with the values from the parent configuration
            ObjectNameQueryUtility queryUtility = new ObjectNameQueryUtility(objectNameQueryTemplate,
                (this.discoveryContext != null) ? this.discoveryContext.getParentResourceContext()
                    .getPluginConfiguration() : null);

            List<EmsBean> beans = connection.queryBeans(queryUtility.getTranslatedQuery());
            if (log.isDebugEnabled()) {
                log.debug("Found [" + beans.size() + "] mbeans for query [" + queryUtility.getTranslatedQuery() + "].");
            }
            for (EmsBean bean : beans) {
                if (queryUtility.setMatchedKeyValues(bean.getBeanName().getKeyProperties())) {
                    // Only use beans that have all the properties we've made variables of

                    // Don't match beans that have unexpected properties
                    if (skipUnknownProps
                        && queryUtility.isContainsExtraKeyProperties(bean.getBeanName().getKeyProperties().keySet())) {
                        continue;
                    }

                    String resourceKey = bean.getBeanName().getCanonicalName(); // The detected object name

                    String nameTemplate = (pluginConfiguration.getSimple(PROPERTY_NAME_TEMPLATE) != null) ? pluginConfiguration
                        .getSimple(PROPERTY_NAME_TEMPLATE).getStringValue() : null;

                    String descriptionTemplate = (pluginConfiguration.getSimple(PROPERTY_DESCRIPTION_TEMPLATE) != null) ? pluginConfiguration
                        .getSimple(PROPERTY_DESCRIPTION_TEMPLATE).getStringValue() : null;

                    String name = resourceKey;

                    if (nameTemplate != null) {
                        name = formatMessage(nameTemplate, queryUtility.getVariableValues());
                    }

                    String description = null;
                    if (descriptionTemplate != null) {
                        description = formatMessage(descriptionTemplate, queryUtility.getVariableValues());
                    }

                    if (resourceKey.length() > MAX_LENGTH) {
                        resourceKey = new MessageDigestGenerator(MessageDigestGenerator.SHA_256).calcDigestString(
                            resourceKey);
                    }
                    DiscoveredResourceDetails service = new DiscoveredResourceDetails(resourceType, resourceKey,
                        name, "", description, null, null);
                    Configuration config = service.getPluginConfiguration();
                    config.put(new PropertySimple(PROPERTY_OBJECT_NAME, bean.getBeanName().toString()));

                    Map<String, String> mappedVariableValues = queryUtility.getVariableValues();
                    for (String key : mappedVariableValues.keySet()) {
                        config.put(new PropertySimple(key, mappedVariableValues.get(key)));
                    }

                    services.add(service);

                    // Clear out the variables for the next bean detected
                    queryUtility.resetVariables();
                }
            }

            if (log.isDebugEnabled()) {
                log.debug("[" + services.size() + "] services have been added");
            }

        }

        return services;
    }

    protected static String formatMessage(String messageTemplate, Map<String, String> variableValues) {

        StringBuffer result = new StringBuffer();

        // collect keys and values to determine value size limit if needed
        final Map<String, String> replaceMap = new HashMap<String, String>();

        final Matcher matcher = PROPERTY_NAME_PATTERN.matcher(messageTemplate);
        int count = 0;
        while (matcher.find()) {
            final String key = matcher.group(1);
            final String value = variableValues.get(key);

            if (value != null) {
                replaceMap.put(key, value);
                matcher.appendReplacement(result, Matcher.quoteReplacement(value));
            } else {
                matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group()));
            }

            count++;
        }
        matcher.appendTail(result);

        // check if the result exceeds MAX_LENGTH for formatted properties
        if (!replaceMap.isEmpty() && result.length() > MAX_LENGTH) {
            // sort values according to size
            final SortedSet<String> values = new TreeSet<String>(new Comparator<String>() {
                public int compare(String o1, String o2) {
                    return o1.length() - o2.length();
                }
            });
            values.addAll(replaceMap.values());

            // find total value characters allowed
            int available = MAX_LENGTH - PROPERTY_NAME_PATTERN.matcher(messageTemplate).replaceAll("").length();

            // fit values from small to large in the allowed size to determine the maximum average
            int averageLength = available / count;
            for (String value : values) {
                final int length = value.length();
                if (length > averageLength) {
                    break;
                }
                available -= length;
                count--;
                averageLength = available / count;
            }

            // replace values
            matcher.reset();
            result.delete(0, result.length());
            while (matcher.find()) {
                String value = replaceMap.get(matcher.group(1));
                if (value != null && value.length() > averageLength) {
                    value = value.substring(0, averageLength);
                }
                matcher.appendReplacement(result, value != null ? value : matcher.group());
            }
            matcher.appendTail(result);
        }

        return result.toString();
    }
}
