/*
 * 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.linkedin.util.clock.Timespan;
import org.linkedin.zookeeper.client.ZKClient;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.measurement.MeasurementDataTrait;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;

import java.io.IOException;
import java.io.StringReader;
import java.util.Properties;
import java.util.Set;

/**
 * Manages Profiles as Resource Groups in RHQ
 *
 * @author dbokde
 */
public class FabricProfileGroupManager implements MeasurementFacet, Watcher {

    private static final String PARENTS_PROPERTY = "parents";
    private Log log = LogFactory.getLog(FabricProfileGroupManager.class);

    // TODO these constants are used in both fabric-plugin and fabric-groups-plugin, move them to a common Enum
    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 CONTAINER_VERSION_TRAIT = "container.version";

    private static final String ZOOKEEPER_TIMEOUT = "zookeeper.timeout";

    private final ResourceContext context;

    public FabricProfileGroupManager(ResourceContext context) {
        this.context = context;
    }

    public void getValues(MeasurementReport measurementReport,
                          Set<MeasurementScheduleRequest> measurementScheduleRequests) throws Exception {

        // get Container resource key
        final String resourceKey = context.getResourceKey();

        // get server's ZK url and password
        Configuration pluginConfiguration = context.getPluginConfiguration();
        final String zookeeperUrl = pluginConfiguration.getSimpleValue(
            FabricContainerDiscoveryComponent.ZOOKEEPER_URL_PROPERTY);
        final String zookeeperPassword = pluginConfiguration.getSimpleValue(
            FabricContainerDiscoveryComponent.ZOOKEEPER_PASSWORD_PROPERTY);

        if (zookeeperUrl != null) {

            log.debug("Processing Fabric Groups for Server " + resourceKey);

            // connect to ZK and get Profiles used by this server
            final Timespan fabricTimeout = Timespan.seconds(Long.valueOf(
                pluginConfiguration.getSimpleValue(ZOOKEEPER_TIMEOUT)));
            ZKClient client = new ZKClient(zookeeperUrl, fabricTimeout, this);

            try {
                // need to call lifecycle methods
                client.start();
                if (!client.isConnected()) {
                    // wait for connection timeout period
                    Thread.sleep(fabricTimeout.getDurationInMilliseconds());
                    if (!client.isConnected()) {
                        throw new IllegalAccessException("Unable to connect to Fabric Registry in " +
                            fabricTimeout.getDurationInSeconds() + " seconds");
                    }
                }

                // use the ZK password after connecting
                if (zookeeperPassword != null) {
                    client.addAuthInfo("digest", ("fabric:" + zookeeperPassword).getBytes("UTF-8"));
                }

                // get container name and version
                final String containerName = pluginConfiguration.getSimpleValue(
                    FabricContainerDiscoveryComponent.KARAF_NAME_PROPERTY);
                final String containerVersion = client.getStringData("/fabric/configs/containers/" +
                    containerName);

                // get container profiles
                final String[] profiles = client.getStringData("/fabric/configs/versions/" +
                    containerVersion + "/containers/" + containerName).split(" ");

                // process each profile to add Container traits
                final FabricContainerTraits traits = new FabricContainerTraits();
                // don't forget container version
                traits.setContainerVersion(containerVersion);

                // set container profiles
                traits.setProfiles(profiles);

                for (String profile : profiles) {
                    log.debug("Processing Server profile " + profile + ":" + containerVersion);
                    try {
                        // add Fabric traits for profile
                        getFabricMetadata(profile, containerVersion, client, traits);
                    } catch (Exception e) {
                        throw new InvalidPluginConfigurationException("Error processing Server Profile " +
                            profile + ":" + containerVersion + ": " + e.getMessage(), e);
                    }
                }

                // add required traits for measurements
                for (MeasurementScheduleRequest request : measurementScheduleRequests) {
                    final String metricName = request.getName();
                    if (metricName.equals(PROFILES_TRAIT)) {
                        measurementReport.addData(new MeasurementDataTrait(request, traits.getProfileTrait()));
                    } else if (metricName.equals(PARENT_PROFILES_TRAIT)) {
                        measurementReport.addData(new MeasurementDataTrait(request, traits.getParentProfilesTrait()));
                    } else if (metricName.equals(MQ_CLUSTERS_TRAIT)) {
                        measurementReport.addData(new MeasurementDataTrait(request, traits.getClustersTrait()));
                    } else if (metricName.equals(CONTAINER_VERSION_TRAIT)) {
                        measurementReport.addData(new MeasurementDataTrait(request, traits.getContainerVersion()));
                    } else {
                        throw new InvalidPluginConfigurationException(
                            String.format("Unknown metric %s for resource %s", metricName, resourceKey));
                    }
                }

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (KeeperException e) {
                throw new InvalidPluginConfigurationException("Error getting Server info from Fabric Registry: " +
                    e.getMessage());
            } catch (InvalidPluginConfigurationException e) {
                throw e;
            } catch (Exception ex) {
                throw new InvalidPluginConfigurationException("Error processing Profiles for [" +
                    context.getResourceKey() + "]: " + ex.getMessage(), ex);
            } finally {
                try {
                    client.destroy();
                } catch (Throwable t) {
                    // ignore destroy errors
                }
            }

        } else {
            log.debug("Plugin property " +
                FabricContainerDiscoveryComponent.ZOOKEEPER_URL_PROPERTY +
                " is not set for [" + context.getResourceKey() + "], no Fabric data to collect");
        }

    }

    protected void getFabricMetadata(String profileName, String containerVersion, ZKClient client,
                                     FabricContainerTraits traits)
        throws InvalidPluginConfigurationException {

        // MQ broker cluster name for MQ Cluster Groups
        String brokerClusterName = null;
        try {

            String brokerPropertiesPath = "/fabric/configs/versions/" + containerVersion +
                "/profiles/" + profileName + "/org.fusesource.mq.fabric.server-" +
                profileName + ".properties";
            boolean brokerFound;
            if (null == client.exists(brokerPropertiesPath)) {
                // try the default properties name
                brokerPropertiesPath = "/fabric/configs/versions/" + containerVersion +
                    "/profiles/" + profileName + "/org.fusesource.mq.fabric.server-broker.properties";
                brokerFound = (null != client.exists(brokerPropertiesPath));
            } else {
                brokerFound = true;
            }

            if (brokerFound) {
                log.debug("Broker properties found in profile " + profileName);
                Properties brokerProperties = new Properties();
                String propertiesData = client.getStringData(brokerPropertiesPath);
                brokerProperties.load(new StringReader(propertiesData));
                brokerClusterName = brokerProperties.getProperty("group");
                log.debug("Found Broker Cluster " + brokerClusterName);
            } else {
                log.debug("Broker properties NOT found in profile " + profileName);
            }

        } catch (InterruptedException e) {
            throw new InvalidPluginConfigurationException(
                "Error looking up MQ broker properties: " + e.getMessage(), e);
        } catch (KeeperException e) {
            throw new InvalidPluginConfigurationException(
                "Error looking up MQ broker properties: " + e.getMessage(), e);
        } catch (IOException e) {
            throw new InvalidPluginConfigurationException(
                "Error looking up MQ broker properties: " + e.getMessage(), e);
        }

        // add the MQ Cluster to traits
        if (brokerClusterName != null) {
            traits.addCluster(brokerClusterName);
        }

        // now do this all over for parent profiles
        // get parent profiles from ZK
        try {

            String parentList = getParents(profileName, containerVersion, client);
            if (parentList != null) {
                String[] parents = parentList.split(" ");
                // add parents to trait, returns true if this is a new child
                if (traits.setParentProfiles(profileName, parents)) {
                    // recursively process parents of new child
                    for (String parent : parents) {
                        // make sure the parent has not been processed already
                        if (!traits.hasChildProfile(parent)) {
                            getFabricMetadata(parent, containerVersion, client, traits);
                        }
                    }
                }
            }

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (KeeperException e) {
            throw new InvalidPluginConfigurationException(
                "Error getting Parent profiles for " + profileName + ": " + e.getMessage(), e);
        }
    }

    // handle old and new style parent lists
    // TODO refactor this to use the Fabric Profile API instead of raw ZKClient
    private String getParents(String profileName, String containerVersion, ZKClient client) throws InterruptedException, KeeperException {
        final String profilePath = "/fabric/configs/versions/" + containerVersion + "/profiles/" + profileName;
        String value = null;
        if (null != client.exists(profilePath)) {
            value = client.getStringData(profilePath);
        } else {
            // check whether this is an un-versioned ensemble profile
            final String ensemblePath = "/fabric/configs/ensemble/profiles/" + profileName;
            if (null != client.exists(ensemblePath)) {
                value = client.getStringData(ensemblePath);
            }
        }

        final Properties props = new Properties();
        if (value != null) {
            try {
                props.load(new StringReader(value));
            } catch (IOException ignore) {}
        }
        // handle old style parent list
        // For compatibility, check if we have instead the list of parents
        if (props.size() == 1) {
            String key = props.stringPropertyNames().iterator().next();
            if (!key.equals(PARENTS_PROPERTY)) {
                String val = props.getProperty(key);
                props.remove(key);
                props.setProperty(PARENTS_PROPERTY, val.isEmpty() ? key : key + " " + val);
            }
        }

        return props.getProperty(PARENTS_PROPERTY);
    }

    public void process(WatchedEvent watchedEvent) {
        // just log the events for now
        if (watchedEvent.getState().equals(Event.KeeperState.Disconnected)) {
            log.warn("Disconnected from Fabric Registry");
        }
    }

}
