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

import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.orm.dao.HostVersionDAO;
import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
import org.apache.ambari.server.orm.dao.UpgradeDAO;
import org.apache.ambari.server.stack.upgrade.RepositoryVersionHelper;
import org.apache.ambari.server.state.CheckHelper;
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.DesiredConfig;
import org.apache.ambari.server.state.ServiceInfo;
import org.apache.ambari.spi.RepositoryVersion;
import org.apache.ambari.spi.upgrade.CheckQualification;
import org.apache.ambari.spi.upgrade.UpgradeCheck;
import org.apache.ambari.spi.upgrade.UpgradeCheckDescription;
import org.apache.ambari.spi.upgrade.UpgradeCheckRequest;
import org.apache.ambari.spi.upgrade.UpgradeCheckResult;
import org.apache.ambari.spi.upgrade.UpgradeCheckType;
import org.apache.commons.lang.StringUtils;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Provider;

/**
 * A base implementation of {@link UpgradeCheck} which offers injected helpers and access
 * to information about repositories and the cluster.
 */
public abstract class ClusterCheck implements UpgradeCheck {

  @Inject
  protected Provider<Clusters> clustersProvider;

  @Inject
  Provider<HostVersionDAO> hostVersionDaoProvider;

  @Inject
  Provider<RepositoryVersionDAO> repositoryVersionDaoProvider;

  @Inject
  Provider<UpgradeDAO> upgradeDaoProvider;

  @Inject
  Provider<RepositoryVersionHelper> repositoryVersionHelper;

  @Inject
  Provider<AmbariMetaInfo> ambariMetaInfo;

  @Inject
  Configuration config;

  @Inject
  Gson gson;

  @Inject
  Provider<CheckHelper> checkHelperProvider;

  private UpgradeCheckDescription m_description;

