/*
 * 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 java.util.ArrayList;
import java.util.List;

import javax.annotation.Nullable;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.core.internal.ssh.process.ProcessTool;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory;
import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
import org.apache.brooklyn.util.core.task.system.internal.SystemProcessTaskFactory;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes;

import com.google.common.base.Function;

/** A factory which acts like {@link ProcessTaskFactory} with special options for knife.
 * Typical usage is to {@link #addKnifeParameters(String)}s for the knife command to be run.
 * You can also {@link #add(String...)} commands as needed; these will run *before* knife,
 * unless you addKnifeCommandHere().
 * <p>
 * This impl will use sensible defaults, including {@link ConfigKey}s on the context entity,
 * for general knife config but not specific commands etc. It supports:
 * <li> {@link ChefConfig#KNIFE_EXECUTABLE}
 * <li> {@link ChefConfig#KNIFE_CONFIG_FILE}
 * <p>
 * (Other fields will typically be used by methods calling to this factory.) 
 *  */
// see e.g. http://docs.opscode.com/knife_bootstrap.html
public class KnifeTaskFactory<RET> extends SystemProcessTaskFactory<KnifeTaskFactory<RET>, RET>{
    
    private static String KNIFE_PLACEHOLDER = "<knife command goes here 1234>";
    public final String taskName;
    protected String knifeExecutable;
    protected List<String> knifeParameters = new ArrayList<String>();
    protected String knifeConfigFile;
    protected String knifeSetupCommands;
    protected Boolean throwOnCommonKnifeErrors;
    
    public KnifeTaskFactory(String taskName) {
        this.taskName = taskName;
        summary(taskName);
        // knife setup usually requires a login shell
        config.put(ProcessTool.PROP_LOGIN_SHELL, true);
    }
    
    @Override
    public List<Function<ProcessTaskWrapper<?>, Void>> getCompletionListeners() {
        MutableList<Function<ProcessTaskWrapper<?>, Void>> result = MutableList.copyOf(super.getCompletionListeners());
        if (throwOnCommonKnifeErrors != Boolean.FALSE)
            insertKnifeCompletionListenerIntoCompletionListenersList(result);
        return result.asUnmodifiable();
    }
    
    public KnifeTaskFactory<RET> notThrowingOnCommonKnifeErrors() {
        throwOnCommonKnifeErrors = false;
        return self();
    }

    protected void insertKnifeCompletionListenerIntoCompletionListenersList(List<Function<ProcessTaskWrapper<?>, Void>> listeners) {
        // give a nice warning if chef/knife not set up correctly
        Function<ProcessTaskWrapper<?>, Void> propagateIfKnifeConfigFileMissing = new Function<ProcessTaskWrapper<?>, Void>() {
            @Override
            public Void apply(@Nullable ProcessTaskWrapper<?> input) {
                if (input.getExitCode()!=0 && input.getStderr().indexOf("WARNING: No knife configuration file found")>=0) {
                    String myConfig = knifeConfigFileOption();
                    if (Strings.isEmpty(myConfig))
                        throw new IllegalStateException("Config file for Chef knife must be specified in "+ChefConfig.KNIFE_CONFIG_FILE+" (or valid knife default set up)");
                    else
                        throw new IllegalStateException("Error reading config file for Chef knife ("+myConfig+") -- does it exist?");
                }
                return null;
            }
        };
        listeners.add(propagateIfKnifeConfigFileMissing);
    }

    
    @Override
    public ProcessTaskWrapper<RET> newTask() {
        return new SystemProcessTaskWrapper("Knife");
    }

    /** Inserts the knife command at the current place in the list.
     * Can be run multiple times. The knife command added at the end of the list
     * if this is not invoked (and it is the only command if nothing is {@link #add(String...)}ed.
     */
    public KnifeTaskFactory<RET> addKnifeCommandToScript() {
        add(KNIFE_PLACEHOLDER);
        return self();
    }
    
