/*
 * 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.stack.upgrade.orchestrate;

import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_DIRECTION;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_FAIL_ON_CHECK_WARNINGS;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_HOST_ORDERED_HOSTS;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_PACK;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_REPO_VERSION_ID;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_REVERT_UPGRADE_ID;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_SKIP_FAILURES;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_SKIP_MANUAL_VERIFICATION;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_SKIP_PREREQUISITE_CHECKS;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_SKIP_SC_FAILURES;
import static org.apache.ambari.server.controller.internal.UpgradeResourceProvider.UPGRADE_TYPE;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.ambari.annotations.Experimental;
import org.apache.ambari.annotations.ExperimentalFeature;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.ServiceNotFoundException;
import org.apache.ambari.server.actionmanager.HostRoleCommandFactory;
import org.apache.ambari.server.agent.ExecutionCommand.KeyNames;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.KerberosDetails;
import org.apache.ambari.server.controller.KerberosHelper;
import org.apache.ambari.server.controller.internal.AbstractControllerResourceProvider;
import org.apache.ambari.server.controller.internal.PreUpgradeCheckResourceProvider;
import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
import org.apache.ambari.server.controller.spi.NoSuchResourceException;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.spi.Request;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.SystemException;
import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
import org.apache.ambari.server.controller.utilities.PredicateBuilder;
import org.apache.ambari.server.controller.utilities.PropertyHelper;
import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
import org.apache.ambari.server.orm.dao.UpgradeDAO;
import org.apache.ambari.server.orm.entities.RepositoryVersionEntity;
import org.apache.ambari.server.orm.entities.UpgradeEntity;
import org.apache.ambari.server.orm.entities.UpgradeHistoryEntity;
import org.apache.ambari.server.serveraction.kerberos.KerberosInvalidConfigurationException;
import org.apache.ambari.server.stack.MasterHostResolver;
import org.apache.ambari.server.stack.upgrade.Direction;
import org.apache.ambari.server.stack.upgrade.Grouping;
import org.apache.ambari.server.stack.upgrade.HostOrderGrouping;
import org.apache.ambari.server.stack.upgrade.HostOrderItem;
import org.apache.ambari.server.stack.upgrade.HostOrderItem.HostOrderActionType;
import org.apache.ambari.server.stack.upgrade.UpgradePack;
import org.apache.ambari.server.stack.upgrade.UpgradeScope;
import org.apache.ambari.server.stageplanner.RoleGraphFactory;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.ConfigHelper;
import org.apache.ambari.server.state.Host;
import org.apache.ambari.server.state.SecurityType;
import org.apache.ambari.server.state.Service;
import org.apache.ambari.server.state.ServiceComponentHost;
import org.apache.ambari.server.state.StackId;
import org.apache.ambari.server.state.StackInfo;
import org.apache.ambari.server.state.repository.ClusterVersionSummary;
import org.apache.ambari.server.state.repository.VersionDefinitionXml;
import org.apache.ambari.spi.RepositoryType;
import org.apache.ambari.spi.RepositoryVersion;
import org.apache.ambari.spi.upgrade.OrchestrationOptions;
import org.apache.ambari.spi.upgrade.UpgradeCheckStatus;
import org.apache.ambari.spi.upgrade.UpgradeInformation;
import org.apache.ambari.spi.upgrade.UpgradeType;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.MoreObjects;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;

/**
 * The {@link UpgradeContext} is used to hold all information pertaining to an
 * upgrade. It is initialized directly from an existing {@link UpgradeEntity} or
 * from a request to create an upgrade/downgrade.
 */
public class UpgradeContext {

  private static final Logger LOG = LoggerFactory.getLogger(UpgradeContext.class);

  public static final String COMMAND_PARAM_CLUSTER_NAME = "clusterName";
  public static final String COMMAND_PARAM_DIRECTION = "upgrade_direction";
  public static final String COMMAND_PARAM_UPGRADE_PACK = "upgrade_pack";
  public static final String COMMAND_PARAM_REQUEST_ID = "request_id";

  public static final String COMMAND_PARAM_UPGRADE_TYPE = "upgrade_type";
  public static final String COMMAND_PARAM_TASKS = "tasks";
  public static final String COMMAND_PARAM_STRUCT_OUT = "structured_out";

