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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.cloud.Server.OperationMode;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.server.core.CoreServerMBean;
import org.rhq.enterprise.server.util.LookupUtil;

/**
 * Serves the remote client binary that is stored in the RHQ Server's download area.
 * This servlet also provides version information regarding the version of the remote
 * client this servlet serves up.
 */
public class RemoteClientServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private static final String RHQ_SERVER_VERSION = "rhq-server.version";
    private static final String RHQ_SERVER_BUILD_NUMBER = "rhq-server.build-number";
    private static final String RHQ_CLIENT_MD5 = "rhq-client.md5";

    // the system property that defines how many concurrent downloads we will allow
    private static String SYSPROP_CLIENT_DOWNLOADS_LIMIT = "rhq.server.client-downloads-limit";

    // if the system property is not set or invalid, this is the default limit for number of concurrent downloads
    // There is no reason this be heavily downloaded.
    private static int DEFAULT_CLIENT_DOWNLOADS_LIMIT = 5;

    // the error code that will be returned if the server has been configured to disable client updates
    private static final int ERROR_CODE_CLIENT_UPDATE_DISABLED = HttpServletResponse.SC_FORBIDDEN;

    // the error code that will be returned if the server has too many clients downloading the binary
    private static final int ERROR_CODE_TOO_MANY_DOWNLOADS = HttpServletResponse.SC_SERVICE_UNAVAILABLE;

    private static int numActiveDownloads = 0;

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

    @Override
    public void init() throws ServletException {
        log.info("Starting the RHQ remote client servlet...");

        // make sure we have a binary distro file; log its location
        try {
            log.info("Remote Client Binary File: " + getRemoteClientZip());
        } catch (Throwable t) {
            log.error("Remote client is not available for deployment - cause: " + t);
            return;
        }

        // make sure we create a version file if we have to by getting the version file now
        try {
            File versionFile = getVersionFile();

            // log the version info - this also makes sure we can read it back in
            log.debug(versionFile + ":\n" + new String(StreamUtil.slurp(new FileInputStream(versionFile))));
        } catch (Throwable t) {
            log.error("Cannot determine the remote client version information.", t);
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // seeing odd browser caching issues, even though we set Last-Modified. so force no caching for now
        disableBrowserCache(resp);

        String servletPath = req.getServletPath();
        if (servletPath != null) {
            if (isServerAcceptingRequests()) {
                if (servletPath.endsWith("version")) {
                    getVersion(req, resp);
                } else if (servletPath.endsWith("download")) {
                    try {
                        numActiveDownloads++;
                        getDownload(req, resp);
                    } finally {
                        numActiveDownloads--;
                    }
                } else {
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid servlet path [" + servletPath
                        + "] - please contact administrator");
                }
            } else {
                sendErrorServerNotAcceptingRequests(resp);
            }
        } else {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid servlet path - please contact administrator");
        }

        return;
    }

    private void getDownload(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int limit = getDownloadLimit();
        if (limit <= 0) {
            sendErrorDownloadDisabled(resp);
            return;
        } else if (limit < numActiveDownloads) {
            sendErrorTooManyDownloads(resp);
            return;
        }

        try {
            File zip = getRemoteClientZip();
            if (!zip.exists()) {
                disableBrowserCache(resp);
                resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Remote Client binary does not exist: "
                    + zip.getName());
                return;
            }

            resp.setContentType("application/octet-stream");
            resp.setHeader("Content-Disposition", "attachment; filename=" + zip.getName());
            resp.setContentLength((int) zip.length());
            resp.setDateHeader("Last-Modified", zip.lastModified());

            FileInputStream zipStream = new FileInputStream(zip);
            try {
                StreamUtil.copy(zipStream, resp.getOutputStream(), false);
            } finally {
                zipStream.close();
            }
        } catch (Throwable t) {
            log.error("Failed to stream remote client zip.", t);
            disableBrowserCache(resp);
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to stream remote client zip");
        }

        return;
    }

    private File getRemoteClientZip() throws Exception {
        File dir = getDownloadDir();
        for (File file : dir.listFiles()) {
            if (file.getName().endsWith(".zip")) {
                return file;
            }
        }

        throw new FileNotFoundException("Missing remote client binary in [" + dir + "]");
    }

    private File getDownloadDir() throws Exception {
        File serverHomeDir = LookupUtil.getCoreServer().getJBossServerHomeDir();
        File downloadDir = new File(serverHomeDir, "deploy/rhq.ear/rhq-downloads/rhq-client");
        if (!downloadDir.exists()) {
            throw new FileNotFoundException("Missing remote client download directory at [" + downloadDir + "]");
        }
        return downloadDir;
    }

    private void getVersion(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            File versionFile = getVersionFile();
            resp.setContentType("text/plain");
            resp.setDateHeader("Last-Modified", versionFile.lastModified());

            FileInputStream stream = new FileInputStream(versionFile);
            byte[] versionData = StreamUtil.slurp(stream);
            resp.getOutputStream().write(versionData);
        } catch (Throwable t) {
            log.error("Failed to stream version info.", t);
            disableBrowserCache(resp);
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to stream version info");
        }

        return;
    }

    private int getDownloadLimit() {
        String limitStr = System.getProperty(SYSPROP_CLIENT_DOWNLOADS_LIMIT);
        int limit;
        try {
            limit = Integer.parseInt(limitStr);
        } catch (Exception e) {
            limit = DEFAULT_CLIENT_DOWNLOADS_LIMIT;
            log.warn("Remote Client downloads limit system property [" + SYSPROP_CLIENT_DOWNLOADS_LIMIT
                + "] is either not set or invalid [" + limitStr + "] - limit will be [" + limit + "].");
        }

        return limit;
    }

    private void disableBrowserCache(HttpServletResponse resp) {
        resp.setHeader("Cache-Control", "no-cache, no-store");
        resp.setHeader("Expires", "-1");
        resp.setHeader("Pragma", "no-cache");
    }

    private void sendErrorServerNotAcceptingRequests(HttpServletResponse resp) throws IOException {
        disableBrowserCache(resp);
        resp.sendError(ERROR_CODE_CLIENT_UPDATE_DISABLED, "Server Is Down For Maintenance");
    }

    private void sendErrorDownloadDisabled(HttpServletResponse resp) throws IOException {
        disableBrowserCache(resp);
        resp.sendError(ERROR_CODE_CLIENT_UPDATE_DISABLED, "Client Download Has Been Disabled");
    }

    private void sendErrorTooManyDownloads(HttpServletResponse resp) throws IOException {
        disableBrowserCache(resp);
        resp.setHeader("Retry-After", "30");
        resp.sendError(ERROR_CODE_TOO_MANY_DOWNLOADS, "Maximum limit exceeded - download client later");
    }

    private File getVersionFile() throws Exception {
        File versionFile = new File(getDownloadDir(), "rhq-client-version.properties");
        if (!versionFile.exists()) {
            // we do not have the version properties file yet, let's extract some info and create one
            StringBuilder serverVersionInfo = new StringBuilder();

            CoreServerMBean coreServer = LookupUtil.getCoreServer();
            serverVersionInfo.append(RHQ_SERVER_VERSION + '=').append(coreServer.getVersion()).append('\n');
            serverVersionInfo.append(RHQ_SERVER_BUILD_NUMBER + '=').append(coreServer.getBuildNumber()).append('\n');

            // calculate the MD5 of the client zip
            File zip = getRemoteClientZip();
            String md5Property = RHQ_CLIENT_MD5 + '=' + MessageDigestGenerator.getDigestString(zip) + '\n';

            // now write the server version info in our internal version file our servlet will use
            FileOutputStream versionFileOutputStream = new FileOutputStream(versionFile);
            try {
                versionFileOutputStream.write(serverVersionInfo.toString().getBytes());
                versionFileOutputStream.write(md5Property.getBytes());
            } finally {
                try {
                    versionFileOutputStream.close();
                } catch (Exception ignore) {
                }
            }
        }

        return versionFile;
    }

    private boolean isServerAcceptingRequests() {
        try {
            OperationMode mode = LookupUtil.getServerManager().getServer().getOperationMode();
            return mode == OperationMode.NORMAL;
        } catch (Exception e) {
            return false;
        }
    }

}