/*
 * 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.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.jbosson.plugins.fuse.JBossFuseContainerDiscoveryComponent.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperic.sigar.OperatingSystem;
import org.hyperic.sigar.Sigar;
import org.hyperic.sigar.SigarException;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.EmsBeanName;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.pluginapi.availability.AvailabilityContext;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.pluginapi.util.ProcessExecutionUtility;
import org.rhq.core.pluginapi.util.StartScriptConfiguration;
import org.rhq.core.system.ProcessExecution;
import org.rhq.core.system.ProcessExecutionResults;
import org.rhq.core.system.ProcessInfo;
import org.rhq.core.system.SystemInfo;

/**
 * Create and Updates Resource Groups corresponding to Profiles for Fabric containers.
 *
 * @author dbokde
 */
public class JBossFuseContainerComponent<T extends ResourceComponent<?>> extends FuseServerComponent<T> implements MeasurementFacet, OperationFacet {

    private final static Log LOG = LogFactory.getLog(JBossFuseContainerComponent.class);
    private static final String SEPARATOR = "\n-----------------------\n";
    private static final int RETRIES = 60;
    private static final int TIMEOUT_INTERVAL = 1000;

    private JBossFuseProfileGroupManager profileGroupManager;
    private StartScriptConfiguration startScriptConfig;
    private Configuration pluginConfiguration;

    @Override
    public void start(ResourceContext context) throws Exception {

        // let super do its thing!
        super.start(context);

        this.pluginConfiguration = context.getPluginConfiguration();

        // create a Profile group manager
        this.profileGroupManager = new JBossFuseProfileGroupManager(context);

        // load start script config
        this.startScriptConfig = new StartScriptConfiguration(context.getPluginConfiguration());
    }

    @Override
    public void stop() {

        profileGroupManager = null;
        startScriptConfig = null;
        super.stop();
    }

    public void getValues(MeasurementReport measurementReport,
                          Set<MeasurementScheduleRequest> measurementScheduleRequests) throws Exception {
        // get Fabric metadata as traits to be used in fabric-groups-plugin for creating DynaGroups
        this.profileGroupManager.getValues(measurementReport, measurementScheduleRequests);
    }

    public OperationResult invokeOperation(String name, Configuration parameters) throws InterruptedException, Exception {
        if (name.equals("start")) {
            return startServer();
        } else if (name.equals("restart")) {
            return restartServer();
        } else if (name.equals("stop")) {
            return stopServer();
        } else throw new UnsupportedOperationException(name);
    }

    /**
     * Start the server by calling the start script defined in the plugin configuration.
     *
     * @return the result of the operation
     */
    protected OperationResult startServer() {
        OperationResult operationResult = new OperationResult();
        if (isManuallyAddedServer(operationResult, "Starting")) {
            return operationResult;
        }

        List<String> errors = validateStartScriptPluginConfigProps();
        if (!errors.isEmpty()) {
            setErrorMessage(operationResult, errors);
            return operationResult;
        }

        String startScriptPrefix = startScriptConfig.getStartScriptPrefix();
        File startScriptFile = getStartScriptFile();
        ProcessExecution processExecution = ProcessExecutionUtility.createProcessExecution(startScriptPrefix,
            startScriptFile);

        List<String> arguments = processExecution.getArguments();
        if (arguments == null) {
            arguments = new ArrayList<String>();
            processExecution.setArguments(arguments);
        }

        List<String> startScriptArgs = startScriptConfig.getStartScriptArgs();
        for (String startScriptArg : startScriptArgs) {
            startScriptArg = replacePropertyPatterns(startScriptArg);
            arguments.add(startScriptArg);
        }

        Map<String, String> startScriptEnv = startScriptConfig.getStartScriptEnv();
        for (String envVarName : startScriptEnv.keySet()) {
            String envVarValue = startScriptEnv.get(envVarName);
            envVarValue = replacePropertyPatterns(envVarValue);
            startScriptEnv.put(envVarName, envVarValue);
        }
        processExecution.setEnvironmentVariables(startScriptEnv);

        // When running on Windows 9x, start.bat need the cwd to be the JBoss Fuse bin dir in order to find start.bat.conf.
        processExecution.setWorkingDirectory(startScriptFile.getParent());
        processExecution.setCaptureOutput(true);
        processExecution.setWaitForCompletion(15000L); // 15 seconds // TODO: Should we wait longer than 15 seconds?
        processExecution.setKillOnTimeout(false);

        if (LOG.isDebugEnabled()) {
            LOG.debug("About to execute the following process: [" + processExecution + "]");
        }
        SystemInfo systemInfo = context.getSystemInformation();
        ProcessExecutionResults results = systemInfo.executeProcess(processExecution);
        logExecutionResults(results);
        if (results.getError() != null) {
            operationResult.setErrorMessage(results.getError().getMessage());
        } else if (results.getExitCode() == null || results.getExitCode() != 0) {
            operationResult.setErrorMessage("Start failed with error code " + results.getExitCode() + ":\n" + results.getCapturedOutput());
        } else {
            // Try to connect to the server - ping once per second, timing out after 60s.
            boolean up = waitForServerToStart();
            if (up) {
                operationResult.setSimpleResult("Success");
            } else {
                operationResult.setErrorMessage("Was not able to start the server");
            }
        }
        context.getAvailabilityContext().requestAvailabilityCheck();

        return operationResult;
    }