  /*
   * The cluster that the upgrade is for.
   */
  private final Cluster m_cluster;

  /**
   * The direction of the upgrade.
   */
  private final Direction m_direction;

  /**
   * The type of upgrade.
   */
  private final UpgradeType m_type;

  /**
   * The upgrade pack for this upgrade.
   */
  private final UpgradePack m_upgradePack;

  /**
   * Upgrades will always have a single version being upgraded to and downgrades
   * will have a single version being downgraded from. This repository
   * represents that version.
   * <p/>
   * When the direction is {@link Direction#UPGRADE}, this represents the target
   * repository. <br/>
   * When the direction is {@link Direction#DOWNGRADE}, this represents the
   * repository being downgraded from.
   */
  private final RepositoryVersionEntity m_repositoryVersion;

  /**
   * Resolves master components on hosts.
   */
  private final MasterHostResolver m_resolver;

  /**
   * A collection of hosts in the cluster which are unhealthy and will not
   * participate in the upgrade.
   */
  private final List<ServiceComponentHost> m_unhealthy = new ArrayList<>();

  /**
   * Mapping of service name to display name.
   */
  private final Map<String, String> m_serviceNames = new HashMap<>();

  /**
   * Mapping of component name to display name.
   */
  private final Map<String, String> m_componentNames = new HashMap<>();

  /**
   * {@code true} if slave/client component failures should be automatically
   * skipped. This will only automatically skip the failure if the task is
   * skippable to begin with.
   */
  private boolean m_autoSkipComponentFailures = false;

  /**
   * {@code true} if service check failures should be automatically skipped.
   * This will only automatically skip the failure if the task is skippable to
   * begin with.
   */
  private boolean m_autoSkipServiceCheckFailures = false;

  /**
   * {@code true} if manual verification tasks should be automatically skipped.
   */
  private boolean m_autoSkipManualVerification = false;

  /**
   * A set of services which are included in this upgrade. This will never be
   * empty - if all services of a cluster are included, then the cluster's
   * current list of services is populated.
   */
  private final Set<String> m_services = new HashSet<>();

  /**
   * A mapping of service to target repository. On an upgrade, this will be the
   * same for all services. On a downgrade, this may be different for each
   * service depending on which repository the service was on before the failed
   * upgrade.
   */
  private final Map<String, RepositoryVersionEntity> m_targetRepositoryMap = new HashMap<>();

  /**
   * A mapping of service to source (from) repository. On an upgrade, this will
   * be the current desired repository of every service. When downgrading, this
   * will be the same for all components and will represent the value returned
   * from {@link #getRepositoryVersion()}.
   */
  private final Map<String, RepositoryVersionEntity> m_sourceRepositoryMap = new HashMap<>();

  /**
   * Used by some {@link Grouping}s to generate commands. It is exposed here
   * mainly for injection purposes since the XML is not created by Guice.
   */
  @Inject
  private HostRoleCommandFactory m_hrcFactory;

  /**
   * Used by some {@link Grouping}s to determine command ordering. It is exposed
   * here mainly for injection purposes since the XML is not created by Guice.
   */
  @Inject
  private RoleGraphFactory m_roleGraphFactory;

  /**
   * Used for serializing the upgrade type.
   */
  @Inject
  private Gson m_gson;

  /**
   * Used for looking up information about components and services.
   */
  @Inject
  private AmbariMetaInfo m_metaInfo;

  /**
   * Used to suggest upgrade packs during creation of an upgrade context.
   */
  @Inject
  private UpgradeHelper m_upgradeHelper;

  /**
   * Used to lookup the repository version from an ID.
   */
  @Inject
  private RepositoryVersionDAO m_repoVersionDAO;

  /**
   * Used to lookup a prior upgrade by ID.
   */
  @Inject
  private UpgradeDAO m_upgradeDAO;

  /**
   * Providers information about the Kerberization of a cluster, such as
   * {@link KerberosDetails}.
   */
  @Inject
  private KerberosHelper m_kerberosHelper;

  /**
   * Used as a quick way to tell if the upgrade is to revert a patch.
   */
  private final boolean m_isRevert;

  /**
   * The ID of the upgrade being reverted if this is a reversion.
   */
  private long m_revertUpgradeId;

