/*
 * 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.controller.internal;

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

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.ClusterNotFoundException;
import org.apache.ambari.server.ConfigGroupNotFoundException;
import org.apache.ambari.server.DuplicateResourceException;
import org.apache.ambari.server.HostNotFoundException;
import org.apache.ambari.server.ParentObjectNotFoundException;
import org.apache.ambari.server.StaticallyInject;
import org.apache.ambari.server.controller.AmbariManagementController;
import org.apache.ambari.server.controller.ConfigGroupRequest;
import org.apache.ambari.server.controller.ConfigGroupResponse;
import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
import org.apache.ambari.server.controller.spi.NoSuchResourceException;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.spi.Request;
import org.apache.ambari.server.controller.spi.RequestStatus;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException;
import org.apache.ambari.server.controller.spi.ResourcePredicateEvaluator;
import org.apache.ambari.server.controller.spi.SystemException;
import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
import org.apache.ambari.server.controller.utilities.PropertyHelper;
import org.apache.ambari.server.orm.dao.HostDAO;
import org.apache.ambari.server.orm.entities.HostEntity;
import org.apache.ambari.server.security.authorization.AuthorizationException;
import org.apache.ambari.server.security.authorization.AuthorizationHelper;
import org.apache.ambari.server.security.authorization.ResourceType;
import org.apache.ambari.server.security.authorization.RoleAuthorization;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
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.Host;
import org.apache.ambari.server.state.configgroup.ConfigGroup;
import org.apache.ambari.server.state.configgroup.ConfigGroupFactory;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Provider;

@StaticallyInject
public class ConfigGroupResourceProvider extends
  AbstractControllerResourceProvider implements ResourcePredicateEvaluator {

  private static final Logger configLogger = LoggerFactory.getLogger("configchange");
  private static final Logger LOG = LoggerFactory.getLogger
    (ConfigGroupResourceProvider.class);

  public static final String CONFIG_GROUP = "ConfigGroup";

  public static final String CLUSTER_NAME_PROPERTY_ID = "cluster_name";
  public static final String ID_PROPERTY_ID =  "id";
  public static final String GROUP_NAME_PROPERTY_ID =  "group_name";
  public static final String TAG_PROPERTY_ID =  "tag";
  public static final String SERVICE_NAME_PROPERTY_ID =  "service_name";
  public static final String DESCRIPTION_PROPERTY_ID =  "description";
  public static final String SERVICE_CONFIG_VERSION_NOTE_PROPERTY_ID =  "service_config_version_note";
  public static final String HOST_NAME_PROPERTY_ID = "host_name";
  public static final String HOSTS_HOSTNAME_PROPERTY_ID = "hosts/host_name";
  public static final String HOSTS_PROPERTY_ID =  "hosts";
  public static final String DESIRED_CONFIGS_PROPERTY_ID = "desired_configs";
  public static final String VERSION_TAGS_PROPERTY_ID = "version_tags";

  public static final String CLUSTER_NAME = PropertyHelper.getPropertyId(CONFIG_GROUP, CLUSTER_NAME_PROPERTY_ID);
  public static final String ID = PropertyHelper.getPropertyId(CONFIG_GROUP, ID_PROPERTY_ID);
  public static final String GROUP_NAME = PropertyHelper.getPropertyId(CONFIG_GROUP, GROUP_NAME_PROPERTY_ID);
  public static final String TAG = PropertyHelper.getPropertyId(CONFIG_GROUP, TAG_PROPERTY_ID);
  public static final String SERVICE_NAME = PropertyHelper.getPropertyId(CONFIG_GROUP, SERVICE_NAME_PROPERTY_ID);
  public static final String DESCRIPTION = PropertyHelper.getPropertyId(CONFIG_GROUP, DESCRIPTION_PROPERTY_ID);
  public static final String SERVICE_CONFIG_VERSION_NOTE = PropertyHelper
      .getPropertyId(CONFIG_GROUP, SERVICE_CONFIG_VERSION_NOTE_PROPERTY_ID);
  public static final String HOST_NAME = PropertyHelper.getPropertyId(null, HOST_NAME_PROPERTY_ID);
  public static final String HOSTS_HOST_NAME = PropertyHelper.getPropertyId(CONFIG_GROUP, HOSTS_HOSTNAME_PROPERTY_ID);
  public static final String HOSTS = PropertyHelper.getPropertyId(CONFIG_GROUP, HOSTS_PROPERTY_ID);
  public static final String DESIRED_CONFIGS = PropertyHelper.getPropertyId(CONFIG_GROUP, DESIRED_CONFIGS_PROPERTY_ID);
  public static final String VERSION_TAGS = PropertyHelper.getPropertyId(CONFIG_GROUP, VERSION_TAGS_PROPERTY_ID);

  /**
   * The key property ids for a ConfigGroup resource.
   */
  private static final Map<Resource.Type, String> keyPropertyIds = ImmutableMap.<Resource.Type, String>builder()
      .put(Resource.Type.Cluster, CLUSTER_NAME)
      .put(Resource.Type.ConfigGroup, ID)
      .build();

  /**
   * The property ids for a ConfigGroup resource.
   */
  private static final Set<String> propertyIds = Sets.newHashSet(
    CLUSTER_NAME,
    ID,
    GROUP_NAME,
    TAG,
    SERVICE_NAME,
    DESCRIPTION,
    SERVICE_CONFIG_VERSION_NOTE,
    HOST_NAME,
    HOSTS_HOST_NAME,
    HOSTS,
    DESIRED_CONFIGS,
    VERSION_TAGS);

  @Inject
  private static HostDAO hostDAO;

  /**
   * Used for creating {@link Config} instances to return in the REST response.
   */
  @Inject
  private static ConfigFactory configFactory;

  @Inject
  private static Provider<ConfigHelper> m_configHelper;

  /**
   * Create a  new resource provider for the given management controller.
   *
   * @param managementController the management controller
   */
  protected ConfigGroupResourceProvider(AmbariManagementController managementController) {
    super(Resource.Type.ConfigGroup, propertyIds, keyPropertyIds, managementController);

    EnumSet<RoleAuthorization> manageGroupsAuthSet =
        EnumSet.of(RoleAuthorization.SERVICE_MANAGE_CONFIG_GROUPS, RoleAuthorization.CLUSTER_MANAGE_CONFIG_GROUPS);

    setRequiredCreateAuthorizations(manageGroupsAuthSet);
    setRequiredDeleteAuthorizations(manageGroupsAuthSet);
    setRequiredUpdateAuthorizations(manageGroupsAuthSet);


    setRequiredGetAuthorizations(EnumSet.of(RoleAuthorization.CLUSTER_VIEW_CONFIGS,
        RoleAuthorization.CLUSTER_MANAGE_CONFIG_GROUPS, RoleAuthorization.SERVICE_VIEW_CONFIGS,
        RoleAuthorization.SERVICE_MANAGE_CONFIG_GROUPS, RoleAuthorization.SERVICE_COMPARE_CONFIGS));
  }

  @Override
  protected Set<String> getPKPropertyIds() {
    return new HashSet<>(keyPropertyIds.values());
  }

  @Override
  public RequestStatus createResourcesAuthorized(Request request) throws
       SystemException, UnsupportedPropertyException,
       ResourceAlreadyExistsException, NoSuchParentResourceException {

    final Set<ConfigGroupRequest> requests = new HashSet<>();
    for (Map<String, Object> propertyMap : request.getProperties()) {
      requests.add(getConfigGroupRequest(propertyMap));
    }
    RequestStatus status = createResources(requests);
    notifyCreate(Resource.Type.ConfigGroup, request);
    return status;
  }

  @Override
  public Set<Resource> getResourcesAuthorized(Request request, Predicate predicate) throws
       SystemException, UnsupportedPropertyException, NoSuchResourceException,
       NoSuchParentResourceException {

    final Set<ConfigGroupRequest> requests = new HashSet<>();
    for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) {
      requests.add(getConfigGroupRequest(propertyMap));
    }

    Set<ConfigGroupResponse> responses = getResources(new Command<Set<ConfigGroupResponse>>() {
      @Override
      public Set<ConfigGroupResponse> invoke() throws AmbariException {
        return getConfigGroups(requests);
      }
    });

    Set<String>   requestedIds = getRequestPropertyIds(request, predicate);
    Set<Resource> resources    = new HashSet<>();

    if (requestedIds.contains(HOSTS_HOST_NAME)) {
      requestedIds.add(HOSTS);
    }

    for (ConfigGroupResponse response : responses) {
      Resource resource = new ResourceImpl(Resource.Type.ConfigGroup);

      setResourceProperty(resource, ID,
        response.getId(), requestedIds);
      setResourceProperty(resource, CLUSTER_NAME,
        response.getClusterName(), requestedIds);
      setResourceProperty(resource, GROUP_NAME,
        response.getGroupName(), requestedIds);
      setResourceProperty(resource, TAG,
        response.getTag(), requestedIds);
      setResourceProperty(resource, DESCRIPTION,
        response.getDescription(), requestedIds);
      setResourceProperty(resource, HOSTS,
        response.getHosts(), requestedIds);
      setResourceProperty(resource, DESIRED_CONFIGS,
        response.getConfigurations(), requestedIds);

      resources.add(resource);
    }

    return resources;
  }

  @Override
  public RequestStatus updateResourcesAuthorized(Request request, Predicate predicate) throws
       SystemException, UnsupportedPropertyException,
       NoSuchResourceException, NoSuchParentResourceException {

    final Set<ConfigGroupRequest> requests = new HashSet<>();

    Iterator<Map<String,Object>> iterator = request.getProperties().iterator();
    if (iterator.hasNext()) {
      for (Map<String, Object> propertyMap : getPropertyMaps(iterator.next(), predicate)) {
        requests.add(getConfigGroupRequest(propertyMap));
      }
    }

    updateResources(requests);

    Set<Resource> associatedResources = new HashSet<>();
    for (ConfigGroupRequest configGroupRequest : requests) {
      ConfigGroupResponse configGroupResponse = getManagementController().getConfigGroupUpdateResults(configGroupRequest);
      Resource resource = new ResourceImpl(Resource.Type.ConfigGroup);

      resource.setProperty(ID, configGroupResponse.getId());
      resource.setProperty(CLUSTER_NAME, configGroupResponse.getClusterName());
      resource.setProperty(GROUP_NAME, configGroupResponse.getGroupName());
      resource.setProperty(TAG, configGroupResponse.getTag());
      resource.setProperty(VERSION_TAGS, configGroupResponse.getVersionTags());

      associatedResources.add(resource);
    }

    notifyUpdate(Resource.Type.ConfigGroup, request, predicate);

    return getRequestStatus(null, associatedResources);
  }

  @Override
  public RequestStatus deleteResourcesAuthorized(Request request, Predicate predicate) throws
       SystemException, UnsupportedPropertyException, NoSuchResourceException,
       NoSuchParentResourceException {

    for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) {
      final ConfigGroupRequest configGroupRequest = getConfigGroupRequest(propertyMap);

      modifyResources(new Command<Void>() {
        @Override
        public Void invoke() throws AmbariException, AuthorizationException {
          deleteConfigGroup(configGroupRequest);
          return null;
        }
      });
    }

    notifyDelete(Resource.Type.ConfigGroup, predicate);

    return getRequestStatus(null);
  }

  @Override
  public Set<String> checkPropertyIds(Set<String> propertyIds) {
    //allow providing service_config_version_note, but we should not return it for config group
    Set<String> unsupportedPropertyIds = super.checkPropertyIds(propertyIds);
    for (Iterator<String> iterator = unsupportedPropertyIds.iterator(); iterator.hasNext(); ) {
      String next = iterator.next();
      next = PropertyHelper.getPropertyName(next);
      if (next.equals("service_config_version_note") || next.equals("/service_config_version_note")) {
        iterator.remove();
      }
    }
    return unsupportedPropertyIds;
  }

  /**
   * Create configuration group resources based on set of config group requests.
   *
   * @param requests  set of config group requests
   *
   * @return a request status
   *
   * @throws SystemException                an internal system exception occurred
   * @throws UnsupportedPropertyException   the request contains unsupported property ids
   * @throws ResourceAlreadyExistsException attempted to create a resource which already exists
   * @throws NoSuchParentResourceException  a parent resource of the resource to create doesn't exist
   */
  public RequestStatus createResources(final Set<ConfigGroupRequest> requests)throws
      SystemException, UnsupportedPropertyException,
      ResourceAlreadyExistsException, NoSuchParentResourceException{

    Set<ConfigGroupResponse> responses =
        createResources(new Command<Set<ConfigGroupResponse>>() {
          @Override
          public Set<ConfigGroupResponse> invoke() throws AmbariException, AuthorizationException {
            return createConfigGroups(requests);
          }
        });

    Set<Resource> associatedResources = new HashSet<>();
    for (ConfigGroupResponse response : responses) {
      Resource resource = new ResourceImpl(Resource.Type.ConfigGroup);
      resource.setProperty(ID, response.getId());
      associatedResources.add(resource);
    }

    return getRequestStatus(null, associatedResources);
  }

  public RequestStatus updateResources(final Set<ConfigGroupRequest> requests)
      throws SystemException,
      UnsupportedPropertyException,
      NoSuchResourceException,
      NoSuchParentResourceException {

    modifyResources(new Command<Void>() {
      @Override
      public Void invoke() throws AmbariException, AuthorizationException {
        updateConfigGroups(requests);
        return null;
      }
    });

    return getRequestStatus(null);
  }

  private synchronized  Set<ConfigGroupResponse> getConfigGroups
    (Set<ConfigGroupRequest> requests) throws AmbariException {
    Set<ConfigGroupResponse> responses = new HashSet<>();
    if (requests != null) {
      for (ConfigGroupRequest request : requests) {
        LOG.debug("Received a Config group request with, clusterName = {}, groupId = {}, groupName = {}, tag = {}",
          request.getClusterName(), request.getId(), request.getGroupName(), request.getTag());

        if (request.getClusterName() == null) {
          LOG.warn("Cluster name is a required field.");
          continue;
        }

        Cluster cluster = getManagementController().getClusters().getCluster
          (request.getClusterName());
        Map<Long, ConfigGroup> configGroupMap = cluster.getConfigGroups();

        // By group id
        if (request.getId() != null) {
          ConfigGroup configGroup = configGroupMap.get(request.getId());
          if (configGroup != null) {
            responses.add(configGroup.convertToResponse());
          } else {
            throw new ConfigGroupNotFoundException(cluster.getClusterName(),
              request.getId().toString());
          }
          continue;
        }
        // By group name
        if (request.getGroupName() != null) {
          for (ConfigGroup configGroup : configGroupMap.values()) {
            if (configGroup.getName().equals(request.getGroupName())) {
              responses.add(configGroup.convertToResponse());
            }
          }
          continue;
        }
        // By tag only
        if (request.getTag() != null && request.getHosts().isEmpty()) {
          for (ConfigGroup configGroup : configGroupMap.values()) {
            if (configGroup.getTag().equals(request.getTag())) {
              responses.add(configGroup.convertToResponse());
            }
          }
          continue;
        }
        // By hostnames only
        if (!request.getHosts().isEmpty() && request.getTag() == null) {
          for (String hostname : request.getHosts()) {
            Map<Long, ConfigGroup> groupMap = cluster
              .getConfigGroupsByHostname(hostname);

            if (!groupMap.isEmpty()) {
              for (ConfigGroup configGroup : groupMap.values()) {
                responses.add(configGroup.convertToResponse());
              }
            }
          }
          continue;
        }
        // By tag and hostnames
        if (request.getTag() != null && !request.getHosts().isEmpty()) {
          for (ConfigGroup configGroup : configGroupMap.values()) {
            // Has tag
            if (configGroup.getTag().equals(request.getTag())) {
              // Has a match with hosts
              List<Long> groupHostIds = new ArrayList<>(configGroup.getHosts().keySet());
              Set<String> groupHostNames = new HashSet<>(hostDAO.getHostNamesByHostIds(groupHostIds));

              groupHostNames.retainAll(request.getHosts());
              if (!groupHostNames.isEmpty()) {
                responses.add(configGroup.convertToResponse());
              }
            }
          }
          continue;
        }
        // Select all
        for (ConfigGroup configGroup : configGroupMap.values()) {
          responses.add(configGroup.convertToResponse());
        }
      }
    }
    return responses;
  }

  private void verifyConfigs(Map<String, Config> configs, String clusterName) throws AmbariException {
    if (configs == null) {
      return;
    }
    Clusters clusters = getManagementController().getClusters();
    for (String key : configs.keySet()) {
      if(!clusters.getCluster(clusterName).isConfigTypeExists(key)){
        throw new AmbariException("Trying to add not existent config type to config group:"+
        " configType = "+ key +
        " cluster = " + clusterName);
      }
    }
  }

  private void verifyHostList(Cluster cluster, Map<Long, Host> hosts,
                              ConfigGroupRequest request) throws AmbariException {

    Map<Long, ConfigGroup> configGroupMap = cluster.getConfigGroups();

    if (configGroupMap != null) {
      for (ConfigGroup configGroup : configGroupMap.values()) {
        if (configGroup.getTag().equals(request.getTag())
            && !configGroup.getId().equals(request.getId())) {
          // Check the new host list for duplicated with this group
          for (Host host : hosts.values()) {
            if (configGroup.getHosts().containsKey(host.getHostId())) {
              throw new DuplicateResourceException("Host is already " +
                "associated with a config group"
                + ", clusterName = " + configGroup.getClusterName()
                + ", configGroupName = " + configGroup.getName()
                + ", tag = " + configGroup.getTag()
                + ", hostname = " + host.getHostName());
            }
          }
        }
      }
    }
  }

  private synchronized void deleteConfigGroup(ConfigGroupRequest request)
      throws AmbariException, AuthorizationException {
    if (request.getId() == null) {
      throw new AmbariException("Config group id is a required field.");
    }

    Clusters clusters = getManagementController().getClusters();

    Cluster cluster;
    try {
      cluster = clusters.getCluster(request.getClusterName());
    } catch (ClusterNotFoundException e) {
      throw new ParentObjectNotFoundException(
        "Attempted to delete a config group from a cluster which doesn't " +
          "exist", e);
    }
    ConfigGroup configGroup = cluster.getConfigGroups().get(request.getId());

    if (configGroup == null) {
      throw new ConfigGroupNotFoundException(cluster.getClusterName(), request.getId().toString());
    }

    if (StringUtils.isEmpty(configGroup.getServiceName())) {
      if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
        RoleAuthorization.CLUSTER_MANAGE_CONFIG_GROUPS)) {
        throw new AuthorizationException("The authenticated user is not authorized to delete config groups");
      }
    } else {
      if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
        RoleAuthorization.SERVICE_MANAGE_CONFIG_GROUPS)) {
        throw new AuthorizationException("The authenticated user is not authorized to delete config groups");
      }
    }

    configLogger.info("(configchange) Deleting configuration group. cluster: '{}', changed by: '{}', config group: '{}', config group id: '{}'",
        cluster.getClusterName(), getManagementController().getAuthName(), configGroup.getName(), request.getId());

    cluster.deleteConfigGroup(request.getId());
  }

  private void validateRequest(ConfigGroupRequest request) {
    if (request.getClusterName() == null
      || request.getClusterName().isEmpty()
      || request.getGroupName() == null
      || request.getGroupName().isEmpty()
      || request.getTag() == null
      || request.getTag().isEmpty()) {

      LOG.debug("Received a config group request with cluster name = {}, group name = {}, tag = {}",
        request.getClusterName(), request.getGroupName(), request.getTag());

      throw new IllegalArgumentException("Cluster name, group name and tag need to be provided.");

    }
  }

  private synchronized Set<ConfigGroupResponse> createConfigGroups
    (Set<ConfigGroupRequest> requests) throws AmbariException, AuthorizationException {

    if (requests.isEmpty()) {
      LOG.warn("Received an empty requests set");
      return null;
    }

    Set<ConfigGroupResponse> configGroupResponses = new
      HashSet<>();

    Clusters clusters = getManagementController().getClusters();
    ConfigGroupFactory configGroupFactory = getManagementController()
      .getConfigGroupFactory();

    Set<String> updatedClusters = new HashSet<>();
    for (ConfigGroupRequest request : requests) {

      Cluster cluster;
      try {
        cluster = clusters.getCluster(request.getClusterName());
      } catch (ClusterNotFoundException e) {
        throw new ParentObjectNotFoundException(
          "Attempted to add a config group to a cluster which doesn't exist", e);
      }

      validateRequest(request);

      Map<Long, ConfigGroup> configGroupMap = cluster.getConfigGroups();
      if (configGroupMap != null) {
        for (ConfigGroup configGroup : configGroupMap.values()) {
          if (configGroup.getName().equals(request.getGroupName()) &&
              configGroup.getTag().equals(request.getTag())) {
            throw new DuplicateResourceException("Config group already " +
              "exists with the same name and tag"
              + ", clusterName = " + request.getClusterName()
              + ", groupName = " + request.getGroupName()
              + ", tag = " + request.getTag());
          }
        }
      }

      // Find hosts
      Map<Long, Host> hosts = new HashMap<>();
      if (request.getHosts() != null && !request.getHosts().isEmpty()) {
        for (String hostname : request.getHosts()) {
          Host host = clusters.getHost(hostname);
          HostEntity hostEntity = hostDAO.findByName(hostname);
          if (host == null || hostEntity == null) {
            throw new HostNotFoundException(hostname);
          }
          hosts.put(hostEntity.getHostId(), host);
        }
      }

      verifyHostList(cluster, hosts, request);

      String serviceName = request.getServiceName();
      if (serviceName == null && !MapUtils.isEmpty(request.getConfigs())) {
        try {
          serviceName = cluster.getServiceForConfigTypes(request.getConfigs().keySet());
        } catch (IllegalArgumentException e) {
          // Ignore this since we may have hit a config type that spans multiple services. This may
          // happen in unit test cases but should not happen with later versions of stacks.
        }
      }

      if (StringUtils.isEmpty(serviceName)) {
        if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
            RoleAuthorization.CLUSTER_MANAGE_CONFIG_GROUPS)) {
          throw new AuthorizationException("The authenticated user is not authorized to create config groups");
        }
      } else {
        if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
            RoleAuthorization.SERVICE_MANAGE_CONFIG_GROUPS)) {
          throw new AuthorizationException("The authenticated user is not authorized to create config groups");
        }
      }

      configLogger.info("(configchange) Creating new configuration group. cluster: '{}', changed by: '{}', config group: '{}', tag: '{}'",
          cluster.getClusterName(), getManagementController().getAuthName(), request.getGroupName(), request.getTag());

      verifyConfigs(request.getConfigs(), cluster.getClusterName());

      ConfigGroup configGroup = configGroupFactory.createNew(cluster, serviceName,
        request.getGroupName(),
        request.getTag(), request.getDescription(),
        request.getConfigs(), hosts);

      cluster.addConfigGroup(configGroup);
      if (serviceName != null) {
        cluster.createServiceConfigVersion(serviceName, getManagementController().getAuthName(),
          request.getServiceConfigVersionNote(), configGroup);
      } else {
        LOG.warn("Could not determine service name for config group {}, service config version not created",
            configGroup.getId());
      }

      ConfigGroupResponse response = new ConfigGroupResponse(configGroup
        .getId(), configGroup.getClusterName(), configGroup.getName(),
        configGroup.getTag(), configGroup.getDescription(), null, null);

      configGroupResponses.add(response);
      updatedClusters.add(cluster.getClusterName());
    }

    m_configHelper.get().updateAgentConfigs(updatedClusters);

    return configGroupResponses;
  }

  private synchronized void updateConfigGroups (Set<ConfigGroupRequest> requests) throws AmbariException, AuthorizationException {
    if (requests.isEmpty()) {
      LOG.warn("Received an empty requests set");
      return;
    }

    Clusters clusters = getManagementController().getClusters();

    Set<String> updatedClusters = new HashSet<>();
    for (ConfigGroupRequest request : requests) {

      Cluster cluster;
      try {
        cluster = clusters.getCluster(request.getClusterName());
      } catch (ClusterNotFoundException e) {
        throw new ParentObjectNotFoundException(
          "Attempted to add a config group to a cluster which doesn't exist", e);
      }

      if (request.getId() == null) {
        throw new AmbariException("Config group Id is a required parameter.");
      }

      validateRequest(request);

      // Find config group
      ConfigGroup configGroup = cluster.getConfigGroups().get(request.getId());
      if (configGroup == null) {
        throw new AmbariException("Config group not found"
                                 + ", clusterName = " + request.getClusterName()
                                 + ", groupId = " + request.getId());
      }

      String serviceName = configGroup.getServiceName();
      String requestServiceName = cluster.getServiceForConfigTypes(request.getConfigs().keySet());

      if (StringUtils.isEmpty(serviceName) && StringUtils.isEmpty(requestServiceName)) {
        if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
            RoleAuthorization.CLUSTER_MANAGE_CONFIG_GROUPS)) {
          throw new AuthorizationException("The authenticated user is not authorized to update config groups");
        }
      } else {
        if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
            RoleAuthorization.SERVICE_MANAGE_CONFIG_GROUPS)) {
          throw new AuthorizationException("The authenticated user is not authorized to update config groups");
        }
      }

      if (serviceName != null && requestServiceName != null && !StringUtils.equals(serviceName, requestServiceName)) {
        throw new IllegalArgumentException("Config group " + configGroup.getId() +
            " is mapped to service " + serviceName + ", " +
            "but request contain configs from service " + requestServiceName);
      } else if (serviceName == null && requestServiceName != null) {
        configGroup.setServiceName(requestServiceName);
        serviceName = requestServiceName;
      }

      int numHosts = (null != configGroup.getHosts()) ? configGroup.getHosts().size() : 0;
      configLogger.info("(configchange) Updating configuration group host membership or config value. cluster: '{}', changed by: '{}', " +
              "service_name: '{}', config group: '{}', tag: '{}', num hosts in config group: '{}', note: '{}'",
          cluster.getClusterName(), getManagementController().getAuthName(),
          serviceName, request.getGroupName(), request.getTag(), numHosts, request.getServiceConfigVersionNote());

      if (!request.getConfigs().isEmpty()) {
        List<String> affectedConfigTypeList = new ArrayList(request.getConfigs().keySet());
        Collections.sort(affectedConfigTypeList);
        String affectedConfigTypesString = "(" + StringUtils.join(affectedConfigTypeList, ", ") + ")";
        configLogger.info("(configchange)    Affected configs: {}", affectedConfigTypesString);

        for (Config config : request.getConfigs().values()) {
          List<String> sortedConfigKeys = new ArrayList(config.getProperties().keySet());
          Collections.sort(sortedConfigKeys);
          String sortedConfigKeysString = StringUtils.join(sortedConfigKeys, ", ");
          configLogger.info("(configchange)    Config type '{}' was  modified with the following keys, {}", config.getType(), sortedConfigKeysString);
        }
      }

      // Update hosts
      Map<Long, Host> hosts = new HashMap<>();
      if (request.getHosts() != null && !request.getHosts().isEmpty()) {
        for (String hostname : request.getHosts()) {
          Host host = clusters.getHost(hostname);
          HostEntity hostEntity = hostDAO.findById(host.getHostId());
          if (hostEntity == null) {
            throw new HostNotFoundException(hostname);
          }
          hosts.put(hostEntity.getHostId(), host);
        }
      }

      verifyHostList(cluster, hosts, request);

      configGroup.setHosts(hosts);

      // Update Configs
      verifyConfigs(request.getConfigs(), request.getClusterName());
      configGroup.setConfigurations(request.getConfigs());

      // Save
      configGroup.setName(request.getGroupName());
      configGroup.setDescription(request.getDescription());
      configGroup.setTag(request.getTag());

      if (serviceName != null) {
        cluster.createServiceConfigVersion(serviceName, getManagementController().getAuthName(),
                request.getServiceConfigVersionNote(), configGroup);

        ConfigGroupResponse configGroupResponse = new ConfigGroupResponse(configGroup.getId(), cluster.getClusterName(), configGroup.getName(),
                request.getTag(), "", new HashSet<>(), new HashSet<>());
        Set<Map<String, Object>> versionTags = new HashSet<>();
        Map<String, Object> tagsMap = new HashMap<>();
        for (Config config : configGroup.getConfigurations().values()) {
          tagsMap.put(config.getType(), config.getTag());
        }
        versionTags.add(tagsMap);
        configGroupResponse.setVersionTags(versionTags);
        getManagementController().saveConfigGroupUpdate(request, configGroupResponse);
        updatedClusters.add(cluster.getClusterName());
      } else {
        LOG.warn("Could not determine service name for config group {}, service config version not created",
            configGroup.getId());
      }
    }

