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

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.ClusterNotFoundException;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.checks.OrchestrationQualification;
import org.apache.ambari.server.checks.UpgradeTypeQualification;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
import org.apache.ambari.server.orm.entities.RepositoryVersionEntity;
import org.apache.ambari.server.state.repository.ClusterVersionSummary;
import org.apache.ambari.server.state.repository.VersionDefinitionXml;
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.UpgradeCheckRequest;
import org.apache.ambari.spi.upgrade.UpgradeCheckResult;
import org.apache.ambari.spi.upgrade.UpgradeCheckStatus;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

@Singleton
public class CheckHelper {
  /**
   * Log.
   */
  private static final Logger LOG = LoggerFactory.getLogger(CheckHelper.class);

  /**
   * Used for retrieving {@link RepositoryVersionEntity} instances.
   */
  @Inject
  protected Provider<RepositoryVersionDAO> repositoryVersionDaoProvider;

  /**
   * Used for retrieving {@link Cluster} instances.
   */
  @Inject
  protected Provider<Clusters> clustersProvider;

  @Inject
  protected Provider<AmbariMetaInfo> metaInfoProvider;


  /**
   * Gets any {@link UpgradeCheck}s which have passed all of their {@link CheckQualification}s
   * for the given upgrade request.
   *
   * @param request
   * @param upgradeChecks
   * @return
   */
  public List<UpgradeCheck> getApplicableChecks(UpgradeCheckRequest request,
      List<UpgradeCheck> upgradeChecks) {

    List<UpgradeCheck> applicableChecks = new LinkedList<>();
    for (UpgradeCheck check : upgradeChecks) {
      // build some required qualifications
      List<CheckQualification> qualifications = Lists.newArrayList(
          new ServiceQualification(check),
          new OrchestrationQualification(check.getClass()),
          new UpgradeTypeQualification(check.getClass()));

      // add any extras from the check
      List<CheckQualification> checkQualifications = check.getQualifications();
      if (CollectionUtils.isNotEmpty(checkQualifications)) {
        qualifications.addAll(checkQualifications);
      }

      // run the applicability check
      try {
        boolean checkIsApplicable = true;
        for (CheckQualification qualification : qualifications) {
          if (!qualification.isApplicable(request)) {
            checkIsApplicable = false;
            break;
          }
        }

        if (checkIsApplicable) {
          applicableChecks.add(check);
        }
      } catch (Exception ex) {
        LOG.error(
            "Unable to determine whether the pre-upgrade check {} is applicable to this upgrade",
            check.getCheckDescription().name(), ex);
      }
    }

    return applicableChecks;
  }

  /**
   * Executes all registered pre-requisite checks.
   *
   * @param request
   *          pre-requisite check request
   * @return list of pre-requisite check results
   */
  public List<UpgradeCheckResult> performChecks(UpgradeCheckRequest request,
                                               List<UpgradeCheck> upgradeChecks, Configuration config) {

    final String clusterName = request.getClusterName();
    final List<UpgradeCheckResult> results = new ArrayList<>();
    final boolean canBypassPreChecks = config.isUpgradePrecheckBypass();

    List<UpgradeCheck> applicablePreChecks = getApplicableChecks(request, upgradeChecks);

    for (UpgradeCheck check : applicablePreChecks) {
      UpgradeCheckResult result = new UpgradeCheckResult(check);
      try {
        result = check.perform(request);
      } catch (ClusterNotFoundException ex) {
        result.setFailReason("Cluster with name " + clusterName + " doesn't exists");
        result.setStatus(UpgradeCheckStatus.FAIL);
      } catch (Exception ex) {
        LOG.error("Check " + check.getCheckDescription().name() + " failed", ex);
        result.setFailReason("Unexpected server error happened");
        result.setStatus(UpgradeCheckStatus.FAIL);
      }

      if (result.getStatus() == UpgradeCheckStatus.FAIL && canBypassPreChecks) {
        LOG.error("Check {} failed but stack upgrade is allowed to bypass failures. Error to bypass: {}. Failed on: {}",
          check.getCheckDescription().name(),
          result.getFailReason(),
          StringUtils.join(result.getFailedOn(), ", "));
        result.setStatus(UpgradeCheckStatus.BYPASS);
      }

      results.add(result);
      request.addResult(check.getCheckDescription(), result.getStatus());
    }

    return results;
  }

  /**
   * Gets a de-serialized {@link VersionDefinitionXml} from the repository for
   * this upgrade.
   *
   * @param request
   *          the upgrade check request.
   * @return the VDF XML
   * @throws AmbariException
   */
  public final VersionDefinitionXml getVersionDefinitionXml(UpgradeCheckRequest request) throws AmbariException {
    RepositoryVersion repositoryVersion = request.getTargetRepositoryVersion();
    RepositoryVersionEntity entity = repositoryVersionDaoProvider.get().findByPK(
        repositoryVersion.getId());

    try {
      VersionDefinitionXml vdf = entity.getRepositoryXml();
      return vdf;
    } catch (Exception exception) {
      throw new AmbariException("Unable to run upgrade checks because of an invalid VDF",
          exception);
    }
  }

  /**
   * Gets the services participating in the upgrade from the VDF.
   *
   * @param request
   *          the upgrade check request.
   * @return the services participating in the upgrade, which can either be all
   *         of the cluster's services or a subset based on repository type.
   */
  public final Set<String> getServicesInUpgrade(UpgradeCheckRequest request) throws AmbariException {
    final Cluster cluster = clustersProvider.get().getCluster(request.getClusterName());

    // the check is scoped to some services, so determine if any of those
    // services are included in this upgrade
    try {
      VersionDefinitionXml vdf = getVersionDefinitionXml(request);
      ClusterVersionSummary clusterVersionSummary = vdf.getClusterSummary(cluster,
          metaInfoProvider.get());
      return clusterVersionSummary.getAvailableServiceNames();
    } catch (Exception exception) {
      throw new AmbariException("Unable to run upgrade checks because of an invalid VDF",
          exception);
    }
  }

  /**
   * The {@link ServiceQualification} class is used to determine if the
   * service(s) associated with an upgraade check are both installed in the
   * cluster and included in thr upgrade.
   * <p/>
   * If a service is installed but not included in the upgrade (for example of
   * the upgrade is a patch upgrade), then the check should not qualify to run.
   */
  final class ServiceQualification implements CheckQualification {

    /**
     *
     */
    private final UpgradeCheck m_upgradeCheck;

    /**
     * Constructor.
     *
     * @param upgradeCheck
     */
    public ServiceQualification(UpgradeCheck upgradeCheck) {
      m_upgradeCheck = upgradeCheck;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isApplicable(UpgradeCheckRequest request) throws AmbariException {

      Set<String> applicableServices = m_upgradeCheck.getApplicableServices();

      // if the check is not scoped to any particular service, then it passes
      // this qualification
      if (applicableServices.isEmpty()) {
        return true;
      }

      Set<String> servicesForUpgrade = getServicesInUpgrade(request);

      for (String serviceInUpgrade : servicesForUpgrade) {
        if (applicableServices.contains(serviceInUpgrade)) {
          return true;
        }
      }

      return false;
    }
  }
}