  /**
   * Defines orchestration type.  This is not the repository type when reverting a patch.
   */
  private RepositoryType m_orchestration = RepositoryType.STANDARD;

  /**
   * Used to lookup overridable settings like default task parallelism
   */
  @Inject
  private Configuration configuration;

  private OrchestrationOptions m_orchestrationOptions;

  /**
   * Reading upgrade type from provided request  or if nothing were provided,
   * from previous upgrade for downgrade direction.
   *
   * @param upgradeRequestMap arguments provided for current upgrade request
   * @param upgradeEntity previous upgrade entity, should be passed only for downgrade direction
   *
   * @return
   * @throws AmbariException
   */
  private UpgradeType calculateUpgradeType(Map<String, Object> upgradeRequestMap,
                                           UpgradeEntity upgradeEntity) throws AmbariException{

    UpgradeType upgradeType = UpgradeType.ROLLING;

    String upgradeTypeProperty = (String) upgradeRequestMap.get(UPGRADE_TYPE);
    boolean upgradeTypePassed = StringUtils.isNotBlank(upgradeTypeProperty);

    if (upgradeTypePassed){
      try {
        upgradeType = UpgradeType.valueOf(upgradeRequestMap.get(UPGRADE_TYPE).toString());
      } catch (Exception e) {
        throw new AmbariException(String.format("Property %s has an incorrect value of %s.",
          UPGRADE_TYPE, upgradeTypeProperty));
      }
    } else if (upgradeEntity != null){
      upgradeType = upgradeEntity.getUpgradeType();
    }

    return upgradeType;
  }

