/*
 * 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.rhq.core.domain.configuration.Configuration;
import org.rhq.core.pluginapi.inventory.ClassLoaderFacet;
import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails;
import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException;
import org.rhq.core.pluginapi.inventory.ResourceDiscoveryContext;
import org.rhq.core.system.ProcessInfo;
import org.rhq.plugins.jmx.JMXDiscoveryComponent;
import org.rhq.plugins.jmx.JMXServerComponent;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author dbokde
 */
public class FabricContainerDiscoveryComponent extends FuseServerDiscoveryComponent implements ClassLoaderFacet {

    public static final Log log = LogFactory.getLog(FabricContainerDiscoveryComponent.class);

    private static final String KARAF_BASE_PROPERTY = "karaf.base";
    private static final String KARAF_HOME_PROPERTY = "karaf.home";
    private static final String CHILD_PREFIX = "Child ";

    private static final String DEFAULT_KARAF_NAME = "root";

    static final String KARAF_NAME_PROPERTY = "karaf.name";
    static final String ZOOKEEPER_URL_PROPERTY = "zookeeper.url";
    static final String ZOOKEEPER_PASSWORD_PROPERTY = "zookeeper.password";

    private static final String ETC_SYSTEM_PROPERTIES = "etc/system.properties";
    private static final String FUSE_ESB_ENTERPRISE_PRODUCT = "Fuse ESB Enterprise";
    private static final String FUSE_MQ_ENTERPRISE_PRODUCT = "Fuse MQ Enterprise";
    private static final String FUSE_MANAGEMENT_CONSOLE_PRODUCT = "Fuse Management Console";
    private static final String GENERIC_FABRIC_CONTAINER = "Fabric Container";
    private static final String CONTAINER_TYPE_PROPERTY = "container.type";

    @Override
    protected boolean populateResourceProperties(ResourceDiscoveryContext context, DiscoveredResourceDetails details) {
        // make sure system properties are set first
        if (!super.populateResourceProperties(context, details)) {
            return false;
        }

        // check if karaf.base matches karaf.home, if it doesn't add CHILD_PREFIX to name and description
        final Configuration pluginConfiguration = details.getPluginConfiguration();
        final String karafHomePath = pluginConfiguration.getSimpleValue(KARAF_HOME_PROPERTY);
        final String karafBasePath = pluginConfiguration.getSimpleValue(KARAF_BASE_PROPERTY);

        final String resourceTypeName = details.getResourceType().getName();
        if (!karafHomePath.equals(karafBasePath)) {
            // replace resource type in name and description to "Child " + resourceType name
            details.setResourceName(details.getResourceName().replace(resourceTypeName, CHILD_PREFIX + resourceTypeName));
            details.setResourceDescription(details.getResourceDescription().replace(resourceTypeName, CHILD_PREFIX + resourceTypeName));
        }

        final File karafHome = new File(karafHomePath);
        final File karafBase = new File(karafBasePath);

        // get container name from system.properties
        final Properties serverProperties = getServerProperties(karafBase);
        String karafName = serverProperties.getProperty(KARAF_NAME_PROPERTY);
        if (log.isDebugEnabled()) {
            log.debug("Found " + details.getResourceType().getName() + " with container name: " + karafName);
        }

        // set Fabric Container name
        pluginConfiguration.setSimpleValue(KARAF_NAME_PROPERTY, karafName);

        // name the product appropriately
        String productName;

        // is this a Fuse ESB install?
        File fuseEsbScript = findVersionFile(karafHome, Pattern.compile("bin/fuseesb(.bat)?$"));
        if (fuseEsbScript != null) {
            productName = FUSE_ESB_ENTERPRISE_PRODUCT;
        } else {

            // is this a Fuse MQ install?
            File fuseMqScript = findVersionFile(karafHome, Pattern.compile("bin/fusemq(.bat)?$"));
            if (fuseMqScript != null) {
                productName = FUSE_MQ_ENTERPRISE_PRODUCT;
            } else {

                // is this a Fuse FMC install?
                File fuseFmcScript = findVersionFile(karafHome, Pattern.compile("bin/fmc(.bat)?$"));
                if (fuseFmcScript != null) {
                    productName = FUSE_MANAGEMENT_CONSOLE_PRODUCT;
                } else {
                    productName = GENERIC_FABRIC_CONTAINER;
                }
            }
        }
        // remember the product name for initLogEventSourcesConfigProp()
        pluginConfiguration.setSimpleValue(CONTAINER_TYPE_PROPERTY, productName);

        // replace generic Fabric server name with product name in resource name, description, and resource key
        final String oldResourceKey = details.getResourceKey();
        details.setResourceKey(oldResourceKey.replace(resourceTypeName,
            productName));
        // replace resource key in name first, so we don't end up adding container name in key prefix
        details.setResourceName(details.getResourceName().replace(oldResourceKey,
            details.getResourceKey()));
        // then replace the trailing product name
        details.setResourceName(details.getResourceName().replace(resourceTypeName,
            productName + " [" + karafName + "]"));
        details.setResourceDescription(details.getResourceDescription().replace(resourceTypeName,
            productName));

        // find zookeeper url if not set already in getConfigWithJmxServiceUrl()
        final String zkProperty = pluginConfiguration.getSimpleValue(ZOOKEEPER_URL_PROPERTY);
        if (zkProperty == null) {
            final String[] zookeeperUrlPassword = getZookeeperUrlPassword(karafHome, karafBase);

            pluginConfiguration.setSimpleValue(ZOOKEEPER_URL_PROPERTY, zookeeperUrlPassword[0]);
            pluginConfiguration.setSimpleValue(ZOOKEEPER_PASSWORD_PROPERTY, zookeeperUrlPassword[1]);
        }

        return true;
    }

