/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 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.plugins.agent;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.ConfigurationUpdateStatus;
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.measurement.AvailabilityType;
import org.rhq.core.pluginapi.configuration.ConfigurationFacet;
import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport;
import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.system.ProcessExecution;
import org.rhq.core.system.ProcessExecutionResults;
import org.rhq.core.system.SystemInfo;
import org.rhq.core.util.exception.ExceptionPackage;
import org.rhq.core.util.exception.Severity;
import org.rhq.enterprise.agent.EnvironmentScriptFileUpdate;
import org.rhq.enterprise.agent.EnvironmentScriptFileUpdate.NameValuePair;

/**
 * The component that represents the agent's Java Service Wrapper (JSW).
 *
 * @author John Mazzitelli
 */
public class AgentJavaServiceWrapperComponent implements ResourceComponent<AgentServerComponent<?>>,
    ConfigurationFacet, OperationFacet {

    private Log log = LogFactory.getLog(AgentJavaServiceWrapperComponent.class);

    private ResourceContext<AgentServerComponent<?>> resourceContext;

    private File launcherScript;
    private File configFile;
    private File environmentFile;
    private File includeFile;

    public void start(ResourceContext<AgentServerComponent<?>> rc) throws Exception {

        this.resourceContext = rc;

        Configuration pc = this.resourceContext.getPluginConfiguration();

        PropertySimple prop;

        prop = pc.getSimple(AgentJavaServiceWrapperDiscoveryComponent.PLUGINCONFIG_LAUNCHER_SCRIPT);
        if (prop == null) {
            throw new InvalidPluginConfigurationException("Missing Launcher Script");
        }
        if (prop.getStringValue() == null) {
            throw new InvalidPluginConfigurationException("Launcher Script property value is null");
        }

        launcherScript = new File(prop.getStringValue());
        if (!launcherScript.exists()) {
            throw new InvalidPluginConfigurationException("Launcher Script [" + launcherScript + "] does not exist");
        }

        prop = pc.getSimple(AgentJavaServiceWrapperDiscoveryComponent.PLUGINCONFIG_CONF_FILE);
        if (prop == null) {
            throw new InvalidPluginConfigurationException("Missing Configuration File");
        }
        if (prop.getStringValue() == null) {
            throw new InvalidPluginConfigurationException("Configuration File property value is null");
        }

        configFile = new File(prop.getStringValue());
        if (!configFile.exists()) {
            throw new InvalidPluginConfigurationException("Config file [" + configFile + "] does not exist");
        }

        log.debug("Starting agent JSW component: " + configFile);

        // get the optional files (these may remain null if the paths were left undefined)
        prop = pc.getSimple(AgentJavaServiceWrapperDiscoveryComponent.PLUGINCONFIG_ENV_FILE);
        if (prop != null && prop.getStringValue() != null) {
            environmentFile = new File(prop.getStringValue());
        }

        prop = pc.getSimple(AgentJavaServiceWrapperDiscoveryComponent.PLUGINCONFIG_INC_FILE);
        if (prop != null && prop.getStringValue() != null) {
            includeFile = new File(prop.getStringValue());
        }

        return;
    }

    public void stop() {
        // nothing to do
        return;
    }

    public AvailabilityType getAvailability() {

        return (launcherScript.exists() && configFile.exists()) ? AvailabilityType.UP : AvailabilityType.DOWN;

        // I would like to do this but:
        // 1. I don't like executing the script like this every 60 seconds; not very efficient and,
        // 2. I don't think executing this script and processing it output will always be faster than
        //    the 5 seconds the plugin container will give us. 
        //        try {
        //            String output = executeLauncherScript("status");
        //            return (output.contains("is installed")) ? AvailabilityType.UP : AvailabilityType.DOWN;
        //        } catch (Throwable t) {
        //            return AvailabilityType.DOWN;
        //        }
    }

    public OperationResult invokeOperation(String name, Configuration params) throws Exception {

        log.info("Wrapper launcher script executing [" + name + "]");

        OperationResult result = null;
        try {
            if (name.equals("Status")) {
                Map<Integer, String> output = executeLauncherScript("status");
                result = new OperationResult();
                Integer exitCode = output.keySet().iterator().next();
                determineServiceStatus(exitCode, result);
                result.getComplexResults().put(new PropertySimple("exitCode", exitCode));
                result.getComplexResults().put(new PropertySimple("output", output.values().toArray()[0]));
            } else if (name.equals("Install")) {
                Map<Integer, String> output = executeLauncherScript("install");
                result = new OperationResult();
                Integer exitCode = output.keySet().iterator().next();
                result.getComplexResults().put(new PropertySimple("exitCode", exitCode));
                result.getComplexResults().put(new PropertySimple("output", output.values().toArray()[0]));
            } else if (name.equals("Restart")) {
                executeLauncherScriptInThread("restart");
            } else if (name.equals("Stop")) {
                executeLauncherScriptInThread("stop");
            } else if (name.equals("Remove")) {
                executeLauncherScriptInThread("remove");
            } else {
                throw new UnsupportedOperationException("Invalid operation name: " + name);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to invoke operation [" + name + "]", e);
        }

        log.info("Launcher script executed [" + name + "]: " + result);
        return result;
    }

    private void determineServiceStatus(Integer exitCode, OperationResult result) {
        if (exitCode == null || exitCode.intValue() < 0) {
            return;
        }

        int bitmask = exitCode.intValue();
        Configuration map = result.getComplexResults();
        map.put(new PropertySimple("disabled", ((bitmask & 32) == 32) ? Boolean.TRUE : Boolean.FALSE));
        map.put(new PropertySimple("requiresManualStart", ((bitmask & 16) == 16) ? Boolean.TRUE : Boolean.FALSE));
        map.put(new PropertySimple("willAutomaticallyStart", ((bitmask & 8) == 8) ? Boolean.TRUE : Boolean.FALSE));
        map.put(new PropertySimple("hasInteractiveConsole", ((bitmask & 4) == 4) ? Boolean.TRUE : Boolean.FALSE));
        map.put(new PropertySimple("isRunning", ((bitmask & 2) == 2) ? Boolean.TRUE : Boolean.FALSE));
        map.put(new PropertySimple("isInstalled", ((bitmask & 1) == 1) ? Boolean.TRUE : Boolean.FALSE));
        return;
    }

    private Map<Integer, String> executeLauncherScript(String arg) throws Exception {
        if (!this.launcherScript.exists()) {
            throw new Exception("Launcher script [" + this.launcherScript + "] does not exist");
        }

        Map<String, String> envvars = new HashMap<String, String>(System.getenv());
        envvars.put("RHQ_AGENT_DEBUG", "false"); // we don't want all that debug output in the beginning

        ProcessExecution exe = new ProcessExecution(this.launcherScript.getAbsolutePath());
        exe.setArguments(new String[] { arg });
        exe.setWorkingDirectory(this.launcherScript.getParent());
        exe.setCaptureOutput(true);
        exe.setWaitForCompletion(30000L);
        exe.setEnvironmentVariables(envvars);
        ProcessExecutionResults results = this.resourceContext.getSystemInformation().executeProcess(exe);
        Throwable error = results.getError();
        if (error != null) {
            throw new Exception("Failed to invoke [" + this.launcherScript + ' ' + arg + "]", error);
        }

        HashMap<Integer, String> map = new HashMap<Integer, String>();
        Integer exitCode = results.getExitCode();
        String output = results.getCapturedOutput();
        map.put((exitCode != null) ? exitCode : Integer.valueOf(-1), (output != null) ? output : "");
        return map;
    }

    /**
     * This will execute the launcher script in a separate thread. This separate thread
     * will sleep for a few seconds before executing to give the caller enough time to
     * return itself. This is used when the launcher script being executed will quickly
     * kill the agent VM process in which we are running.
     * 
     * @param arg the command to pass to the launcher script
     *
     * @throws Exception if failed to even get a chance to spawn the thread and execute the launcher
     */
    private void executeLauncherScriptInThread(final String arg) throws Exception {
        if (!this.launcherScript.exists()) {
            throw new Exception("Launcher script [" + this.launcherScript + "] does not exist");
        }

        final File script = this.launcherScript;
        final SystemInfo sysInfo = this.resourceContext.getSystemInformation();

        Thread thread = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(10000L); // this should be enough to return our operation results back
                    ProcessExecution exe = new ProcessExecution(script.getAbsolutePath());
                    exe.setArguments(new String[] { arg });
                    exe.setWorkingDirectory(script.getParent());
                    ProcessExecutionResults results = sysInfo.executeProcess(exe);
                    if (results != null && results.getError() != null) {
                        throw results.getError();
                    }
                } catch (Throwable t) {
                    log.error("Failed to invoke [" + script + ' ' + arg + "] in a thread", t);
                }
            }
        }, "RHQ Agent Plugin JSW Launcher Thread");
        thread.setDaemon(true);

        // after we start, do not linger; return fast so we can return our operation results before we die
        thread.start();
        return;
    }

    public Configuration loadResourceConfiguration() throws Exception {
        Configuration config = new Configuration();
        PropertyList conf = loadConfigurationFileConfiguration();
        PropertyList env = loadEnvironmentFileConfiguration();
        PropertyList inc = loadIncludeFileConfiguration();
        if (conf != null) {
            config.put(conf);
        }
        if (env != null) {
            config.put(env);
        }
        if (inc != null) {
            config.put(inc);
        }
        return config;
    }

    public void updateResourceConfiguration(ConfigurationUpdateReport request) {
        try {
            updateConfigurationFileConfiguration(request);
            updateEnvironmentFileConfiguration(request);
            updateIncludeFileConfiguration(request);
        } catch (Exception e) {
            request.setErrorMessage(new ExceptionPackage(Severity.Severe, e).toString());
        }

        return;
    }

    private PropertyList loadConfigurationFileConfiguration() throws Exception {
        if (configFile == null || !configFile.exists()) {
            return null;
        }

        // read in the file and get all the settings it defines
        EnvironmentScriptFileUpdate updater = EnvironmentScriptFileUpdate.create(configFile.getAbsolutePath());
        List<NameValuePair> properties = updater.loadExisting();

        // put the env var definitions in a config object
        PropertyList list = new PropertyList("mainConfigurationSettings");

        for (NameValuePair prop : properties) {
            PropertyMap map = new PropertyMap("mainConfigurationSetting");
            map.put(new PropertySimple("name", prop.name));
            map.put(new PropertySimple("value", prop.value));
            list.add(map);
        }

        return list;
    }

    private PropertyList loadEnvironmentFileConfiguration() throws Exception {
        if (environmentFile == null || !environmentFile.exists()) {
            return null;
        }

        // read in the file and get all the settings it defines
        EnvironmentScriptFileUpdate updater = EnvironmentScriptFileUpdate.create(environmentFile.getAbsolutePath());
        List<NameValuePair> properties = updater.loadExisting();

        // put the env var definitions in a config object
        PropertyList list = new PropertyList("environmentSettings");

        for (NameValuePair prop : properties) {
            PropertyMap map = new PropertyMap("environmentSetting");
            map.put(new PropertySimple("name", prop.name));
            map.put(new PropertySimple("value", prop.value));
            list.add(map);
        }

        return list;
    }

    private PropertyList loadIncludeFileConfiguration() throws Exception {
        if (includeFile == null || !includeFile.exists()) {
            return null;
        }

        // read in the file and get all the settings it defines
        EnvironmentScriptFileUpdate updater = EnvironmentScriptFileUpdate.create(includeFile.getAbsolutePath());
        List<NameValuePair> properties = updater.loadExisting();

        // put the env var definitions in a config object
        PropertyList list = new PropertyList("includeSettings");

        for (NameValuePair prop : properties) {
            PropertyMap map = new PropertyMap("includeSetting");
            map.put(new PropertySimple("name", prop.name));
            map.put(new PropertySimple("value", prop.value));
            list.add(map);
        }

        return list;
    }

    private void updateConfigurationFileConfiguration(ConfigurationUpdateReport request) {
        try {
            List<NameValuePair> newSettings = new ArrayList<NameValuePair>();

            Configuration configuration = request.getConfiguration();
            PropertyList list = configuration.getList("mainConfigurationSettings");

            if (list == null) {
                throw new Exception("Missing main config");
            }

            for (Property item : list.getList()) {
                PropertyMap map = (PropertyMap) item;
                PropertySimple name = map.getSimple("name");
                PropertySimple value = map.getSimple("value");

                if (name == null || name.getStringValue() == null) {
                    log.error("Missing a config name: " + configuration.toString(true));
                    throw new IllegalArgumentException("Missing the name of a main config setting");
                }

                if (value != null && value.getStringValue() != null) {
                    newSettings.add(new NameValuePair(name.getStringValue(), value.getStringValue()));
                }
            }

            // update the env script file so it includes the new settings.
            // note that we require the request to contain ALL settings, not a subset; any settings
            // missing in the request config that currently exist in the script will be removed from the script,
            // which would be bad - but that should never occur unless something bad happens in the UI
            EnvironmentScriptFileUpdate updater = EnvironmentScriptFileUpdate.create(configFile.getAbsolutePath());
            updater.update(newSettings, true);

            request.setStatus(ConfigurationUpdateStatus.SUCCESS);
        } catch (Exception e) {
            request.setErrorMessage(new ExceptionPackage(Severity.Severe, e).toString());
        }

        return;
    }

    private void updateEnvironmentFileConfiguration(ConfigurationUpdateReport request) {
        try {
            List<NameValuePair> newSettings = new ArrayList<NameValuePair>();

            Configuration configuration = request.getConfiguration();
            PropertyList list = configuration.getList("environmentSettings");

            // if there is no config, the file should be deleted
            if (list == null || list.getList() == null || list.getList().isEmpty()) {
                if (environmentFile.exists()) {
                    if (!environmentFile.delete()) {
                        throw new Exception("Failed to remove the env file: " + environmentFile);
                    }
                }
                return;
            }

            for (Property item : list.getList()) {
                PropertyMap map = (PropertyMap) item;
                PropertySimple name = map.getSimple("name");
                PropertySimple value = map.getSimple("value");

                if (name == null || name.getStringValue() == null) {
                    log.error("Missing a env name: " + configuration.toString(true));
                    throw new IllegalArgumentException("Missing the name of a env setting");
                }

                if (value != null && value.getStringValue() != null) {
                    newSettings.add(new NameValuePair(name.getStringValue(), value.getStringValue()));
                }
            }

            // update the env script file so it includes the new settings.
            // note that we require the request to contain ALL settings, not a subset; any settings
            // missing in the request config that currently exist in the script will be removed from the script,
            // which would be bad - but that should never occur unless something bad happens in the UI
            EnvironmentScriptFileUpdate updater = EnvironmentScriptFileUpdate.create(environmentFile.getAbsolutePath());
            updater.update(newSettings, true);

            request.setStatus(ConfigurationUpdateStatus.SUCCESS);
        } catch (Exception e) {
            request.setErrorMessage(new ExceptionPackage(Severity.Severe, e).toString());
        }

        return;
    }

    private void updateIncludeFileConfiguration(ConfigurationUpdateReport request) {
        try {
            List<NameValuePair> newSettings = new ArrayList<NameValuePair>();

            Configuration configuration = request.getConfiguration();
            PropertyList list = configuration.getList("includeSettings");

            // if there is no config, the file should be deleted
            if (list == null || list.getList() == null || list.getList().isEmpty()) {
                if (includeFile.exists()) {
                    if (!includeFile.delete()) {
                        throw new Exception("Failed to remove the include file: " + includeFile);
                    }
                }
                return;
            }

            for (Property item : list.getList()) {
                PropertyMap map = (PropertyMap) item;
                PropertySimple name = map.getSimple("name");
                PropertySimple value = map.getSimple("value");

                if (name == null || name.getStringValue() == null) {
                    log.error("Missing a inc name: " + configuration.toString(true));
                    throw new IllegalArgumentException("Missing the name of a include setting");
                }

                if (value != null && value.getStringValue() != null) {
                    newSettings.add(new NameValuePair(name.getStringValue(), value.getStringValue()));
                }
            }

            // update the env script file so it includes the new settings.
            // note that we require the request to contain ALL settings, not a subset; any settings
            // missing in the request config that currently exist in the script will be removed from the script,
            // which would be bad - but that should never occur unless something bad happens in the UI
            EnvironmentScriptFileUpdate updater = EnvironmentScriptFileUpdate.create(includeFile.getAbsolutePath());
            updater.update(newSettings, true);

            request.setStatus(ConfigurationUpdateStatus.SUCCESS);
        } catch (Exception e) {
            request.setErrorMessage(new ExceptionPackage(Severity.Severe, e).toString());
        }

        return;
    }
}