  /**
   * Constructor.
   *
   * @param description description
   */
  protected ClusterCheck(UpgradeCheckDescription description) {
    m_description = description;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public UpgradeCheckDescription getCheckDescription() {
    return m_description;
  }

  /**
   * Gets the set of services that this check is associated with. If the check
   * is not associated with a particular service, then this should be an empty
   * set.
   *
   * @return a set of services which will determine whether this check is
   *         applicable.
   */
  @Override
  public Set<String> getApplicableServices() {
    return Collections.emptySet();
  }

  /**
   * Gets any additional qualifications which an upgrade check should run in
   * order to determine if it's applicable to the upgrade.
   *
   * @return a list of qualifications, or an empty list.
   */
  @Override
  public List<CheckQualification> getQualifications() {
    return Collections.emptyList();
  }

  /**
   * Gets the description of the check.
   *
   * @return the description (not {@code null}).
   */
  public UpgradeCheckDescription getDescription() {
    return m_description;
  }

  /**
   * Gets the default fail reason
   * @param upgradeCheckResult the check being performed
   * @param request           the request
   * @return the failure string
   */
  protected String getFailReason(UpgradeCheckResult upgradeCheckResult, UpgradeCheckRequest request)
      throws AmbariException {
    return getFailReason(UpgradeCheckDescription.DEFAULT, upgradeCheckResult, request);
  }

  /**
   * Gets a cluster configuration property if it exists, or {@code null}
   * otherwise.
   *
   * @param request
   *          the request (not {@code null}).
   * @param configType
   *          the configuration type, such as {@code hdfs-site} (not
   *          {@code null}).
   * @param propertyName
   *          the name of the property (not {@code null}).
   * @return the property value or {@code null} if not found.
   * @throws AmbariException
   */
  protected String getProperty(UpgradeCheckRequest request, String configType, String propertyName)
      throws AmbariException {
    final String clusterName = request.getClusterName();
    final Cluster cluster = clustersProvider.get().getCluster(clusterName);
    final Map<String, DesiredConfig> desiredConfigs = cluster.getDesiredConfigs();
    final DesiredConfig desiredConfig = desiredConfigs.get(configType);

    if (null == desiredConfig) {
      return null;
    }

    final Config config = cluster.getConfig(configType, desiredConfig.getTag());

    Map<String, String> properties = config.getProperties();
    return properties.get(propertyName);
  }

  /**
   * Gets the fail reason
   * @param key               the failure text key
   * @param UpgradeCheck the check being performed
   * @param request           the request
   * @return the failure string
   */
  protected String getFailReason(String key, UpgradeCheckResult upgradeCheckResult,
      UpgradeCheckRequest request) throws AmbariException {
    String fail = m_description.getFailureReason(key);

    RepositoryVersion repositoryVersion = request.getTargetRepositoryVersion();
    if (fail.contains("{{version}}") && null != repositoryVersion) {
      fail = fail.replace("{{version}}", repositoryVersion.getVersion());
    }

    if (fail.contains("{{fails}}")) {
      LinkedHashSet<String> names = upgradeCheckResult.getFailedOn();

      // If Type=PrereqCheckType.HOST, names list is already populated
      if (getDescription().getType() == UpgradeCheckType.SERVICE) {
        Clusters clusters = clustersProvider.get();
        AmbariMetaInfo metaInfo = ambariMetaInfo.get();

        Cluster c = clusters.getCluster(request.getClusterName());
        Map<String, ServiceInfo> services = metaInfo.getServices(
            c.getDesiredStackVersion().getStackName(),
            c.getDesiredStackVersion().getStackVersion());

        LinkedHashSet<String> displays = new LinkedHashSet<>();
        for (String name : names) {
          if (services.containsKey(name)) {
            displays.add(services.get(name).getDisplayName());
          } else {
            displays.add(name);
          }
        }
        names = displays;

      }

      fail = fail.replace("{{fails}}", formatEntityList(names));
    }

    return fail;
  }

  /**
   * Formats lists of given entities to human readable form:
   * [entity1] -> {entity1} {noun}
   * [entity1, entity2] -> {entity1} and {entity2} {noun}s
   * [entity1, entity2, entity3] -> {entity1}, {entity2} and {entity3} {noun}s
   * The noun for the entities is taken from check type, it may be cluster, service or host.
   *
   * @param entities list of entities to format
   * @return formatted entity list
   */
  protected String formatEntityList(LinkedHashSet<String> entities) {
    if (entities == null || entities.isEmpty()) {
      return "";
    }

    final StringBuilder formatted = new StringBuilder(StringUtils.join(entities, ", "));
    if (entities.size() > 1) {
      formatted.replace(formatted.lastIndexOf(","), formatted.lastIndexOf(",") + 1, " and");
    }

    return formatted.toString();
  }

  /**
   * Used to represent information about a service. This class is safe to use in
   * sorted & unique collections.
   */
  static class ServiceDetail implements Comparable<ServiceDetail> {
    @JsonProperty("service_name")
    final String serviceName;

    ServiceDetail(String serviceName) {
      this.serviceName = serviceName;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
      return Objects.hash(serviceName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }

      if (obj == null) {
        return false;
      }

      if (getClass() != obj.getClass()) {
        return false;
      }

      ServiceDetail other = (ServiceDetail) obj;
      return Objects.equals(serviceName, other.serviceName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int compareTo(ServiceDetail other) {
      return serviceName.compareTo(other.serviceName);
    }
  }

  /**
   * Used to represent information about a service component. This class is safe
   * to use in sorted & unique collections.
   */
  static class ServiceComponentDetail implements Comparable<ServiceComponentDetail> {
    @JsonProperty("service_name")
    final String serviceName;

    @JsonProperty("component_name")
    final String componentName;

    ServiceComponentDetail(String serviceName, String componentName) {
      this.serviceName = serviceName;
      this.componentName = componentName;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
      return Objects.hash(serviceName, componentName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }

      if (obj == null) {
        return false;
      }

      if (getClass() != obj.getClass()) {
        return false;
      }

      ServiceComponentDetail other = (ServiceComponentDetail) obj;
      return Objects.equals(serviceName, other.serviceName)
          && Objects.equals(componentName, other.componentName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int compareTo(ServiceComponentDetail other) {
      return Comparator.comparing(
          (ServiceComponentDetail detail) -> detail.serviceName).thenComparing(
              detail -> detail.componentName).compare(this, other);
    }
  }

  /**
   * Used to represent information about a host. This class is safe to use in
   * sorted & unique collections.
   */
  static class HostDetail implements Comparable<HostDetail> {
    @JsonProperty("host_id")
    final Long hostId;

    @JsonProperty("host_name")
    final String hostName;

    HostDetail(Long hostId, String hostName) {
      this.hostId = hostId;
      this.hostName = hostName;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
      return Objects.hash(hostId, hostName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }

      if (obj == null) {
        return false;
      }

      if (getClass() != obj.getClass()) {
        return false;
      }

      HostDetail other = (HostDetail) obj;
      return Objects.equals(hostId, other.hostId) && Objects.equals(hostName, other.hostName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int compareTo(HostDetail other) {
      return hostName.compareTo(other.hostName);
    }
  }
}