    private Properties getServerProperties(File karafBase) {
        final Properties serverProperties = new Properties();
        serverProperties.setProperty(KARAF_NAME_PROPERTY, DEFAULT_KARAF_NAME);

        // can't use findVersionFile, since it could get an instance/*/etc/system.properties for root
        File systemPropertiesFile = new File(karafBase, ETC_SYSTEM_PROPERTIES);
        loadPropertiesFile(systemPropertiesFile, serverProperties);
        return serverProperties;
    }

    private String[] getZookeeperUrlPassword(File karafHome, File karafBase) {

        final Properties serverProperties = getServerProperties(karafBase);
        String zookeeperUrl = serverProperties.getProperty(ZOOKEEPER_URL_PROPERTY);
        String zookeeperPassword = serverProperties.getProperty(ZOOKEEPER_PASSWORD_PROPERTY);

        if (zookeeperUrl == null) {

            if (!karafHome.equals(karafBase)) {

                // optimized search in instance.properties
                String[] zookeeperUrlPassword = getChildZooKeeperUrlPassword(karafBase.getAbsolutePath(),
                    serverProperties.getProperty(KARAF_NAME_PROPERTY));
                zookeeperUrl = zookeeperUrlPassword[0];
                zookeeperPassword = zookeeperUrlPassword[1];

            } else {
                // NOTE last resort, search for zookeeper server config in the ensemble root container
                // only works if the ZK server was created before auto-discovery
                log.warn("Looking for " + ZOOKEEPER_URL_PROPERTY + " in data/cache/**/zookeeper.config");
                File zookeperConfig = findVersionFile(new File(karafBase, "data/cache"),
                    Pattern.compile("config/org/fusesource/fabric/zookeeper.config"));

                if (zookeperConfig != null) {
                    log.debug("Found zookeeper.config at " + zookeperConfig);
                    Properties zkConfigProps = new Properties();
                    loadPropertiesFile(zookeperConfig, zkConfigProps);
                    zookeeperUrl = zkConfigProps.getProperty(ZOOKEEPER_URL_PROPERTY);
                    zookeeperPassword = zkConfigProps.getProperty(ZOOKEEPER_PASSWORD_PROPERTY);
                }
            }

        } else {

            if (log.isDebugEnabled()) {
                log.debug(String.format("Found %s in file %s under directory %s",
                    ZOOKEEPER_URL_PROPERTY, ETC_SYSTEM_PROPERTIES, karafBase));
                log.debug(String.format("Found %s in file %s under directory %s",
                    ZOOKEEPER_PASSWORD_PROPERTY, ETC_SYSTEM_PROPERTIES, karafBase));
            }
        }

        if (zookeeperUrl != null) {
            // strip quotes
            zookeeperUrl = zookeeperUrl.replaceAll("\"", "");
        }
        if (zookeeperPassword != null) {
            zookeeperPassword = zookeeperPassword.replaceAll("\"", "");
        }

        return new String[] { zookeeperUrl, zookeeperPassword };
    }

