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

import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes.wrapBash;

import java.io.File;
import java.util.List;
import java.util.Map;

import com.google.common.base.Strings;
import com.google.common.net.HostAndPort;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.core.effector.EffectorTasks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.Jsonya;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
import org.apache.brooklyn.util.ssh.BashCommands;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.gson.GsonBuilder;

public class KnifeConvergeTaskFactory<RET> extends KnifeTaskFactory<RET> {

    private static final Logger log = LoggerFactory.getLogger(KnifeConvergeTaskFactory.class);
    
    protected Function<? super Entity,String> runList;
    protected Map<Object, Object> knifeAttributes = new MutableMap<Object, Object>();
    protected List<String> extraBootstrapParameters = MutableList.of();
    protected Boolean sudo;
    protected Boolean runTwice;
    protected String nodeName;
    protected Integer port;
    /** null means nothing specified, use user supplied or machine default;
     * false means use machine default (disallow user supplied);
     * true means use knife default (omit the argument and disallow user supplied)
     */
    protected Boolean portOmittedToUseKnifeDefault;

    public KnifeConvergeTaskFactory(String taskName) {
        super(taskName);
    }
    
    @Override
    protected KnifeConvergeTaskFactory<RET> self() {
        return this;
    }
    
    /** construct the knife command, based on the settings on other methods
     * (called when instantiating the script, after all parameters sent)
     */
    @Override
    protected List<String> initialKnifeParameters() {
        // runs inside the task so can detect entity/machine at runtime
        MutableList<String> result = new MutableList<String>();
        SshMachineLocation machine = EffectorTasks.findSshMachine();
        
        result.add("bootstrap");
        result.addAll(extraBootstrapParameters);

        HostAndPort hostAndPort = machine.getSshHostAndPort();
        result.add(wrapBash(hostAndPort.getHostText()));
        Integer whichPort = knifeWhichPort(hostAndPort);
        if (whichPort!=null)
            result.add("-p "+whichPort);

        result.add("-x "+wrapBash(checkNotNull(machine.getUser(), "user")));
        
        File keyfile = ChefServerTasks.extractKeyFile(machine);
        if (keyfile!=null) result.add("-i "+keyfile.getPath());
        else result.add("-P "+checkNotNull(machine.findPassword(), "No password or private key data for "+machine));
        
        result.add("--no-host-key-verify");
        
        if (sudo != Boolean.FALSE) result.add("--sudo");

        if (!Strings.isNullOrEmpty(nodeName)) {
            result.add("--node-name");
            result.add(nodeName);
        }

        result.add("-r "+wrapBash(runList.apply(entity())));
        
        if (!knifeAttributes.isEmpty())
            result.add("-j "+wrapBash(new GsonBuilder().create()
                    .toJson(knifeAttributes)));

        return result;
    }
    
    /** whether knife should attempt to run twice;
     * see {@link ChefConfig#CHEF_RUN_CONVERGE_TWICE} */
    public KnifeConvergeTaskFactory<RET> knifeRunTwice(boolean runTwice) {
        this.runTwice = runTwice;
        return self();
    }
    
    /** whether to pass --sudo to knife; default true */
    public KnifeConvergeTaskFactory<RET> knifeSudo(boolean sudo) {
        this.sudo = sudo;
        return self();
    }

    /** what node name to pass to knife; default = null, meaning chef-client will pick the node name */
    public KnifeConvergeTaskFactory<RET> knifeNodeName(String nodeName) {
        this.nodeName = nodeName;
        return self();
    }

    /** tell knife to use an explicit port */
    public KnifeConvergeTaskFactory<RET> knifePort(int port) {
        if (portOmittedToUseKnifeDefault!=null) {
            log.warn("Port "+port+" specified to "+this+" for when already explicitly told to use a default (overriding previous); see subsequent warning for more details");
        }
        this.port = port;
        return self();
    }

    /** omit the port parameter altogether (let knife use its default) */
    public KnifeConvergeTaskFactory<RET> knifePortUseKnifeDefault() {
        if (port!=null) {
            log.warn("knifePortUseKnifeDefault specified to "+this+" when already told to use "+port+" explicitly (overriding previous); see subsequent warning for more details");
            port = -1;
        }
        portOmittedToUseKnifeDefault = true;
        return self();
    }
    
