/**
 * 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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.ambari.server.api.predicate.InvalidQueryException;
import org.apache.ambari.server.security.encryption.CredentialStoreType;
import org.apache.ambari.server.stack.NoSuchStackException;
import org.apache.ambari.server.topology.ConfigRecommendationStrategy;
import org.apache.ambari.server.topology.Configuration;
import org.apache.ambari.server.topology.ConfigurationFactory;
import org.apache.ambari.server.topology.Credential;
import org.apache.ambari.server.topology.HostGroupInfo;
import org.apache.ambari.server.topology.InvalidTopologyTemplateException;
import org.apache.ambari.server.topology.NoSuchBlueprintException;
import org.apache.ambari.server.topology.RequiredPasswordValidator;
import org.apache.ambari.server.topology.SecurityConfiguration;
import org.apache.ambari.server.topology.TopologyValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Enums;
import com.google.common.base.Optional;
import com.google.common.base.Strings;

/**
 * Request for provisioning a cluster.
 */
@SuppressWarnings("unchecked")
public class ProvisionClusterRequest extends BaseClusterRequest {
  /**
   * host groups property name
   */
  public static final String HOSTGROUPS_PROPERTY = "host_groups";

  /**
   * host group name property name
   */
  public static final String HOSTGROUP_NAME_PROPERTY = "name";

  /**
   * host group host count property name
   */
  public static final String HOSTGROUP_HOST_COUNT_PROPERTY = "host_count";

  /**
   * host group host predicate property name
   */
  public static final String HOSTGROUP_HOST_PREDICATE_PROPERTY = "host_predicate";

  /**
   * host group host fqdn property name
   */
  public static final String HOSTGROUP_HOST_FQDN_PROPERTY = "fqdn";


  /**
   * rack info property name
   */
  public static final String HOSTGROUP_HOST_RACK_INFO_PROPERTY = "rack_info";

  /**
   * host group hosts property name
   */
  public static final String HOSTGROUP_HOSTS_PROPERTY = "hosts";

  /**
   * configurations property name
   */
  public static final String CONFIGURATIONS_PROPERTY = "configurations";

  /**
   * default password property name
   */
  public static final String DEFAULT_PASSWORD_PROPERTY = "default_password";

  /**
   * configuration recommendation strategy property name
   */
  public static final String CONFIG_RECOMMENDATION_STRATEGY = "config_recommendation_strategy";

  /**
   * Support for controlling whether Install and Start tasks are created on
   * blueprint deploy by default.
   */
  public static final String PROVISION_ACTION_PROPERTY = "provision_action";

  /**
   * The repo version to use
   */
  public static final String REPO_VERSION_PROPERTY = "repository_version";


  /**
   * configuration factory
   */
  private static ConfigurationFactory configurationFactory = new ConfigurationFactory();

  /**
   * cluster name
   */
  private String clusterName;

  /**
   * default password
   */
  private String defaultPassword;

  private Map<String, Credential> credentialsMap;

  /**
   * configuration recommendation strategy
   */
  private final ConfigRecommendationStrategy configRecommendationStrategy;

  private final ProvisionAction provisionAction;

  private String repoVersion;

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