  @AssistedInject
  public UpgradeContext(@Assisted Cluster cluster,
      @Assisted Map<String, Object> upgradeRequestMap, Gson gson, UpgradeHelper upgradeHelper,
      UpgradeDAO upgradeDAO, RepositoryVersionDAO repoVersionDAO, ConfigHelper configHelper,
      AmbariMetaInfo metaInfo)
      throws AmbariException {
    // injected constructor dependencies
    m_gson = gson;
    m_upgradeHelper = upgradeHelper;
    m_upgradeDAO = upgradeDAO;
    m_repoVersionDAO = repoVersionDAO;
    m_cluster = cluster;
    m_isRevert = upgradeRequestMap.containsKey(UPGRADE_REVERT_UPGRADE_ID);
    m_metaInfo = metaInfo;

    if (m_isRevert) {
      m_revertUpgradeId = Long.parseLong(upgradeRequestMap.get(UPGRADE_REVERT_UPGRADE_ID).toString());
      UpgradeEntity revertUpgrade = m_upgradeDAO.findUpgrade(m_revertUpgradeId);
      UpgradeEntity revertableUpgrade = m_upgradeDAO.findRevertable(cluster.getClusterId());

      if (null == revertUpgrade) {
        throw new AmbariException(
            String.format("Could not find Upgrade with id %s to revert.", m_revertUpgradeId));
      }

      if (null == revertableUpgrade) {
        throw new AmbariException(
            String.format("There are no upgrades for cluster %s which are marked as revertable",
                cluster.getClusterName()));
      }

      if (!revertUpgrade.getOrchestration().isRevertable()) {
        throw new AmbariException(String.format("The %s repository type is not revertable",
            revertUpgrade.getOrchestration()));
      }

      if (revertUpgrade.getDirection() != Direction.UPGRADE) {
        throw new AmbariException(
            "Only successfully completed upgrades can be reverted. Downgrades cannot be reverted.");
      }

      if (!revertableUpgrade.getId().equals(revertUpgrade.getId())) {
        throw new AmbariException(String.format(
            "The only upgrade which is currently allowed to be reverted for cluster %s is upgrade ID %s which was an upgrade to %s",
            cluster.getClusterName(), revertableUpgrade.getId(),
            revertableUpgrade.getRepositoryVersion().getVersion()));
      }

      m_type = calculateUpgradeType(upgradeRequestMap, revertUpgrade);

      // !!! build all service-specific reversions
      Map<String, Service> clusterServices = cluster.getServices();
      for (UpgradeHistoryEntity history : revertUpgrade.getHistory()) {
        String serviceName = history.getServiceName();
        String componentName = history.getComponentName();

        // if the service is no longer installed, do nothing
        if (!clusterServices.containsKey(serviceName)) {
          LOG.warn("{}/{} will not be reverted since it is no longer installed in the cluster",
              serviceName, componentName);

          continue;
        }

        m_services.add(serviceName);
        m_sourceRepositoryMap.put(serviceName, history.getTargetRepositoryVersion());
        m_targetRepositoryMap.put(serviceName, history.getFromReposistoryVersion());
      }

      // the "associated" repository of the revert is the target of what's being reverted
      m_repositoryVersion = revertUpgrade.getRepositoryVersion();

      // !!! the version is used later in validators
      upgradeRequestMap.put(UPGRADE_REPO_VERSION_ID, m_repositoryVersion.getId().toString());
      // !!! use the same upgrade pack that was used in the upgrade being reverted
      upgradeRequestMap.put(UPGRADE_PACK, revertUpgrade.getUpgradePackage());

      // !!! direction can ONLY be an downgrade on revert
      m_direction = Direction.DOWNGRADE;
      m_orchestration = revertUpgrade.getOrchestration();
      m_upgradePack = getUpgradePack(revertUpgrade);
      m_orchestrationOptions = getOrchestrationOptions(metaInfo, m_upgradePack);

    } else {

      // determine direction
      String directionProperty = (String) upgradeRequestMap.get(UPGRADE_DIRECTION);
      if (StringUtils.isEmpty(directionProperty)) {
        throw new AmbariException(String.format("%s is required", UPGRADE_DIRECTION));
      }

      m_direction = Direction.valueOf(directionProperty);

      // depending on the direction, we must either have a target repository or an upgrade we are downgrading from
      switch(m_direction){
        case UPGRADE:{
          String repositoryVersionId = (String) upgradeRequestMap.get(UPGRADE_REPO_VERSION_ID);
          if (null == repositoryVersionId) {
            throw new AmbariException(
                String.format("The property %s is required when the upgrade direction is %s",
                    UPGRADE_REPO_VERSION_ID, m_direction));
          }

          m_type = calculateUpgradeType(upgradeRequestMap, null);

          // depending on the repository, add services
          m_repositoryVersion = m_repoVersionDAO.findByPK(Long.valueOf(repositoryVersionId));
          m_orchestration = m_repositoryVersion.getType();


          Set<String> serviceNames = getServicesForUpgrade(cluster, m_repositoryVersion);
          // add all of the services participating in the upgrade
          m_services.addAll(serviceNames);

          /*
           * For the unit tests tests, there are multiple upgrade packs for the same
           * type, so allow picking one of them. In prod, this is empty.
           */
          String preferredUpgradePackName = (String) upgradeRequestMap.get(UPGRADE_PACK);

          @Experimental(feature = ExperimentalFeature.PATCH_UPGRADES,
              comment="This is wrong; it assumes that any upgrade source AND target are consistent stacks")
          RepositoryVersionEntity upgradeFromRepositoryVersion = cluster.getService(
              serviceNames.iterator().next()).getDesiredRepositoryVersion();

          m_upgradePack = m_upgradeHelper.suggestUpgradePack(m_cluster.getClusterName(),
              upgradeFromRepositoryVersion.getStackId(), m_repositoryVersion.getStackId(), m_direction,
              m_type, preferredUpgradePackName);

          m_orchestrationOptions = getOrchestrationOptions(metaInfo, m_upgradePack);

          break;
        }
        case DOWNGRADE:{
          UpgradeEntity upgrade = m_upgradeDAO.findLastUpgradeForCluster(
              cluster.getClusterId(), Direction.UPGRADE);

          m_repositoryVersion = upgrade.getRepositoryVersion();
          m_orchestration = upgrade.getOrchestration();
          m_type = calculateUpgradeType(upgradeRequestMap, upgrade);

          // populate the repository maps for all services in the upgrade
          for (UpgradeHistoryEntity history : upgrade.getHistory()) {
            m_services.add(history.getServiceName());
            m_sourceRepositoryMap.put(history.getServiceName(), m_repositoryVersion);
            m_targetRepositoryMap.put(history.getServiceName(), history.getFromReposistoryVersion());
          }

          m_upgradePack = getUpgradePack(upgrade);
          m_orchestrationOptions = getOrchestrationOptions(metaInfo, m_upgradePack);

          break;
        }
        default:
          throw new AmbariException(
              String.format("%s is not a valid upgrade direction.", m_direction));
      }
    }

    // the validator will throw an exception if the upgrade request is not valid
    UpgradeRequestValidator upgradeRequestValidator = buildValidator(m_type);
    upgradeRequestValidator.validate(cluster, m_direction, m_type, m_upgradePack,
        upgradeRequestMap);

    // optionally skip failures - this can be supplied on either the request or
    // in the upgrade pack explicitely, however the request will always override
    // the upgrade pack if explicitely specified
    boolean skipComponentFailures = m_upgradePack.isComponentFailureAutoSkipped();
    boolean skipServiceCheckFailures = m_upgradePack.isServiceCheckFailureAutoSkipped();

    // only override the upgrade pack if set on the request
    if (upgradeRequestMap.containsKey(UPGRADE_SKIP_FAILURES)) {
      skipComponentFailures = Boolean.parseBoolean(
          (String) upgradeRequestMap.get(UPGRADE_SKIP_FAILURES));
    }

    // only override the upgrade pack if set on the request
    if (upgradeRequestMap.containsKey(UPGRADE_SKIP_SC_FAILURES)) {
      skipServiceCheckFailures = Boolean.parseBoolean(
          (String) upgradeRequestMap.get(UPGRADE_SKIP_SC_FAILURES));
    }

    boolean skipManualVerification = false;
    if (upgradeRequestMap.containsKey(UPGRADE_SKIP_MANUAL_VERIFICATION)) {
      skipManualVerification = Boolean.parseBoolean(
          (String) upgradeRequestMap.get(UPGRADE_SKIP_MANUAL_VERIFICATION));
    }

    m_autoSkipComponentFailures = skipComponentFailures;
    m_autoSkipServiceCheckFailures = skipServiceCheckFailures;
    m_autoSkipManualVerification = skipManualVerification;

    m_resolver = new MasterHostResolver(m_cluster, configHelper, this);
  }

