/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.brooklyn.entity.brooklynnode;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
import org.apache.brooklyn.core.entity.drivers.downloads.DownloadSubstituters;
import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.ExistingFileBehaviour;
import org.apache.brooklyn.entity.java.JavaSoftwareProcessSshDriver;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.file.ArchiveBuilder;
import org.apache.brooklyn.util.core.file.ArchiveUtils;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

public class BrooklynNodeSshDriver extends JavaSoftwareProcessSshDriver implements BrooklynNodeDriver {

    private static final Logger log = LoggerFactory.getLogger(BrooklynNodeSshDriver.class);

    public BrooklynNodeSshDriver(BrooklynNodeImpl entity, SshMachineLocation machine) {
        super(entity, machine);
    }

    @Override
    public BrooklynNodeImpl getEntity() {
        return (BrooklynNodeImpl) super.getEntity();
    }

    public String getBrooklynHome() {
        return getRunDir();
    }

    @Override
    protected String getLogFileLocation() {
        return Os.mergePathsUnix(getRunDir(), "console");
    }
    
    protected String getPidFile() {
        return Os.mergePathsUnix(getRunDir(), "pid_java");
    }
    
    @Override
    protected String getInstallLabelExtraSalt() {
        String downloadUrl = entity.getConfig(BrooklynNode.DOWNLOAD_URL);
        String uploadUrl = entity.getConfig(BrooklynNode.DISTRO_UPLOAD_URL);
        if (Objects.equal(downloadUrl, BrooklynNode.DOWNLOAD_URL.getConfigKey().getDefaultValue()) &&
                Objects.equal(uploadUrl, BrooklynNode.DISTRO_UPLOAD_URL.getDefaultValue())) {
            // if both are at the default value, then no salt
            return null;
        }
        return Identifiers.makeIdFromHash(Objects.hashCode(downloadUrl, uploadUrl));
    }

    @Override
    public String getArchiveNameFormat() {
        String subpath = entity.getConfig(BrooklynNode.SUBPATH_IN_ARCHIVE);
        if (subpath==null) {
            // assume the dir name is `basename-VERSION` where download link is `basename-VERSION-dist.tar.gz`
            String uploadUrl = entity.getConfig(BrooklynNode.DISTRO_UPLOAD_URL);
            String origDownloadName = uploadUrl;
            if (origDownloadName==null) 
                origDownloadName = entity.getAttribute(BrooklynNode.DOWNLOAD_URL);
            if (origDownloadName!=null) {
                // BasicDownloadResolver makes it crazy hard to get the template-evaluated value of DOWNLOAD_URL
                origDownloadName = DownloadSubstituters.substitute(origDownloadName, DownloadSubstituters.getBasicEntitySubstitutions(this));
                origDownloadName = Urls.decode(origDownloadName);
                origDownloadName = Urls.getBasename(origDownloadName);
                String downloadName = origDownloadName;
                downloadName = Strings.removeFromEnd(downloadName, ".tar.gz");
                downloadName = Strings.removeFromEnd(downloadName, ".tgz");
                downloadName = Strings.removeFromEnd(downloadName, ".zip");
                if (!downloadName.equals(origDownloadName)) {
                    downloadName = Strings.removeFromEnd(downloadName, "-dist");
                    subpath = downloadName;
                }
            }
        }
        if (subpath==null) subpath = "brooklyn-dist-%s";
        return subpath;
    }

    @Override
    public void clearInstallDir() {
        super.setInstallDir(null);
        super.setExpandedInstallDir(null);
    }
    
    @Override
    public void install() {
        String uploadUrl = entity.getConfig(BrooklynNode.DISTRO_UPLOAD_URL);
        
        // Need to explicitly give file, because for snapshot URLs you don't get a clean filename from the URL.
        // This filename is used to generate the first URL to try: [BROOKLYN_VERSION_BELOW]
        // file://$HOME/.brooklyn/repository/BrooklynNode/1.0.0/brooklynnode-0.8.0-snapshot.tar.gz
        // (DOWNLOAD_URL overrides this and has a default which comes from maven)
        List<String> urls = resolver.getTargets();
        String saveAs = resolver.getFilename();
        
        newScript("createInstallDir")
                .body.append("mkdir -p "+getInstallDir())
                .failOnNonZeroResultCode()
                .execute();

        List<String> commands = Lists.newArrayList();
        // TODO use machine.installTo ... but that only works w a single location currently
        if (uploadUrl != null) {
            // Only upload if not already installed
            boolean exists = newScript("checkIfInstalled")
                    .body.append("cd "+getInstallDir(), "test -f BROOKLYN")
                    .execute() == 0;
            if (!exists) {
                InputStream distroStream = resource.getResourceFromUrl(uploadUrl);
                getMachine().copyTo(distroStream, getInstallDir()+"/"+saveAs);
            }
        } else {
            commands.addAll(BashCommands.commandsToDownloadUrlsAs(urls, saveAs));
        }
        commands.add(BashCommands.INSTALL_TAR);
        commands.add("tar xzfv " + saveAs);
        
        newScript(INSTALLING).
                failOnNonZeroResultCode().
                body.append(commands).execute();
    }

