package org.rhq.plugins.byteman;

import java.io.File;
import java.io.FileInputStream;
import java.util.Arrays;
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.jboss.byteman.agent.submit.Submit;

import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.pluginapi.inventory.DeleteResourceFacet;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.util.stream.StreamUtil;

/**
 * Component that represents a script that is deployed in a Byteman agent.
 * 
 * @author John Mazzitelli
 */
public class BytemanScriptComponent implements ResourceComponent<BytemanAgentComponent>, DeleteResourceFacet {

    // protected scope so BytemanAgentComponent can see it
    static final String PKG_TYPE_NAME_SCRIPT = "bytemanScript";

    private final Log log = LogFactory.getLog(BytemanScriptComponent.class);

    private ResourceContext<BytemanAgentComponent> resourceContext;
    private String scriptContent; // cached content of the script that this component is managing
    private List<String> rules; // cached data for all individual rules found in the script content

    public void start(ResourceContext<BytemanAgentComponent> context) {
        this.resourceContext = context;
        getAvailability(); // forces the scripts/rules caches to load
    }

    public void stop() {
        this.scriptContent = null;
        this.rules = null;
    }

    public AvailabilityType getAvailability() {
        try {
            // refresh our cache
            Map<String, String> scripts = this.resourceContext.getParentResourceComponent().getAllKnownScripts();
            if (scripts != null) {
                this.scriptContent = scripts.get(this.resourceContext.getResourceKey());
            } else {
                this.scriptContent = null;
            }

            // if we can, ensure the script is loaded in the byteman agent
            try {
                addDeployedScript();
            } catch (Throwable t) {
                log.warn("Failed to add managed script to the byteman agent - is it up?", t);
            }

            if (this.scriptContent != null) {
                try {
                    this.rules = getBytemanClient().splitAllRulesFromScript(this.scriptContent);
                } catch (Exception e) {
                    log.warn("Failed to split the rules from a script - might be a client problem or data corruption");
                    this.rules = null;
                }
                return AvailabilityType.UP;
            } else {
                this.rules = null;
                return AvailabilityType.DOWN;
            }
        } catch (Exception e) {
            return AvailabilityType.DOWN;
        }
    }

    public void deleteResource() throws Exception {
        // we need the most up-to-date info - so ask the byteman agent for the latest data
        Map<String, String> allScripts = getBytemanClient().getAllScripts();

        // get the script name and the rules content belonging to that script
        String scriptName = this.resourceContext.getResourceKey();
        String scriptRulesContent = allScripts.get(scriptName);

        // attempt to delete the source file.
        // note: the script name is usually the full path name to the script file
        // note: if there is no script file, ignore this - it was probably a script added
        // via a non-RHQ mechanism. We only manage physical script files that we (RHQ) deployed
        File scriptFile = null;
        try {
            scriptFile = getManagedScriptFile(scriptName);
            if (scriptFile != null && scriptFile.isFile()) {
                if (!scriptFile.delete()) {
                    log.warn("The Byteman script file [" + scriptFile + "] failed to delete");
                }
            }
        } catch (Exception e) {
            log.warn("The Byteman script file [" + scriptFile + "] could not be deleted", e);
        }

        // if the script rule is not loaded, we have nothing else to do.
        // if the script is still loaded, tell Byteman to delete it
        if (scriptRulesContent != null) {
            // tell Byteman to delete that script's rules
            Map<String, String> scriptRulesToDelete = new HashMap<String, String>(1);
            scriptRulesToDelete.put(scriptName, scriptRulesContent);
            getBytemanClient().deleteRules(scriptRulesToDelete);

        }

        return;
    }

    /**
     * Returns a cached copy of all known rules for the script since the last availability check was made.
     * 
     * @return the last known set of rules that were loaded in the remote Byteman agent. <code>null</code>
     *         if a problem occurred attempting to get the scripts
     */
    public List<String> getRules() {
        return this.rules;
    }

    public Submit getBytemanClient() {
        return this.resourceContext.getParentResourceComponent().getBytemanClient();
    }

    /**
     * This returns a File representation of a managed script found at the given path.
     * This returns <code>null</code> if the given script path is not a script managed by this plugin.
     * Even if <code>scriptPath</code> points to a valid script file, if it is not managed by this plugin,
     * <code>null</code> will be returned.
     * 
     * @param scriptPath path to check to see if its a managed script, to be converted to a File if so
     * @return the File of the managed script, or <code>null</code> if the given path is not a managed script
     */
    protected File getManagedScriptFile(String scriptPath) {
        File scriptsDataDir = this.resourceContext.getParentResourceComponent().getScriptsDataDirectory();
        File scriptFile = new File(scriptPath);
        File scriptParentDir = scriptFile.getParentFile();
        boolean isManaged = scriptParentDir != null
            && scriptsDataDir.getAbsolutePath().equals(scriptParentDir.getAbsolutePath());
        return (isManaged) ? scriptFile : null;
    }

    /**
     * This method will attempt to ensure that the Byteman agent has the managed script
     * loaded. If a user created this script resource via RHQ (i.e. using the create-child-facet
     * of the parent resource), then this method will always try to ensure that script is loaded
     * in the Byteman agent. If it isn't, it reloads it. If it is, this method does nothing.
     * If this script resource represents an externally managed script (that is, the script
     * was added to the Byteman agent via some other mechanism, such as Byteman's CLI tool),
     * this method will do nothing - in this case, this resource component will declare the
     * resource's availability as DOWN.
     *  
     * @throws Exception
     */
    protected void addDeployedScript() throws Exception {
        // If the script content is null, that means the Byteman agent does not have our script loaded anymore.
        // In this case, force the Byteman agent to reload our managed script. Note that we only force a
        // reload of our script if it was explicitly created via our parent's create child resource facet.
        // If this script was loaded by some other non-RHQ means, we do not attempt to reload it, we'll just
        // show its availability as DOWN and expect the user to correct the situation manually.
        if (this.scriptContent == null) {
            File scriptFile = getManagedScriptFile(this.resourceContext.getResourceKey());
            if (scriptFile != null) {
                getBytemanClient().addRulesFromFiles(Arrays.asList(scriptFile.getAbsolutePath()));
                this.scriptContent = new String(StreamUtil.slurp(new FileInputStream(scriptFile)));
            }
        }
        return;
    }
}
