/*
 * 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, 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.rhq.core.pluginapi.util;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

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

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyList;
import org.rhq.core.domain.configuration.PropertyMap;
import org.rhq.core.util.stream.StreamUtil;

/**
 * A common snapshot report that allows you to prepare a zip'ed file containing
 * a snapshot of content belonging to a resource. This report contains a snapshot
 * configuration, log and data files along with a customizable set of additional files.
 * Users of this object can pick and choose which types of files to snapshot and filter
 * the files of those types.
 * 
 * This class can be subclassed if you wish to alter the way it collects and stores
 * the snapshot content. For example, if the configuration is not stored as files, but instead
 * stored in memory, you can subclass this to obtain the configuration by reading memory, writing
 * that data to a temporary file and then having this class add that temporary file to the
 * snapshot zip file.
 *
 * @author John Mazzitelli
 */
public class SnapshotReport {
    private static final Log log = LogFactory.getLog(SnapshotReport.class);

    /**
     * The relative directory under the report where the config files can be found. This
     * is the directory that will be found inside the zip file.
     */
    public static final String REPORT_CONFIG_DIRECTORY = "config";

    /**
     * The relative directory under the report where the log files can be found. This
     * is the directory that will be found inside the zip file.
     */
    public static final String REPORT_LOG_DIRECTORY = "log";

    /**
     * The relative directory under the report where the data files can be found. This
     * is the directory that will be found inside the zip file.
     */
    public static final String REPORT_DATA_DIRECTORY = "data";

    /**
     * A boolean config property that dictates if a snapshot of config files should be in the report.
     */
    public static final String PROP_SNAPSHOT_CONFIG_FILES = "snapshotConfigEnabled";

    /**
     * A config property whose value is the directory relative to the base directory where the
     * configuration files are located.
     */
    public static final String PROP_CONFIG_DIRECTORY = "snapshotConfigDirectory";

    /**
     * A config property whose value is a regular expression that matches all config files
     * that are to be snapshotted. This regex must match files under the config directory.
     */
    public static final String PROP_CONFIG_REGEX = "snapshotConfigRegex";

    /**
     * A config property whose value is the boolean flag to indicate if
     * the snapshot should include files found in subdirectories of the config directory.
     */
    public static final String PROP_CONFIG_RECURSIVE = "snapshotConfigRecursive";

    /**
     * A boolean config property that dictates if a snapshot of log files should be in the report.
     */
    public static final String PROP_SNAPSHOT_LOG_FILES = "snapshotLogEnabled";

    /**
     * A config property whose value is the directory relative to the base directory where the
     * log files are located.
     */
    public static final String PROP_LOG_DIRECTORY = "snapshotLogDirectory";

    /**
     * A config property whose value is a regular expression that matches all log files
     * that are to be snapshotted. This regex must match files under the log directory.
     */
    public static final String PROP_LOG_REGEX = "snapshotLogRegex";

    /**
     * A config property whose value is the boolean flag to indicate if
     * the snapshot should include files found in subdirectories of the log directory.
     */
    public static final String PROP_LOG_RECURSIVE = "snapshotLogRecursive";

    /**
     * A boolean config property that dictates if a snapshot of data files should be in the report.
     */
    public static final String PROP_SNAPSHOT_DATA_FILES = "snapshotDataEnabled";

    /**
     * A config property whose value is the directory relative to the base directory where the
     * data files are located.
     */
    public static final String PROP_DATA_DIRECTORY = "snapshotDataDirectory";

    /**
     * A config property whose value is a regular expression that matches all data files
     * that are to be snapshotted. This regex must match files under the data directory.
     */
    public static final String PROP_DATA_REGEX = "snapshotDataRegex";

    /**
     * A config property whose value is the boolean flag to indicate if
     * the snapshot should include files found in subdirectories of the data directory.
     */
    public static final String PROP_DATA_RECURSIVE = "snapshotDataRecursive";

    /**
     * A boolean config property that dictates if a snapshot of the additional files should be in the report.
     */
    public static final String PROP_SNAPSHOT_ADDITIONAL_FILES = "snapshotAdditionalFilesEnabled";

    /**
     * The property name for the list of of additional files.
     */
    public static final String PROP_ADDITIONAL_FILES_LIST = "snapshotAdditionalFilesList";