    @Override
    protected Configuration getConfigWithJmxServiceUrl(ResourceDiscoveryContext context, ProcessInfo process) {

        try {
            final Configuration pluginConfiguration = context.getDefaultPluginConfiguration();

            // read etc/org.apache.karaf.management.cfg to get serviceUrl property
            final String basePath = getSystemPropertyValue(process, KARAF_BASE_PROPERTY);
            final File karafBase = new File(basePath);
            final Properties serverProps = getServerProperties(karafBase);
            loadPropertiesFile(new File(basePath, "etc/org.apache.karaf.management.cfg"), serverProps);

            String serviceUrl = serverProps.getProperty("serviceUrl");
            if (serviceUrl != null) {
                Pattern propPattern = Pattern.compile("\\$\\{[^\\{\\}]+\\}");
                Matcher matcher = propPattern.matcher(serviceUrl);
                while(matcher.find()) {
                    final String property = matcher.group();
                    final String value = serverProps.getProperty(property.substring(2, property.length() - 1));
                    if (value == null) {
                        throw new InvalidPluginConfigurationException("Missing value for property " +
                            property + " in serviceUrl");
                    }
                    serviceUrl = serviceUrl.replace(property, value);
                }
                pluginConfiguration.setSimpleValue(JMXDiscoveryComponent.CONNECTOR_ADDRESS_CONFIG_PROPERTY, serviceUrl);
            } else {
                log.warn("Unable to read JMX URL from etc/org.apache.karaf.management.cfg");
            }

            // get admin role using jmxRole from org.apache.karaf.management.cfg or karaf.admin.role from system.properties
            String adminRole = serverProps.getProperty("jmxRole");
            if (adminRole == null) {
                adminRole = serverProps.getProperty("karaf.admin.role");
                // TODO also handle JAAS principal class in role
                if (adminRole == null) {
                    log.debug("Using default admin role [admin]");
                    adminRole = "admin";
                }
            }

            // get jmx user name and password from users.properties
            // BIG assumption here that the container is using users.properties!!!,
            // if the instance is configured with another provider, we can't get the user/password anyway
            Properties usersProperties = new Properties();
            loadPropertiesFile(new File(basePath, "etc/users.properties"), usersProperties);
            for (Map.Entry<Object, Object> entry : usersProperties.entrySet()) {
                String value = (String) entry.getValue();
                if (value.contains(adminRole)) {
                    pluginConfiguration.setSimpleValue(JMXServerComponent.PRINCIPAL_CONFIG_PROP, (String) entry.getKey());
                    pluginConfiguration.setSimpleValue(JMXServerComponent.CREDENTIALS_CONFIG_PROP, value.substring(0, value.indexOf(',')));
                    break;
                }
            }

            // look for zookeeper.url property for managed containers
            final String[] zookeeperUrlPassword = getZookeeperUrlPassword(
                new File(getSystemPropertyValue(process, KARAF_HOME_PROPERTY)),
                karafBase);
            // put this in the configuration to avoid looking it up again
            pluginConfiguration.setSimpleValue(ZOOKEEPER_URL_PROPERTY, zookeeperUrlPassword[0]);
            pluginConfiguration.setSimpleValue(ZOOKEEPER_PASSWORD_PROPERTY, zookeeperUrlPassword[1]);

            if (zookeeperUrlPassword[0] != null) {
                // TODO try getting JMX user name and password from ZK
                // for now, set a default user name and password
                log.warn("Container uses Fabric Zookeeper registry, using default JMX user");
                pluginConfiguration.setSimpleValue(JMXServerComponent.PRINCIPAL_CONFIG_PROP, "admin");
                pluginConfiguration.setSimpleValue(JMXServerComponent.CREDENTIALS_CONFIG_PROP, "admin");
            }

            return pluginConfiguration;

        } catch (Exception e) {
            // avoid stopping auto discovery for JMX property search errors
            log.warn("Error getting JMX properties from Fabric container: [" + e.getMessage() +
                "], using default connection properties", e);
        }

        // if everything else fails, fall back to defaults from rhq-plugin.xml
        return super.getConfigWithJmxServiceUrl(context, process);
    }