  /**
   * Constructor.
   *
   * @param cluster
   *          the cluster that the upgrade is for
   * @param upgradeEntity
   *          the upgrade entity
   */
  @AssistedInject
  public UpgradeContext(@Assisted Cluster cluster, @Assisted UpgradeEntity upgradeEntity,
      AmbariMetaInfo ambariMetaInfo, ConfigHelper configHelper) {
    m_metaInfo = ambariMetaInfo;

    m_cluster = cluster;
    m_type = upgradeEntity.getUpgradeType();
    m_direction = upgradeEntity.getDirection();
    // !!! this is the overall target stack repo version, not the source repo
    m_repositoryVersion = upgradeEntity.getRepositoryVersion();

    m_autoSkipComponentFailures = upgradeEntity.isComponentFailureAutoSkipped();
    m_autoSkipServiceCheckFailures = upgradeEntity.isServiceCheckFailureAutoSkipped();

    /*
     * This feels wrong.  We need the upgrade pack used when creating the upgrade, as that
     * is really the source, not the target.  Can NOT use upgradeEntity.getRepoVersion() here
     * for the stack id. Consulting the service map should work out since full upgrades are all same source stack,
     * and patches by definition are the same source stack (just different repos of that stack).
     */
    @Experimental(feature = ExperimentalFeature.PATCH_UPGRADES)
    StackId stackId = null;

    List<UpgradeHistoryEntity> allHistory = upgradeEntity.getHistory();
    for (UpgradeHistoryEntity history : allHistory) {
      String serviceName = history.getServiceName();
      RepositoryVersionEntity sourceRepositoryVersion = history.getFromReposistoryVersion();
      RepositoryVersionEntity targetRepositoryVersion = history.getTargetRepositoryVersion();
      m_sourceRepositoryMap.put(serviceName, sourceRepositoryVersion);
      m_targetRepositoryMap.put(serviceName, targetRepositoryVersion);
      m_services.add(serviceName);

      if (null == stackId) {
        stackId = sourceRepositoryVersion.getStackId();
      }
    }

    m_upgradePack = getUpgradePack(upgradeEntity);

    m_resolver = new MasterHostResolver(m_cluster, configHelper, this);
    m_orchestration = upgradeEntity.getOrchestration();

    m_isRevert = upgradeEntity.getOrchestration().isRevertable()
        && upgradeEntity.getDirection() == Direction.DOWNGRADE;

    m_orchestrationOptions = getOrchestrationOptions(ambariMetaInfo, m_upgradePack);
  }