    @Override
    public List<String> getCommands() {
        MutableList<String> result = new MutableList<String>();
        String setupCommands = knifeSetupCommands();
        if (setupCommands != null && Strings.isNonBlank(setupCommands))
            result.add(setupCommands);
        int numKnifes = 0;
        for (String c: super.getCommands()) {
            if (c==KNIFE_PLACEHOLDER)
                result.add(buildKnifeCommand(numKnifes++));
            else
                result.add(c);
        }
        if (numKnifes==0)
            result.add(buildKnifeCommand(numKnifes++));
        return result.asUnmodifiable();
    }
    
    /** creates the command for running knife.
     * in some cases knife may be added multiple times,
     * and in that case the parameter here tells which time it is being added, 
     * on a single run. */
    protected String buildKnifeCommand(int knifeCommandIndex) {
        MutableList<String> words = new MutableList<String>();
        words.add(knifeExecutable());
        words.addAll(initialKnifeParameters());
        words.addAll(knifeParameters());
        String x = knifeConfigFileOption();
        if (Strings.isNonBlank(x)) words.add(knifeConfigFileOption());
        return Strings.join(words, " ");
    }
    
    /** allows a way for subclasses to build up parameters at the start */
    protected List<String> initialKnifeParameters() {
        return new MutableList<String>();
    }
    
    @Nullable /** callers should allow this to be null so task can be used outside of an entity */
    protected Entity entity() {
        return BrooklynTaskTags.getTargetOrContextEntity(Tasks.current());
    }
    protected <T> T entityConfig(ConfigKey<T> key) {
        Entity entity = entity();
        if (entity!=null)
            return entity.getConfig(key);
        return null;
    }
    
    public KnifeTaskFactory<RET> knifeExecutable(String knifeExecutable) {
        this.knifeExecutable = knifeExecutable;
        return this;
    }
    
    protected String knifeExecutable() {
        if (knifeExecutable!=null) return knifeExecutable;
        
        String knifeExecFromConfig = entityConfig(ChefConfig.KNIFE_EXECUTABLE);
        if (knifeExecFromConfig!=null) return BashStringEscapes.wrapBash(knifeExecFromConfig);
        
        // assume on the path, if executable not set
        return "knife";
    }
    
    protected List<String> knifeParameters() {
        return knifeParameters;
    }
    
    public KnifeTaskFactory<RET> knifeAddParameters(String word1, String ...words) {
        knifeParameters.add(word1);
        for (String w: words)
            knifeParameters.add(w);
        return self();
    }

    public KnifeTaskFactory<RET> knifeConfigFile(String knifeConfigFile) {
        this.knifeConfigFile = knifeConfigFile;
        return self();
    }
    
    @Nullable
    protected String knifeConfigFileOption() {
        if (knifeConfigFile!=null) return "-c "+knifeConfigFile;

        String knifeConfigFileFromConfig = entityConfig(ChefConfig.KNIFE_CONFIG_FILE);
        if (knifeConfigFileFromConfig!=null) return "-c "+BashStringEscapes.wrapBash(knifeConfigFileFromConfig);

        // if not supplied will use global config
        return null;
    }

    public KnifeTaskFactory<RET> knifeSetupCommands(String knifeSetupCommands) {
        this.knifeSetupCommands = knifeSetupCommands;
        return self();
    }
    
    @Nullable
    protected String knifeSetupCommands() {
        if (knifeSetupCommands!=null) return knifeSetupCommands;
        
        String knifeSetupCommandsFromConfig = entityConfig(ChefConfig.KNIFE_SETUP_COMMANDS);
        if (knifeSetupCommandsFromConfig!=null) return knifeSetupCommandsFromConfig;
        
        // if not supplied will use global config
        return null;
    }
    
    @Override
    public <T2> KnifeTaskFactory<T2> returning(ScriptReturnType type) {
        return (KnifeTaskFactory<T2>) super.<T2>returning(type);
    }

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