    /**
     * Restart the server by first executing the 'stop' script in karaf.base and then calling
     * the {@link #startServer} method to start it again.
     *
     * @return State of execution
     * @throws Exception If anything goes wrong
     */
    protected OperationResult restartServer() throws Exception {

        OperationResult operationResult = new OperationResult();
        if (isManuallyAddedServer(operationResult, "Restarting")) {
            return operationResult;
        }

        operationResult = stopServer();
        if (operationResult.getErrorMessage() != null) {
            operationResult.setErrorMessage("Restart failed while attempting to shut down: " + operationResult.getErrorMessage());
            return operationResult;
        }

        return startServer();
    }

    private OperationResult stopServer() {
        OperationResult operationResult = new OperationResult();
        if (isManuallyAddedServer(operationResult, "Stopping")) {
            return operationResult;
        }

        List<String> errors = validateStartScriptPluginConfigProps();
        if (!errors.isEmpty()) {
            OperationResult result  = new OperationResult();
            setErrorMessage(result, errors);
            return result;
        }

        final String karafBinPath = pluginConfiguration.getSimpleValue(KARAF_BASE_PROPERTY) + File.separator + "bin";
        final ProcessExecution processExecution = ProcessExecutionUtility.createProcessExecution(
            new File(karafBinPath, OS_IS_WINDOWS ? "stop.bat" : "stop"));

        // When running on Windows 9x, stop.bat need the cwd to be the JBoss Fuse bin dir in order to find stop.bat.conf.
        processExecution.setWorkingDirectory(karafBinPath);
        processExecution.setCaptureOutput(true);
        processExecution.setWaitForCompletion(15000L); // 15 seconds // TODO: Should we wait longer than 15 seconds?
        processExecution.setKillOnTimeout(false);

        if (LOG.isDebugEnabled()) {
            LOG.debug("About to execute the following process: [" + processExecution + "]");
        }
        SystemInfo systemInfo = context.getSystemInformation();
        ProcessExecutionResults results = systemInfo.executeProcess(processExecution);
        logExecutionResults(results);
        if (results.getError() != null) {
            operationResult.setErrorMessage(results.getError().getMessage());
        } else if (results.getExitCode() == null || results.getExitCode() != 0) {
            operationResult.setErrorMessage("Stop failed with error code: " + results.getExitCode() + ":\n" + results.getCapturedOutput());
        } else {
            // Try to disconnect from the server - ping once per second, timing out after 60s.
            boolean down = waitForServerToStop();
            // FUSEJON-126 try to kill the process if shutdown failed, only works for agents with native support
            if (!down) {
                if (getResourceContext().getSystemInformation().isNative()) {

                    ProcessInfo nativeProcess = getResourceContext().getNativeProcess();
                    if (nativeProcess != null) {
                        final long pid = nativeProcess.getPid();
                        LOG.error("Shutdown failed, trying to send SIGKILL to process " + pid);

                        try {
                            nativeProcess.kill(OperatingSystem.IS_WIN32 ? "KILL" : "SIGKILL");
                            LOG.info("Successfully sent SIGKILL to process " + pid);
                            down = waitForServerToStop();
                        } catch (SigarException e) {
                            operationResult.setErrorMessage(String.format("Error killing process %s: %s",
                                pid, e.getMessage()));
                        }
                    }
                }
            }
            if (down) {
                operationResult.setSimpleResult("Success");
            } else {
                operationResult.setErrorMessage("Was not able to stop the server");
            }
        }

        context.getAvailabilityContext().requestAvailabilityCheck();

        return operationResult;
    }

    private boolean isManuallyAddedServer(OperationResult operationResult, String operation) {
        if (pluginConfiguration.get("manuallyAdded")!=null) {
            operationResult.setErrorMessage(operation + " is not enabled for manually added servers");
            return true;
        }
        return false;
    }

    private void setErrorMessage(OperationResult operationResult, List<String> errors) {
        StringBuilder buffer = new StringBuilder("This Resource's connection properties contain errors: ");
        for (int i = 0, errorsSize = errors.size(); i < errorsSize; i++) {
            if (i != 0) {
                buffer.append(", ");
            }
            String error = errors.get(i);
            buffer.append('[').append(error).append(']');
        }
        operationResult.setErrorMessage(buffer.toString());
    }

