/*
 * 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.software.base.lifecycle;

import static java.lang.String.format;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.annotation.Nullable;

import org.apache.brooklyn.api.mgmt.ExecutionContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.TaskQueueingContext;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.util.core.internal.winrm.WinRmTool;
import org.apache.brooklyn.util.core.mutex.WithMutexes;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.TaskBuilder;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.stream.Streams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.Beta;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;

/**
 * <code>org.apache.brooklyn.entity.software.base.lifecycle.ScriptHelper</code> analog for WinRM
 */
public class WinRmExecuteHelper {
    public static final Logger LOG = LoggerFactory.getLogger(WinRmExecuteHelper.class);

    private Task<Integer> task;

    protected final NativeWindowsScriptRunner runner;
    public final String summary;

    private String domain;
    private String command;
    private String psCommand;
    private Map<String, String> env;

    @SuppressWarnings("rawtypes")
    protected final Map flags = new LinkedHashMap();
    protected Predicate<? super Integer> resultCodeCheck = Predicates.alwaysTrue();
    protected ByteArrayOutputStream stdout, stderr, stdin;

    protected Runnable mutexAcquire = new Runnable() {
        @Override
        public void run() {
        }
    };

    protected Runnable mutexRelease = new Runnable() {
        @Override
        public void run() {
        }
    };

    public WinRmExecuteHelper(NativeWindowsScriptRunner runner, String summary) {
        this.runner = runner;
        this.summary = summary;
    }

    public WinRmExecuteHelper setNtDomain(String domain) {
        this.domain = domain;
        return this;
    }

    public WinRmExecuteHelper setCommand(String command) {
        this.command = command;
        return this;
    }

    public WinRmExecuteHelper setPsCommand(String psCommand) {
        this.psCommand = psCommand;
        return this;
    }

    public WinRmExecuteHelper setEnv(Map<String, String> env) {
        this.env = env;
        return this;
    }

    /**
     * indicates that the script should acquire the given mutexId on the given mutexSupport
     * and maintain it for the duration of script execution;
     * typically used to prevent parallel scripts from conflicting in access to a resource
     * (e.g. a folder, or a config file used by a process)
     */
    public WinRmExecuteHelper useMutex(final WithMutexes mutexSupport, final String mutexId, final String description) {
        mutexAcquire = new Runnable() {
            @Override
            public void run() {
                mutexSupport.acquireMutex(mutexId, description);
            }
        };

        mutexRelease = new Runnable() {
            @Override
            public void run() {
                mutexSupport.releaseMutex(mutexId);
            }
        };

        return this;
    }

    /** queues the task for execution if we are in a {@link TaskQueueingContext} (e.g. EffectorTaskFactory);
     * or if we aren't in a queueing context, it will submit the task (assuming there is an {@link ExecutionContext}
     * _and_ block until completion, throwing on error */
    @Beta
    public Task<Integer> queue() {
        return DynamicTasks.queueIfPossible(newTask()).orSubmitAndBlock().getTask();
    }

