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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import org.rhq.bundle.filetemplate.recipe.RecipeContext;
import org.rhq.bundle.filetemplate.recipe.RecipeParser;
import org.rhq.core.domain.bundle.BundleResourceDeployment;
import org.rhq.core.domain.bundle.BundleResourceDeploymentHistory;
import org.rhq.core.domain.content.PackageVersion;
import org.rhq.core.pluginapi.bundle.BundleManagerProvider;
import org.rhq.core.system.OperatingSystemType;
import org.rhq.core.system.ProcessExecution;
import org.rhq.core.system.ProcessExecutionResults;
import org.rhq.core.system.SystemInfo;
import org.rhq.core.util.file.FileUtil;

/**
 * A recipe context that processes the recipe as it adds stuff to the context
 * 
 * @author John Mazzitelli
 */
public class ProcessingRecipeContext extends RecipeContext {

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

    private final SystemInfo systemInfo;
    private final String baseWorkingDirectory;
    private final BundleResourceDeployment bundleResourceDeployment;
    private final BundleManagerProvider bundleManagerProvider;

    public ProcessingRecipeContext(String recipe, Map<PackageVersion, File> packageVersionFiles, SystemInfo systemInfo,
        String baseWorkingDirectory, BundleResourceDeployment bundleResourceDeployment,
        BundleManagerProvider bundleManagerProvider) {

        super(recipe);
        this.systemInfo = systemInfo;
        this.baseWorkingDirectory = baseWorkingDirectory; // the directory that existing bundle filenames will be relative to
        this.bundleResourceDeployment = bundleResourceDeployment;
        this.bundleManagerProvider = bundleManagerProvider;
    }

    @Override
    public void addDeployFile(String filename, String directory) {
        super.addDeployFile(filename, directory);

        String msg;
        File existingFile = new File(this.baseWorkingDirectory, filename);
        ProcessExecution pe = getUnzipExecution(existingFile, directory);

        if (pe != null) {
            ProcessExecutionResults results = this.systemInfo.executeProcess(pe);
            if (results.getError() != null) {
                msg = "Could not unbundle file [" + pe + "]: " + results;
                audit("deploy", BundleResourceDeploymentHistory.Status.FAILURE, msg);
                throw new RuntimeException(msg, results.getError());
            } else if (results.getExitCode() == null || results.getExitCode().intValue() > 0) {
                msg = "Failed to unbundle file [" + pe + "]: " + results;
                audit("deploy", BundleResourceDeploymentHistory.Status.FAILURE, msg);
                throw new RuntimeException(msg);
            } else {
                msg = "extracted files from [" + existingFile + "] to [" + directory + "]";
            }
            // existingFile.delete(); WOULD WE WANT TO REMOVE THE COMPRESSED FILE?
            audit("deploy", BundleResourceDeploymentHistory.Status.SUCCESS, msg);
        } else {
            // not a zipped format - just move the file to the directory as-is
            File newFile = new File(directory, filename);
            if (!existingFile.renameTo(newFile)) {
                msg = "Failed to move [" + existingFile + "] to [" + newFile + "]";
                audit("deploy", BundleResourceDeploymentHistory.Status.FAILURE, msg);
                throw new RuntimeException(msg);
            }
            audit("deploy", BundleResourceDeploymentHistory.Status.SUCCESS, "renamed [" + existingFile + "] to ["
                + newFile + "]");
        }
    }

    @Override
    public void addFile(String source, String destination) {
        super.addFile(source, destination);

        File sourceFile = new File(this.baseWorkingDirectory, source);
        File destinationFile = new File(destination);

        try {
            destinationFile.getParentFile().mkdirs();
            FileUtil.copyFile(sourceFile, destinationFile);
            audit("file", BundleResourceDeploymentHistory.Status.SUCCESS, "copied file [" + sourceFile + "] to ["
                + destinationFile + "]");
        } catch (Exception e) {
            String msg = "Failed to copy file [" + sourceFile + "] to [" + destinationFile + "]";
            audit("file", BundleResourceDeploymentHistory.Status.FAILURE, msg);
            throw new RuntimeException(msg, e);
        }

        return;
    }

