/*
 * 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.jbossas;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

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

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.core.domain.content.PackageType;
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.DeployPackagesResponse;
import org.rhq.core.domain.content.transfer.RemovePackagesResponse;
import org.rhq.core.domain.content.transfer.ResourcePackageDetails;
import org.rhq.core.domain.measurement.MeasurementDataTrait;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.pluginapi.content.ContentFacet;
import org.rhq.core.pluginapi.content.ContentServices;
import org.rhq.core.pluginapi.inventory.DeleteResourceFacet;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.pluginapi.util.FileUtils;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.ZipUtil;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.file.JarContentFileInfo;
import org.rhq.plugins.jbossas.helper.MainDeployer;
import org.rhq.plugins.jmx.MBeanResourceComponent;

/**
 * A resource component for managing an application (e.g. EAR or WAR) deployed to a JBossAS server.
 *
 * @author Ian Springer
 */
public class ApplicationComponent extends MBeanResourceComponent<JBossASServerComponent> implements ContentFacet,
    DeleteResourceFacet, OperationFacet {
    private static final String BACKUP_FILE_EXTENSION = ".rej";
    public static final String RHQ_SHA256 = "RHQ-Sha256";

    /**
     * Name of the backing package type that will be used when discovering packages. This corresponds to the name
     * of the package type defined in the plugin descriptor. For simplicity, the package type for both EARs and
     * WARs is simply called "file". This is still unique within the context of the parent resource type and lets
     * this class use the same package type name in both cases.
     */
    private static final String PKG_TYPE_FILE = "file";

    /**
     * Architecture string used in describing discovered packages.
     */
    private static final String ARCHITECTURE = "noarch";

    protected static final String FILENAME_PLUGIN_CONFIG_PROP = "filename";

    private static final String APPLICATION_PATH_TRAIT = "Application.path";
    private static final String APPLICATION_EXPLODED_TRAIT = "Application.exploded";

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

    // ContentFacet Implementation  --------------------------------------------

    public InputStream retrievePackageBits(ResourcePackageDetails packageDetails) {

        Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
        String fullFileName = pluginConfiguration.getSimpleValue(FILENAME_PLUGIN_CONFIG_PROP, null);

        File packageFile = new File(fullFileName);
        File fileToSend;
        try {
            if (packageFile.isDirectory()) {
                fileToSend = File.createTempFile("rhq", ".zip");
                ZipUtil.zipFileOrDirectory(packageFile, fileToSend);
            } else
                fileToSend = packageFile;
            return new BufferedInputStream(new FileInputStream(fileToSend));
        } catch (IOException e) {
            throw new RuntimeException("Failed to retrieve package bits for " + packageDetails, e);
        }
    }

    public Set<ResourcePackageDetails> discoverDeployedPackages(PackageType type) {

        Set<ResourcePackageDetails> packages = new HashSet<ResourcePackageDetails>();

        Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
        String fullFileName = pluginConfiguration.getSimpleValue(FILENAME_PLUGIN_CONFIG_PROP, null);

        if (fullFileName == null) {
            throw new IllegalStateException(
                "Plugin configuration does not contain the full file name of the EAR/WAR file.");
        }

        // If the parent EAR/WAR resource was found, this file should exist
        File file = new File(fullFileName);
        if (file.exists()) {
            // Package name and file name of the application are the same
            String fileName = new File(fullFileName).getName();

            JarContentFileInfo fileInfo = new JarContentFileInfo(file);
            String sha256 = getSHA256(fileInfo);
            String version = getVersion(fileInfo, sha256);
            PackageDetailsKey key = new PackageDetailsKey(fileName, version, PKG_TYPE_FILE, ARCHITECTURE);
            ResourcePackageDetails details = new ResourcePackageDetails(key);
            details.setFileName(fileName);
            details.setLocation(file.getPath());
            if (!file.isDirectory())
                details.setFileSize(file.length());

            details.setFileCreatedDate(file.lastModified()); // TODO: get created date via SIGAR            
            details.setSHA256(sha256);
            details.setInstallationTimestamp(Long.valueOf(System.currentTimeMillis()));

            packages.add(details);
        }

        return packages;
    }

    // TODO: if needed we can speed this up by looking in the ResourceContainer's installedPackage
    // list for previously discovered packages. If there use the sha256 from that record. We'd have to
    // get access to that info by adding access in org.rhq.core.pluginapi.content.ContentServices
    private String getSHA256(JarContentFileInfo fileInfo) {

        String sha256 = null;

        try {
            sha256 = fileInfo.getAttributeValue(RHQ_SHA256, null);
            if (null == sha256) {
                sha256 = new MessageDigestGenerator(MessageDigestGenerator.SHA_256).calcDigestString(fileInfo
                    .getContentFile());
            }
        } catch (IOException iex) {
            //log exception but move on, discovery happens often. No reason to hold up anything.
            if (log.isDebugEnabled()) {
                log.debug("Problem calculating digest of package [" + fileInfo.getContentFile().getPath() + "]."
                    + iex.getMessage());
            }
        }

        return sha256;
    }

    private String getVersion(JarContentFileInfo fileInfo, String sha256) {
        // Version string in order of preference
        // manifestVersion + sha256, sha256, manifestVersion, "0"
        String version = "0";
        String manifestVersion = fileInfo.getVersion(null);

        if ((null != manifestVersion) && (null != sha256)) {
            // this protects against the occasional differing binaries with poor manifest maintenance  
            version = manifestVersion + " [sha256=" + sha256 + "]";
        } else if (null != sha256) {
            version = "[sha256=" + sha256 + "]";
        } else if (null != manifestVersion) {
            version = manifestVersion;
        }

        return version;
    }

    public RemovePackagesResponse removePackages(Set<ResourcePackageDetails> packages) {
        throw new UnsupportedOperationException("Cannot remove the package backing an EAR/WAR resource.");
    }

    public List<DeployPackageStep> generateInstallationSteps(ResourcePackageDetails packageDetails) {
        // Intentional - there are no steps involved in installing an EAR or WAR.
        return null;
    }

    public DeployPackagesResponse deployPackages(Set<ResourcePackageDetails> packages, ContentServices contentServices) {
        // You can only update the one application file referenced by this resource, so punch out if multiple are
        // specified.
        if (packages.size() != 1) {
            log.warn("Request to update an EAR/WAR file contained multiple packages.");
            DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.FAILURE);
            response
                .setOverallRequestErrorMessage("When deploying an EAR/WAR, only one EAR/WAR can be updated at a time.");
            return response;
        }

        ResourcePackageDetails packageDetails = packages.iterator().next();

        // Find location of existing application
        Configuration pluginConfig = getResourceContext().getPluginConfiguration();
        File appFile = new File(pluginConfig.getSimple(FILENAME_PLUGIN_CONFIG_PROP).getStringValue());
        if (!appFile.exists()) {
            return failApplicationDeployment("Could not find application to update at location: " + appFile,
                packageDetails);
        }

        File tempFile;
        try {
            tempFile = writeNewAppBitsToTempFile(appFile, contentServices, packageDetails);
        } catch (Exception e) {
            return failApplicationDeployment("Error writing new application bits to temporary file - cause: " + e,
                packageDetails);
        }

        // Backup the existing app file/dir to <filename>.rej.
        File backupOfOriginalFile = new File(appFile.getPath() + BACKUP_FILE_EXTENSION);
        appFile.renameTo(backupOfOriginalFile);

        // Write the new bits for the application
        moveTempFileToDeployLocation(tempFile, appFile);

        // The file has been written successfully to the deploy dir. Now try to actually deploy it.
        MainDeployer mainDeployer = getParentResourceComponent().getMainDeployer();
        try {
            mainDeployer.redeploy(appFile);
        } catch (Exception e) {
            // Deploy failed - rollback to the original app file...
            String errorMessage = ThrowableUtil.getAllMessages(e);
            try {
                FileUtils.purge(appFile, true);
                backupOfOriginalFile.renameTo(appFile);
                // Need to redeploy the original file - this generally should succeed.
                mainDeployer.redeploy(appFile);
                errorMessage += " ***** ROLLED BACK TO ORIGINAL APPLICATION FILE. *****";
            } catch (Exception e1) {
                errorMessage += " ***** FAILED TO ROLLBACK TO ORIGINAL APPLICATION FILE. *****: "
                    + ThrowableUtil.getAllMessages(e1);
            }
            return failApplicationDeployment(errorMessage, packageDetails);
        }

        // Deploy was successful!

        deleteBackupOfOriginalFile(backupOfOriginalFile);

        DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.SUCCESS);
        DeployIndividualPackageResponse packageResponse = new DeployIndividualPackageResponse(packageDetails.getKey(),
            ContentResponseResult.SUCCESS);
        response.addPackageResponse(packageResponse);

        return response;
    }

    // DeleteResourceFacet Implementation  --------------------------------------------

    public void deleteResource() throws Exception {
        Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
        String fullFileName = pluginConfiguration.getSimple(FILENAME_PLUGIN_CONFIG_PROP).getStringValue();

        File file = new File(fullFileName);

        if (file.exists()) {
            try {
                getParentResourceComponent().undeployFile(file);
            } catch (Exception e) {
                log.error("Failed to undeploy file [" + file + "].", e);
                throw e;
            } finally {
                try {
                    FileUtils.purge(file, true);
                } catch (IOException e) {
                    log.error("Failed to delete file [" + file + "].", e);
                    // if the undeploy also failed that exception will be lost
                    // and this one will be seen by the caller instead.
                    // arguably both these conditions indicate failure, since
                    // not being able to delete the file will mean that it will
                    // likely get picked up again by the deployment scanner
                    throw e;
                }
            }
        } else {
            log.info("deleteResource: File [" + fullFileName + "] was not found - ignoring.");
        }

    }

    // MeasurementFacet Implementation  --------------------------------------------

    @Override
    public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) {
        if (!requests.isEmpty()) {
            Configuration pluginConfig = getResourceContext().getPluginConfiguration();
            String path = pluginConfig.getSimpleValue(FILENAME_PLUGIN_CONFIG_PROP, null);
            for (MeasurementScheduleRequest request : requests) {
                String metricName = request.getName();
                if (metricName.equals(APPLICATION_PATH_TRAIT)) {
                    MeasurementDataTrait trait = new MeasurementDataTrait(request, path);
                    report.addData(trait);
                } else if (metricName.equals(APPLICATION_EXPLODED_TRAIT)) {
                    boolean exploded = new File(path).isDirectory();
                    MeasurementDataTrait trait = new MeasurementDataTrait(request, (exploded) ? "yes" : "no");
                    report.addData(trait);
                }
            }
        }
    }

    // OperationFacet Implementation ------------------------------------------------

    public OperationResult invokeOperation(String name, Configuration parameters) throws InterruptedException,
        Exception {
        if ("revert".equals(name)) {
            try {
                revertFromBackupFile();
                return new OperationResult("Successfully reverted from backup");
            } catch (Exception e) {
                throw new RuntimeException("Error reverting from Backup: " + e.getMessage());
            }
        } else {
            return super.invokeOperation(name, parameters);
        }
    }

    // Public  --------------------------------------------

    /**
     * Returns the name of the application.
     *
     * @return application name
     */
    public String getApplicationName() {
        String resourceKey = getResourceContext().getResourceKey();
        return resourceKey.substring(resourceKey.lastIndexOf('=') + 1);
    }

    /**
     * Returns the file name of this application.
     *
     * @return full directory and file name of the application
     */
    public String getFileName() {
        Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
        return pluginConfiguration.getSimple(FILENAME_PLUGIN_CONFIG_PROP).getStringValue();
    }

    public JBossASServerComponent getParentResourceComponent() {
        return getResourceContext().getParentResourceComponent();
    }

    /**
     * Revert the resource from a possibly existing backup file with the same basename
     * and ending in .bak
     * @throws Exception If anything goes wrong.
     */
    public void revertFromBackupFile() throws Exception {
        Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
        String fullFileName = pluginConfiguration.getSimple(FILENAME_PLUGIN_CONFIG_PROP).getStringValue();

        File backup;
        String fileName;
        // Check if we have an exploded war or not.
        if (fullFileName != null && fullFileName.endsWith("/")) {
            fileName = fullFileName.substring(0, fullFileName.length() - 1);
        } else {
            fileName = fullFileName;
        }

        backup = new File(fileName + ".bak");

        if (!backup.exists()) {
            throw new FileNotFoundException("Backup file " + backup + " does not exist");
        }
        File directory = backup.getParentFile();
        if (!directory.canWrite()) {
            throw new IOException("Can not modify directory " + directory);
        }

        File tmpBackup = new File(fileName + ".tmp.bak");
        File file = new File(fullFileName);
        boolean good = file.renameTo(tmpBackup);

        if (!good) {
            throw new IOException("Aborted as it is not possible to move to original to a tmp backup at " + tmpBackup);
        }

        // Now that we have moved the original to a backup, try to move the real backup in

        good = backup.renameTo(file);
        if (!good) {
            // move backup in failed
            good = tmpBackup.renameTo(file);
            if (!good) {
                throw new IOException("Installing backup " + backup + " failed and re-installing the original from "
                    + tmpBackup + " failed too. Please correct this manually");
            }
        } else {
            // installing from backup worked
            FileUtils.purge(tmpBackup, true);
        }
        getParentResourceComponent().deployFile(file);

    }

    /**
     * Creates the necessary transfer objects to report a failed application deployment (update).
     *
     * @param errorMessage   reason the deploy failed
     * @param packageDetails describes the update being made
     *
     * @return response populated to reflect a failure
     */
    private DeployPackagesResponse failApplicationDeployment(String errorMessage, ResourcePackageDetails packageDetails) {
        DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.FAILURE);

        DeployIndividualPackageResponse packageResponse = new DeployIndividualPackageResponse(packageDetails.getKey(),
            ContentResponseResult.FAILURE);
        packageResponse.setErrorMessage(errorMessage);

        response.addPackageResponse(packageResponse);

        return response;
    }

    private void deleteBackupOfOriginalFile(File backupOfOriginalFile) {
        try {
            FileUtils.purge(backupOfOriginalFile, true);
        } catch (Exception e) {
            // not critical.
            log.warn("Failed to delete backup of original file: " + backupOfOriginalFile);
        }
    }

    private void moveTempFileToDeployLocation(File tempFile, File appFile) {
        InputStream tempIs = null;
        try {
            if (appFile.isDirectory()) {
                tempIs = new BufferedInputStream(new FileInputStream(tempFile));
                appFile.mkdir();
                ZipUtil.unzipFile(tempIs, appFile);
            } else {
                tempFile.renameTo(appFile);
            }
        } catch (IOException e) {
            log.error("Error writing updated package bits to the existing application location: " + appFile, e);
            //return failApplicationDeployment("Error writing updated package bits to the existing application location: " +
            //    appFile, packageDetails);
        } finally {
            if (tempIs != null) {
                try {
                    tempIs.close();
                } catch (IOException e) {
                    log.error("Error closing temporary input stream", e);
                }
            }
        }
    }

    private File writeNewAppBitsToTempFile(File file, ContentServices contentServices,
        ResourcePackageDetails packageDetails) throws Exception {
        File tempDir = getResourceContext().getTemporaryDirectory();
        File tempFile = new File(tempDir.getAbsolutePath(), file.getName() + System.currentTimeMillis());

        // The temp file shouldn't be there, but check and delete it if it is
        if (tempFile.exists()) {
            log.warn("Existing temporary file found and will be deleted at: " + tempFile);
            tempFile.delete();
        }
        OutputStream tempOutputStream = null;
        try {
            tempOutputStream = new BufferedOutputStream(new FileOutputStream(tempFile));
            contentServices.downloadPackageBits(getResourceContext().getContentContext(), packageDetails.getKey(),
                tempOutputStream, true);
        } catch (IOException e) {
            log.error("Error writing updated application bits to temporary location: " + tempFile, e);
            throw e;
        } finally {
            if (tempOutputStream != null) {
                try {
                    tempOutputStream.close();
                } catch (IOException e) {
                    log.error("Error closing temporary output stream", e);
                }
            }
        }
        if (!tempFile.exists()) {
            log.error("Temporary file for application update not written to: " + tempFile);
            throw new Exception();
        }
        return tempFile;
    }
}