    /**
     * A config property whose value is a directory relative to the base directory where
     * additional files are located.
     * This should be a property inside a map where that map is a list item within a additional files list.
     */
    public static final String PROP_ADDITIONAL_FILES_DIRECTORY = "snapshotAdditionalFilesDirectory";

    /**
     * A config property whose value is a regular expression that matches all additional files
     * that are to be snapshotted. This regex must match files under its associated
     * additional files directory.
     * This should be a property inside a map where that map is a list item within a additional files list.
     */
    public static final String PROP_ADDITIONAL_FILES_REGEX = "snapshotAdditionalFilesRegex";

    /**
     * A config property whose value is the boolean flag to indicate if
     * the snapshot should include files found in subdirectories of the additional files directory.
     */
    public static final String PROP_ADDITIONAL_FILES_RECURSIVE = "snapshotAdditionalFilesRecursive";

    /**
     * A config property whose value is the base directory where all config, logs and data files are under.
     * If a config, log or data directory was already specified as an absolute directory, this base directory
     * will not be used. Only when a config, log or data directory is relative will it be assumed to be under
     * the base directory.
     */
    public static final String PROP_BASE_DIRECTORY = "snapshotBaseDirectory";

    /**
     * Optional property that can be specified to define where to store the output snapshot report.
     * If not specified, the platform's tmp directory will be used.
     */
    public static final String PROP_REPORT_OUTPUT_DIRECTORY = "snapshotReportOutputDirectory";

    private final String name;
    private final String description;
    private final Configuration configuration;

    public SnapshotReport(String name, String description, Configuration config) {
        this.name = name;
        this.description = description;
        this.configuration = config;
    }

    public File generate() throws Exception {
        File outputFile = getSnapshotReportFile();

        if (log.isDebugEnabled()) {
            log.debug("Generating snapshot [" + outputFile + "]");
        }

        FileOutputStream fos = new FileOutputStream(outputFile);
        ZipOutputStream zip = new ZipOutputStream(fos);
        try {
            // generate the file that will contain some general info about the snapshot report
            ZipEntry zipEntry = new ZipEntry("snapshot.properties");
            zip.putNextEntry(zipEntry);
            Properties properties = new Properties();
            properties.setProperty("name", getName());
            properties.setProperty("description", getDescription());
            properties.setProperty("epochmillis", Long.toString(System.currentTimeMillis()));
            properties.store(zip, null);

            // now store a snapshot of each file to the report
            Map<String, URL> allFiles = getAllFilesToSnapshot();
            for (Map.Entry<String, URL> snapshotFileEntry : allFiles.entrySet()) {
                zipEntry = new ZipEntry(snapshotFileEntry.getKey());
                zip.putNextEntry(zipEntry);
                InputStream input = snapshotFileEntry.getValue().openStream();
                try {
                    StreamUtil.copy(input, zip, false);
                } finally {
                    input.close();
                }
            }
        } finally {
            zip.close();
        }

        if (log.isDebugEnabled()) {
            log.debug("Generated snapshot [" + outputFile + "] of size [" + outputFile.length() + "]");
        }

        return outputFile;
    }

    protected String getName() {
        return this.name;
    }

    protected String getDescription() {
        return this.description;
    }

    protected Configuration getConfiguration() {
        return this.configuration;
    }

    /**
     * This only returns the file where the snapshot will be stored. This method does NOT
     * generate the actual snapshot report. Call {@link #generate()} to get the full snapshot content.
     * 
     * @return the file where the snapshot report will be stored when it is generated
     *
     * @throws Exception if the file could not be determined for some reason
     */
    protected File getSnapshotReportFile() throws Exception {
        String dirString = getConfiguration().getSimpleValue(PROP_REPORT_OUTPUT_DIRECTORY, null);
        File dir = null;
        if (dirString != null) {
            dir = new File(dirString);
            dir.mkdirs();
        }
        File outputFile = File.createTempFile(getName(), ".zip", dir);
        return outputFile;
    }

