/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.master.meta.checkconf;

import alluxio.conf.PropertyKey;
import alluxio.conf.PropertyKey.ConsistencyCheckLevel;
import alluxio.grpc.ConfigStatus;
import alluxio.grpc.Scope;
import alluxio.wire.Address;
import alluxio.wire.ConfigCheckReport;
import alluxio.wire.InconsistentProperty;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * This class is responsible for checking server-side configuration.
 */
public class ConfigurationChecker {
  private static final Logger LOG = LoggerFactory.getLogger(ConfigurationChecker.class);
  private static final int LOG_CONF_SIZE = 5;
  private static final String CONSISTENT_CONFIGURATION_INFO
      = "All server-side configurations are consistent.";
  private static final String INCONSISTENT_CONFIGURATION_INFO
      = "Inconsistent configuration detected. "
      + "Only a limited set of inconsistent configuration will be shown here. "
      + "For details, please visit Alluxio web UI or "
      + "run fsadmin doctor CLI.";
  /** Contain all the master configuration information. */
  private final ConfigurationStore mMasterStore;
  /** Contain all the worker configuration information. */
  private final ConfigurationStore mWorkerStore;
  /** Contain the checker results. */
  private ConfigCheckReport mConfigCheckReport;
  /** Whether the configuration has been changed since the last time the report was generated. */
  private volatile boolean mConfigDirty = true;

  /**
   * Constructs a new {@link ConfigurationChecker}.
   *
   * @param masterStore master configuration store
   * @param workerStore worker configuration store
   */
  public ConfigurationChecker(ConfigurationStore masterStore,
      ConfigurationStore workerStore) {
    mMasterStore = masterStore;
    mWorkerStore = workerStore;
    mConfigCheckReport = new ConfigCheckReport();
    mMasterStore.registerChangeListener(() -> mConfigDirty = true);
    mWorkerStore.registerChangeListener(() -> mConfigDirty = true);
  }

  /**
   * Checks the server-side configurations and records the check results.
   */
  public synchronized void regenerateReport() {
    // Generate the configuration map from master and worker configuration records
    Map<PropertyKey, Map<Optional<String>, List<String>>> confMap = generateConfMap();

    // Update the errors and warnings configuration
    Map<Scope, List<InconsistentProperty>> confErrors = new HashMap<>();
    Map<Scope, List<InconsistentProperty>> confWarns = new HashMap<>();

    for (Map.Entry<PropertyKey, Map<Optional<String>, List<String>>> entry : confMap.entrySet()) {
      if (entry.getValue().size() >= 2) {
        PropertyKey key = entry.getKey();
        InconsistentProperty inconsistentProperty = new InconsistentProperty()
            .setName(key.getName()).setValues(entry.getValue());
        Scope scope = key.getScope().equals(Scope.ALL) ? Scope.SERVER : key.getScope();
        if (entry.getKey().getConsistencyLevel().equals(ConsistencyCheckLevel.ENFORCE)) {
          confErrors.putIfAbsent(scope, new ArrayList<>());
          confErrors.get(scope).add(inconsistentProperty);
        } else {
          confWarns.putIfAbsent(scope, new ArrayList<>());
          confWarns.get(scope).add(inconsistentProperty);
        }
      }
    }

    // Update configuration status
    ConfigStatus status = confErrors.values().stream().anyMatch(a -> a.size() > 0)
        ? ConfigStatus.FAILED : confWarns.values().stream().anyMatch(a -> a.size() > 0)
        ? ConfigStatus.WARN : ConfigStatus.PASSED;

    if (!status.equals(mConfigCheckReport.getConfigStatus())) {
      logConfigReport();
    }

    mConfigCheckReport = new ConfigCheckReport(confErrors, confWarns, status);
  }

  /**
   * @return the configuration checker report
   */
  public synchronized ConfigCheckReport getConfigCheckReport() {
    if (mConfigDirty) {
      // Reset mConfigDirty *before* calling regenerateReport so that if the configuration changes
      // during report regeneration, the change will be reflected in the next call to
      // getConfigCheckReport.
      mConfigDirty = false;
      LOG.debug("Regenerating config check report");
      regenerateReport();
    } else {
      LOG.debug("Skipping regenerating config check report");
    }
    return mConfigCheckReport;
  }

  /**
   * Logs the configuration check report information.
   */
  public synchronized void logConfigReport() {
    ConfigStatus reportStatus = mConfigCheckReport.getConfigStatus();
    if (reportStatus.equals(ConfigStatus.PASSED)) {
      LOG.debug(CONSISTENT_CONFIGURATION_INFO);
    } else if (reportStatus.equals(ConfigStatus.WARN)) {
      LOG.warn("{}\nWarnings: {}", INCONSISTENT_CONFIGURATION_INFO,
          mConfigCheckReport.getConfigWarns().values().stream()
              .map(Object::toString).limit(LOG_CONF_SIZE).collect(Collectors.joining(", ")));
    } else {
      LOG.error("{}\nErrors: {}\nWarnings: {}", INCONSISTENT_CONFIGURATION_INFO,
          mConfigCheckReport.getConfigErrors().values().stream()
              .map(Object::toString).limit(LOG_CONF_SIZE).collect(Collectors.joining(", ")),
          mConfigCheckReport.getConfigWarns().values().stream()
              .map(Object::toString).limit(LOG_CONF_SIZE).collect(Collectors.joining(", ")));
    }
  }

  /**
   * Generates the configuration map to find wrong configuration.
   * The map is of format Map&lt;PropertyKey, Map&lt;Value, List&lt;Host:Port&gt;&gt;&gt;.
   *
   * @return the generated configuration map
   */
  private Map<PropertyKey, Map<Optional<String>, List<String>>> generateConfMap() {
    Map<PropertyKey, Map<Optional<String>, List<String>>> confMap = new HashMap<>();
    fillConfMap(confMap, mMasterStore.getConfMap());
    fillConfMap(confMap, mWorkerStore.getConfMap());
    return confMap;
  }

  /**
   * Fills the configuration map.
   *
   * @param targetMap the map to fill
   * @param recordMap the map to get data from
   */
  private void fillConfMap(Map<PropertyKey, Map<Optional<String>, List<String>>> targetMap,
      Map<Address, List<ConfigRecord>> recordMap) {
    for (Map.Entry<Address, List<ConfigRecord>> record : recordMap.entrySet()) {
      Address address = record.getKey();
      String addressStr = String.format("%s:%s", address.getHost(), address.getRpcPort());
      for (ConfigRecord conf : record.getValue()) {
        PropertyKey key = conf.getKey();
        if (key.getConsistencyLevel() == ConsistencyCheckLevel.IGNORE) {
          continue;
        }
        Optional<String> value = conf.getValue();
        targetMap.putIfAbsent(key, new HashMap<>());
        Map<Optional<String>, List<String>> values = targetMap.get(key);
        values.putIfAbsent(value, new ArrayList<>());
        values.get(value).add(addressStr);
      }
    }
  }
}
