/*
 * Jopr 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, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * 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 and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.jboss.on.common.jbossas;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.jboss.jbossnetwork.product.jbpm.handlers.ActionHandlerMessageLog;
import com.jboss.jbossnetwork.product.jbpm.handlers.BaseHandler;
import com.jboss.jbossnetwork.product.jbpm.handlers.ContextVariables;
import com.jboss.jbossnetwork.product.jbpm.handlers.ControlActionFacade;
import com.jboss.jbossnetwork.product.jbpm.handlers.SoftwareValue;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jbpm.context.exe.ContextInstance;
import org.jbpm.graph.def.Action;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.def.SuperState;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.instantiation.Delegation;
import org.jbpm.instantiation.FieldInstantiator;

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.core.domain.content.transfer.ContentResponseResult;
import org.rhq.core.domain.content.transfer.DeployIndividualPackageResponse;
import org.rhq.core.domain.content.transfer.DeployPackageStep;
import org.rhq.core.domain.content.transfer.ResourcePackageDetails;
import org.rhq.core.pluginapi.content.ContentContext;

/**
 * Class responsible for managing the running of a JBPM process to apply a patch to a JBoss instance. This class will
 * take care of populating the context with whatever variables are necessary, including the creation of any temporary
 * directories that may be needed. The results of the workflow run will be parsed and converted into the necessary
 * domain model and returned to the caller.
 *
 * @author Jason Dobies
 */
public class JBPMWorkflowManager {
    private ContentContext contentContext;
    private ControlActionFacade controlFacade;
    private JBossASPaths jbossPaths;

    private final Log log = LogFactory.getLog(this.getClass());

    public JBPMWorkflowManager(ContentContext contentContext, ControlActionFacade controlFacade, JBossASPaths jbossPaths) {
        this.contentContext = contentContext;
        this.controlFacade = controlFacade;
        this.jbossPaths = jbossPaths;
    }

    /**
     * Runs the JBPM process included in the provided package description. This method will make calls back into
     * the plugin container as necessary to retrieve information or execute operations.
     *
     * @param packageDetails contains data to describe the package being installed
     * @return plugin container domain model representation of the result of attempting to install the package
     * @throws Exception if there are any unexpected errors during the process
     */
    public DeployIndividualPackageResponse run(ResourcePackageDetails packageDetails) throws Exception {

        checkCompatibility(packageDetails);

        // Grab the JBPM process
        byte[] metadataBytes = packageDetails.getMetadata();
        if (metadataBytes == null) {
            throw new IllegalArgumentException("The 'metadata' field of the 'packageDetails' parameter is null.");
        }
        String process = new String(metadataBytes);

        // Generate the list of steps to execute first. If the workflow fails, we won't have log entries for the
        // unexecuted steps. Link these steps up against the log and include unexecuted steps from this list
        // in the response.
        List<DeployPackageStep> unexecutedSteps = translateSteps(packageDetails);

        // Load the JBPM process into the executor
        ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(process);
        ProcessInstance processInstance = new ProcessInstance(processDefinition);
        ContextInstance context = processInstance.getContextInstance();

        // Populate the variables we'll need in the handlers
        context.setVariable(ContextVariables.CONTENT_CONTEXT, contentContext);
        context.setVariable(ContextVariables.CONTROL_ACTION_FACADE, controlFacade);
        context.setVariable(ContextVariables.PACKAGE_DETAILS_KEY, packageDetails.getKey());

        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        Date timestamp = new Date();
        String formattedTimestamp = format.format(timestamp);
        context.setVariable(ContextVariables.TIMESTAMP, formattedTimestamp);

        try {
            File downloadDir = createTempDir("jon_download");
            if (downloadDir != null) {
                context.setVariable(ContextVariables.DOWNLOAD_DIR, downloadDir.getAbsolutePath());
            }

            File patchDir = createTempDir("jon_patch");
            if (patchDir != null) {
                context.setVariable(ContextVariables.PATCH_DIR, patchDir.getAbsolutePath());
            }
        } catch (IOException e) {
            // No need to throw this error, a handler will check for these to be valid and fail the step accordingly
            log.error("Error creating temporary directories", e);
        }

        // Populate the variables describing the AS instance
        String jbossHomeDir = jbossPaths.getHomeDir();
        log.debug("jbossHomeDir: " + (jbossHomeDir == null ? " is NULL" : jbossHomeDir));
        jbossHomeDir += File.separator; // Just to make sure it ends with the separator
        context.setVariable(ContextVariables.JBOSS_HOME_DIR, jbossHomeDir);

        String jbossClientDir = jbossHomeDir + File.separator + "client" + File.separator;
        log.debug("jbossClientDir: " + jbossClientDir);
        context.setVariable(ContextVariables.JBOSS_CLIENT_DIR, jbossClientDir);

        String jbossServerDir = jbossPaths.getServerDir();
        log.debug("jbossServerDir: " + (jbossServerDir == null ? " is NULL" : jbossServerDir));
        jbossServerDir += File.separator; // Just to make sure
        context.setVariable(ContextVariables.JBOSS_SERVER_DIR, jbossServerDir);

        // The workflow will reference values inside of this object for substitution
        // Ultimately, we should parse out the workflows to use the domain object directly
        SoftwareValue softwareValue = resourcePackageDetailsToSoftwareValue(packageDetails);
        context.setVariable(ContextVariables.SOFTWARE, softwareValue);

        // Perform the workflow
        try {
            processInstance.signal();
        } catch (Exception e) {
            log.error("Error received from the workflow", e);
        }

        // Parse the logs from the workflow execution into the domain model
        List logs = processInstance.getLoggingInstance().getLogs();
        DeployIndividualPackageResponse response = parseLogs(logs, unexecutedSteps, packageDetails.getKey());

        return response;
    }

