/*
 * RHQ Management Platform
 * Copyright (C) 2005-2011 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package org.rhq.enterprise.server.sync.importers;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLStreamException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rhq.core.clientapi.agent.configuration.ConfigurationUtility;
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.configuration.definition.ConfigurationDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionList;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionMap;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple;
import org.rhq.core.domain.configuration.definition.PropertySimpleType;
import org.rhq.core.domain.measurement.MeasurementDefinition;
import org.rhq.core.domain.sync.entity.MetricTemplate;
import org.rhq.enterprise.server.measurement.MeasurementScheduleManagerLocal;
import org.rhq.enterprise.server.sync.ExportReader;
import org.rhq.enterprise.server.sync.validators.EntityValidator;
import org.rhq.enterprise.server.sync.validators.UniquenessValidator;
import org.rhq.enterprise.server.util.LookupUtil;

/**
 * 
 *
 * @author Lukas Krejci
 */
public class MetricTemplateImporter implements Importer<MeasurementDefinition, MetricTemplate> {

    private static final Log LOG = LogFactory.getLog(MetricTemplateImporter.class);

    private static final boolean UPDATE_SCHEDULES_DEFAULT = false;
    public static final String UPDATE_ALL_SCHEDULES_PROPERTY = "updateAllSchedules";
    public static final String METRIC_NAME_PROPERTY = "metricName";
    public static final String RESOURCE_TYPE_NAME_PROPERTY = "resourceTypeName";
    public static final String RESOURCE_TYPE_PLUGIN_PROPERTY = "resourceTypePlugin";
    public static final String UPDATE_SCHEDULES_PROPERTY = "updateSchedules";
    public static final String METRIC_UPDATE_OVERRIDES_PROPERTY = "metricUpdateOverrides";
    public static final String METRIC_UPDATE_OVERRIDE_PROPERTY = "metricUpdateOverride";

    private static final String IMPORT_NOTES_PROLOGUE = "The following metric templates were not imported because they do not correspond to any metric defined by the existing resource types:\n";
    
    private static class UpdateKey {
        public final long collectionInterval;
        public final boolean updateSchedules;
        public final boolean enable;

        public UpdateKey(long collectionInterval, boolean updateSchedules, boolean enable) {
            this.collectionInterval = collectionInterval;
            this.updateSchedules = updateSchedules;
            this.enable = enable;
        }

        @Override
        public int hashCode() {
            return (int) collectionInterval * (updateSchedules ? 31 : 1) * (enable ? 31 : 1);
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }

            if (!(other instanceof UpdateKey)) {
                return false;
            }

            UpdateKey o = (UpdateKey) other;