  /**
   * Getting stackId from the set of versions. Is is possible until we upgrading components on the same stack.
   *
   * Note: Function should be modified for cross-stack upgrade.
   *
   * @param version {@link Set} of services repository versions
   * @return
   * {@link StackId} based on provided versions
   */
  @Experimental(feature = ExperimentalFeature.PATCH_UPGRADES, comment="This is wrong")
  public StackId getStackIdFromVersions(Map<String, RepositoryVersionEntity> version) {
    return version.values().iterator().next().getStackId();
  }

  /**
   * Gets the upgrade pack for this upgrade.
   *
   * @return the upgrade pack
   */
  public UpgradePack getUpgradePack() {
    return m_upgradePack;
  }

  /**
   * Gets the cluster that the upgrade is for.
   *
   * @return the cluster (never {@code null}).
   */
  public Cluster getCluster() {
    return m_cluster;
  }

  /**
   * Gets the version that components are being considered to be "coming from".
   * <p/>
   * With a {@link Direction#UPGRADE}, this value represent the services'
   * desired repository. However, {@link Direction#DOWNGRADE} will use the same
   * value for all services which is the version that the downgrade is coming
   * from.
   *
   * @return the source version for the upgrade
   */
  public Map<String, RepositoryVersionEntity> getSourceVersions() {
    return new HashMap<>(m_sourceRepositoryMap);
  }

  /**
   * Gets the version that service is being considered to be "coming from".
   * <p/>
   * With a {@link Direction#UPGRADE}, this value represent the services'
   * desired repository. However, {@link Direction#DOWNGRADE} will use the same
   * value for all services which is the version that the downgrade is coming
   * from.
   *
   * @return the source repository for the upgrade
   */
  public RepositoryVersionEntity getSourceRepositoryVersion(String serviceName) {
    return m_sourceRepositoryMap.get(serviceName);
  }

  /**
   * Gets the version that service is being considered to be "coming from".
   * <p/>
   * With a {@link Direction#UPGRADE}, this value represent the services'
   * desired repository. However, {@link Direction#DOWNGRADE} will use the same
   * value for all services which is the version that the downgrade is coming
   * from.
   *
   * @return the source repository for the upgrade
   * @see #getSourceRepositoryVersion(String)
   */
  public String getSourceVersion(String serviceName) {
    RepositoryVersionEntity serviceSourceVersion = m_sourceRepositoryMap.get(serviceName);
    return serviceSourceVersion.getVersion();
  }

  /**
   * Gets the version being upgraded to or downgraded to for all services
   * participating. This is the version that the service will be on if the
   * upgrade or downgrade succeeds.
   * <p/>
   * With a {@link Direction#UPGRADE}, all services should be targetting the
   * same repository version. However, {@link Direction#DOWNGRADE} will target
   * the original repository that the service was on.
   *
   * @return the target version for the upgrade
   */
  public Map<String, RepositoryVersionEntity> getTargetVersions() {
    return new HashMap<>(m_targetRepositoryMap);
  }

  /**
   * Gets the repository being upgraded to or downgraded to for the given
   * service. This is the version that the service will be on if the upgrade or
   * downgrade succeeds.
   * <p/>
   * With a {@link Direction#UPGRADE}, all services should be targeting the
   * same repository version. However, {@link Direction#DOWNGRADE} will target
   * the original repository that the service was on.
   *
   * @return the target repository for the upgrade
   */
  public RepositoryVersionEntity getTargetRepositoryVersion(String serviceName) {
    return m_targetRepositoryMap.get(serviceName);
  }

  /**
   * Gets the version being upgraded to or downgraded to for the given service.
   * This is the version that the service will be on if the upgrade or downgrade
   * succeeds.
   * <p/>
   * With a {@link Direction#UPGRADE}, all services should be targetting the
   * same repository version. However, {@link Direction#DOWNGRADE} will target
   * the original repository that the service was on.
   *
   * @return the target version for the upgrade
   * @see #getTargetRepositoryVersion(String)
   */
  public String getTargetVersion(String serviceName) {
    RepositoryVersionEntity serviceTargetVersion = m_targetRepositoryMap.get(serviceName);
    return serviceTargetVersion.getVersion();
  }