    /** creates a task which will execute this script; note this can only be run once per instance of this class */
    public synchronized Task<Integer> newTask() {
        if (task!=null) throw new IllegalStateException("task can only be generated once");
        TaskBuilder<Integer> tb = Tasks.<Integer>builder().displayName("winrm: "+summary).body(
                new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return executeInternal();
                    }
                });

        try {
            ByteArrayOutputStream stdin = new ByteArrayOutputStream();
            if (command != null) {
                stdin.write(command.getBytes());
            } else if (psCommand != null) {
                stdin.write(psCommand.getBytes());
            }
            tb.tag(BrooklynTaskTags.tagForStreamSoft(BrooklynTaskTags.STREAM_STDIN, stdin));
        } catch (IOException e) {
            LOG.warn("Error registering stream "+BrooklynTaskTags.STREAM_STDIN+" on "+tb+": "+e, e);
        }

        Map flags = getFlags();

        Map<?,?> env = (Map<?,?>) flags.get("env");
        if (env!=null) {
            // if not explicitly set, env will come from getShellEnv in AbstractSoftwareProcessSshDriver.execute,
            // which will also update this tag appropriately
            tb.tag(BrooklynTaskTags.tagForEnvStream(BrooklynTaskTags.STREAM_ENV, env));
        }

        if (gatherOutput) {
            stdout = new ByteArrayOutputStream();
            tb.tag(BrooklynTaskTags.tagForStreamSoft(BrooklynTaskTags.STREAM_STDOUT, stdout));
            stderr = new ByteArrayOutputStream();
            tb.tag(BrooklynTaskTags.tagForStreamSoft(BrooklynTaskTags.STREAM_STDERR, stderr));
        }
        task = tb.build();
        return task;
    }

    public int execute() {
        if (DynamicTasks.getTaskQueuingContext()!=null) {
            return queue().getUnchecked();
        } else {
            return executeInternal();
        }
    }

    public int executeInternal() {
        int result;
        mutexAcquire.run();
        try {
            if (gatherOutput) {
                if (stdout==null) stdout = new ByteArrayOutputStream();
                if (stderr==null) stderr = new ByteArrayOutputStream();
                flags.put("out", stdout);
                flags.put("err", stderr);
            }
            flags.put(WinRmTool.COMPUTER_NAME, domain);
            if (env!=null) flags.put(WinRmTool.ENVIRONMENT, env);
            result = runner.executeNativeOrPsCommand(flags, command, psCommand, summary, false);
            if (!resultCodeCheck.apply(result)) {
                throw logWithDetailsAndThrow(format("Execution failed, invalid result %s for %s", result, summary), null);
            }
        } finally {
            mutexRelease.run();
        }
        return result;
    }

    public WinRmExecuteHelper failOnNonZeroResultCode() {
        return updateTaskAndFailOnNonZeroResultCode();
    }

    public WinRmExecuteHelper updateTaskAndFailOnNonZeroResultCode() {
        gatherOutput();
        // a failure listener would be a cleaner way

        resultCodeCheck = new Predicate<Integer>() {
            @Override
            public boolean apply(@Nullable Integer input) {
                if (input==0) return true;

                try {
                    String notes = "";
                    if (!getResultStderr().isEmpty())
                        notes += "STDERR\n" + getResultStderr()+"\n";
                    if (!getResultStdout().isEmpty())
                        notes += "\n" + "STDOUT\n" + getResultStdout()+"\n";
                    Tasks.setExtraStatusDetails(notes.trim());
                } catch (Exception e) {
                    LOG.warn("Unable to collect additional metadata on failure of "+summary+": "+e);
                }

                return false;
            }
        };

        return this;
    }

    protected boolean gatherOutput = false;

    public WinRmExecuteHelper gatherOutput() {
        return gatherOutput(true);
    }
    public WinRmExecuteHelper gatherOutput(boolean gather) {
        gatherOutput = gather;
        return this;
    }

    protected RuntimeException logWithDetailsAndThrow(String message, Throwable optionalCause) {
        LOG.warn(message + " (throwing)");
        int maxLength = 1024;
        LOG.warn(message + " (throwing)");
        Streams.logStreamTail(LOG, "STDERR of problem in "+Tasks.current(), stderr, maxLength);
        Streams.logStreamTail(LOG, "STDOUT of problem in "+Tasks.current(), stdout, maxLength);
        Streams.logStreamTail(LOG, "STDIN of problem in "+Tasks.current(), Streams.byteArrayOfString(command != null ? command : psCommand), 4096);
        if (optionalCause!=null) throw new IllegalStateException(message, optionalCause);
        throw new IllegalStateException(message);
    }

    @SuppressWarnings("rawtypes")
    public Map getFlags() {
        return flags;
    }

    public String getResultStdout() {
        if (stdout==null) throw new IllegalStateException("output not available on "+this+"; ensure gatherOutput(true) is set");
        return stdout.toString();
    }
    public String getResultStderr() {
        if (stderr==null) throw new IllegalStateException("output not available on "+this+"; ensure gatherOutput(true) is set");
        return stderr.toString();
    }
}