    /**
     * Translates the metadata inside the given package into a list of readable steps that will be executed
     * during this package's installation. This call will <em>not</em> execute the package deployment nor cause
     * any changes to be made to the resource.
     *
     * @param packageDetails generate the steps for this package; cannot be <code>null</code>
     * @return translated steps if the metadata in this package was populated; <code>null</code> otherwise.
     * @throws Exception if there is an error during the translation
     */
    public List<DeployPackageStep> translateSteps(ResourcePackageDetails packageDetails) throws Exception {

        checkCompatibility(packageDetails);

        // Grab the JBPM process
        byte[] metadataBytes = packageDetails.getMetadata();
        if (metadataBytes == null) {
            return null;
        }

        String process = new String(metadataBytes);

        // Load the JBPM process into the executor
        ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(process);

        // Iterate over the nodes manually, creating a domain representation of the step in the process
        SuperState mainProcess = (SuperState) processDefinition.getNode("main_process");
        if (mainProcess == null) {
            log.warn("Could not retrieve main process for package [" + packageDetails + "]");
            return null;
        }

        List<DeployPackageStep> steps = new ArrayList<DeployPackageStep>();
        List<Node> nodes = mainProcess.getNodes();

        for (Node node : nodes) {

            Action action = node.getAction();
            if (action != null) {

                Delegation delegation = action.getActionDelegation();
                String configProps = delegation.getConfiguration();
                String actionHandlerClassName = delegation.getClassName();
                FieldInstantiator instantiator = new FieldInstantiator();

                BaseHandler handler = (BaseHandler) instantiator.instantiate(Class.forName(actionHandlerClassName),
                    configProps);
                handler.setPropertyDefaults();

                String description = handler.getDescription();

                DeployPackageStep step = new DeployPackageStep(node.getName(), description);
                step.setStepResult(ContentResponseResult.NOT_PERFORMED);
                steps.add(step);
            }
        }

        return steps;
    }

    /**
     * Parses through the entire log list returned from the workflow application and converts the relevant entries into
     * the domain model's package installation steps. Any steps that are not present in the log list will be added
     * from the unexecuted step list and marked as "not executed".
     *
     * @param logs              logs made available from the process instance after executing the workflow
     * @param unexecutedSteps   list of all steps that are to be executed, generated prior to running the workflow
     * @param packageDetailsKey identifies the package that was deployed
     * @return domain representation of the result of the workflow
     */
    private DeployIndividualPackageResponse parseLogs(List logs, List<DeployPackageStep> unexecutedSteps,
        PackageDetailsKey packageDetailsKey) {

        List<DeployPackageStep> steps = new ArrayList<DeployPackageStep>();
        ContentResponseResult overallResult = ContentResponseResult.SUCCESS;

        for (Object uncastedLog : logs) {
            if (uncastedLog instanceof ActionHandlerMessageLog) {
                ActionHandlerMessageLog messageLog = (ActionHandlerMessageLog) uncastedLog;
                DeployPackageStep executedStep = messageLog.getStep();
                steps.add(executedStep);

                // The executed and unexecuted steps are link by their step key and the fact that the equals method
                // uses this key (as such, their descriptions may be different). This is currently simply a counter.
                // That counter is handled in different ways for translate v. logging an actual step. They should
                // still map up, however this log message should output enough of a description for the reader
                // to ensure that the unexecuted step being removed does correspond to the step found in the logs.
                if (log.isDebugEnabled()) {

                    String executedStepKey = executedStep.getStepKey();

                    DeployPackageStep unexecutedStep = null;
                    for (DeployPackageStep s : unexecutedSteps) {
                        if (s.getStepKey().equals(executedStepKey)) {
                            unexecutedStep = s;
                            break;
                        }
                    }

                    if (unexecutedStep != null && log.isDebugEnabled()) {
                        log.debug("Mapped up steps:");
                        log.debug("Executed Step: " + executedStep);
                        log.debug("Unexecuted Step: " + unexecutedStep);
                    }
                }

                boolean unexecutedStepDeleted = unexecutedSteps.remove(executedStep);
                if (!unexecutedStepDeleted) {
                    log.warn("Could not remove the following step from the unexecuted step list: " + executedStep);
                }

                // If any steps fail, mark the entire response as failed
                if (executedStep.getStepResult() == ContentResponseResult.FAILURE) {
                    overallResult = ContentResponseResult.FAILURE;
                }
            }
        }

        // For every step that was supposed to run but didn't (those remaining in the unexecuted step list), tack
        // them on to the end as not executed.
        for (DeployPackageStep unexecutedStep : unexecutedSteps) {
            unexecutedStep.setStepResult(ContentResponseResult.NOT_PERFORMED);
            steps.add(unexecutedStep);
        }

        DeployIndividualPackageResponse response = new DeployIndividualPackageResponse(packageDetailsKey, overallResult);
        response.setDeploymentSteps(steps);

        return response;
    }