    @Override
    public void customize() {
        newScript(CUSTOMIZING)
                .failOnNonZeroResultCode()
                .body.append(
                        // workaround for AMP distribution placing everything in the root of this archive, but
                        // brooklyn distribution placing everything in a subdirectory: check to see if subdirectory
                        // with expected name exists; symlink to same directory if it doesn't
                        // FIXME remove when all downstream usages don't use this
                        format("[ -d %1$s ] || ln -s . %1$s", getExpandedInstallDir(), getExpandedInstallDir()),
                        
                        // previously we only copied bin,conf and set BROOKLYN_HOME to the install dir;
                        // but that does not play nicely if installing dists other than brooklyn
                        // (such as what is built by our artifact)  
                        format("cp -R %s/* .", getExpandedInstallDir()),
                        "mkdir -p ./lib/dropins/")
                .execute();
        
        SshMachineLocation machine = getMachine();
        BrooklynNode entity = getEntity();
        
        String brooklynGlobalPropertiesRemotePath = entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_REMOTE_PATH);
        String brooklynGlobalPropertiesContents = entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_CONTENTS);
        String brooklynGlobalPropertiesUri = entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_URI);

        String brooklynLocalPropertiesRemotePath = processTemplateContents(entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_REMOTE_PATH));
        String brooklynLocalPropertiesContents = entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_CONTENTS);
        String brooklynLocalPropertiesUri = entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_URI);

        String brooklynCatalogInitialBomRemotePath = processTemplateContents(entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_INITIAL_BOM_REMOTE_PATH));
        String brooklynCatalogInitialBomUri = entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_INITIAL_BOM_URI);
        String brooklynCatalogInitialBomContents = entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_INITIAL_BOM_CONTENTS);
        
        // Override the ~/.brooklyn/brooklyn.properties if required
        if (brooklynGlobalPropertiesContents != null || brooklynGlobalPropertiesUri != null) {
            ExistingFileBehaviour onExisting = entity.getConfig(BrooklynNode.ON_EXISTING_PROPERTIES_FILE);
            Integer checkExists = DynamicTasks.queue(SshEffectorTasks.ssh("ls \""+brooklynGlobalPropertiesRemotePath+"\"").allowingNonZeroExitCode()).get();
            boolean doUpload = true;
            if (checkExists==0) {
                switch (onExisting) {
                case USE_EXISTING: doUpload = false; break;
                case OVERWRITE: break;
                case DO_NOT_USE: 
                    throw new IllegalStateException("Properties file "+brooklynGlobalPropertiesContents+" already exists and "+
                        "even though it is not being used, content for it was supplied");
                case FAIL: 
                    throw new IllegalStateException("Properties file "+brooklynGlobalPropertiesContents+" already exists and "+
                        BrooklynNode.ON_EXISTING_PROPERTIES_FILE+" response is to fail");
                default:
                    throw new IllegalStateException("Properties file "+brooklynGlobalPropertiesContents+" already exists and "+
                        BrooklynNode.ON_EXISTING_PROPERTIES_FILE+" response "+onExisting+" is unknown");
                }
            }
            if (onExisting==ExistingFileBehaviour.DO_NOT_USE) {
                log.warn("Global properties supplied when told not to use them; no global properties exists, so it will be installed, but it will not be used.");
            }
            if (doUpload)
                uploadFileContents(brooklynGlobalPropertiesContents, brooklynGlobalPropertiesUri, brooklynGlobalPropertiesRemotePath);
        }
        
        // Upload a local-brooklyn.properties if required
        if (brooklynLocalPropertiesContents != null || brooklynLocalPropertiesUri != null) {
            uploadFileContents(brooklynLocalPropertiesContents, brooklynLocalPropertiesUri, brooklynLocalPropertiesRemotePath);
        }

        // Upload a local-catalog.bom if required
        if (brooklynCatalogInitialBomContents != null || brooklynCatalogInitialBomUri != null) {
            uploadFileContents(brooklynCatalogInitialBomContents, brooklynCatalogInitialBomUri, brooklynCatalogInitialBomRemotePath);
        }

        // Copy additional resources to the server
        for (Map.Entry<String,String> entry : getEntity().getAttribute(BrooklynNode.COPY_TO_RUNDIR).entrySet()) {
            Map<String, String> substitutions = ImmutableMap.of("RUN", getRunDir());
            String localResource = entry.getKey();
            String remotePath = entry.getValue();
            String resolvedRemotePath = remotePath;
            for (Map.Entry<String,String> substitution : substitutions.entrySet()) {
                String key = substitution.getKey();
                String val = substitution.getValue();
                resolvedRemotePath = resolvedRemotePath.replace("${"+key+"}", val).replace("$"+key, val);
            }
            machine.copyTo(MutableMap.of("permissions", "0600"), resource.getResourceFromUrl(localResource), resolvedRemotePath);
        }

        for (Object entry : getEntity().getClasspath()) {
            String filename = null;
            String url = null;

            if (entry instanceof String) {
                url = (String) entry;
            } else {
                if (entry instanceof Map) {
                    url = (String) ((Map) entry).get("url");
                    filename = (String) ((Map) entry).get("filename");
                }
            }
            checkNotNull(url, "url");

            // If a local folder, then create archive from contents first
            if (Urls.isDirectory(url)) {
                File jarFile = ArchiveBuilder.jar().addDirContentsAt(new File(url), "").create();
                url = jarFile.getAbsolutePath();
            }

            if (filename == null) {
                // Determine filename
                filename = getFilename(url);
            }
            ArchiveUtils.deploy(MutableMap.<String, Object>of(), url, machine, getRunDir(), Os.mergePaths(getRunDir(), "lib", "dropins"), filename);
        }

        String cmd = entity.getConfig(BrooklynNode.EXTRA_CUSTOMIZATION_SCRIPT);
        if (Strings.isNonBlank(cmd)) {
            DynamicTasks.queueIfPossible( SshEffectorTasks.ssh(cmd).summary("Bespoke BrooklynNode customization script")
                .requiringExitCodeZero() )
                .orSubmitAndBlock(getEntity());
        }
    }

    private String getFilename(String url) {
        String destFile = url.contains("?") ? url.substring(0, url.indexOf('?')) : url;
        destFile = destFile.substring(destFile.lastIndexOf('/') + 1);
        return destFile;
    }

    @Override
    public void launch() {
        String app = getEntity().getAttribute(BrooklynNode.APP);
        String locations = getEntity().getAttribute(BrooklynNode.LOCATIONS);
        boolean hasLocalBrooklynProperties = getEntity().getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_CONTENTS) != null || getEntity().getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_URI) != null;
        boolean hasLocalCatalogInitialBom = getEntity().getConfig(BrooklynNode.BROOKLYN_CATALOG_INITIAL_BOM_CONTENTS) != null || getEntity().getConfig(BrooklynNode.BROOKLYN_CATALOG_INITIAL_BOM_URI) != null;
        String localBrooklynPropertiesPath = processTemplateContents(getEntity().getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_REMOTE_PATH));
        String localCatalogInitialBomPath = processTemplateContents(getEntity().getConfig(BrooklynNode.BROOKLYN_CATALOG_INITIAL_BOM_REMOTE_PATH));
        InetAddress bindAddress = getEntity().getAttribute(BrooklynNode.WEB_CONSOLE_BIND_ADDRESS);
        InetAddress publicAddress = getEntity().getAttribute(BrooklynNode.WEB_CONSOLE_PUBLIC_ADDRESS);

        String cmd = entity.getConfig(BrooklynNode.LAUNCH_COMMAND);
        if (Strings.isBlank(cmd)) cmd = "./bin/brooklyn";
        cmd = "nohup " + cmd + " launch";
        if (app != null) {
            cmd += " --app "+app;
        }
        if (locations != null) {
            cmd += " --locations "+locations;
        }
        if (entity.getConfig(BrooklynNode.ON_EXISTING_PROPERTIES_FILE)==ExistingFileBehaviour.DO_NOT_USE) {
            cmd += " --noGlobalBrooklynProperties";
        }
        if (hasLocalBrooklynProperties) {
            cmd += " --localBrooklynProperties "+localBrooklynPropertiesPath;
        }
        if (hasLocalCatalogInitialBom) {
            cmd += " --catalogInitial "+localCatalogInitialBomPath;
        }
        Integer webPort = null;
        if (getEntity().isHttpProtocolEnabled("http")) {
            webPort = getEntity().getAttribute(BrooklynNode.HTTP_PORT);
            Networking.checkPortsValid(ImmutableMap.of("webPort", webPort));
        } else if (getEntity().isHttpProtocolEnabled("https")) {
            webPort = getEntity().getAttribute(BrooklynNode.HTTPS_PORT);
            Networking.checkPortsValid(ImmutableMap.of("webPort", webPort));
        }
        if (webPort!=null) {
            cmd += " --port "+webPort;
        } else if (getEntity().getEnabledHttpProtocols().isEmpty()) {
            // TODO sensors will probably not work in this mode
            cmd += " --noConsole";
        } else {
            throw new IllegalStateException("Unknown web protocol in "+BrooklynNode.ENABLED_HTTP_PROTOCOLS+" "
                + "("+getEntity().getEnabledHttpProtocols()+"); expecting 'http' or 'https'");
        }
        
        if (bindAddress != null) {
            cmd += " --bindAddress "+bindAddress.getHostAddress();
        }
        if (publicAddress != null) {
            cmd += " --publicAddress "+publicAddress.getHostName();
        }
        if (getEntity().getAttribute(BrooklynNode.NO_WEB_CONSOLE_AUTHENTICATION)) {
            cmd += " --noConsoleSecurity";
        }
        if (Strings.isNonBlank(getEntity().getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS))) {
            cmd += " "+getEntity().getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS);
        }
        cmd += format(" >> %s/console 2>&1 </dev/null &", getRunDir());
        
        log.info("Starting brooklyn on {} using command {}", getMachine(), cmd);
        
        // relies on brooklyn script creating pid file
        newScript(ImmutableMap.of("usePidFile", 
                entity.getConfig(BrooklynNode.LAUNCH_COMMAND_CREATES_PID_FILE) ? false : getPidFile()), 
            LAUNCHING).
            body.append(
                format("export BROOKLYN_CLASSPATH=%s", getRunDir()+"/lib/\"*\""),
                format("export BROOKLYN_HOME=%s", getBrooklynHome()),
                format(cmd)
        ).failOnNonZeroResultCode().execute();
    }

    @Override
    public boolean isRunning() {
        Map<String,String> flags = ImmutableMap.of("usePidFile", getPidFile());
        int result = newScript(flags, CHECK_RUNNING).execute();
        return result == 0;
    }

    @Override
    public void stop() {
        Map<String,String> flags = ImmutableMap.of("usePidFile", getPidFile());
        newScript(flags, STOPPING).execute();
    }

    @Override
    public void kill() {
        Map<String,String> flags = ImmutableMap.of("usePidFile", getPidFile());
        newScript(flags, KILLING).execute();
    }
    
    @Override
    public Map<String, String> getShellEnvironment() {
        Map<String, String> orig = super.getShellEnvironment();
        String origClasspath = orig.get("CLASSPATH");
        String newClasspath = (origClasspath == null ? "" : origClasspath+":") + 
                getRunDir()+"/conf/" + ":" +
                getRunDir()+"/lib/\"*\"";
        Map<String,String> results = new LinkedHashMap<String,String>();
        results.putAll(orig);
        results.put("BROOKLYN_CLASSPATH", newClasspath);
        results.put("BROOKLYN_HOME", getBrooklynHome());
        results.put("RUN", getRunDir());
        return results;
    }
    
    private void uploadFileContents(String contents, String alternativeUri, String remotePath) {
        checkNotNull(remotePath, "remotePath");
        SshMachineLocation machine = getMachine();
        String tempRemotePath = String.format("%s/upload.tmp", getRunDir());

        if (contents == null && alternativeUri == null) {
            throw new IllegalStateException("No contents supplied for file " + remotePath);
        }
        InputStream stream = contents != null
                ? new ByteArrayInputStream(contents.getBytes())
                : resource.getResourceFromUrl(alternativeUri);
        Map<String, String> flags = MutableMap.of(SshTool.PROP_PERMISSIONS.getName(), "0600");
        machine.copyTo(flags, stream, tempRemotePath);
        newScript(CUSTOMIZING)
                .failOnNonZeroResultCode()
                .body.append(
                        format("mkdir -p %s", remotePath.subSequence(0, remotePath.lastIndexOf("/"))),
                        format("cp -p %s %s", tempRemotePath, remotePath),
                        format("rm -f %s", tempRemotePath))
                .execute();
    }
}
