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

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.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.persistence.RollbackException;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.ConfigGroupNotFoundException;
import org.apache.ambari.server.DuplicateResourceException;
import org.apache.ambari.server.ObjectNotFoundException;
import org.apache.ambari.server.ParentObjectNotFoundException;
import org.apache.ambari.server.ServiceComponentHostNotFoundException;
import org.apache.ambari.server.ServiceNotFoundException;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.AmbariSessionManager;
import org.apache.ambari.server.controller.ClusterResponse;
import org.apache.ambari.server.controller.ConfigurationResponse;
import org.apache.ambari.server.controller.MaintenanceStateHelper;
import org.apache.ambari.server.controller.ServiceConfigVersionResponse;
import org.apache.ambari.server.orm.RequiresSession;
import org.apache.ambari.server.orm.cache.ConfigGroupHostMapping;
import org.apache.ambari.server.orm.cache.HostConfigMapping;
import org.apache.ambari.server.orm.dao.AlertDefinitionDAO;
import org.apache.ambari.server.orm.dao.AlertDispatchDAO;
import org.apache.ambari.server.orm.dao.ClusterDAO;
import org.apache.ambari.server.orm.dao.ClusterStateDAO;
import org.apache.ambari.server.orm.dao.ClusterVersionDAO;
import org.apache.ambari.server.orm.dao.ConfigGroupHostMappingDAO;
import org.apache.ambari.server.orm.dao.HostConfigMappingDAO;
import org.apache.ambari.server.orm.dao.HostDAO;
import org.apache.ambari.server.orm.dao.HostVersionDAO;
import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
import org.apache.ambari.server.orm.dao.ServiceConfigDAO;
import org.apache.ambari.server.orm.dao.UpgradeDAO;
import org.apache.ambari.server.orm.entities.ClusterConfigEntity;
import org.apache.ambari.server.orm.entities.ClusterConfigMappingEntity;
import org.apache.ambari.server.orm.entities.ClusterEntity;
import org.apache.ambari.server.orm.entities.ClusterServiceEntity;
import org.apache.ambari.server.orm.entities.ClusterStateEntity;
import org.apache.ambari.server.orm.entities.ClusterVersionEntity;
import org.apache.ambari.server.orm.entities.ConfigGroupEntity;
import org.apache.ambari.server.orm.entities.HostComponentStateEntity;
import org.apache.ambari.server.orm.entities.HostEntity;
import org.apache.ambari.server.orm.entities.HostVersionEntity;
import org.apache.ambari.server.orm.entities.PermissionEntity;
import org.apache.ambari.server.orm.entities.PrivilegeEntity;
import org.apache.ambari.server.orm.entities.RepositoryVersionEntity;
import org.apache.ambari.server.orm.entities.RequestScheduleEntity;
import org.apache.ambari.server.orm.entities.ResourceEntity;
import org.apache.ambari.server.orm.entities.ServiceConfigEntity;
import org.apache.ambari.server.security.authorization.AuthorizationHelper;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.ClusterHealthReport;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.ComponentInfo;
import org.apache.ambari.server.state.Config;
import org.apache.ambari.server.state.ConfigFactory;
import org.apache.ambari.server.state.ConfigHelper;
import org.apache.ambari.server.state.ConfigVersionHelper;
import org.apache.ambari.server.state.DesiredConfig;
import org.apache.ambari.server.state.Host;
import org.apache.ambari.server.state.HostHealthStatus;
import org.apache.ambari.server.state.MaintenanceState;
import org.apache.ambari.server.state.RepositoryVersionState;
import org.apache.ambari.server.state.SecurityType;
import org.apache.ambari.server.state.Service;
import org.apache.ambari.server.state.ServiceComponent;
import org.apache.ambari.server.state.ServiceComponentHost;
import org.apache.ambari.server.state.ServiceComponentHostEvent;
import org.apache.ambari.server.state.ServiceComponentHostEventType;
import org.apache.ambari.server.state.ServiceFactory;
import org.apache.ambari.server.state.ServiceInfo;
import org.apache.ambari.server.state.StackId;
import org.apache.ambari.server.state.State;
import org.apache.ambari.server.state.configgroup.ConfigGroup;
import org.apache.ambari.server.state.configgroup.ConfigGroupFactory;
import org.apache.ambari.server.state.fsm.InvalidStateTransitionException;
import org.apache.ambari.server.state.scheduler.RequestExecution;
import org.apache.ambari.server.state.scheduler.RequestExecutionFactory;
import org.apache.ambari.server.state.svccomphost.ServiceComponentHostSummary;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.persist.Transactional;

public class ClusterImpl implements Cluster {

  private static final Logger LOG =
    LoggerFactory.getLogger(ClusterImpl.class);
  private static final Logger configChangeLog =
    LoggerFactory.getLogger("configchange");

  /**
   * Prefix for cluster session attributes name.
   */
  private static final String CLUSTER_SESSION_ATTRIBUTES_PREFIX = "cluster_session_attributes:";

  @Inject
  private Clusters clusters;

  private StackId desiredStackVersion;

  private volatile Map<String, Service> services = null;

  /**
   * [ Config Type -> [ Config Version Tag -> Config ] ]
   */
  private Map<String, Map<String, Config>> allConfigs;

  /**
   * [ ServiceName -> [ ServiceComponentName -> [ HostName -> [ ... ] ] ] ]
   */
  private Map<String, Map<String, Map<String, ServiceComponentHost>>>
    serviceComponentHosts;

  /**
   * [ HostName -> [ ... ] ]
   */
  private Map<String, List<ServiceComponentHost>>
    serviceComponentHostsByHost;

  /**
   * Map of existing config groups
   */
  private Map<Long, ConfigGroup> clusterConfigGroups;

  /**
   * Map of Request schedules for this cluster
   */
  private Map<Long, RequestExecution> requestExecutions;

  private final ReadWriteLock clusterGlobalLock = new ReentrantReadWriteLock();

  // This is a lock for operations that do not need to be cluster global
  private final ReentrantReadWriteLock hostTransitionStateLock = new ReentrantReadWriteLock();
  private final Lock hostTransitionStateWriteLock = hostTransitionStateLock.writeLock();

  private ClusterEntity clusterEntity;

  private final ConfigVersionHelper configVersionHelper;

  @Inject
  private ClusterDAO clusterDAO;
  @Inject
  private ClusterStateDAO clusterStateDAO;
  @Inject
  private ClusterVersionDAO clusterVersionDAO;
  @Inject
  private HostDAO hostDAO;
  @Inject
  private HostVersionDAO hostVersionDAO;
  @Inject
  private ServiceFactory serviceFactory;
  @Inject
  private ConfigFactory configFactory;
  @Inject
  private Gson gson;
  @Inject
  private HostConfigMappingDAO hostConfigMappingDAO;
  @Inject
  private ConfigGroupFactory configGroupFactory;
  @Inject
  private ConfigGroupHostMappingDAO configGroupHostMappingDAO;
  @Inject
  private RequestExecutionFactory requestExecutionFactory;
  @Inject
  private ConfigHelper configHelper;
  @Inject
  private MaintenanceStateHelper maintenanceStateHelper;
  @Inject
  private AmbariMetaInfo ambariMetaInfo;
  @Inject
  private ServiceConfigDAO serviceConfigDAO;

  @Inject
  private AlertDefinitionDAO alertDefinitionDAO;

  @Inject
  private AlertDispatchDAO alertDispatchDAO;

  @Inject
  private UpgradeDAO upgradeDAO;

  @Inject
  private RepositoryVersionDAO repositoryVersionDAO;

  @Inject
  private Configuration configuration;

  @Inject
  private AmbariSessionManager sessionManager;

  private volatile boolean svcHostsLoaded = false;

  private volatile Multimap<String, String> serviceConfigTypes;

  @Inject
  public ClusterImpl(@Assisted ClusterEntity clusterEntity,
                     Injector injector) throws AmbariException {
    injector.injectMembers(this);
    this.clusterEntity = clusterEntity;

    serviceComponentHosts = new HashMap<String,
      Map<String, Map<String, ServiceComponentHost>>>();
    serviceComponentHostsByHost = new HashMap<String,
      List<ServiceComponentHost>>();
    desiredStackVersion = gson.fromJson(
      clusterEntity.getDesiredStackVersion(), StackId.class);
    allConfigs = new HashMap<String, Map<String, Config>>();
    if (!clusterEntity.getClusterConfigEntities().isEmpty()) {
      for (ClusterConfigEntity entity : clusterEntity.getClusterConfigEntities()) {

        if (!allConfigs.containsKey(entity.getType())) {
          allConfigs.put(entity.getType(), new HashMap<String, Config>());
        }

        Config config = configFactory.createExisting(this, entity);

        allConfigs.get(entity.getType()).put(entity.getTag(), config);
      }
    }

    if (desiredStackVersion != null && !StringUtils.isEmpty(desiredStackVersion.getStackName()) && !
      StringUtils.isEmpty(desiredStackVersion.getStackVersion())) {
      loadServiceConfigTypes();
    }

    configVersionHelper = new ConfigVersionHelper(getConfigLastVersions());
  }


  @Override
  public ReadWriteLock getClusterGlobalLock() {
    return clusterGlobalLock;
  }


  private void loadServiceConfigTypes() throws AmbariException {
    try {
      serviceConfigTypes = collectServiceConfigTypesMapping();
    } catch (AmbariException e) {
      LOG.error("Cannot load stack info:", e);
      throw e;
    }
    LOG.info("Service config types loaded: {}", serviceConfigTypes);
  }

  /**
   * Construct config type to service name mapping
   * @throws AmbariException when stack or its part not found
   */
  private Multimap<String, String> collectServiceConfigTypesMapping() throws AmbariException {
    Multimap<String, String> serviceConfigTypes = HashMultimap.create();

    Map<String, ServiceInfo> serviceInfoMap = null;
    try {
      serviceInfoMap = ambariMetaInfo.getServices(desiredStackVersion.getStackName(), desiredStackVersion.getStackVersion());
    } catch (ParentObjectNotFoundException e) {
      LOG.error("Service config versioning disabled due to exception: ", e);
      return serviceConfigTypes;
    }
    for (Entry<String, ServiceInfo> entry : serviceInfoMap.entrySet()) {
      String serviceName = entry.getKey();
      ServiceInfo serviceInfo = entry.getValue();
      Set<String> configTypes = serviceInfo.getConfigTypeAttributes().keySet();
      for (String configType : configTypes) {
        serviceConfigTypes.put(serviceName, configType);
      }
    }

    return serviceConfigTypes;
  }