  /**
   * Constructor.
   *
   * @param properties  request properties
   * @param securityConfiguration  security config related properties
   */
  public ProvisionClusterRequest(Map<String, Object> properties, SecurityConfiguration securityConfiguration) throws
    InvalidTopologyTemplateException {
    setClusterName(String.valueOf(properties.get(
      ClusterResourceProvider.CLUSTER_NAME_PROPERTY_ID)));

    if (properties.containsKey(REPO_VERSION_PROPERTY)) {
      repoVersion = properties.get(REPO_VERSION_PROPERTY).toString();
    }

    if (properties.containsKey(DEFAULT_PASSWORD_PROPERTY)) {
      defaultPassword = String.valueOf(properties.get(DEFAULT_PASSWORD_PROPERTY));
    }

    try {
      parseBlueprint(properties);
    } catch (NoSuchStackException e) {
      throw new InvalidTopologyTemplateException("The specified stack doesn't exist: " + e, e);
    } catch (NoSuchBlueprintException e) {
      throw new InvalidTopologyTemplateException("The specified blueprint doesn't exist: " + e, e);
    }

    this.securityConfiguration = securityConfiguration;

    Configuration configuration = configurationFactory.getConfiguration(
        (Collection<Map<String, String>>) properties.get(CONFIGURATIONS_PROPERTY));
    configuration.setParentConfiguration(blueprint.getConfiguration());
    setConfiguration(configuration);

    parseHostGroupInfo(properties);

    this.credentialsMap = parseCredentials(properties);

    this.configRecommendationStrategy = parseConfigRecommendationStrategy(properties);

    this.provisionAction = parseProvisionAction(properties);
  }

  private Map<String, Credential> parseCredentials(Map<String, Object> properties) throws
    InvalidTopologyTemplateException {
    HashMap<String, Credential> credentialHashMap = new HashMap<>();
    Set<Map<String, String>> credentialsSet = (Set<Map<String, String>>) properties.get(ClusterResourceProvider.CREDENTIALS_PROPERTY_ID);
    if (credentialsSet != null) {
      for (Map<String, String> credentialMap : credentialsSet) {
        String alias = Strings.emptyToNull(credentialMap.get("alias"));
        if (alias == null) {
          throw new InvalidTopologyTemplateException("credential.alias property is missing.");
        }
        String principal = Strings.emptyToNull(credentialMap.get("principal"));
        if (principal == null) {
          throw new InvalidTopologyTemplateException("credential.principal property is missing.");
        }
        String key = Strings.emptyToNull(credentialMap.get("key"));
        if (key == null) {
          throw new InvalidTopologyTemplateException("credential.key is missing.");
        }
        String typeString = Strings.emptyToNull(credentialMap.get("type"));
        if (typeString == null) {
          throw new InvalidTopologyTemplateException("credential.type is missing.");
        }
        CredentialStoreType type = Enums.getIfPresent(CredentialStoreType.class, typeString.toUpperCase()).orNull();
        if (type == null) {
          throw new InvalidTopologyTemplateException("credential.type is invalid.");
        }
        credentialHashMap.put(alias, new Credential(alias, principal, key, type));
      }
    }
    return credentialHashMap;
  }

  public Map<String, Credential> getCredentialsMap() {
    return credentialsMap;
  }

  public String getClusterName() {
    return clusterName;
  }

  public void setClusterName(String clusterName) {
    this.clusterName = clusterName;
  }

  public ConfigRecommendationStrategy getConfigRecommendationStrategy() {
    return configRecommendationStrategy;
  }

  @Override
  public Long getClusterId() {
    return clusterId;
  }

  public void setClusterId(Long clusterId) {
    this.clusterId = clusterId;
  }

  @Override
  public Type getType() {
    return Type.PROVISION;
  }

  @Override
  public List<TopologyValidator> getTopologyValidators() {
    return Collections.<TopologyValidator>singletonList(new RequiredPasswordValidator(defaultPassword));
  }

  @Override
  public String getDescription() {
    return String.format("Provision Cluster '%s'", clusterName);
  }

  /**
   * Parse blueprint.
   *
   * @param properties  request properties
   *
   * @throws NoSuchStackException     if specified stack doesn't exist
   * @throws NoSuchBlueprintException if specified blueprint doesn't exist
   */
  private void parseBlueprint(Map<String, Object> properties) throws NoSuchStackException, NoSuchBlueprintException {
    String blueprintName = String.valueOf(properties.get(ClusterResourceProvider.BLUEPRINT_PROPERTY_ID));
    // set blueprint field
    setBlueprint(getBlueprintFactory().getBlueprint(blueprintName));

    if (blueprint == null) {
      throw new NoSuchBlueprintException(blueprintName);
    }
  }