    @Override
    public void addRealizedFile(String file) {
        super.addRealizedFile(file);

        String msg;
        File trueFile = new File(file);
        RecipeParser parser = getParser();
        File realizedTmpFile = null;
        FileWriter realizedTmpFileWriter = null;
        BufferedReader reader = null;

        try {

            realizedTmpFile = File.createTempFile("rhq-realize-", ".tmp", trueFile.getParentFile());
            realizedTmpFileWriter = new FileWriter(realizedTmpFile);

            reader = new BufferedReader(new FileReader(trueFile));
            String line = reader.readLine();
            while (line != null) {
                line = parser.replaceReplacementVariables(this, line);
                realizedTmpFileWriter.write(line);
                realizedTmpFileWriter.write("\n");
                line = reader.readLine();
            }

            realizedTmpFileWriter.close();
            realizedTmpFileWriter = null;
            reader.close();
            reader = null;

            audit("realize", BundleResourceDeploymentHistory.Status.SUCCESS, "realized [" + file + "]");

            trueFile.delete(); // remove the one with the replacement variables in it
            if (!realizedTmpFile.renameTo(trueFile)) {
                msg = "Failed to rename realized tmp file [" + realizedTmpFile + "] to [" + trueFile + "]";
                audit("realize", BundleResourceDeploymentHistory.Status.FAILURE, msg);
                throw new RuntimeException(msg);
            }

            audit("realize", BundleResourceDeploymentHistory.Status.SUCCESS, "renamed realized file ["
                + realizedTmpFile + "] to [" + trueFile + "]");
        } catch (Exception e) {
            msg = "Cannot realize file [" + file + "]";
            audit("realize", BundleResourceDeploymentHistory.Status.FAILURE, msg);
            throw new RuntimeException(msg, e);
        } finally {
            if (realizedTmpFileWriter != null) {
                try {
                    realizedTmpFileWriter.close();
                } catch (Exception e) {
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception e) {
                }
            }
            if (realizedTmpFile != null && realizedTmpFile.exists()) {
                realizedTmpFile.delete();
            }
        }
    }

    @Override
    public void addReplacementVariables(Set<String> replacementVariables) {
        super.addReplacementVariables(replacementVariables);
    }

    @Override
    public void addScript(String exe, List<String> exeArgs) {
        super.addScript(exe, exeArgs);

        File scriptFile = new File(this.baseWorkingDirectory, exe);

        ensureExecutable(scriptFile);

        ProcessExecution pe = new ProcessExecution(scriptFile.getAbsolutePath());
        pe.setArguments(exeArgs);
        pe.setWaitForCompletion(30 * 60 * 1000L);
        pe.setWorkingDirectory(scriptFile.getParent());

        String msg;
        ProcessExecutionResults results = this.systemInfo.executeProcess(pe);
        if (results.getError() != null) {
            msg = "Could not execute script [" + pe + "]: " + results;
            audit("script", BundleResourceDeploymentHistory.Status.FAILURE, msg);
            throw new RuntimeException(msg, results.getError());
        } else {
            msg = "Executed script [" + pe + "]";
            audit("script", BundleResourceDeploymentHistory.Status.SUCCESS, msg);
        }

        return;
    }

    @Override
    public void addCommand(String exe, List<String> exeArgs) {
        super.addCommand(exe, exeArgs);

        ProcessExecution pe = new ProcessExecution(exe);
        pe.setArguments(exeArgs);
        pe.setWaitForCompletion(30 * 60 * 1000L);
        pe.setCheckExecutableExists(false);
        pe.setWorkingDirectory(this.baseWorkingDirectory);

        String msg;
        ProcessExecutionResults results = this.systemInfo.executeProcess(pe);
        if (results.getError() != null) {
            msg = "Could not execute command [" + pe + "]: " + results;
            audit("command", BundleResourceDeploymentHistory.Status.FAILURE, msg);
            throw new RuntimeException(msg, results.getError());
        } else {
            msg = "Executed command [" + pe + "]";
            audit("command", BundleResourceDeploymentHistory.Status.SUCCESS, msg);
        }

        return;
    }