  /**
   * Make sure we load all the service host components.
   * We need this for live status checks.
   */
  public void loadServiceHostComponents() {
    loadServices();
    if (svcHostsLoaded) {
      return;
    }

    clusterGlobalLock.writeLock().lock();

    try {
      LOG.info("Loading Service Host Components");
      if (svcHostsLoaded) {
        return;
      }
      if (services != null) {
        for (Entry<String, Service> serviceKV : services.entrySet()) {
          /* get all the service component hosts **/
          Service service = serviceKV.getValue();
          if (!serviceComponentHosts.containsKey(service.getName())) {
            serviceComponentHosts.put(service.getName(),
                new HashMap<String, Map<String, ServiceComponentHost>>());
          }
          for (Entry<String, ServiceComponent> svcComponent : service.getServiceComponents().entrySet()) {
            ServiceComponent comp = svcComponent.getValue();
            String componentName = svcComponent.getKey();
            if (!serviceComponentHosts.get(service.getName()).containsKey(
                componentName)) {
              serviceComponentHosts.get(service.getName()).put(componentName,
                  new HashMap<String, ServiceComponentHost>());
            }
            /** Get Service Host Components **/
            for (Entry<String, ServiceComponentHost> svchost : comp.getServiceComponentHosts().entrySet()) {
              String hostname = svchost.getKey();
              ServiceComponentHost svcHostComponent = svchost.getValue();
              if (!serviceComponentHostsByHost.containsKey(hostname)) {
                serviceComponentHostsByHost.put(hostname,
                    new ArrayList<ServiceComponentHost>());
              }
              List<ServiceComponentHost> compList = serviceComponentHostsByHost.get(hostname);
              compList.add(svcHostComponent);

              if (!serviceComponentHosts.get(service.getName()).get(
                  componentName).containsKey(hostname)) {
                serviceComponentHosts.get(service.getName()).get(componentName).put(
                    hostname, svcHostComponent);
              }
            }
          }
        }
      }
      svcHostsLoaded = true;
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  private void loadServices() {
    if (services == null) {
      clusterGlobalLock.writeLock().lock();

      try {
        if (services == null) {
          services = new TreeMap<String, Service>();
          if (!clusterEntity.getClusterServiceEntities().isEmpty()) {
            for (ClusterServiceEntity serviceEntity : clusterEntity.getClusterServiceEntities()) {
              StackId stackId = getCurrentStackVersion();
              try {
                if (ambariMetaInfo.getService(stackId.getStackName(),
                    stackId.getStackVersion(), serviceEntity.getServiceName()) != null) {
                  services.put(serviceEntity.getServiceName(),
                      serviceFactory.createExisting(this, serviceEntity));
                }
              } catch (AmbariException e) {
                LOG.error(String.format(
                    "Can not get service info: stackName=%s, stackVersion=%s, serviceName=%s",
                    stackId.getStackName(), stackId.getStackVersion(),
                    serviceEntity.getServiceName()));
                e.printStackTrace();
              }
            }
          }
        }
      } finally {
        clusterGlobalLock.writeLock().unlock();
      }
    }
  }

  private void loadConfigGroups() {
    if (clusterConfigGroups == null) {
      clusterGlobalLock.writeLock().lock();

      try {
        if (clusterConfigGroups == null) {
          clusterConfigGroups = new HashMap<Long, ConfigGroup>();
          if (!clusterEntity.getConfigGroupEntities().isEmpty()) {
            for (ConfigGroupEntity configGroupEntity : clusterEntity.getConfigGroupEntities()) {
              clusterConfigGroups.put(configGroupEntity.getGroupId(),
                  configGroupFactory.createExisting(this, configGroupEntity));
            }
          }
        }
      } finally {
        clusterGlobalLock.writeLock().unlock();
      }
    }
  }

  private void loadRequestExecutions() {
    if (requestExecutions == null) {
      clusterGlobalLock.writeLock().lock();
      try {
        if (requestExecutions == null) {
          requestExecutions = new HashMap<Long, RequestExecution>();
          if (!clusterEntity.getRequestScheduleEntities().isEmpty()) {
            for (RequestScheduleEntity scheduleEntity : clusterEntity.getRequestScheduleEntities()) {
              requestExecutions.put(scheduleEntity.getScheduleId(),
                  requestExecutionFactory.createExisting(this, scheduleEntity));
            }
          }
        }
      } finally {
        clusterGlobalLock.writeLock().unlock();
      }
    }
  }

  @Override
  public void addConfigGroup(ConfigGroup configGroup) throws AmbariException {
    loadConfigGroups();
    clusterGlobalLock.writeLock().lock();
    try {
      LOG.debug("Adding a new Config group" + ", clusterName = "
          + getClusterName() + ", groupName = " + configGroup.getName()
          + ", tag = " + configGroup.getTag());

      if (clusterConfigGroups.containsKey(configGroup.getId())) {
        // The loadConfigGroups will load all groups to memory
        LOG.debug("Config group already exists" + ", clusterName = "
            + getClusterName() + ", groupName = " + configGroup.getName()
            + ", groupId = " + configGroup.getId() + ", tag = "
            + configGroup.getTag());
      } else {
        clusterConfigGroups.put(configGroup.getId(), configGroup);
        configHelper.invalidateStaleConfigsCache();
      }

    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public Map<Long, ConfigGroup> getConfigGroups() {
    loadConfigGroups();
    clusterGlobalLock.readLock().lock();
    try {
      return Collections.unmodifiableMap(clusterConfigGroups);
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public Map<Long, ConfigGroup> getConfigGroupsByHostname(String hostname)
    throws AmbariException {
    Map<Long, ConfigGroup> configGroups = new HashMap<Long, ConfigGroup>();
    Map<Long, ConfigGroup> configGroupMap = getConfigGroups();

    clusterGlobalLock.readLock().lock();
    try {
      Set<ConfigGroupHostMapping> hostMappingEntities = configGroupHostMappingDAO.findByHost(hostname);

      if (hostMappingEntities != null && !hostMappingEntities.isEmpty()) {
        for (ConfigGroupHostMapping entity : hostMappingEntities) {
          ConfigGroup configGroup = configGroupMap.get(entity.getConfigGroupId());
          if (configGroup != null
              && !configGroups.containsKey(configGroup.getId())) {
            configGroups.put(configGroup.getId(), configGroup);
          }
        }
      }
      return configGroups;

    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public void addRequestExecution(RequestExecution requestExecution) throws AmbariException {
    loadRequestExecutions();
    clusterGlobalLock.writeLock().lock();
    try {
      LOG.info("Adding a new request schedule" + ", clusterName = "
          + getClusterName() + ", id = " + requestExecution.getId()
          + ", description = " + requestExecution.getDescription());

      if (requestExecutions.containsKey(requestExecution.getId())) {
        LOG.debug("Request schedule already exists" + ", clusterName = "
            + getClusterName() + ", id = " + requestExecution.getId()
            + ", description = " + requestExecution.getDescription());
      } else {
        requestExecutions.put(requestExecution.getId(), requestExecution);
      }
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public Map<Long, RequestExecution> getAllRequestExecutions() {
    loadRequestExecutions();
    clusterGlobalLock.readLock().lock();
    try {
      return Collections.unmodifiableMap(requestExecutions);
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public void deleteRequestExecution(Long id) throws AmbariException {
    loadRequestExecutions();
    clusterGlobalLock.writeLock().lock();
    try {
      RequestExecution requestExecution = requestExecutions.get(id);
      if (requestExecution == null) {
        throw new AmbariException("Request schedule does not exists, "
            + "id = " + id);
      }
      LOG.info("Deleting request schedule" + ", clusterName = "
          + getClusterName() + ", id = " + requestExecution.getId()
          + ", description = " + requestExecution.getDescription());

      requestExecution.delete();
      requestExecutions.remove(id);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public void deleteConfigGroup(Long id) throws AmbariException {
    loadConfigGroups();
    clusterGlobalLock.writeLock().lock();
    try {
      ConfigGroup configGroup = clusterConfigGroups.get(id);
      if (configGroup == null) {
        throw new ConfigGroupNotFoundException(getClusterName(), id.toString());
      }
      LOG.debug("Deleting Config group" + ", clusterName = " + getClusterName()
          + ", groupName = " + configGroup.getName() + ", groupId = "
          + configGroup.getId() + ", tag = " + configGroup.getTag());

      configGroup.delete();
      clusterConfigGroups.remove(id);
      configHelper.invalidateStaleConfigsCache();
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  public ServiceComponentHost getServiceComponentHost(String serviceName,
      String serviceComponentName, String hostname) throws AmbariException {
    loadServiceHostComponents();
    clusterGlobalLock.readLock().lock();
    try {
      if (!serviceComponentHosts.containsKey(serviceName)
          || !serviceComponentHosts.get(serviceName).containsKey(
              serviceComponentName)
          || !serviceComponentHosts.get(serviceName).get(serviceComponentName).containsKey(
              hostname)) {
        throw new ServiceComponentHostNotFoundException(getClusterName(),
            serviceName, serviceComponentName, hostname);
      }
      return serviceComponentHosts.get(serviceName).get(serviceComponentName).get(
          hostname);
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public String getClusterName() {
    return clusterEntity.getClusterName();
  }

  @Override
  public void setClusterName(String clusterName) {
    clusterGlobalLock.writeLock().lock();
    try {
      String oldName = clusterEntity.getClusterName();
      clusterEntity.setClusterName(clusterName);

      // RollbackException possibility if UNIQUE constraint violated
      clusterDAO.merge(clusterEntity);
      clusters.updateClusterName(oldName, clusterName);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  public void addServiceComponentHost(ServiceComponentHost svcCompHost)
      throws AmbariException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Trying to add component {} of service {} on {} to the cache",
          svcCompHost.getServiceComponentName(), svcCompHost.getServiceName(),
          svcCompHost.getHostName());
    }

    loadServiceHostComponents();

    final String hostname = svcCompHost.getHostName();
    final String serviceName = svcCompHost.getServiceName();
    final String componentName = svcCompHost.getServiceComponentName();

    Set<Cluster> cs = clusters.getClustersForHost(hostname);

    clusterGlobalLock.writeLock().lock();

    try {
      boolean clusterFound = false;
      Iterator<Cluster> iter = cs.iterator();
      while (iter.hasNext()) {
        Cluster c = iter.next();
        if (c.getClusterId() == getClusterId()) {
          clusterFound = true;
          break;
        }
      }

      if (!clusterFound) {
        throw new AmbariException("Host does not belong this cluster"
            + ", hostname=" + hostname + ", clusterName=" + getClusterName()
            + ", clusterId=" + getClusterId());
      }

      if (!serviceComponentHosts.containsKey(serviceName)) {
        serviceComponentHosts.put(serviceName,
            new HashMap<String, Map<String, ServiceComponentHost>>());
      }

      if (!serviceComponentHosts.get(serviceName).containsKey(componentName)) {
        serviceComponentHosts.get(serviceName).put(componentName,
            new HashMap<String, ServiceComponentHost>());
      }

      if (serviceComponentHosts.get(serviceName).get(componentName).containsKey(
          hostname)) {
        throw new AmbariException("Duplicate entry for ServiceComponentHost"
            + ", serviceName=" + serviceName + ", serviceComponentName"
            + componentName + ", hostname= " + hostname);
      }

      if (!serviceComponentHostsByHost.containsKey(hostname)) {
        serviceComponentHostsByHost.put(hostname,
            new ArrayList<ServiceComponentHost>());
      }

      if (LOG.isDebugEnabled()) {
        LOG.debug("Adding a new ServiceComponentHost" + ", clusterName="
            + getClusterName() + ", clusterId=" + getClusterId()
            + ", serviceName=" + serviceName + ", serviceComponentName"
            + componentName + ", hostname= " + hostname);
      }

      serviceComponentHosts.get(serviceName).get(componentName).put(hostname,
          svcCompHost);
      serviceComponentHostsByHost.get(hostname).add(svcCompHost);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public void removeServiceComponentHost(ServiceComponentHost svcCompHost)
    throws AmbariException {
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Trying to remove component {} of service {} on {} from the cache",
          svcCompHost.getServiceComponentName(), svcCompHost.getServiceName(),
          svcCompHost.getHostName());
    }

    loadServiceHostComponents();

    final String hostname = svcCompHost.getHostName();
    final String serviceName = svcCompHost.getServiceName();
    final String componentName = svcCompHost.getServiceComponentName();
    Set<Cluster> cs = clusters.getClustersForHost(hostname);

    clusterGlobalLock.writeLock().lock();
    try {
      boolean clusterFound = false;
      Iterator<Cluster> iter = cs.iterator();
      while (iter.hasNext()) {
        Cluster c = iter.next();
        if (c.getClusterId() == getClusterId()) {
          clusterFound = true;
          break;
        }
      }

      if (!clusterFound) {
        throw new AmbariException("Host does not belong this cluster"
            + ", hostname=" + hostname + ", clusterName=" + getClusterName()
            + ", clusterId=" + getClusterId());
      }

      if (!serviceComponentHosts.containsKey(serviceName)
          || !serviceComponentHosts.get(serviceName).containsKey(componentName)
          || !serviceComponentHosts.get(serviceName).get(componentName).containsKey(
              hostname)) {
        throw new AmbariException("Invalid entry for ServiceComponentHost"
            + ", serviceName=" + serviceName + ", serviceComponentName"
            + componentName + ", hostname= " + hostname);
      }

      if (!serviceComponentHostsByHost.containsKey(hostname)) {
        throw new AmbariException("Invalid host entry for ServiceComponentHost"
            + ", serviceName=" + serviceName + ", serviceComponentName"
            + componentName + ", hostname= " + hostname);
      }

      ServiceComponentHost schToRemove = null;
      for (ServiceComponentHost sch : serviceComponentHostsByHost.get(hostname)) {
        if (sch.getServiceName().equals(serviceName)
            && sch.getServiceComponentName().equals(componentName)
            && sch.getHostName().equals(hostname)) {
          schToRemove = sch;
          break;
        }
      }

      if (schToRemove == null) {
        LOG.warn("Unavailable in per host cache. ServiceComponentHost"
            + ", serviceName=" + serviceName
            + ", serviceComponentName" + componentName
            + ", hostname= " + hostname);
      }

      if (LOG.isDebugEnabled()) {
        LOG.debug("Removing a ServiceComponentHost" + ", clusterName="
            + getClusterName() + ", clusterId=" + getClusterId()
            + ", serviceName=" + serviceName + ", serviceComponentName"
            + componentName + ", hostname= " + hostname);
      }

      serviceComponentHosts.get(serviceName).get(componentName).remove(hostname);
      if (schToRemove != null) {
        serviceComponentHostsByHost.get(hostname).remove(schToRemove);
      }
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public long getClusterId() {
    return clusterEntity.getClusterId();
  }

  @Override
  public List<ServiceComponentHost> getServiceComponentHosts(
    String hostname) {
    loadServiceHostComponents();
    clusterGlobalLock.readLock().lock();
    try {
      if (serviceComponentHostsByHost.containsKey(hostname)) {
        return new CopyOnWriteArrayList<ServiceComponentHost>(
            serviceComponentHostsByHost.get(hostname));
      }
      return new ArrayList<ServiceComponentHost>();
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public void addService(Service service)
    throws AmbariException {
    loadServices();
    clusterGlobalLock.writeLock().lock();
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Adding a new Service" + ", clusterName=" + getClusterName()
            + ", clusterId=" + getClusterId() + ", serviceName="
            + service.getName());
      }
      if (services.containsKey(service.getName())) {
        throw new AmbariException("Service already exists" + ", clusterName="
            + getClusterName() + ", clusterId=" + getClusterId()
            + ", serviceName=" + service.getName());
      }
      services.put(service.getName(), service);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public Service addService(String serviceName) throws AmbariException {
    loadServices();
    clusterGlobalLock.writeLock().lock();
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Adding a new Service" + ", clusterName=" + getClusterName()
            + ", clusterId=" + getClusterId() + ", serviceName=" + serviceName);
      }
      if (services.containsKey(serviceName)) {
        throw new AmbariException("Service already exists" + ", clusterName="
            + getClusterName() + ", clusterId=" + getClusterId()
            + ", serviceName=" + serviceName);
      }
      Service s = serviceFactory.createNew(this, serviceName);
      services.put(s.getName(), s);
      return s;
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public Service getService(String serviceName)
    throws AmbariException {
    loadServices();
    clusterGlobalLock.readLock().lock();
    try {
      if (!services.containsKey(serviceName)) {
        throw new ServiceNotFoundException(getClusterName(), serviceName);
      }
      return services.get(serviceName);
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public Map<String, Service> getServices() {
    loadServices();
    clusterGlobalLock.readLock().lock();
    try {
      return new HashMap<String, Service>(services);
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public StackId getDesiredStackVersion() {
    clusterGlobalLock.readLock().lock();
    try {
      return desiredStackVersion;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public void setDesiredStackVersion(StackId stackVersion) throws AmbariException {
    clusterGlobalLock.writeLock().lock();
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Changing DesiredStackVersion of Cluster" + ", clusterName="
            + getClusterName() + ", clusterId=" + getClusterId()
            + ", currentDesiredStackVersion=" + desiredStackVersion
            + ", newDesiredStackVersion=" + stackVersion);
      }
      desiredStackVersion = stackVersion;
      clusterEntity.setDesiredStackVersion(gson.toJson(stackVersion));
      clusterDAO.merge(clusterEntity);
      loadServiceConfigTypes();
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public StackId getCurrentStackVersion() {
    clusterGlobalLock.readLock().lock();
    try {
      ClusterStateEntity clusterStateEntity = clusterEntity.getClusterStateEntity();
      if (clusterStateEntity != null) {
        String stackVersion = clusterStateEntity.getCurrentStackVersion();
        if (stackVersion != null && !stackVersion.isEmpty()) {
          return gson.fromJson(stackVersion, StackId.class);
        }
      }
      return null;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public State getProvisioningState() {
    clusterGlobalLock.readLock().lock();
    State provisioningState = null;
    try {
      provisioningState = clusterEntity.getProvisioningState();

      if (null == provisioningState) {
        provisioningState = State.INIT;
      }

      return provisioningState;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public void setProvisioningState(State provisioningState) {
    clusterGlobalLock.writeLock().lock();
    try {
      clusterEntity.setProvisioningState(provisioningState);
      clusterDAO.merge(clusterEntity);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public SecurityType getSecurityType() {
    clusterGlobalLock.readLock().lock();
    SecurityType securityType = null;
    try {
      securityType = clusterEntity.getSecurityType();

      if (null == securityType) {
        securityType = SecurityType.NONE;
      }

      return securityType;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public void setSecurityType(SecurityType securityType) {
    clusterGlobalLock.writeLock().lock();
    try {
      clusterEntity.setSecurityType(securityType);
      clusterDAO.merge(clusterEntity);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  /**
   * Get the ClusterVersionEntity object whose state is CURRENT.
   * @return
   */
  @Override
  public ClusterVersionEntity getCurrentClusterVersion() {
    return clusterVersionDAO.findByClusterAndStateCurrent(getClusterName());
  }

  /**
   * Get all of the ClusterVersionEntity objects for the cluster.
   * @return
   */
  @Override
  public Collection<ClusterVersionEntity> getAllClusterVersions() {
    return clusterVersionDAO.findByCluster(getClusterName());
  }

  /**
   * During the Finalize Action, want to transition all Host Versions from UPGRADED to CURRENT, and the last CURRENT one to INSTALLED.
   * @param hostNames Collection of host names
   * @param currentClusterVersion Entity that contains the cluster's current stack (with its name and version)
   * @param desiredState Desired state must be {@link RepositoryVersionState#CURRENT} or {@link RepositoryVersionState#UPGRADING}
   * @throws AmbariException
   */
  @Override
  public void mapHostVersions(Set<String> hostNames, ClusterVersionEntity currentClusterVersion, RepositoryVersionState desiredState) throws AmbariException {
    if (currentClusterVersion == null) {
      throw new AmbariException("Could not find current stack version of cluster " + getClusterName());
    }

    final Set<RepositoryVersionState> validStates = new HashSet<RepositoryVersionState>(){{
      add(RepositoryVersionState.CURRENT);
    }};

    if (!validStates.contains(desiredState)) {
      throw new AmbariException("The state must be one of [" + StringUtils.join(validStates, ", ") + "]");
    }

    clusterGlobalLock.writeLock().lock();
    try {
      Map<String, HostVersionEntity> existingHostToHostVersionEntity = new HashMap<String, HostVersionEntity>();
      List<HostVersionEntity> existingHostVersionEntities = hostVersionDAO.findByClusterStackAndVersion(
          getClusterName(),
          currentClusterVersion.getRepositoryVersion().getStack(),
          currentClusterVersion.getRepositoryVersion().getVersion());

      if (existingHostVersionEntities != null) {
        for (HostVersionEntity entity : existingHostVersionEntities) {
          existingHostToHostVersionEntity.put(entity.getHostName(), entity);
        }
      }

      Sets.SetView<String> intersection = Sets.intersection(
          existingHostToHostVersionEntity.keySet(), hostNames);

      for (String hostname : hostNames) {
        List<HostVersionEntity> currentHostVersions = hostVersionDAO.findByClusterHostAndState(
            getClusterName(), hostname, RepositoryVersionState.CURRENT);
        HostVersionEntity currentHostVersionEntity = (currentHostVersions != null && currentHostVersions.size() == 1) ? currentHostVersions.get(0)
            : null;

          // Notice that if any hosts already have the desired stack and version, regardless of the state, we try
          // to be robust and only insert records for the missing hosts.
          if (!intersection.contains(hostname)) {
            // According to the business logic, we don't create objects in a CURRENT state.
            HostEntity hostEntity = hostDAO.findByName(hostname);
            HostVersionEntity hostVersionEntity = new HostVersionEntity(hostname, currentClusterVersion.getRepositoryVersion(), desiredState);
            hostVersionEntity.setHostEntity(hostEntity);
            hostVersionDAO.create(hostVersionEntity);
          } else {
            HostVersionEntity hostVersionEntity = existingHostToHostVersionEntity.get(hostname);
            if (hostVersionEntity.getState() != desiredState) {
              hostVersionEntity.setState(desiredState);
              hostVersionDAO.merge(hostVersionEntity);
            }

          // Maintain the invariant that only one HostVersionEntity is allowed
          // to have a state of CURRENT.
          if (currentHostVersionEntity != null
              && !currentHostVersionEntity.getRepositoryVersion().equals(
                  hostVersionEntity.getRepositoryVersion())
              && desiredState == RepositoryVersionState.CURRENT
              && currentHostVersionEntity.getState() == RepositoryVersionState.CURRENT) {
            currentHostVersionEntity.setState(RepositoryVersionState.INSTALLED);
            hostVersionDAO.merge(currentHostVersionEntity);
          }
        }
      }
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  /**
   * Because this is a top-down approach, it should only be called for the purposes of bootstrapping data, such as
   * installing a brand new cluster (through Blueprints).
   * @param sourceClusterVersion cluster version to be queried for a stack name/version info and desired RepositoryVersionState.
   * The only valid state of this cluster version is {@link RepositoryVersionState#INSTALLING}
   * @throws AmbariException
   */
  @Override
  public void inferHostVersions(ClusterVersionEntity sourceClusterVersion) throws AmbariException {
    if (sourceClusterVersion == null) {
      throw new AmbariException("Could not find current stack version of cluster " + getClusterName());
    }

    RepositoryVersionState desiredState = sourceClusterVersion.getState();

    @SuppressWarnings("serial")
    Set<RepositoryVersionState> validStates = new HashSet<RepositoryVersionState>(){{
      add(RepositoryVersionState.INSTALLING);
    }};

    if (!validStates.contains(desiredState)) {
      throw new AmbariException("The state must be one of " + validStates);
    }

    Map<String, Host> hosts = clusters.getHostsForCluster(getClusterName());
    Set<String> existingHostsWithClusterStackAndVersion = new HashSet<String>();
    HashMap<String, HostVersionEntity> existingHostStackVersions = new HashMap<String, HostVersionEntity>();

    clusterGlobalLock.writeLock().lock();
    try {
      List<HostVersionEntity> existingHostVersionEntities = hostVersionDAO.findByClusterStackAndVersion(
          getClusterName(),
          sourceClusterVersion.getRepositoryVersion().getStack(),
          sourceClusterVersion.getRepositoryVersion().getVersion());

      if (existingHostVersionEntities != null) {
        for (HostVersionEntity entity : existingHostVersionEntities) {
          existingHostsWithClusterStackAndVersion.add(entity.getHostName());
          existingHostStackVersions.put(entity.getHostName(), entity);
        }
      }

      Sets.SetView<String> hostsMissingRepoVersion = Sets.difference(
          hosts.keySet(), existingHostsWithClusterStackAndVersion);

      for (String hostname : hosts.keySet()) {
        if (hostsMissingRepoVersion.contains(hostname)) {
          // Create new host stack version
          HostEntity hostEntity = hostDAO.findByName(hostname);
          HostVersionEntity hostVersionEntity = new HostVersionEntity(hostname,
              sourceClusterVersion.getRepositoryVersion(),
              RepositoryVersionState.INSTALLING);
          hostVersionEntity.setHostEntity(hostEntity);
          hostVersionDAO.create(hostVersionEntity);
        } else {
          // Update existing host stack version
          HostVersionEntity hostVersionEntity = existingHostStackVersions.get(hostname);
          hostVersionEntity.setState(desiredState);
          hostVersionDAO.merge(hostVersionEntity);
        }
      }
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  /**
   * Calculate the effective Cluster Version State based on the state of its hosts.
   *
   * CURRENT: all hosts are CURRENT
   * UPGRADE_FAILED: at least one host in UPGRADE_FAILED
   * UPGRADED: all hosts are UPGRADED
   * UPGRADING: at least one host is UPGRADING, and the rest in UPGRADING|INSTALLED
   * INSTALLED: all hosts in INSTALLED
   * INSTALL_FAILED: at least one host in INSTALL_FAILED
   * INSTALLING: all hosts in INSTALLING. Notice that if one host is CURRENT and another is INSTALLING, then the
   * effective version will be OUT_OF_SYNC.
   * OUT_OF_SYNC: otherwise
   * @param stateToHosts Map from state to the collection of hosts with that state
   * @return Return the effective Cluster Version State
   */
  private RepositoryVersionState getEffectiveState(Map<RepositoryVersionState, Set<String>> stateToHosts) {
    if (stateToHosts == null || stateToHosts.keySet().size() < 1) {
      return null;
    }

    int totalHosts = 0;
    for (Set<String> hosts : stateToHosts.values()) {
      totalHosts += hosts.size();
    }

    if (stateToHosts.containsKey(RepositoryVersionState.CURRENT) && stateToHosts.get(RepositoryVersionState.CURRENT).size() == totalHosts) {
      return RepositoryVersionState.CURRENT;
    }
    if (stateToHosts.containsKey(RepositoryVersionState.UPGRADE_FAILED) && !stateToHosts.get(RepositoryVersionState.UPGRADE_FAILED).isEmpty()) {
      return RepositoryVersionState.UPGRADE_FAILED;
    }
    if (stateToHosts.containsKey(RepositoryVersionState.UPGRADED) && stateToHosts.get(RepositoryVersionState.UPGRADED).size() == totalHosts) {
      return RepositoryVersionState.UPGRADED;
    }
    if (stateToHosts.containsKey(RepositoryVersionState.UPGRADING) && !stateToHosts.get(RepositoryVersionState.UPGRADING).isEmpty()) {
      return RepositoryVersionState.UPGRADING;
    }
    if (stateToHosts.containsKey(RepositoryVersionState.INSTALLED) && stateToHosts.get(RepositoryVersionState.INSTALLED).size() == totalHosts) {
      return RepositoryVersionState.INSTALLED;
    }
    if (stateToHosts.containsKey(RepositoryVersionState.INSTALL_FAILED) && !stateToHosts.get(RepositoryVersionState.INSTALL_FAILED).isEmpty()) {
      return RepositoryVersionState.INSTALL_FAILED;
    }

    final int totalINSTALLING = stateToHosts.containsKey(RepositoryVersionState.INSTALLING) ? stateToHosts.get(RepositoryVersionState.INSTALLING).size() : 0;
    final int totalINSTALLED = stateToHosts.containsKey(RepositoryVersionState.INSTALLED) ? stateToHosts.get(RepositoryVersionState.INSTALLED).size() : 0;
    if (totalINSTALLING + totalINSTALLED == totalHosts) {
      return RepositoryVersionState.INSTALLING;
    }

    // Also returns when have a mix of CURRENT and INSTALLING|INSTALLED|UPGRADING|UPGRADED
    return RepositoryVersionState.OUT_OF_SYNC;
  }

  /**
   * Recalculate what the Cluster Version state should be, depending on the individual Host Versions.
   * @param repositoryVersion Repository version (e.g., 2.2.0.0-1234)
   * @throws AmbariException
   */
  @Override
  public void recalculateClusterVersionState(String repositoryVersion) throws AmbariException {
    if (repositoryVersion == null) {
      return;
    }

    Map<String, Host> hosts = clusters.getHostsForCluster(getClusterName());

    clusterGlobalLock.writeLock().lock();
    try {
      // Part 1, bootstrap cluster version if necessary.
      StackId stackId = getCurrentStackVersion();

      ClusterVersionEntity clusterVersion = clusterVersionDAO.findByClusterAndStackAndVersion(
          getClusterName(), stackId.getStackId(), repositoryVersion);

      if (clusterVersion == null) {
        if (clusterVersionDAO.findByCluster(getClusterName()).isEmpty()) {
          // During an Ambari Upgrade from 1.7.0 -> 2.0.0, the Cluster Version
          // will not exist, so bootstrap it.
          // This can still fail if the Repository Version has not yet been created,
          // which can happen if the first HostComponentState to trigger this method
          // cannot advertise a version.
          createClusterVersionInternal(
              stackId.getStackId(),
              repositoryVersion,
              AuthorizationHelper.getAuthenticatedName(configuration.getAnonymousAuditName()),
              RepositoryVersionState.UPGRADING);
          clusterVersion = clusterVersionDAO.findByClusterAndStackAndVersion(
              getClusterName(), stackId.getStackId(), repositoryVersion);

          if (clusterVersion == null) {
            LOG.warn(String.format(
                "Could not create a cluster version for cluster %s and stack %s using repo version %s",
                getClusterName(), stackId.getStackId(), repositoryVersion));
            return;
          }
        } else {
          LOG.warn(String.format(
              "Repository version %s not found for cluster %s",
              repositoryVersion, getClusterName()));
          return;
        }
      }

      // Ignore if cluster version is CURRENT or UPGRADE_FAILED
      if (clusterVersion.getState() != RepositoryVersionState.INSTALL_FAILED &&
              clusterVersion.getState() != RepositoryVersionState.OUT_OF_SYNC &&
              clusterVersion.getState() != RepositoryVersionState.INSTALLING &&
              clusterVersion.getState() != RepositoryVersionState.INSTALLED &&
              clusterVersion.getState() != RepositoryVersionState.UPGRADING &&
              clusterVersion.getState() != RepositoryVersionState.UPGRADED) {
        // anything else is not supported as of now
        return;
      }

      // Part 2, check for transitions.
      Set<Host> hostsWithoutHostVersion = new HashSet<Host>();
      Map<RepositoryVersionState, Set<String>> stateToHosts = new HashMap<RepositoryVersionState, Set<String>>();
      for (Host host : hosts.values()) {
        String hostName = host.getHostName();
        HostVersionEntity hostVersion = hostVersionDAO.findByClusterStackVersionAndHost(
            getClusterName(), stackId.getStackId(), repositoryVersion, hostName);

        if (hostVersion == null) {
          // This host either has not had a chance to heartbeat yet with its
          // installed component, or it has components
          // that do not advertise a version.
          hostsWithoutHostVersion.add(host);
          continue;
        }

        RepositoryVersionState hostState = hostVersion.getState();
        if (stateToHosts.containsKey(hostState)) {
          stateToHosts.get(hostState).add(hostName);
        } else {
          Set<String> hostsInState = new HashSet<String>();
          hostsInState.add(hostName);
          stateToHosts.put(hostState, hostsInState);
        }
      }

      // Ensure that all of the hosts without a Host Version only have
      // Components that do not advertise a version.
      // Otherwise, operations are still in progress.
      for (Host host : hostsWithoutHostVersion) {
        HostEntity hostEntity = hostDAO.findByName(host.getHostName());
        final Collection<HostComponentStateEntity> allHostComponents = hostEntity.getHostComponentStateEntities();

        for (HostComponentStateEntity hostComponentStateEntity : allHostComponents) {
          if (hostComponentStateEntity.getVersion().equalsIgnoreCase(
              State.UNKNOWN.toString())) {
            // Some Components cannot advertise a version. E.g., ZKF, AMBARI_METRICS,
            // Kerberos
            ComponentInfo compInfo = ambariMetaInfo.getComponent(
                stackId.getStackName(), stackId.getStackVersion(),
                hostComponentStateEntity.getServiceName(),
                hostComponentStateEntity.getComponentName());

            if (compInfo.isVersionAdvertised()) {
              LOG.debug("Skipping transitioning the cluster version because host "
                  + host.getHostName() + " does not have a version yet.");
              return;
            }
          }
        }
      }

      RepositoryVersionState effectiveClusterVersionState = getEffectiveState(stateToHosts);
      if (effectiveClusterVersionState != null
          && effectiveClusterVersionState != clusterVersion.getState()) {
        // Any mismatch will be caught while transitioning, and raise an
        // exception.
        try {
          transitionClusterVersion(stackId.getStackId(), repositoryVersion,
              effectiveClusterVersionState);
        } catch (AmbariException e) {
          ;
        }
      }
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  /**
   * Transition the Host Version across states.
   * @param host Host object
   * @param repositoryVersion Repository Version with stack and version information
   * @param stack Stack information
   * @throws AmbariException
   */
  @Transactional
  public HostVersionEntity transitionHostVersionState(HostEntity host, final RepositoryVersionEntity repositoryVersion, final StackId stack) throws AmbariException {
    HostVersionEntity hostVersionEntity = hostVersionDAO.findByClusterStackVersionAndHost(getClusterName(), repositoryVersion.getStack(), repositoryVersion.getVersion(), host.getHostName());

    hostTransitionStateWriteLock.lock();
    try {
      // Create one if it doesn't already exist. It will be possible to make further transitions below.
      if (hostVersionEntity == null) {
        hostVersionEntity = new HostVersionEntity(host.getHostName(), repositoryVersion, RepositoryVersionState.UPGRADING);
        hostVersionEntity.setHostEntity(host);
        hostVersionDAO.create(hostVersionEntity);
      }

      HostVersionEntity currentVersionEntity = hostVersionDAO.findByHostAndStateCurrent(getClusterName(), host.getHostName());
      boolean isCurrentPresent = (currentVersionEntity != null);
      final ServiceComponentHostSummary hostSummary = new ServiceComponentHostSummary(ambariMetaInfo, host, stack);

      if (!isCurrentPresent) {
        // Transition from UPGRADING -> CURRENT. This is allowed because Host Version Entity is bootstrapped in an UPGRADING state.
        if (hostSummary.isUpgradeFinished() && hostVersionEntity.getState().equals(RepositoryVersionState.UPGRADING)) {
          hostVersionEntity.setState(RepositoryVersionState.CURRENT);
          hostVersionDAO.merge(hostVersionEntity);
        }
      } else {
        // Handle transitions during a Rolling Upgrade

        // If a host only has one Component to update, that single report can still transition the host version from
        // INSTALLED->UPGRADING->UPGRADED in one shot.
        if (hostSummary.isUpgradeInProgress(currentVersionEntity.getRepositoryVersion().getVersion()) && hostVersionEntity.getState().equals(RepositoryVersionState.INSTALLED)) {
          hostVersionEntity.setState(RepositoryVersionState.UPGRADING);
          hostVersionDAO.merge(hostVersionEntity);
        }

        if (hostSummary.isUpgradeFinished() && hostVersionEntity.getState().equals(RepositoryVersionState.UPGRADING)) {
          hostVersionEntity.setState(RepositoryVersionState.UPGRADED);
          hostVersionDAO.merge(hostVersionEntity);
        }
      }
    } finally {
      hostTransitionStateWriteLock.unlock();
    }
    return hostVersionEntity;
  }

  @Override
  public void recalculateAllClusterVersionStates() throws AmbariException {
    clusterGlobalLock.writeLock().lock();
    try {
      List<ClusterVersionEntity> clusterVersionEntities = clusterVersionDAO.findByCluster(getClusterName());
      StackId currentStackId = getCurrentStackVersion();
      for (ClusterVersionEntity clusterVersionEntity : clusterVersionEntities) {
        if (clusterVersionEntity.getRepositoryVersion().getStack().equals(
            currentStackId.getStackId())
            && clusterVersionEntity.getState() != RepositoryVersionState.CURRENT) {
          recalculateClusterVersionState(clusterVersionEntity.getRepositoryVersion().getVersion());
        }
      }
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public void createClusterVersion(String stack, String version, String userName, RepositoryVersionState state) throws AmbariException {
    clusterGlobalLock.writeLock().lock();
    try {
      createClusterVersionInternal(stack, version, userName, state);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  /**
   * See {@link #createClusterVersion}
   *
   * This method is intended to be called only when cluster lock is already acquired.
   */
  private void createClusterVersionInternal(String stack, String version, String userName, RepositoryVersionState state) throws AmbariException {
    Set<RepositoryVersionState> allowedStates = new HashSet<RepositoryVersionState>();
    Collection<ClusterVersionEntity> allClusterVersions = getAllClusterVersions();
    if (allClusterVersions == null || allClusterVersions.isEmpty()) {
      allowedStates.add(RepositoryVersionState.UPGRADING);
    } else {
      allowedStates.add(RepositoryVersionState.INSTALLING);
    }

    if (!allowedStates.contains(state)) {
      throw new AmbariException("The allowed state for a new cluster version must be within " + allowedStates);
    }

    ClusterVersionEntity existing = clusterVersionDAO.findByClusterAndStackAndVersion(getClusterName(), stack, version);
    if (existing != null) {
      throw new DuplicateResourceException("Duplicate item, a cluster version with stack=" + stack + ", version=" +
          version + " for cluster " + getClusterName() + " already exists");
    }

    RepositoryVersionEntity repositoryVersionEntity = repositoryVersionDAO.findByStackAndVersion(stack, version);
    if (repositoryVersionEntity == null) {
      LOG.warn("Could not find repository version for stack=" + stack + ", version=" + version);
      return;
    }

    ClusterVersionEntity clusterVersionEntity = new ClusterVersionEntity(clusterEntity, repositoryVersionEntity, state, System.currentTimeMillis(), System.currentTimeMillis(), userName);
    clusterVersionDAO.create(clusterVersionEntity);
  }

  /**
   * Transition an existing cluster version from one state to another.
   * @param stack Stack name
   * @param version Stack version
   * @param state Desired state
   * @throws AmbariException
   */
  @Override
  @Transactional
  public void transitionClusterVersion(String stack, String version, RepositoryVersionState state) throws AmbariException {
    Set<RepositoryVersionState> allowedStates = new HashSet<RepositoryVersionState>();
    clusterGlobalLock.writeLock().lock();
    try {
      ClusterVersionEntity existingClusterVersion = clusterVersionDAO.findByClusterAndStackAndVersion(
          getClusterName(), stack, version);
      if (existingClusterVersion == null) {
        throw new AmbariException(
            "Existing cluster version not found for cluster="
                + getClusterName() + ", stack=" + stack + ", version="
                + version);
      }

      if (existingClusterVersion.getState() != state) {
        switch (existingClusterVersion.getState()) {
          case CURRENT:
            // If CURRENT state is changed here cluster will not have CURRENT
            // state.
            // CURRENT state will be changed to INSTALLED when another CURRENT
            // state is added.
            // allowedStates.add(RepositoryVersionState.INSTALLED);
            break;
          case INSTALLING:
            allowedStates.add(RepositoryVersionState.INSTALLED);
            allowedStates.add(RepositoryVersionState.INSTALL_FAILED);
            allowedStates.add(RepositoryVersionState.OUT_OF_SYNC);
            break;
          case INSTALL_FAILED:
            allowedStates.add(RepositoryVersionState.INSTALLING);
            break;
          case INSTALLED:
            allowedStates.add(RepositoryVersionState.INSTALLING);
            allowedStates.add(RepositoryVersionState.UPGRADING);
            allowedStates.add(RepositoryVersionState.OUT_OF_SYNC);
            break;
          case OUT_OF_SYNC:
            allowedStates.add(RepositoryVersionState.INSTALLING);
            break;
          case UPGRADING:
            allowedStates.add(RepositoryVersionState.UPGRADED);
            allowedStates.add(RepositoryVersionState.UPGRADE_FAILED);
            if (clusterVersionDAO.findByClusterAndStateCurrent(getClusterName()) == null) {
              allowedStates.add(RepositoryVersionState.CURRENT);
            }
            break;
          case UPGRADED:
            allowedStates.add(RepositoryVersionState.CURRENT);
            break;
          case UPGRADE_FAILED:
            allowedStates.add(RepositoryVersionState.UPGRADING);
            break;
        }

        if (!allowedStates.contains(state)) {
          throw new AmbariException("Invalid cluster version transition from "
              + existingClusterVersion.getState() + " to " + state);
        }

        // There must be at most one cluster version whose state is CURRENT at
        // all times.
        if (state == RepositoryVersionState.CURRENT) {
          ClusterVersionEntity currentVersion = clusterVersionDAO.findByClusterAndStateCurrent(getClusterName());
          if (currentVersion != null) {
            currentVersion.setState(RepositoryVersionState.INSTALLED);
            clusterVersionDAO.merge(currentVersion);
          }
        }

        existingClusterVersion.setState(state);
        existingClusterVersion.setEndTime(System.currentTimeMillis());
        clusterVersionDAO.merge(existingClusterVersion);

        if (RepositoryVersionState.CURRENT == state) {
          for (HostEntity he : clusterEntity.getHostEntities()) {
            if (hostHasReportables(existingClusterVersion.getRepositoryVersion(), he)) {
              continue;
            }

            Collection<HostVersionEntity> versions = hostVersionDAO.findByHost(
                he.getHostName());

            HostVersionEntity target = null;
            if (null != versions) {
              // Set anything that was previously marked CURRENT as INSTALLED, and the matching version as CURRENT
              for (HostVersionEntity entity : versions) {
                if (entity.getRepositoryVersion().getId().equals(existingClusterVersion.getRepositoryVersion().getId())) {
                  target = entity;
                  target.setState(state);
                  hostVersionDAO.merge(target);
                } else if (entity.getState() == RepositoryVersionState.CURRENT) {
                  entity.setState(RepositoryVersionState.INSTALLED);
                  hostVersionDAO.merge(entity);
                }
              }
            }
            if (null == target) {
              // If no matching version was found, create one with the desired state
              HostVersionEntity hve = new HostVersionEntity();
              hve.setHostEntity(he);
              hve.setHostName(he.getHostName());
              hve.setRepositoryVersion(existingClusterVersion.getRepositoryVersion());
              hve.setState(state);
              hostVersionDAO.create(hve);
            }
          }
        }
      }
    } catch (RollbackException e) {
      String message = "Unable to transition stack " + stack + " at version "
          + version + " for cluster " + getClusterName() + " to state " + state;
      LOG.warn(message);
      throw new AmbariException(message, e);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  /**
   * Checks if the host has any components reporting version information.
   * @param repoVersion the repo version
   * @param host        the host entity
   * @return {@code true} if the host has any component that report version
   * @throws AmbariException
   */
  private boolean hostHasReportables(RepositoryVersionEntity repoVersion, HostEntity host)
      throws AmbariException {

    for (HostComponentStateEntity hcse : host.getHostComponentStateEntities()) {
      ComponentInfo ci = ambariMetaInfo.getComponent(
          repoVersion.getStackName(),
          repoVersion.getStackVersion(),
          hcse.getServiceName(),
          hcse.getComponentName());

      if (ci.isVersionAdvertised()) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void setCurrentStackVersion(StackId stackVersion)
    throws AmbariException {
    clusterGlobalLock.writeLock().lock();
    try {
      ClusterStateEntity clusterStateEntity = clusterStateDAO.findByPK(clusterEntity.getClusterId());
      if (clusterStateEntity == null) {
        clusterStateEntity = new ClusterStateEntity();
        clusterStateEntity.setClusterId(clusterEntity.getClusterId());
        clusterStateEntity.setCurrentStackVersion(gson.toJson(stackVersion));
        clusterStateEntity.setClusterEntity(clusterEntity);
        clusterStateDAO.create(clusterStateEntity);
        clusterStateEntity = clusterStateDAO.merge(clusterStateEntity);
        clusterEntity.setClusterStateEntity(clusterStateEntity);
        clusterEntity = clusterDAO.merge(clusterEntity);
      } else {
        clusterStateEntity.setCurrentStackVersion(gson.toJson(stackVersion));
        clusterStateDAO.merge(clusterStateEntity);
        clusterEntity = clusterDAO.merge(clusterEntity);
      }
    } catch (RollbackException e) {
      LOG.warn("Unable to set version " + stackVersion + " for cluster "
          + getClusterName());
      throw new AmbariException("Unable to set" + " version=" + stackVersion
          + " for cluster " + getClusterName(), e);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public Map<String, Config> getConfigsByType(String configType) {
    clusterGlobalLock.readLock().lock();
    try {
      if (!allConfigs.containsKey(configType)) {
        return null;
      }

      return Collections.unmodifiableMap(allConfigs.get(configType));
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public Config getConfig(String configType, String versionTag) {
    clusterGlobalLock.readLock().lock();
    try {
      if (!allConfigs.containsKey(configType)
          || !allConfigs.get(configType).containsKey(versionTag)) {
        return null;
      }
      return allConfigs.get(configType).get(versionTag);
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public void addConfig(Config config) {
    clusterGlobalLock.writeLock().lock();
    try {
      if (config.getType() == null || config.getType().isEmpty()) {
        throw new IllegalArgumentException("Config type cannot be empty");
      }
      if (!allConfigs.containsKey(config.getType())) {
        allConfigs.put(config.getType(), new HashMap<String, Config>());
      }

      allConfigs.get(config.getType()).put(config.getTag(), config);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public Collection<Config> getAllConfigs() {
    clusterGlobalLock.readLock().lock();
    try {
      List<Config> list = new ArrayList<Config>();
      for (Entry<String, Map<String, Config>> entry : allConfigs.entrySet()) {
        for (Config config : entry.getValue().values()) {
          list.add(config);
        }
      }
      return Collections.unmodifiableList(list);
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public ClusterResponse convertToResponse()
    throws AmbariException {
    String clusterName = getClusterName();
    Map<String, Host> hosts = clusters.getHostsForCluster(clusterName);
    clusterGlobalLock.readLock().lock();
    try {
      return new ClusterResponse(getClusterId(), clusterName,
          getProvisioningState(), getSecurityType(), hosts.keySet(),
          hosts.size(), getDesiredStackVersion().getStackId(),
          getClusterHealthReport(hosts));
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  public void debugDump(StringBuilder sb) {
    loadServices();
    clusterGlobalLock.readLock().lock();
    try {
      sb.append("Cluster={ clusterName=").append(getClusterName()).append(
          ", clusterId=").append(getClusterId()).append(
          ", desiredStackVersion=").append(desiredStackVersion.getStackId()).append(
          ", services=[ ");
      boolean first = true;
      for (Service s : services.values()) {
        if (!first) {
          sb.append(" , ");
        }
        first = false;
        sb.append("\n    ");
        s.debugDump(sb);
        sb.append(' ');
      }
      sb.append(" ] }");
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  @Transactional
  public void refresh() {
    clusterGlobalLock.writeLock().lock();
    try {
      clusterEntity = clusterDAO.findById(clusterEntity.getClusterId());
      clusterDAO.refresh(clusterEntity);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  @Transactional
  public void deleteAllServices() throws AmbariException {
    loadServices();
    clusterGlobalLock.writeLock().lock();
    try {
      LOG.info("Deleting all services for cluster" + ", clusterName="
          + getClusterName());
      for (Service service : services.values()) {
        if (!service.canBeRemoved()) {
          throw new AmbariException(
              "Found non removable service when trying to"
                  + " all services from cluster" + ", clusterName="
                  + getClusterName() + ", serviceName=" + service.getName());
        }
      }

      for (Service service : services.values()) {
        service.delete();
      }

      services.clear();
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public void deleteService(String serviceName)
    throws AmbariException {
    loadServices();
    clusterGlobalLock.writeLock().lock();
    try {
      Service service = getService(serviceName);
      LOG.info("Deleting service for cluster" + ", clusterName="
          + getClusterName() + ", serviceName=" + service.getName());
      // FIXME check dependencies from meta layer
      if (!service.canBeRemoved()) {
        throw new AmbariException("Could not delete service from cluster"
          + ", clusterName=" + getClusterName()
          + ", serviceName=" + service.getName());
      }
      service.delete();
      services.remove(serviceName);
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public boolean canBeRemoved() {
    loadServices();
    clusterGlobalLock.readLock().lock();
    try {
      boolean safeToRemove = true;
      for (Service service : services.values()) {
        if (!service.canBeRemoved()) {
          safeToRemove = false;
          LOG.warn("Found non removable service" + ", clusterName="
              + getClusterName() + ", serviceName=" + service.getName());
        }
      }
      return safeToRemove;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  @Transactional
  public void delete() throws AmbariException {
    clusterGlobalLock.writeLock().lock();
    try {
      refresh();
      deleteAllServices();
      removeEntities();
      allConfigs.clear();
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Transactional
  protected void removeEntities() throws AmbariException {
    long clusterId = getClusterId();
    alertDefinitionDAO.removeAll(clusterId);
    alertDispatchDAO.removeAllGroups(clusterId);
    upgradeDAO.removeAll(clusterId);
    clusterDAO.removeByPK(clusterId);
  }

  @Override
  public ServiceConfigVersionResponse addDesiredConfig(String user, Set<Config> configs) {
    return addDesiredConfig(user, configs, null);
  }

  @Override
  public ServiceConfigVersionResponse addDesiredConfig(String user, Set<Config> configs, String serviceConfigVersionNote) {
    if (null == user) {
      throw new NullPointerException("User must be specified.");
    }

    clusterGlobalLock.writeLock().lock();
    try {
      if (configs == null) {
        return null;
      }

      Iterator<Config> configIterator = configs.iterator();

      while (configIterator.hasNext()) {
        Config config = configIterator.next();
        if (config == null) {
          configIterator.remove();
          continue;
        }
        Config currentDesired = getDesiredConfigByType(config.getType());

        // do not set if it is already the current
        if (null != currentDesired
            && currentDesired.getTag().equals(config.getTag())) {
          configIterator.remove();
        }
      }

      ServiceConfigVersionResponse serviceConfigVersionResponse = applyConfigs(
          configs, user, serviceConfigVersionNote);

      configHelper.invalidateStaleConfigsCache();
      return serviceConfigVersionResponse;
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public Map<String, DesiredConfig> getDesiredConfigs() {
    clusterGlobalLock.readLock().lock();
    try {
      Map<String, DesiredConfig> map = new HashMap<String, DesiredConfig>();
      Collection<String> types = new HashSet<String>();

      for (ClusterConfigMappingEntity e : clusterEntity.getConfigMappingEntities()) {
        if (e.isSelected() > 0) {
          DesiredConfig c = new DesiredConfig();
          c.setServiceName(null);
          c.setTag(e.getTag());
          c.setUser(e.getUser());
          c.setVersion(allConfigs.get(e.getType()).get(e.getTag()).getVersion());

          map.put(e.getType(), c);
          types.add(e.getType());
        }
      }

      if (!map.isEmpty()) {
        Map<String, List<HostConfigMapping>> hostMappingsByType = hostConfigMappingDAO.findSelectedHostsByTypes(
            clusterEntity.getClusterId(), types);

        for (Entry<String, DesiredConfig> entry : map.entrySet()) {
          List<DesiredConfig.HostOverride> hostOverrides = new ArrayList<DesiredConfig.HostOverride>();
          for (HostConfigMapping mappingEntity : hostMappingsByType.get(entry.getKey())) {
            hostOverrides.add(new DesiredConfig.HostOverride(
                mappingEntity.getHostName(), mappingEntity.getVersion()));
          }
          entry.getValue().setHostOverrides(hostOverrides);
        }
      }

      return map;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }


  @Override
  public ServiceConfigVersionResponse createServiceConfigVersion(String serviceName, String user, String note,
                                                                 ConfigGroup configGroup) {

    //create next service config version
    ServiceConfigEntity serviceConfigEntity = new ServiceConfigEntity();
    serviceConfigEntity.setServiceName(serviceName);
    serviceConfigEntity.setClusterEntity(clusterEntity);
    serviceConfigEntity.setVersion(configVersionHelper.getNextVersion(serviceName));
    serviceConfigEntity.setUser(user);
    serviceConfigEntity.setNote(note);

    if (configGroup != null) {
      serviceConfigEntity.setGroupId(configGroup.getId());
      Collection<Config> configs = configGroup.getConfigurations().values();
      List<ClusterConfigEntity> configEntities = new ArrayList<ClusterConfigEntity>(configs.size());
      for (Config config : configs) {
        configEntities.add(clusterDAO.findConfig(getClusterId(), config.getType(), config.getTag()));
      }
      serviceConfigEntity.setClusterConfigEntities(configEntities);

      serviceConfigEntity.setHostNames(new ArrayList<String>(configGroup.getHosts().keySet()));

    } else {
      List<ClusterConfigEntity> configEntities = getClusterConfigEntitiesByService(serviceName);
      serviceConfigEntity.setClusterConfigEntities(configEntities);
    }

    serviceConfigDAO.create(serviceConfigEntity);

    configChangeLog.info("Cluster '{}' changed by: '{}'; service_name='{}' config_group='{}' config_group_id='{}' " +
      "version='{}'", getClusterName(), user, serviceName,
      configGroup==null?"default":configGroup.getName(),
      configGroup==null?"-1":configGroup.getId(),
      serviceConfigEntity.getVersion());

    ServiceConfigVersionResponse response = new ServiceConfigVersionResponse();
    response.setUserName(user);
    response.setClusterName(getClusterName());
    response.setVersion(serviceConfigEntity.getVersion());
    response.setServiceName(serviceConfigEntity.getServiceName());
    response.setCreateTime(serviceConfigEntity.getCreateTimestamp());
    response.setUserName(serviceConfigEntity.getUser());
    response.setNote(serviceConfigEntity.getNote());
    response.setGroupId(serviceConfigEntity.getGroupId());
    response.setHosts(serviceConfigEntity.getHostNames());
    response.setGroupName(configGroup != null ? configGroup.getName() : null);

    return response;
  }

  @Override
  public String getServiceForConfigTypes(Collection<String> configTypes) {
    //debug
    LOG.info("Looking for service for config types {}", configTypes);
    String serviceName = null;
    for (String configType : configTypes) {
      for (Entry<String, String> entry : serviceConfigTypes.entries()) {
        if (StringUtils.equals(entry.getValue(), configType)) {
          if (serviceName != null) {
            if (entry.getKey()!=null && !StringUtils.equals(serviceName, entry.getKey())) {
              throw new IllegalArgumentException("Config type {} belongs to {} service, " +
                "but config group qualified for {}");
            }
          } else {
            serviceName = entry.getKey();
          }
        }
      }
    }
    LOG.info("Service {} returning", serviceName);
    return serviceName;
  }

  public String getServiceByConfigType(String configType) {
    for (Entry<String, String> entry : serviceConfigTypes.entries()) {
      String serviceName = entry.getKey();
      String type = entry.getValue();
      if (StringUtils.equals(type, configType)) {
        return serviceName;
      }
    }
    return null;
  }

  @Override
  public ServiceConfigVersionResponse setServiceConfigVersion(String serviceName, Long version, String user, String note) throws AmbariException {
    if (null == user) {
      throw new NullPointerException("User must be specified.");
    }

    clusterGlobalLock.writeLock().lock();
    try {
      ServiceConfigVersionResponse serviceConfigVersionResponse = applyServiceConfigVersion(
          serviceName, version, user, note);
      configHelper.invalidateStaleConfigsCache();
      return serviceConfigVersionResponse;
    } finally {
      clusterGlobalLock.writeLock().unlock();
    }
  }

  @Override
  public Map<String, Collection<ServiceConfigVersionResponse>> getActiveServiceConfigVersions() {
    clusterGlobalLock.readLock().lock();
    try {
      Map<String, Collection<ServiceConfigVersionResponse>> map = new HashMap<String, Collection<ServiceConfigVersionResponse>>();

      Set<ServiceConfigVersionResponse> responses = getActiveServiceConfigVersionSet();
      for (ServiceConfigVersionResponse response : responses) {
        if (map.get(response.getServiceName()) == null) {
          map.put(response.getServiceName(),
              new ArrayList<ServiceConfigVersionResponse>());
        }
        map.get(response.getServiceName()).add(response);
      }
      return map;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  @Override
  @RequiresSession
  public List<ServiceConfigVersionResponse> getServiceConfigVersions() {
    clusterGlobalLock.readLock().lock();
    try {
      List<ServiceConfigVersionResponse> serviceConfigVersionResponses = new ArrayList<ServiceConfigVersionResponse>();
      Set<Long> activeIds = getActiveServiceConfigVersionIds();

      for (ServiceConfigEntity serviceConfigEntity : serviceConfigDAO.getServiceConfigs(getClusterId())) {
        ServiceConfigVersionResponse serviceConfigVersionResponse = convertToServiceConfigVersionResponse(serviceConfigEntity);

        serviceConfigVersionResponse.setHosts(serviceConfigEntity.getHostNames());
        serviceConfigVersionResponse.setConfigurations(new ArrayList<ConfigurationResponse>());
        serviceConfigVersionResponse.setIsCurrent(activeIds.contains(serviceConfigEntity.getServiceConfigId()));

        List<ClusterConfigEntity> clusterConfigEntities = serviceConfigEntity.getClusterConfigEntities();
        for (ClusterConfigEntity clusterConfigEntity : clusterConfigEntities) {
          Config config = allConfigs.get(clusterConfigEntity.getType()).get(
              clusterConfigEntity.getTag());
          serviceConfigVersionResponse.getConfigurations().add(
              new ConfigurationResponse(getClusterName(), config.getType(),
                  config.getTag(), config.getVersion(), config.getProperties(),
                  config.getPropertiesAttributes()));
        }

        serviceConfigVersionResponses.add(serviceConfigVersionResponse);
      }

      return serviceConfigVersionResponses;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }

  private Set<ServiceConfigVersionResponse> getActiveServiceConfigVersionSet() {
    Set<ServiceConfigVersionResponse> responses = new HashSet<ServiceConfigVersionResponse>();
    List<ServiceConfigEntity> activeServiceConfigVersions = getActiveServiceConfigVersionEntities();

    for (ServiceConfigEntity lastServiceConfig : activeServiceConfigVersions) {
      ServiceConfigVersionResponse response = convertToServiceConfigVersionResponse(lastServiceConfig);
      response.setIsCurrent(true); //mark these as current, as they are
      responses.add(response);
    }
    return responses;
  }

  private Set<Long> getActiveServiceConfigVersionIds() {
    Set<Long> idSet = new HashSet<Long>();
    for (ServiceConfigEntity entity : getActiveServiceConfigVersionEntities()) {
      idSet.add(entity.getServiceConfigId());
    }
    return idSet;
  }

  private List<ServiceConfigEntity> getActiveServiceConfigVersionEntities() {

    List<ServiceConfigEntity> activeServiceConfigVersions = new ArrayList<ServiceConfigEntity>();
    //for services
    activeServiceConfigVersions.addAll(serviceConfigDAO.getLastServiceConfigs(getClusterId()));
    //for config groups
    if (clusterConfigGroups != null) {
      activeServiceConfigVersions.addAll(
        serviceConfigDAO.getLastServiceConfigVersionsForGroups(clusterConfigGroups.keySet()));
    }

    return activeServiceConfigVersions;
  }

  @RequiresSession
  ServiceConfigVersionResponse getActiveServiceConfigVersion(String serviceName) {
    ServiceConfigEntity lastServiceConfig = serviceConfigDAO.getLastServiceConfig(getClusterId(), serviceName);
    if (lastServiceConfig == null) {
      LOG.debug("No service config version found for service {}", serviceName);
      return null;
    }
    return convertToServiceConfigVersionResponse(lastServiceConfig);
  }

  @RequiresSession
  ServiceConfigVersionResponse convertToServiceConfigVersionResponse(ServiceConfigEntity serviceConfigEntity) {
    ServiceConfigVersionResponse serviceConfigVersionResponse = new ServiceConfigVersionResponse();

    serviceConfigVersionResponse.setClusterName(getClusterName());
    serviceConfigVersionResponse.setServiceName(serviceConfigEntity.getServiceName());
    serviceConfigVersionResponse.setVersion(serviceConfigEntity.getVersion());
    serviceConfigVersionResponse.setCreateTime(serviceConfigEntity.getCreateTimestamp());
    serviceConfigVersionResponse.setUserName(serviceConfigEntity.getUser());
    serviceConfigVersionResponse.setNote(serviceConfigEntity.getNote());

    Long groupId = serviceConfigEntity.getGroupId();

    if (groupId != null) {
      serviceConfigVersionResponse.setGroupId(groupId);
      ConfigGroup configGroup = null;
      if (clusterConfigGroups != null) {
        configGroup = clusterConfigGroups.get(groupId);
      }

      if (configGroup != null) {
        serviceConfigVersionResponse.setGroupName(configGroup.getName());
      } else {
        serviceConfigVersionResponse.setGroupName("deleted");
      }
    } else {
      serviceConfigVersionResponse.setGroupId(-1L); // -1 if no group
      serviceConfigVersionResponse.setGroupName("default");
    }

    return serviceConfigVersionResponse;
  }

  @Transactional
  ServiceConfigVersionResponse applyServiceConfigVersion(String serviceName, Long serviceConfigVersion, String user,
                                 String serviceConfigVersionNote) throws AmbariException {
    ServiceConfigEntity serviceConfigEntity = serviceConfigDAO.findByServiceAndVersion(serviceName, serviceConfigVersion);
    if (serviceConfigEntity == null) {
      throw new ObjectNotFoundException("Service config version with serviceName={} and version={} not found");
    }

    //disable all configs related to service
    if (serviceConfigEntity.getGroupId() == null) {
      Collection<String> configTypes = serviceConfigTypes.get(serviceName);
      for (ClusterConfigMappingEntity entity : clusterEntity.getConfigMappingEntities()) {
        if (configTypes.contains(entity.getType()) && entity.isSelected() > 0) {
          entity.setSelected(0);
        }
      }
      clusterDAO.merge(clusterEntity);

      for (ClusterConfigEntity configEntity : serviceConfigEntity.getClusterConfigEntities()) {
        selectConfig(configEntity.getType(), configEntity.getTag(), user);
      }
    } else {
      Long configGroupId = serviceConfigEntity.getGroupId();
      ConfigGroup configGroup = clusterConfigGroups.get(configGroupId);
      if (configGroup != null) {
        Map<String, Config> groupDesiredConfigs = new HashMap<String, Config>();
        for (ClusterConfigEntity entity : serviceConfigEntity.getClusterConfigEntities()) {
          Config config = allConfigs.get(entity.getType()).get(entity.getTag());
          groupDesiredConfigs.put(config.getType(), config);
        }
        configGroup.setConfigurations(groupDesiredConfigs);

        Map<String, Host> groupDesiredHosts = new HashMap<String, Host>();
        for (String hostname : serviceConfigEntity.getHostNames()) {
          Host host = clusters.getHost(hostname);
          if (host != null) {
            groupDesiredHosts.put(hostname, host);
          } else {
            LOG.warn("Host {} doesn't exist anymore, skipping", hostname);
          }
        }
        configGroup.setHosts(groupDesiredHosts);
        configGroup.persist();
      } else {
        throw new IllegalArgumentException("Config group {} doesn't exist");
      }
    }

    ServiceConfigEntity serviceConfigEntityClone = new ServiceConfigEntity();
    serviceConfigEntityClone.setCreateTimestamp(System.currentTimeMillis());
    serviceConfigEntityClone.setUser(user);
    serviceConfigEntityClone.setServiceName(serviceName);
    serviceConfigEntityClone.setClusterEntity(clusterEntity);
    serviceConfigEntityClone.setClusterConfigEntities(serviceConfigEntity.getClusterConfigEntities());
    serviceConfigEntityClone.setClusterId(serviceConfigEntity.getClusterId());
    serviceConfigEntityClone.setHostNames(serviceConfigEntity.getHostNames());
    serviceConfigEntityClone.setGroupId(serviceConfigEntity.getGroupId());
    serviceConfigEntityClone.setNote(serviceConfigVersionNote);
    serviceConfigEntityClone.setVersion(configVersionHelper.getNextVersion(serviceName));

    serviceConfigDAO.create(serviceConfigEntityClone);

    return convertToServiceConfigVersionResponse(serviceConfigEntityClone);
  }

  @Transactional
  void selectConfig(String type, String tag, String user) {
    Collection<ClusterConfigMappingEntity> entities = clusterEntity.getConfigMappingEntities();

    //disable previous config
    for (ClusterConfigMappingEntity e : entities) {
      if (e.isSelected() > 0 && e.getType().equals(type)) {
        e.setSelected(0);
      }
    }

    ClusterConfigMappingEntity entity = new ClusterConfigMappingEntity();
    entity.setClusterEntity(clusterEntity);
    entity.setClusterId(clusterEntity.getClusterId());
    entity.setCreateTimestamp(System.currentTimeMillis());
    entity.setSelected(1);
    entity.setUser(user);
    entity.setType(type);
    entity.setTag(tag);
    entities.add(entity);

    clusterDAO.merge(clusterEntity);

  }

  @Transactional
  ServiceConfigVersionResponse applyConfigs(Set<Config> configs, String user, String serviceConfigVersionNote) {

    String serviceName = null;
    for (Config config : configs) {
      for (Entry<String, String> entry : serviceConfigTypes.entries()) {
        if (StringUtils.equals(entry.getValue(), config.getType())) {
          if (serviceName == null) {
            serviceName = entry.getKey();
            break;
          } else if (!serviceName.equals(entry.getKey())) {
            String error = "Updating configs for multiple services by a " +
                "single API request isn't supported";
            IllegalArgumentException exception = new IllegalArgumentException(error);
            LOG.error(error + ", config version not created");
            throw exception;
          } else {
            break;
          }
        }
      }
    }

    for (Config config: configs) {
      selectConfig(config.getType(), config.getTag(), user);
    }

    if (serviceName == null) {
      LOG.error("No service found for config type '{}', service config version not created");
      return null;
    } else {
      return createServiceConfigVersion(serviceName, user, serviceConfigVersionNote);
    }

  }

  private ServiceConfigVersionResponse createServiceConfigVersion(String serviceName, String user,
                                                                  String serviceConfigVersionNote) {
    //create next service config version
    return createServiceConfigVersion(serviceName, user, serviceConfigVersionNote, null);
  }

  private List<ClusterConfigEntity> getClusterConfigEntitiesByService(String serviceName) {
    List<ClusterConfigEntity> configEntities = new ArrayList<ClusterConfigEntity>();

    //add configs from this service
    Collection<String> configTypes = serviceConfigTypes.get(serviceName);
    for (ClusterConfigMappingEntity mappingEntity : clusterEntity.getConfigMappingEntities()) {
      if (mappingEntity.isSelected() > 0 && configTypes.contains(mappingEntity.getType())) {
        ClusterConfigEntity configEntity =
          clusterDAO.findConfig(getClusterId(), mappingEntity.getType(), mappingEntity.getTag());
        if (configEntity != null) {
          configEntities.add(configEntity);
        } else {
          LOG.error("Desired cluster config type={}, tag={} is not present in database," +
            " unable to add to service config version");
        }
      }
    }
    return configEntities;
  }

  @Override
  public Config getDesiredConfigByType(String configType) {
    clusterGlobalLock.readLock().lock();
    try {
      for (ClusterConfigMappingEntity e : clusterEntity.getConfigMappingEntities()) {
        if (e.isSelected() > 0 && e.getType().equals(configType)) {
          return getConfig(e.getType(), e.getTag());
        }
      }

      return null;
    } finally {
      clusterGlobalLock.readLock().unlock();
    }
  }


  @Override
  public Map<String, Map<String, DesiredConfig>> getHostsDesiredConfigs(Collection<String> hostnames) {

    if (hostnames == null || hostnames.isEmpty()) {
      return Collections.emptyMap();
    }

    Set<HostConfigMapping> mappingEntities =
      hostConfigMappingDAO.findSelectedByHosts(clusterEntity.getClusterId(), hostnames);

    Map<String, Map<String, DesiredConfig>> desiredConfigsByHost = new HashMap<String, Map<String, DesiredConfig>>();

    for (String hostname : hostnames) {
      desiredConfigsByHost.put(hostname, new HashMap<String, DesiredConfig>());
    }

    for (HostConfigMapping mappingEntity : mappingEntities) {
      DesiredConfig desiredConfig = new DesiredConfig();
      desiredConfig.setTag(mappingEntity.getVersion());
      desiredConfig.setServiceName(mappingEntity.getServiceName());
      desiredConfig.setUser(mappingEntity.getUser());

      desiredConfigsByHost.get(mappingEntity.getHostName()).put(mappingEntity.getType(), desiredConfig);
    }

    return desiredConfigsByHost;
  }

  @Override
  public Map<String, Map<String, DesiredConfig>> getAllHostsDesiredConfigs() {

    Collection<String> hostnames;
    try {
      hostnames = clusters.getHostsForCluster(clusterEntity.getClusterName()).keySet();
    } catch (AmbariException ignored) {
      return Collections.emptyMap();
    }

    return getHostsDesiredConfigs(hostnames);
  }

  @Override
  public Long getNextConfigVersion(String type) {
    return configVersionHelper.getNextVersion(type);
  }

  private Map<String, Long> getConfigLastVersions() {
    Map<String, Long> maxVersions = new HashMap<String, Long>();
    //config versions
    for (Entry<String, Map<String, Config>> mapEntry : allConfigs.entrySet()) {
      String type = mapEntry.getKey();
      Long lastVersion = 0L;
      for (Entry<String, Config> configEntry : mapEntry.getValue().entrySet()) {
        Long version = configEntry.getValue().getVersion();
        if (version > lastVersion) {
          lastVersion = version;
        }
      }
      maxVersions.put(type, lastVersion);
    }

    //service config versions
    maxVersions.putAll(serviceConfigDAO.findMaxVersions(getClusterId()));

    return maxVersions;
  }

  @Transactional
  @Override
  public List<ServiceComponentHostEvent> processServiceComponentHostEvents(ListMultimap<String, ServiceComponentHostEvent> eventMap) {
    List<ServiceComponentHostEvent> failedEvents = new ArrayList<ServiceComponentHostEvent>();

    clusterGlobalLock.readLock().lock();
    try {
      for (Entry<String, ServiceComponentHostEvent> entry : eventMap.entries()) {
        String serviceName = entry.getKey();
        ServiceComponentHostEvent event = entry.getValue();
        try {
          Service service = getService(serviceName);
          ServiceComponent serviceComponent = service.getServiceComponent(event.getServiceComponentName());
          ServiceComponentHost serviceComponentHost = serviceComponent.getServiceComponentHost(event.getHostName());
          serviceComponentHost.handleEvent(event);
        } catch (AmbariException e) {
          LOG.error("ServiceComponentHost lookup exception ", e.getMessage());
          failedEvents.add(event);
        } catch (InvalidStateTransitionException e) {
          LOG.error("Invalid transition ", e);
          if ((e.getEvent() == ServiceComponentHostEventType.HOST_SVCCOMP_START) &&
            (e.getCurrentState() == State.STARTED)) {
            LOG.warn("Component request for component = " + event.getServiceComponentName() + " to start is invalid, since component is already started. Ignoring this request.");
            // skip adding this as a failed event, to work around stack ordering issues with Hive
          } else {
            failedEvents.add(event);
          }


        }
      }
    } finally {
      clusterGlobalLock.readLock().unlock();
    }

    return failedEvents;
  }

  /**
   * @param serviceName name of the service
   * @param componentName name of the component
   * @return the set of hosts for the provided service and component
   */
  @Override
  public Set<String> getHosts(String serviceName, String componentName) {
    Map<String, Service> services = getServices();

    if (!services.containsKey(serviceName)) {
      return Collections.emptySet();
    }

    Service service = services.get(serviceName);
    Map<String, ServiceComponent> components = service.getServiceComponents();

    if (!components.containsKey(componentName) ||
        components.get(componentName).getServiceComponentHosts().size() == 0) {
      return Collections.emptySet();
    }

    return components.get(componentName).getServiceComponentHosts().keySet();
  }

  private ClusterHealthReport getClusterHealthReport(
      Map<String, Host> clusterHosts) throws AmbariException {

    int staleConfigsHosts = 0;
    int maintenanceStateHosts = 0;

    int healthyStateHosts = 0;
    int unhealthyStateHosts = 0;
    int initStateHosts = 0;
    int healthyStatusHosts = 0;

    int unhealthyStatusHosts = 0;
    int unknownStatusHosts = 0;
    int alertStatusHosts = 0;
    int heartbeatLostStateHosts = 0;

    Collection<Host> hosts = clusterHosts.values();
    Iterator<Host> iterator = hosts.iterator();
    while (iterator.hasNext()) {
      Host host = iterator.next();
      String hostName = host.getHostName();

      switch (host.getState()) {
        case HEALTHY:
          healthyStateHosts++;
          break;
        case UNHEALTHY:
          unhealthyStateHosts++;
          break;
        case INIT:
          initStateHosts++;
          break;
        case HEARTBEAT_LOST:
          heartbeatLostStateHosts++;
          break;
      }

      switch (HostHealthStatus.HealthStatus.valueOf(host.getStatus())) {
        case HEALTHY:
          healthyStatusHosts++;
          break;
        case UNHEALTHY:
          unhealthyStatusHosts++;
          break;
        case UNKNOWN:
          unknownStatusHosts++;
          break;
        case ALERT:
          alertStatusHosts++;
          break;
      }

      boolean staleConfig = false;
      boolean maintenanceState = false;

      if (serviceComponentHostsByHost.containsKey(hostName)) {
        for (ServiceComponentHost sch : serviceComponentHostsByHost.get(hostName)) {
          staleConfig = staleConfig || configHelper.isStaleConfigs(sch);
          maintenanceState = maintenanceState ||
            maintenanceStateHelper.getEffectiveState(sch) != MaintenanceState.OFF;
        }
      }

      if (staleConfig) {
        staleConfigsHosts++;
      }
      if (maintenanceState) {
        maintenanceStateHosts++;
      }
    }

    ClusterHealthReport chr = new ClusterHealthReport();
    chr.setAlertStatusHosts(alertStatusHosts);
    chr.setHealthyStateHosts(healthyStateHosts);
    chr.setUnknownStatusHosts(unknownStatusHosts);
    chr.setUnhealthyStatusHosts(unhealthyStatusHosts);
    chr.setUnhealthyStateHosts(unhealthyStateHosts);
    chr.setStaleConfigsHosts(staleConfigsHosts);
    chr.setMaintenanceStateHosts(maintenanceStateHosts);
    chr.setInitStateHosts(initStateHosts);
    chr.setHeartbeatLostStateHosts(heartbeatLostStateHosts);
    chr.setHealthyStatusHosts(healthyStatusHosts);

    return chr;
  }

  @Override
  public boolean checkPermission(PrivilegeEntity privilegeEntity, boolean readOnly) {
    ResourceEntity resourceEntity = clusterEntity.getResource();
    if (resourceEntity != null) {
      Integer permissionId = privilegeEntity.getPermission().getId();
      // CLUSTER.READ or CLUSTER.OPERATE for the given cluster resource.
      if (privilegeEntity.getResource().equals(resourceEntity)) {
        if ((readOnly && permissionId.equals(PermissionEntity.CLUSTER_READ_PERMISSION)) ||
            permissionId.equals(PermissionEntity.CLUSTER_OPERATE_PERMISSION)) {
          return true;
        }
      }
    }
    return false;
  }

  @Override
  public void addSessionAttributes(Map<String, Object> attributes) {
    if (attributes != null && !attributes.isEmpty()) {
      Map<String, Object>  sessionAttributes = new HashMap<String, Object>(getSessionAttributes());
      sessionAttributes.putAll(attributes);
      setSessionAttributes(attributes);
    }
  }

  @Override
  public void setSessionAttribute(String key, Object value){
    if (key != null && !key.isEmpty()) {
      Map<String, Object> sessionAttributes = new HashMap<String, Object>(getSessionAttributes());
      sessionAttributes.put(key, value);
      setSessionAttributes(sessionAttributes);
    }
  }

  @Override
  public void removeSessionAttribute(String key) {
    if (key != null && !key.isEmpty()) {
      Map<String, Object> sessionAttributes = new HashMap<String, Object>(getSessionAttributes());
      sessionAttributes.remove(key);
      setSessionAttributes(sessionAttributes);
    }
  }

  @Override
  public Map<String, Object> getSessionAttributes() {
    Map<String, Object>  attributes =
        (Map<String, Object>) getSessionManager().getAttribute(getClusterSessionAttributeName());

    return attributes == null ? Collections.<String, Object>emptyMap() : attributes;
  }

  /**
   * Get the associated session manager.
   *
   * @return the session manager
   */
  protected AmbariSessionManager getSessionManager() {
    return sessionManager;
  }

  /**
   * Set the map of session attributes for this cluster.
   * <p/>
   * This is a private method so that it may be used as a utility for add and update operations.
   *
   * @param sessionAttributes the map of session attributes for this cluster; never null
   */
  private void setSessionAttributes(Map<String, Object> sessionAttributes) {
    getSessionManager().setAttribute(getClusterSessionAttributeName(), sessionAttributes);
  }

  /**
   * Generates and returns the cluster-specific attribute name to use to set and get cluster-specific
   * session attributes.
   *
   * @return the name of the cluster-specific session attribute
   */
  private String getClusterSessionAttributeName() {
    return CLUSTER_SESSION_ATTRIBUTES_PREFIX + getClusterName();
  }
}