    protected Map<String, URL> getAllFilesToSnapshot() throws Exception {
        Map<String, URL> allFiles = new HashMap<String, URL>();

        Map<String, URL> configFiles = getConfigFilesToSnapshot();
        if (configFiles != null) {
            allFiles.putAll(configFiles);
        }

        Map<String, URL> logFiles = getLogFilesToSnapshot();
        if (logFiles != null) {
            allFiles.putAll(logFiles);
        }

        Map<String, URL> dataFiles = getDataFilesToSnapshot();
        if (dataFiles != null) {
            allFiles.putAll(dataFiles);
        }

        Map<String, URL> additionalFiles = getAdditionalFilesToSnapshot();
        if (additionalFiles != null) {
            allFiles.putAll(additionalFiles);
        }

        return allFiles;
    }

    protected Map<String, URL> getConfigFilesToSnapshot() throws Exception {
        Configuration config = getConfiguration();
        if (!"true".equals(config.getSimpleValue(PROP_SNAPSHOT_CONFIG_FILES, "false"))) {
            return null; // config files are not to be snapshotted into the report, abort
        }

        String baseDir = config.getSimpleValue(PROP_BASE_DIRECTORY, null);
        String confDir = config.getSimpleValue(PROP_CONFIG_DIRECTORY, "conf");
        String confRegex = config.getSimpleValue(PROP_CONFIG_REGEX, null);
        String recursive = config.getSimpleValue(PROP_CONFIG_RECURSIVE, "false");

        Map<String, URL> filesMap = null;

        File confDirFile = new File(confDir);
        if (!confDirFile.isAbsolute()) {
            confDirFile = new File(baseDir, confDir);
        }

        RegexFilenameFilter filter = new RegexFilenameFilter(confRegex, recursive);
        File[] confFiles = confDirFile.listFiles(filter);
        if (confFiles != null) {
            filesMap = new HashMap<String, URL>(confFiles.length);
            populateFilesMap(confFiles, filesMap, REPORT_CONFIG_DIRECTORY, filter);
        } else {
            // the directory probably doesn't exist, this may not be fatal so just log and keep going
            log.warn("Failed to get list of conf files from [" + confDirFile + "]");
        }

        return filesMap;
    }

    protected Map<String, URL> getLogFilesToSnapshot() throws Exception {
        Configuration config = getConfiguration();
        if (!"true".equals(config.getSimpleValue(PROP_SNAPSHOT_LOG_FILES, "false"))) {
            return null; // log files are not to be snapshotted into the report, abort
        }

        String baseDir = config.getSimpleValue(PROP_BASE_DIRECTORY, null);
        String logDir = config.getSimpleValue(PROP_LOG_DIRECTORY, "logs");
        String logRegex = config.getSimpleValue(PROP_LOG_REGEX, null);
        String recursive = config.getSimpleValue(PROP_LOG_RECURSIVE, "false");

        Map<String, URL> filesMap = null;

        File logDirFile = new File(logDir);
        if (!logDirFile.isAbsolute()) {
            logDirFile = new File(baseDir, logDir);
        }

        RegexFilenameFilter filter = new RegexFilenameFilter(logRegex, recursive);
        File[] logFiles = logDirFile.listFiles(filter);
        if (logFiles != null) {
            filesMap = new HashMap<String, URL>(logFiles.length);
            populateFilesMap(logFiles, filesMap, REPORT_LOG_DIRECTORY, filter);
        } else {
            // the directory probably doesn't exist, this may not be fatal so just log and keep going
            log.warn("Failed to get list of log files from [" + logDirFile + "]");
        }

        return filesMap;
    }

    protected Map<String, URL> getDataFilesToSnapshot() throws Exception {
        Configuration config = getConfiguration();
        if (!"true".equals(config.getSimpleValue(PROP_SNAPSHOT_DATA_FILES, "false"))) {
            return null; // data files are not to be snapshotted into the report, abort
        }

        String baseDir = config.getSimpleValue(PROP_BASE_DIRECTORY, null);
        String dataDir = config.getSimpleValue(PROP_DATA_DIRECTORY, "data");
        String dataRegex = config.getSimpleValue(PROP_DATA_REGEX, null);
        String recursive = config.getSimpleValue(PROP_DATA_RECURSIVE, "false");

        Map<String, URL> filesMap = null;

        File dataDirFile = new File(dataDir);
        if (!dataDirFile.isAbsolute()) {
            dataDirFile = new File(baseDir, dataDir);
        }

        RegexFilenameFilter filter = new RegexFilenameFilter(dataRegex, recursive);
        File[] dataFiles = dataDirFile.listFiles(filter);
        if (dataFiles != null) {
            filesMap = new HashMap<String, URL>(dataFiles.length);
            populateFilesMap(dataFiles, filesMap, REPORT_DATA_DIRECTORY, filter);
        } else {
            // the directory probably doesn't exist, this may not be fatal so just log and keep going
            log.warn("Failed to get list of data files from [" + dataDirFile + "]");
        }

        return filesMap;
    }

