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

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import mazz.i18n.Logger;

import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.agent.i18n.AgentI18NFactory;
import org.rhq.enterprise.agent.i18n.AgentI18NResourceKeys;

/**
 * Downloads the agent update binary from the server, if one is available.
 *  
 * @author John Mazzitelli
 */
public class AgentUpdateDownload {
    private static final Logger LOG = AgentI18NFactory.getLogger(AgentUpdateDownload.class);

    private final AgentMain agent;
    private final AgentUpdateVersion agentUpdateVersion;
    private File downloadedFile;

    public AgentUpdateDownload(AgentMain agent) {
        this.agent = agent;
        this.agentUpdateVersion = new AgentUpdateVersion(agent);
        this.downloadedFile = null;
    }

    public AgentUpdateVersion getAgentUpdateVersion() {
        return agentUpdateVersion;
    }

    /**
     * Returns the URL that will be accessed to download the agent update binary.
     * 
     * @return download URL
     * 
     * @throws Exception if for some reason a valid URL could not be obtained
     */
    public URL getDownloadUrl() throws Exception {
        return new URL(this.agent.getConfiguration().getAgentUpdateDownloadUrl());
    }

    /**
     * If the agent update binary has been {@link #download() downloaded}, this will return
     * the file where the downloaded content was stored.
     * 
     * @return the file of the downloaded agent update binary; <code>null</code> if it was
     *         never downloaded
     */
    public File getAgentUpdateBinaryFile() {
        return this.downloadedFile;
    }

    /**
     * Returns the location on the local file system where any downloaded
     * files will be stored.
     * 
     * @return local file system location where the downloaded files are stored
     */
    public File getLocalDownloadDirectory() {
        String agentHome = this.agent.getAgentHomeDirectory();
        File dir = null;

        if (agentHome != null && agentHome.length() > 0) {
            dir = (new File(agentHome)).getParentFile();
        }

        if (dir == null) {
            dir = new File(System.getProperty("java.io.tmpdir"));
        }

        return dir;
    }

    /**
     * This will validate the MD5 of the {@link #getAgentUpdateBinaryFile() downloaded binary file}.
     * If it validates, this method returns normally, otherwise, an exception is thrown.
     * You must first download the file first before calling this method.
     * 
     * @throws Exception if the downloaded agent update binary file does not validate with the expected
     *                   MD5, or the agent update binary has not been downloaded yet.
     */
    public void validate() throws Exception {
        File fileToValidate = this.downloadedFile;
        if (fileToValidate == null || !fileToValidate.exists()) {
            throw new IllegalStateException(this.agent.getI18NMsg().getMsg(
                AgentI18NResourceKeys.UPDATE_DOWNLOAD_MD5_MISSING_FILE, fileToValidate));
        }

        AgentUpdateInformation info = this.agentUpdateVersion.getAgentUpdateInformation();
        String md5 = info.getUpdateMd5();
        if (!validateFile(this.downloadedFile, md5)) {
            // its invalid, move it out of the way so we can download a new one
            File invalidFile = new File(fileToValidate.getParentFile(), fileToValidate.getName() + ".invalid");
            invalidFile.delete(); // remove any old one that might be hanging around
            fileToValidate.renameTo(invalidFile);
            throw new IllegalStateException(this.agent.getI18NMsg().getMsg(
                AgentI18NResourceKeys.UPDATE_DOWNLOAD_MD5_INVALID, fileToValidate));

        }

        return; // it validates OK
    }