  /**
   * Parse host group information.
   *
   * @param properties  request properties
   *
   * @throws InvalidTopologyTemplateException  if any validation checks on properties fail
   */
  private void parseHostGroupInfo(Map<String, Object> properties) throws InvalidTopologyTemplateException {
    Collection<Map<String, Object>> hostGroups =
        (Collection<Map<String, Object>>) properties.get(HOSTGROUPS_PROPERTY);

    if (hostGroups == null || hostGroups.isEmpty()) {
      throw new InvalidTopologyTemplateException("'host_groups' element must be included in cluster create body");
    }

    for (Map<String, Object> hostGroupProperties : hostGroups) {
      processHostGroup(hostGroupProperties);
    }
  }

  /**
   * Process host group properties.
   *
   * @param hostGroupProperties  host group properties
   *
   * @throws InvalidTopologyTemplateException if any validation checks on properties fail
   */
  private void processHostGroup(Map<String, Object> hostGroupProperties) throws InvalidTopologyTemplateException {
    String name = String.valueOf(hostGroupProperties.get(HOSTGROUP_NAME_PROPERTY));
    // String.valueOf() converts null to "null"
    if (name == null || name.equals("null") || name.isEmpty()) {
      throw new InvalidTopologyTemplateException("All host groups must contain a 'name' element");
    }

    HostGroupInfo hostGroupInfo = new HostGroupInfo(name);
    getHostGroupInfo().put(name, hostGroupInfo);

    processHostCountAndPredicate(hostGroupProperties, hostGroupInfo);
    processGroupHosts(name, (Collection<Map<String, String>>)
        hostGroupProperties.get(HOSTGROUP_HOSTS_PROPERTY), hostGroupInfo);

    // don't set the parent configuration
    hostGroupInfo.setConfiguration(configurationFactory.getConfiguration(
        (Collection<Map<String, String>>) hostGroupProperties.get(CONFIGURATIONS_PROPERTY)));
  }

  /**
   * Process host count and host predicate for a host group.
   *
   * @param hostGroupProperties  host group properties
   * @param hostGroupInfo        associated host group info instance
   *
   * @throws InvalidTopologyTemplateException  specified host group properties fail validation
   */
  private void processHostCountAndPredicate(Map<String, Object> hostGroupProperties, HostGroupInfo hostGroupInfo)
      throws InvalidTopologyTemplateException {

    if (hostGroupProperties.containsKey(HOSTGROUP_HOST_COUNT_PROPERTY)) {
      hostGroupInfo.setRequestedCount(Integer.valueOf(String.valueOf(
          hostGroupProperties.get(HOSTGROUP_HOST_COUNT_PROPERTY))));
      LOG.info("Stored expected hosts count {} for group {}",
               hostGroupInfo.getRequestedHostCount(), hostGroupInfo.getHostGroupName());
    }

    if (hostGroupProperties.containsKey(HOSTGROUP_HOST_PREDICATE_PROPERTY)) {
      if (hostGroupInfo.getRequestedHostCount() == 0) {
        throw new InvalidTopologyTemplateException(String.format(
            "Host group '%s' must not specify 'host_predicate' without 'host_count'",
            hostGroupInfo.getHostGroupName()));
      }

      String hostPredicate = String.valueOf(hostGroupProperties.get(HOSTGROUP_HOST_PREDICATE_PROPERTY));
      validateHostPredicateProperties(hostPredicate);
      try {
        hostGroupInfo.setPredicate(hostPredicate);
        LOG.info("Compiled host predicate {} for group {}", hostPredicate, hostGroupInfo.getHostGroupName());
      } catch (InvalidQueryException e) {
        throw new InvalidTopologyTemplateException(
            String.format("Unable to compile host predicate '%s': %s", hostPredicate, e), e);
      }
    }
  }