  /**
   * @return the direction of the upgrade
   */
  public Direction getDirection() {
    return m_direction;
  }

  /**
   * @return the type of upgrade.
   */
  public UpgradeType getType() {
    return m_type;
  }

  /**
   * @return the resolver
   */
  public MasterHostResolver getResolver() {
    return m_resolver;
  }

  /**
   * @return the metainfo for access to service definitions
   */
  public AmbariMetaInfo getAmbariMetaInfo() {
    return m_metaInfo;
  }

  /**
   * @param unhealthy a list of unhealthy host components
   */
  public void addUnhealthy(List<ServiceComponentHost> unhealthy) {
    m_unhealthy.addAll(unhealthy);
  }

  /**
   * Gets the single repository version for the upgrade depending on the
   * direction.
   * <p/>
   * If the direction is {@link Direction#UPGRADE} then this will return the
   * target repository which every service will be on if the upgrade is
   * finalized. <p/>
   * If the direction is {@link Direction#DOWNGRADE} then this will return the
   * repository from which the downgrade is coming from.
   *
   * @return the target repository version for this upgrade (never
   *         {@code null}).
   */
  public RepositoryVersionEntity getRepositoryVersion() {
    return m_repositoryVersion;
  }

  /**
   * @return the service display name, or the service name if not set
   */
  public String getServiceDisplay(String service) {
    if (m_serviceNames.containsKey(service)) {
      return m_serviceNames.get(service);
    }

    return service;
  }

  /**
   * @return the component display name, or the component name if not set
   */
  public String getComponentDisplay(String service, String component) {
    String key = service + ":" + component;
    if (m_componentNames.containsKey(key)) {
      return m_componentNames.get(key);
    }

    return component;
  }

  /**
   * @param service     the service name
   * @param displayName the display name for the service
   */
  public void setServiceDisplay(String service, String displayName) {
    m_serviceNames.put(service, (displayName == null) ? service : displayName);
  }

  /**
   * @param service     the service name that owns the component
   * @param component   the component name
   * @param displayName the display name for the component
   */
  public void setComponentDisplay(String service, String component, String displayName) {
    String key = service + ":" + component;
    m_componentNames.put(key, displayName);
  }

  /**
   * Gets whether skippable components that failed are automatically skipped.
   *
   * @return the skipComponentFailures
   */
  public boolean isComponentFailureAutoSkipped() {
    return m_autoSkipComponentFailures;
  }

  /**
   * Gets whether skippable service checks that failed are automatically
   * skipped.
   *
   * @return the skipServiceCheckFailures
   */
  public boolean isServiceCheckFailureAutoSkipped() {
    return m_autoSkipServiceCheckFailures;
  }

  /**
   * Gets whether manual verification tasks can be automatically skipped.
   *
   * @return the skipManualVerification
   */
  public boolean isManualVerificationAutoSkipped() {
    return m_autoSkipManualVerification;
  }

  /**
   * Gets the services participating in the upgrade.
   *
   * @return the set of supported services. This collection should never be
   *         empty.
   */
  @Experimental(feature=ExperimentalFeature.PATCH_UPGRADES)
  public Set<String> getSupportedServices() {
    return Collections.unmodifiableSet(m_services);
  }

  /**
   * Gets if a service is supported.
   *
   * @param serviceName
   *          the service name to check.
   * @return {@code true} when the service is supported
   */
  @Experimental(feature=ExperimentalFeature.PATCH_UPGRADES)
  public boolean isServiceSupported(String serviceName) {
    return m_services.contains(serviceName);
  }

  @Experimental(feature = ExperimentalFeature.PATCH_UPGRADES)
  public boolean isScoped(UpgradeScope scope) {
    if (scope == UpgradeScope.ANY) {
      return true;
    }

    switch (m_orchestration) {
      case PATCH:
      case SERVICE:
      case MAINT:
        return scope == UpgradeScope.PARTIAL;
      case STANDARD:
        return scope == UpgradeScope.COMPLETE;
      default:
        break;
    }

    return false;
  }

  /**