    private List<String> validateStartScriptPluginConfigProps() {
        List<String> errors = new ArrayList<String>();

        File startScriptFile = getStartScriptFile();

        if (!startScriptFile.exists()) {
            errors.add("Start script '" + startScriptFile + "' does not exist.");
        } else {
            if (!startScriptFile.isFile()) {
                errors.add("Start script '" + startScriptFile + "' is not a regular file.");
            } else {
                if (!startScriptFile.canRead()) {
                    errors.add("Start script '" + startScriptFile + "' is not readable.");
                }
                if (!startScriptFile.canExecute()) {
                    errors.add("Start script '" + startScriptFile + "' is not executable.");
                }
            }
        }

        Map<String, String> startScriptEnv = startScriptConfig.getStartScriptEnv();
        if (startScriptEnv.isEmpty()) {
            errors.add("No start script environment variables are set. At a minimum, PATH should be set "
                + "(on UNIX, it should contain at least /bin and /usr/bin). It is recommended that "
                + "JAVA_HOME also be set, otherwise the PATH will be used to find java.");
        }

        return errors;
    }

    private File getStartScriptFile() {
        File startScriptFile = startScriptConfig.getStartScript();
        File homeDir = new File(pluginConfiguration.getSimpleValue(
            KARAF_BASE_PROPERTY));
        if (startScriptFile != null) {
            if (!startScriptFile.isAbsolute()) {
                startScriptFile = new File(homeDir, startScriptFile.getPath());
            }
        } else {
            // Use the default start script.
            String startScriptFileName = (OS_IS_WINDOWS ? "start.bat" : "start");
            File binDir = new File(homeDir, "bin");
            startScriptFile = new File(binDir, startScriptFileName);
        }
        return startScriptFile;
    }

    private boolean waitForServerToStart() {
        boolean up = false;
        int count = 0;
        while (!up && count < RETRIES) {
            try{
                // check availability first
                final AvailabilityContext availabilityContext = getResourceContext().getAvailabilityContext();
                availabilityContext.requestAvailabilityCheck();
                if (availabilityContext.getLastReportedAvailability() == AvailabilityType.UP) {
                    Set<EmsBean> emsBeans = getEmsConnection().getBeans();
                    if (emsBeans != null && !emsBeans.isEmpty()) { // If Core framework is present, server is not down
                        for (EmsBean bean : emsBeans) {
                            final EmsBeanName beanName = bean.getBeanName();
                            if ("osgi.core".equals(beanName.getDomain())
                                && "framework".equals(beanName.getKeyProperty("type"))) {
                                up = true;
                                break;
                            }
                        }
                    }
                }
            } catch (Exception e) {
                //do absolutely nothing
                //if an exception is thrown that means the server is still down, so consider this
                //a single failed attempt, equivalent to res.isSuccess == false
            }

            if (!up) {
                try {
                    Thread.sleep(TIMEOUT_INTERVAL); // Wait 1s
                } catch (InterruptedException e) {
                    // ignore
                }
            }
            count++;
        }
        return up;
    }

    private boolean waitForServerToStop() {
        boolean down = false;
        int count = 0;
        while (!down && count < RETRIES) {
            try {
                // check availability first
                final AvailabilityContext availabilityContext = getResourceContext().getAvailabilityContext();
                availabilityContext.requestAvailabilityCheck();
                if (availabilityContext.getLastReportedAvailability() == AvailabilityType.DOWN) {
                    down = true;
                }
            } catch (Exception e) {
                //do absolutely nothing
                //if an exception is thrown that means the server is still down, so consider this
                //a single failed attempt, equivalent to res.isSuccess == false
            }

            if (!down) {
                try {
                    Thread.sleep(TIMEOUT_INTERVAL); // Wait 1s
                } catch (InterruptedException e) {
                    // ignore
                }
            }
            count++;
        }
        return down;
    }

    private void logExecutionResults(ProcessExecutionResults results) {
        // Always log the output at info level. On Unix we could switch depending on a exitCode being !=0, but ...
        LOG.info("Exit code from process execution: " + results.getExitCode());
        LOG.info("Output from process execution: " + SEPARATOR + results.getCapturedOutput() + SEPARATOR);
    }

    // Replace any "%xxx%" substrings with the values of plugin config props "xxx".
    private String replacePropertyPatterns(String value) {
        Pattern pattern = Pattern.compile("(%([^%]*)%)");
        Matcher matcher = pattern.matcher(value);
        Configuration pluginConfig = context.getPluginConfiguration();
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            String propName = matcher.group(2);
            PropertySimple prop = pluginConfig.getSimple(propName);
            String propValue = ((prop != null) && (prop.getStringValue() != null)) ? prop.getStringValue() : "";
            String propPattern = matcher.group(1);
            String replacement = (prop != null) ? propValue : propPattern;
            matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement));
        }

        matcher.appendTail(buffer);
        return buffer.toString();
    }

}
