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

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.location.OsDetails;
import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
import org.apache.brooklyn.entity.software.base.lifecycle.ScriptHelper;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.time.Duration;

public abstract class AbstractMongoDBSshDriver extends AbstractSoftwareProcessSshDriver {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractMongoDBSshDriver.class);
    
    public AbstractMongoDBSshDriver(EntityLocal entity, SshMachineLocation machine) {
        super(entity, machine);
    }

    @Override
    public String getArchiveNameFormat() {
        return getOsTag() + "-%s";
    }

    @Override
    public void install() {
        List<String> urls = resolver.getTargets();
        String saveAs = resolver.getFilename();
    
        List<String> commands = new LinkedList<>();
        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() {
        Map<?,?> ports = ImmutableMap.of("port", getServerPort());
        Networking.checkPortsValid(ports);
        List<String> commands = new LinkedList<>();
        commands.add(String.format("mkdir -p %s", getDataDirectory()));

        if (MongoDBAuthenticationUtils.usesAuthentication(entity)) {
            String destinationLocation = Os.mergePaths(getRunDir(), "mongodb-keyfile");
            entity.sensors().set(AbstractMongoDBServer.MONGODB_KEYFILE_DESTINATION, destinationLocation);
            String keyfileContents = entity.config().get(AbstractMongoDBServer.MONGODB_KEYFILE_CONTENTS);
            if (Strings.isNullOrEmpty(keyfileContents)) {
                String keyfileUrl = entity.config().get(AbstractMongoDBServer.MONGODB_KEYFILE_URL);
                if (Strings.isNullOrEmpty(keyfileUrl)) {
                    throw new IllegalStateException("MongoDBAuthenticationUtils.usesAuthentication returned true, but neither keyfileContents nor keyfileUrl are set");
                }
                copyResource(keyfileUrl, destinationLocation);
            } else {
                commands.add(BashCommands.pipeTextToFile(keyfileContents, destinationLocation));
            }
            commands.add("chmod 600 " + destinationLocation);
        }

        newScript(CUSTOMIZING)
                .updateTaskAndFailOnNonZeroResultCode()
                .body.append(commands).execute();
        String templateUrl = entity.getConfig(MongoDBServer.MONGODB_CONF_TEMPLATE_URL);
        if (!Strings.isNullOrEmpty(templateUrl)) copyTemplate(templateUrl, getConfFile());
        if (MongoDBAuthenticationUtils.usesAuthentication(entity)) {
            launch(getArgsBuilderWithNoAuthentication((AbstractMongoDBServer) getEntity())
                    .add("--dbpath", getDataDirectory()));
            newScript("create-user")
                    .body.append(String.format("%s --port %s" +
                            " --host localhost admin --eval \"db.createUser({user: '%s',pwd: '%s',roles: [ 'root' ]})\"",
                    Os.mergePaths(getExpandedInstallDir(), "bin/mongo"), getServerPort(), getRootUsername(), MongoDBAuthenticationUtils.getRootPassword(entity)))
                    .updateTaskAndFailOnNonZeroResultCode()
                    .execute();
            stop();
        }
    }

    @Override
    public boolean isRunning() {
        try {
            if (entity instanceof MongoDBServerImpl && !((MongoDBServerImpl)entity).clientAccessEnabled()) {
                // No direct access via MongoDB port; only use ssh-port
                return newScript(MutableMap.of(USE_PID_FILE, getPidFile()), CHECK_RUNNING).execute() == 0;
            } else {
                return MongoDBClientSupport.forServer((AbstractMongoDBServer) entity).ping();
            }
        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            return false;
        }
    }
    
    /**
     * Kills the server with SIGINT. Sending SIGKILL is likely to result in data corruption.
     * @see <a href="http://docs.mongodb.org/manual/tutorial/manage-mongodb-processes/#sending-a-unix-int-or-term-signal">http://docs.mongodb.org/manual/tutorial/manage-mongodb-processes/#sending-a-unix-int-or-term-signal</a>
     */
    @Override
    public void stop() {
        // TODO: Wait for process to terminate. Currently, this will send the signal and then immediately continue with next steps, 
        // which could involve stopping VM etc.
        
        // We could also use SIGTERM (15)
        new ScriptHelper(this, "Send SIGINT to MongoDB server")
                .body.append("MONGO_PID=$(cat " + getPidFile() + ")\n")
                .body.append("kill -2 $MONGO_PID\n")
                .body.append("for i in {1..10}\n" +
                    "do\n" +
                    "    kill -0 $MONGO_PID || exit \n" +
                    "    sleep 1\n" +
                    "done\n" +
                    "echo \"mongoDB process still running after 10 seconds; continuing but may subsequently fail\"")
                .execute();
    }

    // IDE note: This is used by MongoDBServer.DOWNLOAD_URL
    public String getOsDir() {
        return (getLocation().getOsDetails().isMac()) ? "osx" : "linux";
    }

    public String getOsTag() {
        OsDetails os = getLocation().getOsDetails();
        if (os == null) {
            // Default to generic linux
            return "mongodb-linux-x86_64";
        } else if (os.isMac()) {
            // Mac is 64bit only
            return "mongodb-osx-x86_64";
        } else {
            String arch = os.is64bit() ? "x86_64" : "i686";
            return "mongodb-linux-" + arch;
        }
    }

    public String getDataDirectory() {
        String result = entity.getConfig(MongoDBServer.DATA_DIRECTORY);
        if (result!=null) return result;
        return getRunDir() + "/data";
    }

    protected String getLogFile() {
        return getRunDir() + "/log.txt";
    }

    protected String getPidFile() {
        return getRunDir() + "/pid";
    }

    protected Integer getServerPort() {
        return entity.getAttribute(MongoDBServer.PORT);
    }

    protected String getConfFile() {
        return getRunDir() + "/mongo.conf";
    }

    protected String getRootUsername() {
        return entity.config().get(AbstractMongoDBServer.ROOT_USERNAME);
    }

    protected ImmutableList.Builder<String> getArgsBuilderWithDefaults(AbstractMongoDBServer server) {
        ImmutableList.Builder<String> builder = getArgsBuilderWithNoAuthentication(server);
        if (MongoDBAuthenticationUtils.usesAuthentication(entity)) {
            builder.add("--keyFile", entity.getAttribute(AbstractMongoDBServer.MONGODB_KEYFILE_DESTINATION));
        }
        return builder;
    }

    protected ImmutableList.Builder<String> getArgsBuilderWithNoAuthentication(AbstractMongoDBServer server) {
        Integer port = server.getAttribute(MongoDBServer.PORT);
        ImmutableList.Builder<String> builder = ImmutableList.builder();
        builder.add("--config", getConfFile());
        builder.add("--pidfilepath", getPidFile());
        builder.add("--logpath", getLogFile());
        builder.add("--port", port.toString());
        builder.add("--fork");
        return builder;
    }
    
    protected void launch(ImmutableList.Builder<String> argsBuilder) {
        String args = Joiner.on(" ").join(argsBuilder.build());
        String command = String.format("%s/bin/mongod %s >> out.log 2>> err.log < /dev/null", getExpandedInstallDir(), args);

        newScript(LAUNCHING)
                .setFlag(SshTool.PROP_CONNECT_TIMEOUT, Duration.TEN_SECONDS.toMilliseconds())
                .updateTaskAndFailOnNonZeroResultCode()
                .body.append(command).execute();
    }

}