    /** use the default port known to brooklyn for the target machine for ssh */
    public KnifeConvergeTaskFactory<RET> knifePortUseMachineSshPort() {
        if (port!=null) {
            log.warn("knifePortUseMachineSshPort specified to "+this+" when already told to use "+port+" explicitly (overriding previous); see subsequent warning for more details");
            port = -1;
        }
        portOmittedToUseKnifeDefault = false;
        return self();
    }
    
    protected Integer knifeWhichPort(HostAndPort hostAndPort) {
        if (port==null) {
            if (Boolean.TRUE.equals(portOmittedToUseKnifeDefault))
                // user has explicitly said to use knife default, omitting port here
                return null;
            // default is to use the machine port
            return hostAndPort.getPort();
        }
        if (port==-1) {
            // port was supplied by user, then portDefault (true or false)
            port = null;
            Integer whichPort = knifeWhichPort(hostAndPort);
            log.warn("knife port conflicting instructions for "+this+" at entity "+entity()+" on "+hostAndPort+"; using default ("+whichPort+")");
            return whichPort;
        }
        if (portOmittedToUseKnifeDefault!=null) {
            // portDefault was specified (true or false), then overridden with a port
            log.warn("knife port conflicting instructions for "+this+" at entity "+entity()+" on "+hostAndPort+"; using supplied port "+port);
        }
        // port was supplied by user, use that
        return port;
    }
    
    /** parameters to pass to knife after the bootstrap command */
    public KnifeConvergeTaskFactory<RET> knifeAddExtraBootstrapParameters(String extraBootstrapParameter1, String ...extraBootstrapParameters) {
        this.extraBootstrapParameters.add(extraBootstrapParameter1);
        for (String p: extraBootstrapParameters)
            this.extraBootstrapParameters.add(p);
        return self();
    }
    
    /** function supplying the run list to be passed to knife, evaluated at the last moment */
    public KnifeConvergeTaskFactory<RET> knifeRunList(Function<? super Entity, String> runList) {
        this.runList = runList;
        return self();
    }
    public KnifeConvergeTaskFactory<RET> knifeRunList(String runList) {
        this.runList = Functions.constant(runList);
        return self();
    }
    
    /** includes the given attributes in the attributes to be passed to chef; 
     * when combining with other attributes, this uses {@link Jsonya} semantics to add 
     * (a deep add, combining lists and maps) */
    public KnifeConvergeTaskFactory<RET> knifeAddAttributes(Map<? extends Object, ? extends Object> attributes) {
        if (attributes!=null && !attributes.isEmpty()) {
            Jsonya.of(knifeAttributes).add(attributes);
        }
        return self();
    }
    
    @Override
    protected String buildKnifeCommand(int knifeCommandIndex) {
        String result = super.buildKnifeCommand(knifeCommandIndex);
        if (Boolean.TRUE.equals(runTwice))
            result = BashCommands.alternatives(result, result);
        return result;
    }

    @Override
    public <T2> KnifeConvergeTaskFactory<T2> returning(ScriptReturnType type) {
        return (KnifeConvergeTaskFactory<T2>) super.<T2>returning(type);
    }

    @Override
    public <RET2> KnifeConvergeTaskFactory<RET2> returning(Function<ProcessTaskWrapper<?>, RET2> resultTransformation) {
        return (KnifeConvergeTaskFactory<RET2>) super.returning(resultTransformation);
    }
    
    @Override
    public KnifeConvergeTaskFactory<Boolean> returningIsExitCodeZero() {
        return (KnifeConvergeTaskFactory<Boolean>) super.returningIsExitCodeZero();
    }
    
    @Override
    public KnifeConvergeTaskFactory<String> requiringZeroAndReturningStdout() {
        return (KnifeConvergeTaskFactory<String>) super.requiringZeroAndReturningStdout();
    }

    @Override
    public KnifeConvergeTaskFactory<RET> knifeAddParameters(String word1, String ...words) {
        super.knifeAddParameters(word1, words);
        return self();
    }

    // TODO other methods from KnifeTaskFactory will return KTF class not KCTF;
    // should make it generic so it returns the right type...
}