    /**
     * Creates a temporary directory in which to store the bits being downloaded from the server. The name of the
     * directory will be generated by Java's built in algorithm and returned to the caller.
     *
     * @param prefix will be the first part of the file, with Java file utilities coming up with the remainder of the
     *               file name
     * @return name of the unique, randomly generated temporary directory
     * @throws IOException if the process cannot create a temporary file
     */
    private File createTempDir(String prefix) throws IOException {
        // Let's reuse the algorithm the JDK uses to determine a unique name:
        // 1) create a temp file to get a unique name using JDK createTempFile
        // 2) then quickly delete the file and...
        // 3) convert it to a directory
        // ccrouch, version 1.4

        File tmpDir = File.createTempFile(prefix, "", null); // create file with unique name
        boolean deleteSuccessful = tmpDir.delete(); // delete the tmp file and...
        boolean mkdirSuccessful = tmpDir.mkdirs(); // ...convert it to a directory

        if (deleteSuccessful && mkdirSuccessful) {
            return tmpDir;
        } else {
            return null;
        }
    }

    /**
     * Will throw an exception if the package does not contain a set of compatible installation instructions.
     *
     * @param packageDetails package in question
     * @throws UnsupportedOperationException if the instruction compatibility version is not supported
     */
    private void checkCompatibility(ResourcePackageDetails packageDetails) {
        Configuration extraProperties = packageDetails.getExtraProperties();

        // Check to make sure we know how to parse the instruction set
        String compatibilityVersion = safeGet(extraProperties, "instructionCompatibilityVersion");
        if (compatibilityVersion != null && !compatibilityVersion.equals("1.4")) {
            throw new UnsupportedOperationException("Instruction set for this package is not supported. Version: "
                + compatibilityVersion);
        }
    }

    /**
     * Converts the domain model package representation into the legacy software object that is referenced from the JBPM
     * workflows.
     *
     * @param pkg sent from the server to be installed, the values for the software entity will be taken from here
     * @return object mirroring the domain's package representation usable for substitutions into the workflow
     */
    private SoftwareValue resourcePackageDetailsToSoftwareValue(ResourcePackageDetails pkg) {
        Configuration extraProperties = pkg.getExtraProperties();

        SoftwareValue softwareValue = new SoftwareValue();

        softwareValue.setDownloadUrl(pkg.getLocation());
        softwareValue.setFilename(pkg.getFileName());
        softwareValue.setFileSize(pkg.getFileSize());
        softwareValue.setInstructionCompatibilityVersion(safeGet(extraProperties, "instructionCompatibilityVersion"));
        softwareValue.setIssueReference(safeGet(extraProperties, "jiraId"));
        softwareValue.setLicenseName(pkg.getLicenseName());
        softwareValue.setLicenseVersion(pkg.getLicenseVersion());
        softwareValue.setLongDescription(pkg.getLongDescription());
        softwareValue.setMD5(pkg.getMD5());
        softwareValue.setSHA256(pkg.getSHA256());
        softwareValue.setShortDescription(pkg.getShortDescription());
        softwareValue.setTitle(pkg.getName());

        return softwareValue;
    }

    /**
     * Utility to extract a potentially null value from a configuration.
     *
     * @param configuration may be <code>null</code>
     * @param key           value being retrieved
     * @return value if it is found in the configuration; <code>null</code> otherwise.
     */
    private String safeGet(Configuration configuration, String key) {
        if (configuration == null) {
            return null;
        }

        PropertySimple simple = configuration.getSimple(key);

        return (simple != null) ? simple.getStringValue() : null;
    }
}