/*
 * 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.io.File;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.apache.ambari.server.controller.StackVersionResponse;
import org.apache.ambari.server.stack.Validable;
import org.apache.ambari.server.stack.upgrade.ConfigUpgradePack;
import org.apache.ambari.server.stack.upgrade.UpgradePack;
import org.apache.ambari.server.state.repository.DefaultStackVersion;
import org.apache.ambari.server.state.repository.VersionDefinitionXml;
import org.apache.ambari.server.state.stack.LatestRepoCallable;
import org.apache.ambari.server.state.stack.RepositoryXml;
import org.apache.ambari.server.state.stack.StackRoleCommandOrder;
import org.apache.ambari.server.utils.VersionUtils;
import org.apache.ambari.spi.stack.StackReleaseVersion;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.io.Files;
import com.google.inject.Injector;

public class StackInfo implements Comparable<StackInfo>, Validable {
  private static final Logger LOG = LoggerFactory.getLogger(StackInfo.class);

  private String minJdk;
  private String maxJdk;
  private String name;
  private String version;
  private String minUpgradeVersion;
  private boolean active;
  private String rcoFileLocation;
  private String kerberosDescriptorPreConfigurationFileLocation;
  private List<RepositoryInfo> repositories;
  private Collection<ServiceInfo> services;
  private Collection<ExtensionInfo> extensions;
  private String parentStackVersion;
  // stack-level properties
  private List<PropertyInfo> properties;
  private Map<String, Map<String, Map<String, String>>> configTypes;
  private Map<String, UpgradePack> upgradePacks;
  private ConfigUpgradePack configUpgradePack;
  private StackRoleCommandOrder roleCommandOrder;
  private boolean valid = true;
  private Map<String, Map<PropertyInfo.PropertyType, Set<String>>> propertiesTypesCache =
      Collections.synchronizedMap(new HashMap<String, Map<PropertyInfo.PropertyType, Set<String>>>());
  private Map<String, Map<String, Map<String, String>>> configPropertyAttributes =  null;
  private String upgradesFolder = null;
  private volatile Map<String, PropertyInfo> requiredProperties;
  private Map<String, VersionDefinitionXml> versionDefinitions = new ConcurrentHashMap<>();
  private Set<String> errorSet = new HashSet<>();
  private RepositoryXml repoXml = null;

  private VersionDefinitionXml latestVersion = null;

  private String releaseVersionClass = null;

  /**
   * A {@link ClassLoader} for any JARs discovered in the stack's library
   * folder.
   */
  private URLClassLoader libraryClassLoader = null;

  /**
   * List of services removed from current stack
   * */
  private List<String> removedServices = new ArrayList<>();

  /**
  * List of services withnot configurations
  * */
  private List<String> servicesWithNoConfigs = new ArrayList<>();

  private RefreshCommandConfiguration refreshCommandConfiguration = new RefreshCommandConfiguration();

  public String getMinJdk() {
    return minJdk;
  }

  public void setMinJdk(String minJdk) {
    this.minJdk = minJdk;
  }

  public String getMaxJdk() {
    return maxJdk;
  }

  public void setMaxJdk(String maxJdk) {
    this.maxJdk = maxJdk;
  }

  public void setReleaseVersionClass(String className) {
    releaseVersionClass = className;
  }

  /**
   *
   * @return valid xml flag
   */
  @Override
  public boolean isValid() {
    return valid;
  }

  /**
   *
   * @param valid set validity flag
   */
  @Override
  public void setValid(boolean valid) {
    this.valid = valid;
  }

  @Override
  public void addError(String error) {
    errorSet.add(error);
  }

  @Override
  public Collection<String> getErrors() {
    return errorSet;
  }

  @Override
  public void addErrors(Collection<String> errors) {
    errorSet.addAll(errors);
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getVersion() {
    return version;
  }

  public void setVersion(String version) {
    this.version = version;
  }

  public List<RepositoryInfo> getRepositories() {
    if( repositories == null ) {
      repositories = new ArrayList<>();
    }
    return repositories;
  }

  /**
   * @return A list containing all repos for this stack, grouped by os
   */
  public ListMultimap<String, RepositoryInfo> getRepositoriesByOs() {
    return Multimaps.index(getRepositories(), RepositoryInfo.GET_OSTYPE_FUNCTION);
  }

  public synchronized Collection<ServiceInfo> getServices() {
    if (services == null) {
      services = new ArrayList<>();
    }
    return services;
  }

  public ServiceInfo getService(String name) {
    Collection<ServiceInfo> services = getServices();
    for (ServiceInfo service : services) {
      if (service.getName().equals(name)) {
        return service;
      }
    }
    //todo: exception?
    return null;
  }

  public synchronized void setServices(Collection<ServiceInfo> services) {
    this.services = services;
  }

  public synchronized Collection<ExtensionInfo> getExtensions() {
    if (extensions == null) {
      extensions = new ArrayList<>();
    }
    return extensions;
  }

  public ExtensionInfo getExtension(String name) {
    Collection<ExtensionInfo> extensions = getExtensions();
    for (ExtensionInfo extension : extensions) {
      if (extension.getName().equals(name)) {
        return extension;
      }
    }
    //todo: exception?
    return null;
  }

  public ExtensionInfo getExtensionByService(String serviceName) {
    Collection<ExtensionInfo> extensions = getExtensions();
    for (ExtensionInfo extension : extensions) {
      Collection<ServiceInfo> services = extension.getServices();
      for (ServiceInfo service : services) {
        if (service.getName().equals(serviceName)) {
          return extension;
        }
      }
    }
    //todo: exception?
    return null;
  }

  public void addExtension(ExtensionInfo extension) {
    Collection<ExtensionInfo> extensions = getExtensions();
    extensions.add(extension);
    Collection<ServiceInfo> services = getServices();
    services.addAll(extension.getServices());
  }

  public void removeExtension(ExtensionInfo extension) {
    Collection<ExtensionInfo> extensions = getExtensions();
    extensions.remove(extension);
    Collection<ServiceInfo> services = getServices();
    for (ServiceInfo service : extension.getServices()) {
      services.remove(service);
    }
  }

  public List<PropertyInfo> getProperties() {
    if (properties == null) {
      properties = new ArrayList<>();
    }
    return properties;
  }

  public void setProperties(List<PropertyInfo> properties) {
    this.properties = properties;
  }

  /**
   * Obtain the config types associated with this stack.
   * The returned map is an unmodifiable view.
   * @return copy of the map of config types associated with this stack
   */
  public synchronized Map<String, Map<String, Map<String, String>>> getConfigTypeAttributes() {
    return configTypes == null ?
        Collections.emptyMap() :
        Collections.unmodifiableMap(configTypes);
  }


  /**
   * Add the given type and set it's attributes.
   *
   * @param type            configuration type
   * @param typeAttributes  attributes associated with the type
   */
  public synchronized void setConfigTypeAttributes(String type, Map<String, Map<String, String>> typeAttributes) {
    if (configTypes == null) {
      configTypes = new HashMap<>();
    }
    // todo: no exclusion mechanism for stack config types
    configTypes.put(type, typeAttributes);
  }

  /**
   * Set all types and associated attributes.  Any previously existing types and
   * attributes are removed prior to setting the new values.
   *
   * @param types map of type attributes
   */
  public synchronized void setAllConfigAttributes(Map<String, Map<String, Map<String, String>>> types) {
    configTypes = new HashMap<>();
    for (Map.Entry<String, Map<String, Map<String, String>>> entry : types.entrySet()) {
      setConfigTypeAttributes(entry.getKey(), entry.getValue());
    }
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder("Stack name:" + name + "\nversion:" +
      version + "\nactive:" + active + " \nvalid:" + isValid());
    if (services != null) {
      sb.append("\n\t\tService:");
      for (ServiceInfo service : services) {
        sb.append("\t\t");
        sb.append(service);
      }
    }

    if (repositories != null) {
      sb.append("\n\t\tRepositories:");
      for (RepositoryInfo repository : repositories) {
        sb.append("\t\t");
        sb.append(repository);
      }
    }

    return sb.toString();
  }


  @Override
  public int hashCode() {
    return 31  + name.hashCode() + version.hashCode();
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof StackInfo)) {
      return false;
    }
    if (this == obj) {
      return true;
    }
    StackInfo stackInfo = (StackInfo) obj;
    return getName().equals(stackInfo.getName()) && getVersion().equals(stackInfo.getVersion());
  }

  public StackVersionResponse convertToResponse() {

    // Collect the services' Kerberos descriptor files
    Collection<ServiceInfo> serviceInfos = getServices();
    // The collection of service descriptor files. A Set is being used because some Kerberos descriptor
    // files contain multiple services, therefore the same File may be encountered more than once.
    // For example the YARN directory may contain YARN and MAPREDUCE2 services.
    Collection<File> serviceDescriptorFiles = new HashSet<>();
    if (serviceInfos != null) {
      for (ServiceInfo serviceInfo : serviceInfos) {
        File file = serviceInfo.getKerberosDescriptorFile();
        if (file != null) {
          serviceDescriptorFiles.add(file);
        }
      }
    }

    return new StackVersionResponse(getVersion(),
        isActive(), getParentStackVersion(), getConfigTypeAttributes(),
        serviceDescriptorFiles,
        null == upgradePacks ? Collections.emptySet() : upgradePacks.keySet(),
        isValid(), getErrors(), getMinJdk(), getMaxJdk());
  }

  public boolean isActive() {
    return active;
  }

  public void setActive(boolean active) {
    this.active = active;
  }

  public String getParentStackVersion() {
    return parentStackVersion;
  }

  public void setParentStackVersion(String parentStackVersion) {
    this.parentStackVersion = parentStackVersion;
  }

  public StackRoleCommandOrder getRoleCommandOrder() {
    return roleCommandOrder;
  }

  public void setRoleCommandOrder(StackRoleCommandOrder roleCommandOrder) {
    this.roleCommandOrder = roleCommandOrder;
  }

  public String getRcoFileLocation() {
    return rcoFileLocation;
  }

  public void setRcoFileLocation(String rcoFileLocation) {
    this.rcoFileLocation = rcoFileLocation;
  }

  /**
   * Gets the path to the stack-level Kerberos descriptor pre-configuration file
   *
   * @return a String containing the path to the stack-level Kerberos descriptor pre-configuration file
   */
  public String getKerberosDescriptorPreConfigurationFileLocation() {
    return kerberosDescriptorPreConfigurationFileLocation;
  }

  /**
   * Sets the path to the stack-level Kerberos descriptor file
   *
   * @param kerberosDescriptorPreConfigurationFileLocation a String containing the path to the stack-level Kerberos
   *                                                       descriptor file
   */
  public void setKerberosDescriptorPreConfigurationFileLocation(String kerberosDescriptorPreConfigurationFileLocation) {
    this.kerberosDescriptorPreConfigurationFileLocation = kerberosDescriptorPreConfigurationFileLocation;
  }

  /**
   * Set the path of the stack upgrade directory.
   *
   * @param path the path to the upgrades directory
   */
  public void setUpgradesFolder(String path) {
    upgradesFolder = path;
  }

  /**
   * Obtain the path of the upgrades folder or null if directory doesn't exist.
   *
   * @return the upgrades folder, or {@code null} if not set
   */
  public String getUpgradesFolder() {
    return upgradesFolder;
  }

  /**
   * Obtain all stack upgrade packs.
   *
   * @return map of upgrade pack name to upgrade pack or {@code null} if no packs
   */
  public Map<String, UpgradePack> getUpgradePacks() {
    return upgradePacks;
  }

  /**
   * Set upgrade packs.
   *
   * @param upgradePacks map of upgrade packs
   */
  public void setUpgradePacks(Map<String, UpgradePack> upgradePacks) {
    if (null != upgradePacks) {
      upgradePacks.values().forEach(pack -> {
        pack.setOwnerStackId(new StackId(this));
      });
    }
    this.upgradePacks = upgradePacks;
  }

  /**
   * Get config upgrade pack for stack
   * @return config upgrade pack for stack or null if it is
   * not defined
   */
  public ConfigUpgradePack getConfigUpgradePack() {
    return configUpgradePack;
  }

  /**
   * Set config upgrade pack for stack
   * @param configUpgradePack config upgrade pack for stack or null if it is
   * not defined
   */
  public void setConfigUpgradePack(ConfigUpgradePack configUpgradePack) {
    this.configUpgradePack = configUpgradePack;
  }

  @Override
  public int compareTo(StackInfo o) {
    if (name.equals(o.name)) {
      return VersionUtils.compareVersions(version, o.version);
    }
    return name.compareTo(o.name);
  }

  //todo: ensure that required properties are never modified...
  public Map<String, PropertyInfo> getRequiredProperties() {
    Map<String, PropertyInfo> result = requiredProperties;
    if (result == null) {
      synchronized(this) {
        result = requiredProperties;
        if (result == null) {
          requiredProperties = result = new HashMap<>();
          List<PropertyInfo> properties = getProperties();
          for (PropertyInfo propertyInfo : properties) {
            if (propertyInfo.isRequireInput()) {
              result.put(propertyInfo.getName(), propertyInfo);
            }
          }
        }
      }
    }
    return result;
  }

  public Map<PropertyInfo.PropertyType, Set<String>> getConfigPropertiesTypes(String configType) {
    if (!propertiesTypesCache.containsKey(configType)) {
      Map<PropertyInfo.PropertyType, Set<String>> propertiesTypes = new HashMap<>();

      collectPropertyTypes(propertiesTypes, getProperties(), configType);

      Collection<ServiceInfo> services = getServices();
      for (ServiceInfo serviceInfo : services) {
        collectPropertyTypes(propertiesTypes, serviceInfo.getProperties(), configType);
      }
      propertiesTypesCache.put(configType, propertiesTypes);
    }
    return propertiesTypesCache.get(configType);
  }

  private void collectPropertyTypes(Map<PropertyInfo.PropertyType, Set<String>> target,
                                    Collection<PropertyInfo> sources,
                                    String configType) {
    if (sources == null) {
      return;
    }

    for (PropertyInfo propertyInfo : sources) {
      String filename = propertyInfo.getFilename();
      if (filename == null || !filename.contains(configType)) {
        continue;
      }

      Set<PropertyInfo.PropertyType> types = propertyInfo.getPropertyTypes();
      if (types == null || types.isEmpty()) {
        continue;
      }

      for (PropertyInfo.PropertyType propertyType : types) {
        target.computeIfAbsent(propertyType, key -> new HashSet<>()).add(propertyInfo.getName());
      }
    }
  }

  /**
   * Return default config attributes for specified config type.
   * @param configType config type
   * @return config attributes
   */
  public synchronized Map<String, Map<String, String>> getDefaultConfigAttributesForConfigType(String configType){
    if(configPropertyAttributes == null){
      configPropertyAttributes = getDefaultConfigAttributes();
    }
    if(configPropertyAttributes.containsKey(configType)){
      return configPropertyAttributes.get(configType);
    }
    return null;
  }

  private Map<String,  Map<String, Map<String, String>>> getDefaultConfigAttributes(){
    Map<String,  Map<String, Map<String, String>>> result = new HashMap<>();
    for(ServiceInfo si : services){
      for(PropertyInfo pi : si.getProperties())
      {
        String propertyConfigType = Files.getNameWithoutExtension(pi.getFilename());
        String propertyName = pi.getName();
        String hidden = pi.getPropertyValueAttributes().getHidden();
        if(hidden != null){
          if(!result.containsKey(propertyConfigType)){
            result.put(propertyConfigType, new HashMap<>());
          }
          if(!result.get(propertyConfigType).containsKey("hidden")){
            result.get(propertyConfigType).put("hidden", new HashMap<>());
          }
          result.get(propertyConfigType).get("hidden").put(propertyName, hidden);
        }
      }
    }
    return result;
  }

  /**
   * @param key the version that the xml represents
   * @param xml the version definition object
   */
  public void addVersionDefinition(String key, VersionDefinitionXml xml) {
    versionDefinitions.put(key, xml);
  }

  /**
   * @return the list of available definitions on this stack
   */
  public Collection<VersionDefinitionXml> getVersionDefinitions() {
    return versionDefinitions.values();
  }

  /**
   * @param rxml  the repository xml object
   */
  public void setRepositoryXml(RepositoryXml rxml) {
    repoXml = rxml;
  }

  /**
   * @return the repository xml object, or {@code null} if it couldn't be loaded
   */
  public RepositoryXml getRepositoryXml() {
    return repoXml;
  }

  public List<String> getRemovedServices() {
    return removedServices;
  }

  public void setRemovedServices(List<String> removedServices) {
    this.removedServices = removedServices;
  }

  public List<String> getServicesWithNoConfigs() {
    return servicesWithNoConfigs;
  }

  public void setServicesWithNoConfigs(List<String> servicesWithNoConfigs) {
    this.servicesWithNoConfigs = servicesWithNoConfigs;
  }

  /**
   * @param xml the version definition parsed from {@link LatestRepoCallable}
   */
  public void setLatestVersionDefinition(VersionDefinitionXml xml) {
    latestVersion = xml;
  }

  /**
   * @param xml the version definition parsed from {@link LatestRepoCallable}
   */
  public VersionDefinitionXml getLatestVersionDefinition() {
    return latestVersion;
  }

  public RefreshCommandConfiguration getRefreshCommandConfiguration() {
    return refreshCommandConfiguration;
  }

  public void setRefreshCommandConfiguration(RefreshCommandConfiguration refreshCommandConfiguration) {
    this.refreshCommandConfiguration = refreshCommandConfiguration;
  }

  /**
   * @return names of each service in the stack
   */
  public Set<String> getServiceNames() {
    return getServices().stream().map(ServiceInfo::getName).collect(Collectors.toSet());
  }

  /**
   * Gets the instance of the {@code StackReleaseVersion}.  If not specified
   * or there is an error instantiating the class, return a default implementation.
   *
   * @return the stack release information.
   */
  public StackReleaseVersion getReleaseVersion() {

    if (StringUtils.isNotEmpty(releaseVersionClass)) {
      try {
        return getLibraryInstance(releaseVersionClass);
      } catch (Exception e) {
        LOG.error("Could not create stack release instance.  Using default. {}", e.getMessage());
        return new DefaultStackVersion();
      }
    } else {
      return new DefaultStackVersion();
    }
  }

  /**
   * Gets the {@link ClassLoader} that can be used to load classes found in JARs
   * in the stack's library folder.
   *
   * @return the class loader for 3rd party JARs supplied by the stack or
   *         {@code null} if there are no libraries for this stack.
   */
  public @Nullable URLClassLoader getLibraryClassLoader() {
    return libraryClassLoader;
  }

  /**
   * Sets the {@link ClassLoader} that can be used to load classes found in JARs
   * in the stack's library folder.
   *
   * @param libraryClassLoader
   *          the class loader.
   */
  public void setLibraryClassLoader(URLClassLoader libraryClassLoader) {
    this.libraryClassLoader = libraryClassLoader;
  }

  /**
   * Loads an instance of the class from the stack classloader, if available.
   *
   * @param className
   *          the name of the class to get an instance
   * @return
   *          the instance of the class
   * @throws Exception
   *          when the class cannot be loaded or instantiated
   */
  public <T> T getLibraryInstance(String className) throws Exception {
    return getLibraryInstance(null, className);
  }

  /**
   * Loads an instance of the class from the stack classloader, if available.
   *
   * @param injector
   *          the injector to use, or {@code null} to invoke the default, no-arg
   *          constructor
   * @param className
   *          the name of the class to get an instance
   * @return
   *          the instance of the class
   * @throws Exception
   *          when the class cannot be loaded or instantiated
   */
  @SuppressWarnings("unchecked")
  public <T> T getLibraryInstance(Injector injector, String className) throws Exception {
    Class<? extends T> clazz;

    if (null != libraryClassLoader) {
      clazz = (Class<? extends T>) libraryClassLoader.loadClass(className);
    } else {
      clazz = (Class<? extends T>) Class.forName(className);
    }

    return (null == injector) ? clazz.newInstance() : injector.getInstance(clazz);
  }

}