    protected Map<String, URL> getAdditionalFilesToSnapshot() throws Exception {
        Configuration config = getConfiguration();
        if (!"true".equals(config.getSimpleValue(PROP_SNAPSHOT_ADDITIONAL_FILES, "false"))) {
            return null; // any additional files are not to be snapshotted into the report, abort
        }

        String baseDir = config.getSimpleValue(PROP_BASE_DIRECTORY, null);
        PropertyList additionalList = config.getList(PROP_ADDITIONAL_FILES_LIST);
        if (additionalList == null || additionalList.getList() == null) {
            return null; // there are no additional files defined, just skip it
        }

        Map<String, URL> filesMap = new HashMap<String, URL>();

        for (Property property : additionalList.getList()) {
            PropertyMap additionalFileMap = (PropertyMap) property; // must be a map, let it throw exception if not
            String additionalFilesDir = additionalFileMap.getSimpleValue(PROP_ADDITIONAL_FILES_DIRECTORY, "");
            String additionalFilesRegex = additionalFileMap.getSimpleValue(PROP_ADDITIONAL_FILES_REGEX, null);

            // it is possible that the user wanted to just disable one of the additional files
            String additionalFilesEnabled = additionalFileMap.getSimpleValue(PROP_SNAPSHOT_ADDITIONAL_FILES, "true");
            if (!"true".equals(additionalFilesEnabled)) {
                continue;
            }

            File additionalFilesDirFile = new File(additionalFilesDir);
            if (!additionalFilesDirFile.isAbsolute()) {
                additionalFilesDirFile = new File(baseDir, additionalFilesDir);
            }

            String recursive = additionalFileMap.getSimpleValue(PROP_ADDITIONAL_FILES_RECURSIVE, "false");
            RegexFilenameFilter filter = new RegexFilenameFilter(additionalFilesRegex, recursive);
            File[] additionalFiles = additionalFilesDirFile.listFiles(filter);
            if (additionalFiles != null) {
                populateFilesMap(additionalFiles, filesMap, additionalFilesDir, filter);
            } else {
                // the directory probably doesn't exist, this may not be fatal so just log and keep going
                log.warn("Failed to get list of additional files from [" + additionalFilesDirFile + "]");
            }
        }

        return filesMap;
    }

    private void populateFilesMap(File[] filesDirectories, Map<String, URL> filesMap, String directoryPrefix,
        RegexFilenameFilter filter) throws Exception {

        for (File fileDirectory : filesDirectories) {
            if (fileDirectory.isDirectory()) {
                File[] subDirectoryFiles = fileDirectory.listFiles(filter);
                if (subDirectoryFiles != null) {
                    populateFilesMap(subDirectoryFiles, filesMap, directoryPrefix + "/" + fileDirectory.getName(),
                        filter);
                } else {
                    log.warn("Failed to get subdirectory files for [" + fileDirectory + "]");
                }
            } else {
                filesMap.put(directoryPrefix + '/' + fileDirectory.getName(), fileDirectory.toURI().toURL());
            }
        }

        return;
    }

    /**
     * Filename filter that matches the filename if it matches a given regular expression.
     * If the given regular expression is <code>null</code>, this filter matches everything.
     */
    protected static class RegexFilenameFilter implements FilenameFilter {
        private String regex;
        private boolean recursive;

        public RegexFilenameFilter(String regex, String recursive) {
            this.regex = regex;
            this.recursive = Boolean.parseBoolean(recursive);
        }

        public boolean accept(File dir, String name) {
            if (new File(dir, name).isDirectory()) {
                return recursive;
            }
            return this.regex == null || name.matches(this.regex);
        }
    }
}