    private void audit(String action, BundleResourceDeploymentHistory.Status status, String message) {
        try {
            bundleManagerProvider.auditDeployment(bundleResourceDeployment, action, "recipe", null, status, message,
                null);
            if (log.isDebugEnabled()) {
                log.debug("Deployment [" + bundleResourceDeployment.getBundleDeployment().getBundleVersion()
                    + "] audit: action=[" + action + "], status=[" + status + "], message: " + message);
            }
        } catch (Exception e) {
            log.warn("Failed to send audit message for deployment of ["
                + bundleResourceDeployment.getBundleDeployment().getBundleVersion() + "]. audit action=[" + action
                + "], status=[" + status + "], message: " + message);
        }
    }

    private void ensureExecutable(File scriptFile) {
        boolean success;

        try {
            Method m = File.class.getMethod("setExecutable", Boolean.TYPE, Boolean.TYPE);
            Boolean r = (Boolean) m.invoke(scriptFile, Boolean.TRUE, Boolean.TRUE);
            success = r.booleanValue();
        } catch (Exception e) {
            // Oh, if only we'd just stop dilly-dallying and move to JDK6 already
            if (this.systemInfo.getOperatingSystemType() != OperatingSystemType.WINDOWS) {
                ProcessExecution pe = new ProcessExecution("chmod");
                pe.setArguments(new String[] { "u+x", scriptFile.getAbsolutePath() });
                pe.setWaitForCompletion(30000L);
                pe.setCheckExecutableExists(false);
                pe.setWorkingDirectory(scriptFile.getParent());
                ProcessExecutionResults chmodResults = this.systemInfo.executeProcess(pe);
                success = (chmodResults.getExitCode() != null && chmodResults.getExitCode().intValue() == 0);
            } else {
                success = true; // assume we can execute it on windows
            }
        }

        if (!success) {
            String msg = "Cannot ensure that script [" + scriptFile + "] is executable";
            audit("ensureExecutable", BundleResourceDeploymentHistory.Status.FAILURE, msg);
            throw new RuntimeException(msg);
        }

        return;
    }

    private ProcessExecution getUnzipExecution(File file, String directory) {
        String exe;
        List<String> args = null;

        String filepath = file.getAbsolutePath();

        if (filepath.endsWith(".tar")) {
            exe = "tar";
            args = new ArrayList<String>();
            args.add("xf");
            args.add(filepath);
            args.add("-C");
            args.add(directory);
        } else if (filepath.endsWith(".tar.bz2") || filepath.endsWith(".tbz2") || filepath.endsWith(".tbz")) {
            exe = "tar";
            args = new ArrayList<String>();
            args.add("xfj");
            args.add(filepath);
            args.add("-C");
            args.add(directory);
        } else if (filepath.endsWith(".tar.gz") || filepath.endsWith(".tgz")) {
            exe = "tar";
            args = new ArrayList<String>();
            args.add("xfz");
            args.add(filepath);
            args.add("-C");
            args.add(directory);
        } else if (filepath.endsWith(".zip")) {
            exe = "unzip";
            args = new ArrayList<String>();
            args.add("-o");
            args.add(filepath);
            args.add("-d");
            args.add(directory);
        } else if (filepath.endsWith(".rpm")) {
            exe = "rpm";
            args = new ArrayList<String>();
            args.add("-i");
            args.add(filepath);
            if (directory != null && directory.length() > 0) {
                args.add("--prefix");
                args.add(directory);
            }
        } else {
            return null; // isn't a unzippable package, we'll just copy it as is
        }

        ProcessExecution pe = new ProcessExecution(exe);
        pe.setArguments(args);
        pe.setWaitForCompletion(30 * 60 * 1000L);
        pe.setCheckExecutableExists(false);
        pe.setWorkingDirectory(this.baseWorkingDirectory);
        return pe;
    }
}