            return o.collectionInterval == collectionInterval && o.updateSchedules == updateSchedules
                && enable == o.enable;
        }
    }

    private static final Comparator<MetricTemplate> METRIC_TEMPLATE_COMPARATOR = new Comparator<MetricTemplate>() {
        
        @Override
        public int compare(MetricTemplate o1, MetricTemplate o2) {
            //sort by plugin, type name, and metric name in that order
            
            int ret = o1.getResourceTypePlugin().compareTo(o2.getResourceTypePlugin());
            if (ret != 0) {
                return ret;
            }
            
            ret = o1.getResourceTypeName().compareTo(o2.getResourceTypeName());
            if (ret != 0) {
                return ret;
            }
            
            return o1.getMetricName().compareTo(o2.getMetricName());
        }
    }; 
    
    private Subject subject;
    private EntityManager entityManager;
    private Map<UpdateKey, List<MeasurementDefinition>> definitionsByUpdateKey =
        new HashMap<UpdateKey, List<MeasurementDefinition>>();
    private Configuration importConfiguration;
    private Unmarshaller unmarshaller;
    private MeasurementScheduleManagerLocal measurementScheduleManager;
    private Set<MetricTemplate> unmatchedTemplates = new TreeSet<MetricTemplate>(METRIC_TEMPLATE_COMPARATOR);
    
    public MetricTemplateImporter(Subject subject, EntityManager entityManager) {
        this(subject, entityManager, LookupUtil.getMeasurementScheduleManager());
    }

    public MetricTemplateImporter(Subject subject, EntityManager entityManager,
        MeasurementScheduleManagerLocal measurementScheduleManager) {
        try {
            this.subject = subject;
            this.entityManager = entityManager;
            JAXBContext context = JAXBContext.newInstance(MetricTemplate.class);
            unmarshaller = context.createUnmarshaller();
        } catch (JAXBException e) {
            throw new IllegalStateException("Failed to initialize JAXB marshaller for MetricTemplate.", e);
        }
        this.measurementScheduleManager = measurementScheduleManager;
    }

    @Override
    public ConfigurationDefinition getImportConfigurationDefinition() {
        ConfigurationDefinition def = new ConfigurationDefinition("MetricTemplateImportConfiguration", null);
        PropertyDefinitionSimple updateAllSchedules =
            new PropertyDefinitionSimple(
                UPDATE_ALL_SCHEDULES_PROPERTY,
                "If set to true, all the metric templates will update all the existing schedules on corresponding resources.",
                true, PropertySimpleType.BOOLEAN);
        updateAllSchedules.setDefaultValue(Boolean.toString(UPDATE_SCHEDULES_DEFAULT));
        def.put(updateAllSchedules);

        PropertyDefinitionSimple metricName =
            new PropertyDefinitionSimple(METRIC_NAME_PROPERTY, "The name of the metric", true,
                PropertySimpleType.STRING);
        PropertyDefinitionSimple resourceTypeName =
            new PropertyDefinitionSimple(RESOURCE_TYPE_NAME_PROPERTY,
                "The name of the resource type defining the metric", true, PropertySimpleType.STRING);
        PropertyDefinitionSimple resourceTypePlugin =
            new PropertyDefinitionSimple(RESOURCE_TYPE_PLUGIN_PROPERTY,
                "The name of the plugin defining the resource type that defines the metric", true,
                PropertySimpleType.STRING);
        PropertyDefinitionSimple updateSchedules =
            new PropertyDefinitionSimple(UPDATE_SCHEDULES_PROPERTY,
                "Whether to update the schedules of this metric on existing resources", true,
                PropertySimpleType.BOOLEAN);

        PropertyDefinitionMap metricUpdateOverride =
            new PropertyDefinitionMap(METRIC_UPDATE_OVERRIDE_PROPERTY, null, true, metricName, resourceTypeName,
                resourceTypePlugin, updateSchedules);
        PropertyDefinitionList metricUpdateOverrides =
            new PropertyDefinitionList(METRIC_UPDATE_OVERRIDES_PROPERTY, "Per metric settings", false,
                metricUpdateOverride);

        def.put(metricUpdateOverrides);

        ConfigurationUtility.initializeDefaultTemplate(def);

        return def;
    }

    @Override
    public void configure(Configuration configuration) {
        this.importConfiguration = configuration;
    }

    @Override
    public ExportedEntityMatcher<MeasurementDefinition, MetricTemplate> getExportedEntityMatcher() {
        return new ExportedEntityMatcher<MeasurementDefinition, MetricTemplate>() {

            private Map<MetricTemplate, MeasurementDefinition> cache;
            
            {
                //this instance will be used many many times to find the measurement
                //definitions that correspond to the templates stored in the export file
                //it is therefore more optimal to just preload all the mds in a cache
                //and use that to find matches than to query the database each time.
                cache = new HashMap<MetricTemplate, MeasurementDefinition>();
                
                Query q = entityManager.createQuery("SELECT md FROM MeasurementDefinition md");

                for (Object r : q.getResultList()) {
                    MeasurementDefinition md = (MeasurementDefinition) r;

                    cache.put(new MetricTemplate(md), md);
                }
                
            }
            @Override
            public MeasurementDefinition findMatch(MetricTemplate object) {
                MeasurementDefinition md = cache.get(object);
                
                if (md == null && LOG.isDebugEnabled()) {
                        LOG.debug("Failed to find a measurement definition corresponding to "
                            + object
                            + ". This means that the plugins in the source RHQ install were different than in this RHQ install "
                            + "but the DeployedAgentPluginsValidator failed to catch that. This most probably means that the "
                            + "export file has been tampered with. Letting the import continue because that might have been intentional change by the user.");
                                      
                }
                
                return md;
            }
        };
    }

    @Override
    public Set<EntityValidator<MetricTemplate>> getEntityValidators() {
        return Collections.<EntityValidator<MetricTemplate>>singleton(new UniquenessValidator<MetricTemplate>());
    }
    
    @Override
    public void update(MeasurementDefinition entity, MetricTemplate exportedEntity) {
        if (entity == null) {
            unmatchedTemplates.add(exportedEntity);
            return;
        }

        addToUpdateMap(exportedEntity, entity);
    }

    @Override
    public MetricTemplate unmarshallExportedEntity(ExportReader reader) throws XMLStreamException {
        try {
            return (MetricTemplate) unmarshaller.unmarshal(reader);
        } catch (JAXBException e) {
            throw new XMLStreamException("Failed to unmarshal metric template.", e);
        }
    }

    @Override
    public String finishImport() {
        for (Map.Entry<UpdateKey, List<MeasurementDefinition>> e : definitionsByUpdateKey.entrySet()) {
            int[] ids = getIdsFromDefs(e.getValue());
            boolean enable = e.getKey().enable;
            boolean updateSchedules = e.getKey().updateSchedules;
            long collectionInterval = e.getKey().collectionInterval;
            
            measurementScheduleManager.updateDefaultCollectionIntervalAndEnablementForMeasurementDefinitions(subject,
                ids, collectionInterval, enable, updateSchedules);
        }
        
        if (unmatchedTemplates.isEmpty()) {
            return null;
        } else {
            return getUnmatchedMetricTemplatesReport(unmatchedTemplates);
        }
    }

    //public for testability
    public static String getUnmatchedMetricTemplatesReport(Set<MetricTemplate> metricTemplates) {
        StringBuilder bld = new StringBuilder(IMPORT_NOTES_PROLOGUE);
        
        for(MetricTemplate t : metricTemplates) {
            bld.append(t).append("\n");
        }
        
        return bld.toString();
    }
    
    private static int[] getIdsFromDefs(Collection<MeasurementDefinition> defs) {
        int[] ids = new int[defs.size()];

        int i = 0;
        for (MeasurementDefinition d : defs) {
            ids[i++] = d.getId();
        }

        return ids;
    }

    private void addToUpdateMap(MetricTemplate metricTemplate, MeasurementDefinition def) {
        UpdateKey key = new UpdateKey(metricTemplate.getDefaultInterval(), shouldUpdateSchedules(metricTemplate), metricTemplate.isEnabled());

        List<MeasurementDefinition> defs = definitionsByUpdateKey.get(key);
        if (defs == null) {
            defs = new ArrayList<MeasurementDefinition>();
            definitionsByUpdateKey.put(key, defs);
        }

        defs.add(def);
    }

    private boolean shouldUpdateSchedules(MetricTemplate def) {
        if (importConfiguration == null) {
            return UPDATE_SCHEDULES_DEFAULT;
        }

        String updateAll = importConfiguration.getSimpleValue(UPDATE_ALL_SCHEDULES_PROPERTY, null);
        if (updateAll != null) {
            if (Boolean.parseBoolean(updateAll)) {
                return true;
            }
        }

        PropertyList perMetricOverrides = importConfiguration.getList(METRIC_UPDATE_OVERRIDES_PROPERTY);
        if (perMetricOverrides == null) {
            return UPDATE_SCHEDULES_DEFAULT;
        }

        PropertySimple override = findOverrideForMetric(def, perMetricOverrides);

        if (override == null) {
            return UPDATE_SCHEDULES_DEFAULT;
        }

        return override.getBooleanValue();
    }

    PropertySimple findOverrideForMetric(MetricTemplate def, PropertyList overrides) {
        for (Property p : overrides.getList()) {
            PropertyMap map = (PropertyMap) p;

            String metricName = map.getSimpleValue(METRIC_NAME_PROPERTY, null);
            String resourceTypeName = map.getSimpleValue(RESOURCE_TYPE_NAME_PROPERTY, null);
            String resourceTypePlugin = map.getSimpleValue(RESOURCE_TYPE_PLUGIN_PROPERTY, null);
            PropertySimple updateSchedules = map.getSimple(UPDATE_SCHEDULES_PROPERTY);

            if (metricName == null || resourceTypeName == null || resourceTypePlugin == null || updateSchedules == null) {
                continue;
            }

            if (metricName.equals(def.getMetricName()) && resourceTypeName.equals(def.getResourceTypeName())
                && resourceTypePlugin.equals(def.getResourceTypePlugin())) {

                return updateSchedules;
            }
        }

        return null;
    }
}