    /**
     * Downloads the agent update binary and stores it to the local file system.
     *  
     * @throws Exception if agent has disabled updates or it failed to download the update
     */
    public void download() throws Exception {

        if (!agent.getConfiguration().isAgentUpdateEnabled()) {
            throw new Exception(this.agent.getI18NMsg().getMsg(AgentI18NResourceKeys.UPDATE_DOWNLOAD_DISABLED_BY_AGENT));
        }

        // if need be, asks the server for the info - let this throw its own exceptions if it needs to
        AgentUpdateInformation info = this.agentUpdateVersion.getAgentUpdateInformation();

        URL url = null;
        boolean keep_going = true;
        File binaryFile = null;

        while (keep_going) {
            HttpURLConnection conn = null;
            InputStream inStream = null;

            try {
                // we only support http/s
                url = getDownloadUrl();
                LOG.info(AgentI18NResourceKeys.UPDATE_DOWNLOAD_RETRIEVAL, info, url);
                conn = (HttpURLConnection) url.openConnection();
                inStream = conn.getInputStream();

                // put the update content in the local file system
                // determine what the name should be of the agent update binary based on the header
                // Content-Disposition: attachment; filename=<filename.is.here>
                String fileName = conn.getHeaderField("Content-Disposition");

                if (fileName != null) {
                    int filenameIndex = fileName.indexOf("filename=");
                    if (filenameIndex > -1 && (filenameIndex + "filename=".length()) < fileName.length()) {
                        fileName = fileName.substring(filenameIndex + "filename=".length()).trim();
                    } else {
                        LOG.warn(AgentI18NResourceKeys.UPDATE_DOWNLOAD_BAD_NAME, fileName);
                        fileName = null;
                    }
                }

                if (fileName == null || fileName.length() == 0) {
                    // this should never happen, server should always give us the content-disposition
                    // but just in case the download URL is not pointing to a RHQ server and the URL it
                    // is pointing to doesn't give us that header, make a best guess at the name
                    fileName = "rhq-enterprise-agent-" + info.getUpdateVersion() + ".jar";
                    LOG.info(AgentI18NResourceKeys.UPDATE_DOWNLOAD_NO_NAME, fileName);
                }

                File dir = getLocalDownloadDirectory();
                binaryFile = new File(dir, fileName);

                // don't bother downloading if we already have it!
                if (validateFile(binaryFile, info.getUpdateMd5())) {
                    LOG.debug(AgentI18NResourceKeys.UPDATE_DOWNLOAD_ALREADY_HAVE_IT, binaryFile);
                    // we end up closing the stream before reading the data which causes an ugly error in the server log
                    // should we consider doing a HTTP HEAD request first?
                    keep_going = false;
                    break;
                }

                // slurp the entire agent update binary content and store it to our file
                binaryFile.delete();
                FileOutputStream fos = new FileOutputStream(binaryFile);
                StreamUtil.copy(inStream, fos, true);
                inStream = null;
                keep_going = false;
            } catch (Exception e) {
                if (conn != null) {
                    int responseCode = 0;
                    try {
                        responseCode = conn.getResponseCode();
                    } catch (Exception ignore) {
                    }
                    if (responseCode == HttpURLConnection.HTTP_UNAVAILABLE) {
                        // server is overloaded with other agents downloading, we must wait
                        LOG.info(AgentI18NResourceKeys.UPDATE_DOWNLOAD_UNAVAILABLE, info, url);
                        Thread.sleep(getRetryAfter(conn)); // sleep alittle bit to give the server some time (allow us to be interrupted!)
                        keep_going = true;
                    } else if (responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
                        // server has disabled agent updates
                        Exception e1 = new Exception(this.agent.getI18NMsg().getMsg(
                            AgentI18NResourceKeys.UPDATE_DOWNLOAD_DISABLED_BY_SERVER, url), e);
                        LOG.warn(AgentI18NResourceKeys.UPDATE_DOWNLOAD_FAILURE, url, ThrowableUtil.getAllMessages(e1));
                        throw e1;
                    } else {
                        // some unexpected error occurred
                        LOG.warn(AgentI18NResourceKeys.UPDATE_DOWNLOAD_FAILURE, url, ThrowableUtil.getAllMessages(e));
                        throw e;
                    }
                } else {
                    LOG.warn(AgentI18NResourceKeys.UPDATE_DOWNLOAD_FAILURE, url, ThrowableUtil.getAllMessages(e));
                    throw e;
                }
            } finally {
                if (inStream != null) {
                    try {
                        inStream.close();
                    } catch (Exception e) {
                    }
                }
                if (conn != null) {
                    try {
                        conn.disconnect();
                    } catch (Exception e) {
                    }
                }
            }
        }

        // we only ever get here when we are successful
        LOG.info(AgentI18NResourceKeys.UPDATE_DOWNLOAD_DONE, info, url, binaryFile);
        this.downloadedFile = binaryFile;
        return;
    }

    /**
     * Gets the "Retry-After" header and returns its value as a long to indicate how long
     * we should wait before retrying. If can't get the header, a default time interval will be returned.
     * 
     * @param conn the connection where the header can be found
     * 
     * @return the header value, as a long
     */
    private long getRetryAfter(HttpURLConnection conn) {
        try {
            // get the header - by spec, it must be in seconds
            int retryAfter = conn.getHeaderFieldInt("Retry-After", 30);
            return 1000L * retryAfter;
        } catch (Exception e) {
            return 30000L;
        }
    }

    /**
     * This will validate the MD5 of the given file.
     * 
     * @return <code>true</code> if the file's MD5 matches the given MD5, <code>false</code> otherwise
     */
    private boolean validateFile(File file, String md5) {
        try {
            String filemd5 = MessageDigestGenerator.getDigestString(file);
            return (filemd5 != null && filemd5.equals(md5));
        } catch (Exception e) {
            return false;
        }
    }
}