  /**
   * Process host group hosts.
   *
   * @param name           host group name
   * @param hosts          collection of host group host properties
   * @param hostGroupInfo  associated host group info instance
   *
   * @throws InvalidTopologyTemplateException specified host group properties fail validation
   */
  private void processGroupHosts(String name, Collection<Map<String, String>> hosts, HostGroupInfo hostGroupInfo)
      throws InvalidTopologyTemplateException {

    if (hosts != null) {
      if (hostGroupInfo.getRequestedHostCount() != 0) {
        throw new InvalidTopologyTemplateException(String.format(
            "Host group '%s' must not contain both a 'hosts' element and a 'host_count' value", name));
      }

      if (hostGroupInfo.getPredicate() != null) {
        throw new InvalidTopologyTemplateException(String.format(
            "Host group '%s' must not contain both a 'hosts' element and a 'host_predicate' value", name));
      }

      for (Map<String, String> hostProperties : hosts) {
        if (hostProperties.containsKey(HOSTGROUP_HOST_FQDN_PROPERTY)) {
          hostGroupInfo.addHost(hostProperties.get(HOSTGROUP_HOST_FQDN_PROPERTY));
        }

        if (hostProperties.containsKey(HOSTGROUP_HOST_RACK_INFO_PROPERTY)) {
          hostGroupInfo.addHostRackInfo(
              hostProperties.get(HOSTGROUP_HOST_FQDN_PROPERTY),
              hostProperties.get(HOSTGROUP_HOST_RACK_INFO_PROPERTY));
        }
      }
    }

    if (hostGroupInfo.getRequestedHostCount() == 0) {
      throw new InvalidTopologyTemplateException(String.format(
          "Host group '%s' must contain at least one 'hosts/fqdn' or a 'host_count' value", name));
    }
  }

  /**
   * Parse config recommendation strategy. Throws exception in case of the value is not correct.
   * The default value is {@link ConfigRecommendationStrategy#NEVER_APPLY}
   * @param properties request properties
   * @throws InvalidTopologyTemplateException specified config recommendation strategy property fail validation
   */
  private ConfigRecommendationStrategy parseConfigRecommendationStrategy(Map<String, Object> properties)
    throws InvalidTopologyTemplateException {
    if (properties.containsKey(CONFIG_RECOMMENDATION_STRATEGY)) {
      String configRecommendationStrategy = String.valueOf(properties.get(CONFIG_RECOMMENDATION_STRATEGY));
      Optional<ConfigRecommendationStrategy> configRecommendationStrategyOpt =
        Enums.getIfPresent(ConfigRecommendationStrategy.class, configRecommendationStrategy);
      if (!configRecommendationStrategyOpt.isPresent()) {
        throw new InvalidTopologyTemplateException(String.format(
          "Config recommendation strategy is not supported: %s", configRecommendationStrategy));
      }
      return configRecommendationStrategyOpt.get();
    } else {
      // default
      return ConfigRecommendationStrategy.NEVER_APPLY;
    }
  }

  /**
   * Parse Provision Action specified in RequestInfo properties.
   */
  private ProvisionAction parseProvisionAction(Map<String, Object> properties) throws InvalidTopologyTemplateException {
    if (properties.containsKey(PROVISION_ACTION_PROPERTY)) {
      String provisionActionStr = String.valueOf(properties.get(PROVISION_ACTION_PROPERTY));
      Optional<ProvisionAction> provisionActionOptional =
        Enums.getIfPresent(ProvisionAction.class, provisionActionStr);

      if (!provisionActionOptional.isPresent()) {
        throw new InvalidTopologyTemplateException(String.format(
          "Invalid provision_action specified in the template: %s", provisionActionStr));
      }
      return provisionActionOptional.get();
    } else {
      return ProvisionAction.INSTALL_AND_START;
    }
  }

  /**
   * Get requested @ProvisionClusterRequest.ProvisionAction
   */
  public ProvisionAction getProvisionAction() {
    return provisionAction;
  }

  /**
   * @return the repository version, if any
   */
  public String getRepositoryVersion() {
    return repoVersion;
  }

}
