/*
 * 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.ambari.server.actionmanager;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.annotation.Nullable;

import org.apache.ambari.server.Role;
import org.apache.ambari.server.RoleCommand;
import org.apache.ambari.server.agent.AgentCommand.AgentCommandType;
import org.apache.ambari.server.agent.ExecutionCommand;
import org.apache.ambari.server.metadata.RoleCommandPair;
import org.apache.ambari.server.orm.dao.HostRoleCommandDAO;
import org.apache.ambari.server.orm.entities.RoleSuccessCriteriaEntity;
import org.apache.ambari.server.orm.entities.StageEntity;
import org.apache.ambari.server.serveraction.ServerAction;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.Host;
import org.apache.ambari.server.state.ServiceComponentHostEvent;
import org.apache.ambari.server.state.svccomphost.ServiceComponentHostServerActionEvent;
import org.apache.ambari.server.utils.StageUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.google.inject.persist.Transactional;

//This class encapsulates the stage. The stage encapsulates all the information
//required to persist an action.
public class Stage {

  /**
   * Used because in-memory storage of commands requires a hostname for maps
   * when the underlying store does not (host_id is {@code null}).  We also
   * don't want stages getting confused with Ambari vs cluster hosts, so
   * don't use {@link StageUtils#getHostName()}
   */
  public static final String INTERNAL_HOSTNAME = "_internal_ambari";

  private static final Logger LOG = LoggerFactory.getLogger(Stage.class);
  private final long requestId;
  private String clusterName;
  private long clusterId = -1L;
  private long stageId = -1;
  private final String logDir;
  private final String requestContext;
  private HostRoleStatus status = HostRoleStatus.PENDING;
  private HostRoleStatus displayStatus = HostRoleStatus.PENDING;
  private String commandParamsStage;
  private String hostParamsStage;

  private CommandExecutionType commandExecutionType = CommandExecutionType.STAGE;

  private boolean skippable;
  private boolean supportsAutoSkipOnFailure;

  private int stageTimeout = -1;

  private volatile boolean wrappersLoaded = false;

  //Map of roles to successFactors for this stage. Default is 1 i.e. 100%
  private Map<Role, Float> successFactors = new HashMap<>();

  //Map of host to host-roles
  Map<String, Map<String, HostRoleCommand>> hostRoleCommands =
    new TreeMap<>();
  private Map<String, List<ExecutionCommandWrapper>> commandsToSend =
    new TreeMap<>();

  @Inject
  private HostRoleCommandFactory hostRoleCommandFactory;

  @Inject
  private ExecutionCommandWrapperFactory ecwFactory;

  @AssistedInject
  public Stage(@Assisted long requestId,
      @Assisted("logDir") String logDir,
      @Assisted("clusterName") @Nullable String clusterName,
      @Assisted("clusterId") long clusterId,
      @Assisted("requestContext") @Nullable String requestContext,
      @Assisted("commandParamsStage") String commandParamsStage,
      @Assisted("hostParamsStage") String hostParamsStage,
      HostRoleCommandFactory hostRoleCommandFactory, ExecutionCommandWrapperFactory ecwFactory) {
    wrappersLoaded = true;
    this.requestId = requestId;
    this.logDir = logDir;
    this.clusterName = clusterName;
    this.clusterId = clusterId;
    this.requestContext = requestContext == null ? "" : requestContext;
    this.commandParamsStage = commandParamsStage;
    this.hostParamsStage = hostParamsStage;

    skippable = false;
    supportsAutoSkipOnFailure = false;

    this.hostRoleCommandFactory = hostRoleCommandFactory;
    this.ecwFactory = ecwFactory;
  }

  @AssistedInject
  public Stage(@Assisted StageEntity stageEntity, HostRoleCommandDAO hostRoleCommandDAO,
      ActionDBAccessor dbAccessor, Clusters clusters, HostRoleCommandFactory hostRoleCommandFactory,
      ExecutionCommandWrapperFactory ecwFactory) {
    this.hostRoleCommandFactory = hostRoleCommandFactory;
    this.ecwFactory = ecwFactory;

    requestId = stageEntity.getRequestId();
    stageId = stageEntity.getStageId();
    skippable = stageEntity.isSkippable();
    supportsAutoSkipOnFailure = stageEntity.isAutoSkipOnFailureSupported();
    logDir = stageEntity.getLogInfo();

    clusterId = stageEntity.getClusterId().longValue();
    if (-1L != clusterId) {
      try {
        clusterName = clusters.getClusterById(clusterId).getClusterName();
      } catch (Exception e) {
        LOG.debug("Could not load cluster with id {}, the cluster may have been removed for stage {}",
            Long.valueOf(clusterId), Long.valueOf(stageId));
      }
    }

    requestContext = stageEntity.getRequestContext();
    commandParamsStage = stageEntity.getCommandParamsStage();
    hostParamsStage = stageEntity.getHostParamsStage();
    commandExecutionType = stageEntity.getCommandExecutionType();
    status = stageEntity.getStatus();
    displayStatus = stageEntity.getDisplayStatus();

    List<Long> taskIds = hostRoleCommandDAO.findTaskIdsByStage(requestId, stageId);
    Collection<HostRoleCommand> commands = dbAccessor.getTasks(taskIds);

    for (HostRoleCommand command : commands) {
      // !!! some commands won't have a hostname, because they are server-side and
      // don't hold that information.  In that case, use the special key to
      // use in the map
      String hostname = getSafeHost(command.getHostName());

      if (!hostRoleCommands.containsKey(hostname)) {
        hostRoleCommands.put(hostname, new LinkedHashMap<>());
      }

      hostRoleCommands.get(hostname).put(command.getRole().toString(), command);
    }

    for (RoleSuccessCriteriaEntity successCriteriaEntity : stageEntity.getRoleSuccessCriterias()) {
      successFactors.put(successCriteriaEntity.getRole(), successCriteriaEntity.getSuccessFactor().floatValue());
    }
  }

  /**
   * Creates object to be persisted in database
   * @return StageEntity
   */
  public synchronized StageEntity constructNewPersistenceEntity() {
    StageEntity stageEntity = new StageEntity();
    stageEntity.setRequestId(requestId);
    stageEntity.setStageId(getStageId());
    stageEntity.setLogInfo(logDir);
    stageEntity.setSkippable(skippable);
    stageEntity.setAutoSkipFailureSupported(supportsAutoSkipOnFailure);
    stageEntity.setRequestContext(requestContext);
    stageEntity.setHostRoleCommands(new ArrayList<>());
    stageEntity.setRoleSuccessCriterias(new ArrayList<>());
    stageEntity.setCommandParamsStage(commandParamsStage);
    if (null != hostParamsStage) {
      stageEntity.setHostParamsStage(hostParamsStage);
    }
    stageEntity.setCommandExecutionType(commandExecutionType);
    stageEntity.setStatus(status);
    stageEntity.setDisplayStatus(displayStatus);

    for (Role role : successFactors.keySet()) {
      RoleSuccessCriteriaEntity roleSuccessCriteriaEntity = new RoleSuccessCriteriaEntity();
      roleSuccessCriteriaEntity.setRole(role);
      roleSuccessCriteriaEntity.setStage(stageEntity);
      roleSuccessCriteriaEntity.setSuccessFactor(successFactors.get(role).doubleValue());
      stageEntity.getRoleSuccessCriterias().add(roleSuccessCriteriaEntity);
    }
    return stageEntity;
  }

  void checkWrappersLoaded() {
    if (!wrappersLoaded) {
      synchronized (this) { // Stages are not used concurrently now, but it won't be performance loss
        if (!wrappersLoaded) {
          loadExecutionCommandWrappers();
        }
      }
    }
  }

  @Transactional
  void loadExecutionCommandWrappers() {
    for (Map.Entry<String, Map<String, HostRoleCommand>> hostRoleCommandEntry : hostRoleCommands.entrySet()) {
      String hostname = hostRoleCommandEntry.getKey();
      List<ExecutionCommandWrapper> wrappers = new ArrayList<>();
      Map<String, HostRoleCommand> roleCommandMap = hostRoleCommandEntry.getValue();
      for (Map.Entry<String, HostRoleCommand> roleCommandEntry : roleCommandMap.entrySet()) {
        wrappers.add(roleCommandEntry.getValue().getExecutionCommandWrapper());
      }
      commandsToSend.put(hostname, wrappers);
    }
  }

  public List<HostRoleCommand> getOrderedHostRoleCommands() {
    List<HostRoleCommand> commands = new ArrayList<>();
    //Correct due to ordered maps
    for (Map.Entry<String, Map<String, HostRoleCommand>> hostRoleCommandEntry : hostRoleCommands.entrySet()) {
      for (Map.Entry<String, HostRoleCommand> roleCommandEntry : hostRoleCommandEntry.getValue().entrySet()) {
        commands.add(roleCommandEntry.getValue());
      }
    }
    return commands;
  }

  /**
   * Returns <Role, RoleCommand> pairs which are in progress.
   * @return
   */
  public Set<RoleCommandPair> getHostRolesInProgress() {
    Set<RoleCommandPair> commandsToScheduleSet = new HashSet<>();
    for (Map.Entry<String, Map<String, HostRoleCommand>> hostRoleCommandEntry : hostRoleCommands.entrySet()) {
      for (Map.Entry<String, HostRoleCommand> roleCommandEntry : hostRoleCommandEntry.getValue().entrySet()) {
        if (HostRoleStatus.IN_PROGRESS_STATUSES.contains(roleCommandEntry.getValue().getStatus())) {
          commandsToScheduleSet.add(
            new RoleCommandPair(roleCommandEntry.getValue().getRole(), roleCommandEntry.getValue().getRoleCommand()));
        }
      }
    }
    return commandsToScheduleSet;
  }

  public String getCommandParamsStage() {
    return commandParamsStage;
  }

  public void setCommandParamsStage(String commandParamsStage) {
    this.commandParamsStage = commandParamsStage;
  }

  public String getHostParamsStage() {
    return hostParamsStage;
  }

  public void setHostParamsStage(String hostParamsStage) {
    this.hostParamsStage = hostParamsStage;
  }

  public CommandExecutionType getCommandExecutionType() {
    return commandExecutionType;
  }

  public void setCommandExecutionType(CommandExecutionType commandExecutionType) {
    this.commandExecutionType = commandExecutionType;
  }

  /**
   * get current status of the stage
   * @return {@link HostRoleStatus}
   */
  public HostRoleStatus getStatus() {
    return status;
  }

  /**
   * sets status of the stage
   * @param status {@link HostRoleStatus}
   */
  public void setStatus(HostRoleStatus status) {
    this.status = status;
  }


  public synchronized void setStageId(long stageId) {
    if (this.stageId != -1) {
      throw new RuntimeException("Attempt to set stageId again! Not allowed.");
    }
    //used on stage creation only, no need to check if wrappers loaded
    this.stageId = stageId;
    for (String host: commandsToSend.keySet()) {
      for (ExecutionCommandWrapper wrapper : commandsToSend.get(host)) {
        ExecutionCommand cmd = wrapper.getExecutionCommand();
        cmd.setRequestAndStage(requestId, stageId);
      }
    }
  }

  public synchronized long getStageId() {
    return stageId;
  }

  public String getActionId() {
    return StageUtils.getActionId(requestId, getStageId());
  }

  private synchronized ExecutionCommandWrapper addGenericExecutionCommand(String clusterName,
      String hostName, Role role, RoleCommand command, ServiceComponentHostEvent event,
      boolean retryAllowed, boolean autoSkipFailure) {

    boolean isHostRoleCommandAutoSkippable = autoSkipFailure && supportsAutoSkipOnFailure
        && skippable;

    // used on stage creation only, no need to check if wrappers loaded
    HostRoleCommand hrc = hostRoleCommandFactory.create(hostName, role, event, command,
        retryAllowed, isHostRoleCommandAutoSkippable);

    return addGenericExecutionCommand(clusterName, hostName, role, command, event, hrc);
  }

  private ExecutionCommandWrapper addGenericExecutionCommand(Cluster cluster, Host host, Role role,
      RoleCommand command, ServiceComponentHostEvent event, boolean retryAllowed,
      boolean autoSkipFailure) {

    boolean isHostRoleCommandAutoSkippable = autoSkipFailure && supportsAutoSkipOnFailure
        && skippable;

    HostRoleCommand hrc = hostRoleCommandFactory.create(host, role, event, command, retryAllowed,
        isHostRoleCommandAutoSkippable);

    return addGenericExecutionCommand(cluster.getClusterName(), host.getHostName(), role, command,
        event, hrc);
  }

  //TODO refactor method to use Host object (host_id support)
  private ExecutionCommandWrapper addGenericExecutionCommand(String clusterName, String hostName, Role role, RoleCommand command, ServiceComponentHostEvent event, HostRoleCommand hrc) {
    ExecutionCommand cmd = new ExecutionCommand();
    ExecutionCommandWrapper wrapper = ecwFactory.createFromCommand(cmd);
    hrc.setExecutionCommandWrapper(wrapper);
    cmd.setClusterId(Long.toString(clusterId));
    cmd.setHostname(hostName);
    cmd.setClusterName(clusterName);
    cmd.setRequestAndStage(requestId, stageId);
    cmd.setRole(role.name());
    cmd.setRoleCommand(command);

    cmd.setServiceName("");

    Map<String, HostRoleCommand> hrcMap = hostRoleCommands.get(hostName);
    if (hrcMap == null) {
      hrcMap = new LinkedHashMap<>();
      hostRoleCommands.put(hostName, hrcMap);
    }
    if (hrcMap.get(role.toString()) != null) {
      throw new RuntimeException(
          "Setting the host role command second time for same stage: stage="
              + getActionId() + ", host=" + hostName + ", role=" + role);
    }
    hrcMap.put(role.toString(), hrc);
    List<ExecutionCommandWrapper> execCmdList = commandsToSend.get(hostName);
    if (execCmdList == null) {
      execCmdList = new ArrayList<>();
      commandsToSend.put(hostName, execCmdList);
    }

    if (execCmdList.contains(wrapper)) {
      //todo: proper exception
      throw new RuntimeException(
          "Setting the execution command second time for same stage: stage="
              + getActionId() + ", host=" + hostName + ", role=" + role+ ", event="+event);
    }
    execCmdList.add(wrapper);
    return wrapper;
  }

  /**
   * A new host role command is created for execution. Creates both
   * ExecutionCommand and HostRoleCommand objects and adds them to the Stage.
   * This should be called only once for a host-role for a given stage.
   */
  public synchronized void addHostRoleExecutionCommand(String host, Role role, RoleCommand command,
      ServiceComponentHostEvent event, String clusterName, String serviceName, boolean retryAllowed,
      boolean autoSkipFailure) {

    boolean isHostRoleCommandAutoSkippable = autoSkipFailure && supportsAutoSkipOnFailure
        && skippable;

    ExecutionCommandWrapper commandWrapper = addGenericExecutionCommand(clusterName, host, role,
        command, event, retryAllowed, isHostRoleCommandAutoSkippable);

    commandWrapper.getExecutionCommand().setServiceName(serviceName);
  }

  /**
   * A new host role command is created for execution. Creates both
   * ExecutionCommand and HostRoleCommand objects and adds them to the Stage.
   * This should be called only once for a host-role for a given stage.
   */
  public synchronized void addHostRoleExecutionCommand(Host host, Role role, RoleCommand command,
      ServiceComponentHostEvent event, Cluster cluster, String serviceName, boolean retryAllowed,
      boolean autoSkipFailure) {

    boolean isHostRoleCommandAutoSkippable = autoSkipFailure && supportsAutoSkipOnFailure
        && skippable;

    ExecutionCommandWrapper commandWrapper = addGenericExecutionCommand(cluster, host, role,
        command, event, retryAllowed, isHostRoleCommandAutoSkippable);

    commandWrapper.getExecutionCommand().setServiceName(serviceName);
  }

  /**
   * <p/>
   * Creates server-side execution command.
   * <p/>
   * The action name for this command is expected to be the classname of a
   * {@link org.apache.ambari.server.serveraction.ServerAction} implementation
   * which will be instantiated and invoked as needed.
   *
   * @param actionName
   *          a String declaring the action name (in the form of a classname) to
   *          execute
   * @param userName
   *          the name of the user who created this stage; may be null for
   *          anonymous user
   * @param role
   *          the Role for this command
   * @param command
   *          the RoleCommand for this command
   * @param clusterName
   *          a String identifying the cluster on which to to execute this
   *          command
   * @param event
   *          a ServiceComponentHostServerActionEvent
   * @param commandParams
   *          a Map of String to String data used to pass to the action - this
   *          may be empty or null if no data is relevant
   * @param commandDetail
   *          a String declaring a descriptive name to pass to the action - null
   *          or an empty string indicates no value is to be set
   * @param configTags
   *          a Map of configuration tags to set for this command - if null, no
   *          configurations will be available for the command
   * @param timeout
   *          an Integer declaring the timeout for this action - if null, a
   *          default
   * @param retryAllowed
   *          indicates whether retry after failure is allowed
   */
  public synchronized void addServerActionCommand(String actionName,
      @Nullable String userName,
      Role role, RoleCommand command, String clusterName,
      ServiceComponentHostServerActionEvent event, @Nullable Map<String, String> commandParams,
      @Nullable String commandDetail, @Nullable Map<String, Map<String, String>> configTags,
      @Nullable Integer timeout, boolean retryAllowed, boolean autoSkipFailure) {

    boolean isHostRoleCommandAutoSkippable = autoSkipFailure && supportsAutoSkipOnFailure
        && skippable;

    ExecutionCommandWrapper commandWrapper = addGenericExecutionCommand(clusterName,
        INTERNAL_HOSTNAME, role, command, event, retryAllowed, isHostRoleCommandAutoSkippable);

    ExecutionCommand cmd = commandWrapper.getExecutionCommand();

    Map<String, String> cmdParams = new HashMap<>();
    if (commandParams != null) {
      cmdParams.putAll(commandParams);
    }
    if (timeout != null) {
      cmdParams.put(ExecutionCommand.KeyNames.COMMAND_TIMEOUT, Long.toString(timeout));
    }
    cmd.setCommandParams(cmdParams);

    cmd.setConfigurations(new TreeMap<>());

    Map<String, String> roleParams = new HashMap<>();
    roleParams.put(ServerAction.ACTION_NAME, actionName);
    if (userName != null) {
      roleParams.put(ServerAction.ACTION_USER_NAME, userName);
    }
    cmd.setRoleParams(roleParams);

    if(commandDetail != null) {
      HostRoleCommand hostRoleCommand = getHostRoleCommand(INTERNAL_HOSTNAME, role.toString());
      if (hostRoleCommand != null) {
        hostRoleCommand.setCommandDetail(commandDetail);
        hostRoleCommand.setCustomCommandName(actionName);
      }
    }
  }

  /**
   *  Adds cancel command to stage for given cancelTargets collection of
   *  task id's that has to be canceled in Agent layer.
   */
  public synchronized void addCancelRequestCommand(List<Long> cancelTargets, String clusterName, String hostName) {
    ExecutionCommandWrapper commandWrapper = addGenericExecutionCommand(clusterName, hostName,
        Role.AMBARI_SERVER_ACTION, RoleCommand.ABORT, null, false, false);
    ExecutionCommand cmd = commandWrapper.getExecutionCommand();
    cmd.setCommandType(AgentCommandType.CANCEL_COMMAND);

    Assert.notEmpty(cancelTargets, "Provided targets task Id are empty.");

    Map<String, String> roleParams = new HashMap<>();

    roleParams.put("cancelTaskIdTargets", StringUtils.join(cancelTargets, ','));
    cmd.setRoleParams(roleParams);
  }

  /**
   *
   * @return list of hosts
   */
  public synchronized List<String> getHosts() { // TODO: Check whether method should be synchronized
    return new ArrayList<>(hostRoleCommands.keySet());
  }

  synchronized float getSuccessFactor(Role r) {
    Float f = successFactors.get(r);
    if (f == null) {
      if (r.equals(Role.DATANODE) || r.equals(Role.TASKTRACKER) || r.equals(Role.GANGLIA_MONITOR) ||
          r.equals(Role.HBASE_REGIONSERVER)) {
        return (float) 0.5;
      } else {
        return 1;
      }
    } else {
      return f;
    }
  }

  public synchronized void setSuccessFactors(Map<Role, Float> suc) {
    successFactors = suc;
  }

  public synchronized Map<Role, Float> getSuccessFactors() {
    return successFactors;
  }

  public long getRequestId() {
    return requestId;
  }

  public String getClusterName() {
    return clusterName;
  }

  public long getClusterId() {
    return clusterId;
  }


  public String getRequestContext() {
    return requestContext;
  }

  /**
   * @param hostname  the hostname; {@code null} for a server-side stage
   * @param role      the role
   * @return the last attempt time
   */
  public long getLastAttemptTime(String hostname, String role) {
    return hostRoleCommands.get(getSafeHost(hostname)).get(role).getLastAttemptTime();
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @return the number of attempts
   */
  public short getAttemptCount(String hostname, String role) {
    return hostRoleCommands.get(getSafeHost(hostname)).get(role).getAttemptCount();
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   */
  public void incrementAttemptCount(String hostname, String role) {
    hostRoleCommands.get(getSafeHost(hostname)).get(role).incrementAttemptCount();
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @param t           the last time the role was attempted
   */
  public void setLastAttemptTime(String hostname, String role, long t) {
    hostRoleCommands.get(getSafeHost(hostname)).get(role).setLastAttemptTime(t);
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @return            the wrapper
   */
  public ExecutionCommandWrapper getExecutionCommandWrapper(String hostname,
      String role) {
    HostRoleCommand hrc = hostRoleCommands.get(getSafeHost(hostname)).get(role);
    if (hrc != null) {
      return hrc.getExecutionCommandWrapper();
    } else {
      return null;
    }
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @return  the list of commands for the host
   */
  public List<ExecutionCommandWrapper> getExecutionCommands(String hostname) {
    checkWrappersLoaded();
    return commandsToSend.get(getSafeHost(hostname));
  }

/**
 * @param hostname    the hostname; {@code null} for a server-side stage
 * @param role        the role
 * @return the start time for the task
 */
  public long getStartTime(String hostname, String role) {
    return hostRoleCommands.get(getSafeHost(hostname)).get(role).getStartTime();
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @param startTime   the start time
   */
  public void setStartTime(String hostname, String role, long startTime) {
    hostRoleCommands.get(getSafeHost(hostname)).get(role).setStartTime(startTime);
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @return the status
   */
  public HostRoleStatus getHostRoleStatus(String hostname, String role) {
    return hostRoleCommands.get(getSafeHost(hostname)).get(role).getStatus();
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @param status      the status
   */
  public void setHostRoleStatus(String hostname, String role,
      HostRoleStatus status) {
    hostRoleCommands.get(getSafeHost(hostname)).get(role).setStatus(status);
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param roleStr     the role name
   * @return the wrapper event
   */
  public ServiceComponentHostEventWrapper getFsmEvent(String hostname, String roleStr) {
    return hostRoleCommands.get(getSafeHost(hostname)).get(roleStr).getEvent();
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @param exitCode    the exit code
   */
  public void setExitCode(String hostname, String role, int exitCode) {
    hostRoleCommands.get(getSafeHost(hostname)).get(role).setExitCode(exitCode);
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @return the exit code
   */
  public int getExitCode(String hostname, String role) {
    return hostRoleCommands.get(getSafeHost(hostname)).get(role).getExitCode();
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @param stdErr      the standard error string
   */
  public void setStderr(String hostname, String role, String stdErr) {
    hostRoleCommands.get(getSafeHost(hostname)).get(role).setStderr(stdErr);
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @param stdOut      the standard output string
   */
  public void setStdout(String hostname, String role, String stdOut) {
    hostRoleCommands.get(getSafeHost(hostname)).get(role).setStdout(stdOut);
  }

  public synchronized boolean isStageInProgress() {
    for(String host: hostRoleCommands.keySet()) {
      for (String role : hostRoleCommands.get(host).keySet()) {
        HostRoleCommand hrc = hostRoleCommands.get(host).get(role);
        if (hrc == null) {
          return false;
        }
        if (hrc.getStatus().equals(HostRoleStatus.PENDING) ||
            hrc.getStatus().equals(HostRoleStatus.QUEUED) ||
            hrc.getStatus().equals(HostRoleStatus.IN_PROGRESS)) {
          return true;
        }
      }
    }
    return false;
  }

  public synchronized boolean doesStageHaveHostRoleStatus(
      Set<HostRoleStatus> statuses) {
    for(String host: hostRoleCommands.keySet()) {
      for (String role : hostRoleCommands.get(host).keySet()) {
        HostRoleCommand hrc = hostRoleCommands.get(host).get(role);
        if (hrc == null) {
          return false;
        }
        for (HostRoleStatus status : statuses) {
          if (hrc.getStatus().equals(status)) {
            return true;
          }
        }
      }
    }
    return false;
  }

  public Map<String, List<ExecutionCommandWrapper>> getExecutionCommands() {
    checkWrappersLoaded();
    return commandsToSend;
  }

  public String getLogDir() {
    return logDir;
  }

  public Map<String, Map<String, HostRoleCommand>> getHostRoleCommands() {
    return hostRoleCommands;
  }

  /**
   * Gets the {@link HostRoleCommand} matching the specified ID from this stage.
   * This will not hit the database, instead using the pre-cached list of HRCs
   * from the construction of the stage.
   *
   * @param taskId
   *          the ID to match
   * @return the {@link HostRoleCommand} or {@code null} if none match.
   */
  public HostRoleCommand getHostRoleCommand(long taskId) {
    for (Map.Entry<String, Map<String, HostRoleCommand>> hostEntry : hostRoleCommands.entrySet()) {
      Map<String, HostRoleCommand> hostCommands = hostEntry.getValue();
      for (Map.Entry<String, HostRoleCommand> hostCommand : hostCommands.entrySet()) {
        HostRoleCommand hostRoleCommand = hostCommand.getValue();
        if (null != hostRoleCommand && hostRoleCommand.getTaskId() == taskId) {
          return hostRoleCommand;
        }
      }
    }

    return null;
  }

  /**
   * This method should be used only in stage planner. To add
   * a new execution command use
   * {@link #addHostRoleExecutionCommand(String, Role, RoleCommand, ServiceComponentHostEvent, String, String, boolean, boolean)}
   * @param origStage the stage
   * @param hostname  the hostname; {@code null} for a server-side stage
   * @param r         the role
   */
  public synchronized void addExecutionCommandWrapper(Stage origStage,
      String hostname, Role r) {
    //used on stage creation only, no need to check if wrappers loaded

    hostname = getSafeHost(hostname);

    String role = r.toString();
    if (commandsToSend.get(hostname) == null) {
      commandsToSend.put(hostname, new ArrayList<>());
    }
    commandsToSend.get(hostname).add(
        origStage.getExecutionCommandWrapper(hostname, role));
    if (hostRoleCommands.get(hostname) == null) {
      hostRoleCommands.put(hostname, new LinkedHashMap<>());
    }
    // TODO add reference to ExecutionCommand into HostRoleCommand
    hostRoleCommands.get(hostname).put(role,
        origStage.getHostRoleCommand(hostname, role));
  }

  /**
   * @param hostname    the hostname; {@code null} for a server-side stage
   * @param role        the role
   * @return the role command
   */
  public HostRoleCommand getHostRoleCommand(String hostname, String role) {
    return hostRoleCommands.get(getSafeHost(hostname)).get(role);
  }

  /**
   * In this method we sum up all timeout values for all commands inside stage
   */
  public synchronized int getStageTimeout() {
    checkWrappersLoaded();
    if (stageTimeout == -1) {
      for (String host: commandsToSend.keySet()) {
        int summaryTaskTimeoutForHost = 0;
        for (ExecutionCommandWrapper command : commandsToSend.get(host)) {
          Map<String, String> commandParams =
                command.getExecutionCommand().getCommandParams();
          String timeoutKey = ExecutionCommand.KeyNames.COMMAND_TIMEOUT;
          if (commandParams != null && commandParams.containsKey(timeoutKey)) {
            String timeoutStr = commandParams.get(timeoutKey);
            long commandTimeout =
                Long.parseLong(timeoutStr) * 1000; // Converting to milliseconds
            summaryTaskTimeoutForHost += commandTimeout;
          } else {
            LOG.error("Execution command has no timeout parameter" +
              command);
          }
        }
        if (summaryTaskTimeoutForHost > stageTimeout) {
          stageTimeout = summaryTaskTimeoutForHost;
        }
      }
    }
    return stageTimeout;
  }

  /**
   * Determine whether or not this stage is skippable.
   *
   * A skippable stage can be skipped on failure so that the
   * remaining stages of the request can execute.
   * If a stage is not skippable, a failure will cause the
   * remaining stages of the request to be aborted.
   *
   * @return true if this stage is skippable
   */
  public boolean isSkippable() {
    return skippable;
  }

  /**
   * Set skippable for this stage.
   *
   * A skippable stage can be skipped on failure so that the
   * remaining stages of the request can execute.
   * If a stage is not skippable, a failure will cause the
   * remaining stages of the request to be aborted.
   *
   * @param skippable  true if this stage should be skippable
   */
  public void setSkippable(boolean skippable) {
    this.skippable = skippable;
  }

  /**
   * Determine whether this stage supports automatically skipping failures of
   * its commands.
   *
   * @return {@code true} if this stage supports automatically skipping failures
   *         of its commands.
   */
  public boolean isAutoSkipOnFailureSupported() {
    return supportsAutoSkipOnFailure;
  }

  /**
   * Sets whether this stage supports automatically skipping failures of its
   * commands.
   *
   * @param supportsAutoSkipOnFailure