    private String[] getChildZooKeeperUrlPassword(String basePath, String karafName) {

        final File karafBase = new File(basePath);
        Properties instanceProps = new Properties();

        final File propertiesFile = new File(karafBase.getParent(), "instance.properties");
        loadPropertiesFile(propertiesFile, instanceProps);

        String zooKeeperUrl = null;
        String zooKeeperPassword = null;
        for (Map.Entry<Object, Object> entry : instanceProps.entrySet()) {
            if (karafName.equals(entry.getValue())) {
                final String key = (String)entry.getKey();
                final String instanceOpts = instanceProps.getProperty(key.replace("name", "opts"));
                // extract ZK url if set
                if (instanceOpts.contains(ZOOKEEPER_URL_PROPERTY)) {
                    if (log.isDebugEnabled()) {
                        log.debug("Found " + ZOOKEEPER_URL_PROPERTY + " in " + propertiesFile);
                    }
                    // strip the leading \="
                    zooKeeperUrl = instanceOpts.substring(
                        instanceOpts.indexOf(ZOOKEEPER_URL_PROPERTY) + ZOOKEEPER_URL_PROPERTY.length() + 2);
                    // strip the trailing "
                    final int end = zooKeeperUrl.indexOf(' ') != -1  ?
                        (zooKeeperUrl.indexOf(' ')  - 1) : zooKeeperUrl.length();
                    zooKeeperUrl = zooKeeperUrl.substring(0, end);
                }
                // extract ZK password if set
                if (instanceOpts.contains(ZOOKEEPER_PASSWORD_PROPERTY)) {
                    if (log.isDebugEnabled()) {
                        log.debug("Found " + ZOOKEEPER_PASSWORD_PROPERTY + " in " + propertiesFile);
                    }
                    // strip the leading \="
                    zooKeeperPassword = instanceOpts.substring(
                        instanceOpts.indexOf(ZOOKEEPER_PASSWORD_PROPERTY) + ZOOKEEPER_PASSWORD_PROPERTY.length() + 2);
                    // strip the trailing "
                    final int end = zooKeeperPassword.indexOf(' ') != -1  ?
                        (zooKeeperPassword.indexOf(' ')  - 1) : zooKeeperPassword.length();
                    zooKeeperPassword = zooKeeperPassword.substring(0, end);
                }
                break;
            }
        }

        return new String[] { zooKeeperUrl, zooKeeperPassword };
    }

    @Override
    protected void initLogEventSourcesConfigProp(File homeDir, Configuration pluginConfiguration, ProcessInfo process) {
        // look for log file based on product type since Enterprise products use product specific log file name
        final String karafHome = pluginConfiguration.getSimpleValue(KARAF_HOME_PROPERTY);
        final String karafBase = pluginConfiguration.getSimpleValue(KARAF_BASE_PROPERTY);
        // default log file for all containers
        String logFile = "{karaf.base}/data/log/karaf.log";
        // all child containers use the same log file name karaf.log
        if (karafHome.equals(karafBase)) {
            final String productType = pluginConfiguration.getSimpleValue(CONTAINER_TYPE_PROPERTY);
            if (FUSE_ESB_ENTERPRISE_PRODUCT.equals(productType)) {
                logFile = "{karaf.base}/data/log/fuseesb.log";
            } else if (FUSE_MQ_ENTERPRISE_PRODUCT.equals(productType)) {
                logFile = "{karaf.base}/data/log/fusemq.log";
            }
        }
        // update the logFile property to create the appropriate log event configuration
        pluginConfiguration.setSimpleValue(LOG_FILE_PROPERTY, logFile);

        super.initLogEventSourcesConfigProp(homeDir, pluginConfiguration, process);
    }

    private void loadPropertiesFile(File propertiesFile, Properties properties) {

        if (propertiesFile.exists()) {

            FileInputStream stream = null;
            try {
                stream = new FileInputStream(propertiesFile);
                properties.load(stream);
            } catch (IOException e) {
                String message = "Error reading " + propertiesFile;
                log.warn(message, e);
            } finally {
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (IOException e) {
                        // ignore
                    }
                }
            }

        } else {
            log.warn("File " + propertiesFile.getAbsolutePath() + " does not exist");
        }

    }

    // add fabric-linkedin-zookeeper jar from container's system directory
    public List<URL> getAdditionalClasspathUrls(ResourceDiscoveryContext context, DiscoveredResourceDetails details) throws Exception {

        final String karafHomePath = details.getPluginConfiguration().getSimpleValue(KARAF_HOME_PROPERTY);
        final File zooKeeperLib = findVersionFile(new File(karafHomePath + "/system"),
            Pattern.compile("fabric-linkedin-zookeeper-.*\\.jar"));

        if (zooKeeperLib == null) {
            throw new InvalidPluginConfigurationException("Missing ZooKeeper library fabric-linkedin-zookeeper*.jar in " +
                karafHomePath + "/system");
        }

        return Arrays.asList(zooKeeperLib.toURI().toURL());
    }

}
