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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Defines the cluster created by gsInstaller.
 */
public class ClusterDefinition {

  private static final String CLUSTER_DEFINITION_FILE = "gsInstaller-hosts.txt";
  private static final String DEFAULT_CLUSTER_NAME    = "ambari";
  private static final String CLUSTER_NAME_TAG        = "CLUSTER=";
  private static final String DEFAULT_VERSION_ID      = "HDP-1.2.0";
  private static final String VERSION_ID_TAG          = "VERSION=";

  private final Set<String> services = new HashSet<String>();
  private final Set<String> hosts = new HashSet<String>();
  private final Map<String, Set<String>> components = new HashMap<String, Set<String>>();
  private final Map<String, Map<String, Set<String>>> hostComponents = new HashMap<String, Map<String, Set<String>>>();

  private final GSInstallerStateProvider stateProvider;
  private String clusterName;
  private String versionId;

  /**
   * Index of host names to host component state.
   */
  private final Map<String, Set<HostComponentState>> hostStateMap = new HashMap<String, Set<HostComponentState>>();

  /**
   * Index of service names to host component state.
   */
  private final Map<String, Set<HostComponentState>> serviceStateMap = new HashMap<String, Set<HostComponentState>>();

  /**
   * Index of component names to host component state.
   */
  private final Map<String, Set<HostComponentState>> componentStateMap = new HashMap<String, Set<HostComponentState>>();

  /**
   * Index of host component names to host component state.
   */
  private final Map<String, HostComponentState> hostComponentStateMap = new HashMap<String, HostComponentState>();

  /**
   * Expiry for the health value.
   */
  private static final int DEFAULT_STATE_EXPIRY = 15000;

  /**
   * Component name mapping to account for differences in what is provided by the gsInstaller
   * and what is expected by the Ambari providers.
   */
  private static final Map<String, String> componentNameMap = new HashMap<String, String>();

  static {
    componentNameMap.put("GANGLIA", "GANGLIA_SERVER");
  }

  // ----- Constructors ------------------------------------------------------

  /**
   * Create a cluster definition.
   *
   * @param stateProvider  the state provider
   */
  public ClusterDefinition(GSInstallerStateProvider stateProvider) {
    this(stateProvider, DEFAULT_STATE_EXPIRY);
  }

  /**
   * Create a cluster definition.
   *
   * @param stateProvider  the state provider
   * @param stateExpiry    the state expiry
   */
  public ClusterDefinition(GSInstallerStateProvider stateProvider, int stateExpiry) {
    this.stateProvider = stateProvider;
    this.clusterName   = DEFAULT_CLUSTER_NAME;
    this.versionId     = DEFAULT_VERSION_ID;
    readClusterDefinition();
    setHostComponentState(stateExpiry);
  }

  // ----- ClusterDefinition -------------------------------------------------

  /**
   * Get the name of the cluster.
   *
   * @return the cluster name
   */
  public String getClusterName() {
    return clusterName;
  }

  /**
   * Get the name of the cluster.
   *
   * @return the cluster name
   */
  public String getVersionId() {
    return versionId;
  }

  /**
   * Get the services for the cluster.
   *
   * @return the set of service names
   */
  public Set<String> getServices() {
    return services;
  }

  /**
   * Get the hosts for the cluster.
   *
   * @return the set of hosts names
   */
  public Set<String> getHosts() {
    return hosts;
  }

  /**
   * Get the components for the given service.
   *
   * @param service  the service name
   *
   * @return the set of component names for the given service name
   */
  public Set<String> getComponents(String service) {
    return components.get(service);
  }

  /**
   * Get the host components for the given service and host.
   *
   * @param service  the service name
   * @param host     the host name
   *
   * @return the set of host component names for the given service and host names
   */
  public Set<String> getHostComponents(String service, String host) {
    Set<String> resultSet = null;
    Map<String, Set<String>> serviceHostComponents = hostComponents.get(service);
    if (serviceHostComponents != null) {
      resultSet = serviceHostComponents.get(host);
    }
    return resultSet == null ? Collections.<String>emptySet() : resultSet;
  }

  /**
   * Get the host state from the given host name.
   *
   * @param hostName  the host name
   *
   * @return the host state
   */
  public String getHostState(String hostName) {
    return isHealthy(hostStateMap.get(hostName)) ? "HEALTHY" : "INIT";
  }

  /**
   * Get the service state from the given service name.
   *
   * @param serviceName  the service name
   *
   * @return the service state
   */
  public String getServiceState(String serviceName) {
    return isHealthy(serviceStateMap.get(serviceName)) ? "STARTED" : "INIT";
  }

  /**
   * Get the component state from the give service name and component name.
   *
   * @param serviceName    the service name
   * @param componentName  the component name
   *
   * @return the component state
   */
  public String getComponentState(String serviceName, String componentName) {
    return isHealthy(componentStateMap.get(getComponentKey(serviceName, componentName))) ? "STARTED" : "INIT";
  }

  /**
   * Get the host component name from the given host name, service name and component name.
   *
   * @param hostName       the host name
   * @param serviceName    the service name
   * @param componentName  the component name
   *
   * @return the host component state
   */
  public String getHostComponentState(String hostName, String serviceName, String componentName) {
    return isHealthy(hostComponentStateMap.get(getHostComponentKey(hostName, serviceName, componentName))) ? "STARTED" : "INIT";
  }


  // ----- helper methods ----------------------------------------------------

  /**
   * Read the gsInstaller cluster definition file.
   */
  private void readClusterDefinition() {
    try {
      InputStream    is = this.getClass().getClassLoader().getResourceAsStream(CLUSTER_DEFINITION_FILE);
      BufferedReader br = new BufferedReader(new InputStreamReader(is));

      String line;
      while ((line = br.readLine()) != null) {
        line = line.trim();
        if (line.startsWith(CLUSTER_NAME_TAG)) {
          clusterName = line.substring(CLUSTER_NAME_TAG.length());
        }
        else if (line.startsWith(VERSION_ID_TAG)) {
          versionId = line.substring(VERSION_ID_TAG.length());
        }
        else {
          String[] parts = line.split("\\s+");
          assert(parts.length == 3);

          String serviceName   = parts[0];
          String componentName = parts[1];
          String hostName      = parts[2];

          // translate the component name if required
          if (componentNameMap.containsKey(componentName)) {
            componentName = componentNameMap.get(componentName);
          }

          services.add(serviceName);
          Set<String> serviceComponents = components.get(serviceName);
          if (serviceComponents == null) {
            serviceComponents = new HashSet<String>();
            components.put(serviceName, serviceComponents);
          }
          serviceComponents.add(componentName);

          Map<String, Set<String>> serviceHostComponents = hostComponents.get(serviceName);
          if (serviceHostComponents == null) {
            serviceHostComponents = new HashMap<String, Set<String>>();
            hostComponents.put(serviceName, serviceHostComponents);
          }

          Set<String> hostHostComponents = serviceHostComponents.get(hostName);
          if (hostHostComponents == null) {
            hostHostComponents = new HashSet<String>();
            serviceHostComponents.put(hostName, hostHostComponents);
          }
          hostHostComponents.add(componentName);
          hosts.add(hostName);
        }
      }
    } catch (IOException e) {
      String msg = "Caught exception reading " + CLUSTER_DEFINITION_FILE + ".";
      throw new IllegalStateException(msg, e);
    }
  }

  /**
   * Set the host component state maps.
   */
  private void setHostComponentState(int stateExpiry) {
    for (Map.Entry<String, Map<String, Set<String>>> serviceEntry : hostComponents.entrySet()) {
      String serviceName = serviceEntry.getKey();

      for (Map.Entry<String, Set<String>> hostEntry : serviceEntry.getValue().entrySet()) {
        String hostName = hostEntry.getKey();

        for (String componentName : hostEntry.getValue()) {

          HostComponentState state = new HostComponentState(hostName, componentName, stateExpiry);

          // add state to hosts
          addState(hostName, hostStateMap, state);

          // add state to services
          addState(serviceName, serviceStateMap, state);

          // add state to components
          addState(getComponentKey(serviceName, componentName), componentStateMap, state);

          // add state to host components
          hostComponentStateMap.put(getHostComponentKey(hostName, serviceName, componentName), state);
        }
      }
    }
  }

  /**
   * Add the given host component state object to the given map of state objects.
   *
   * @param hostName  the host name
   * @param stateMap  the map of state objects
   * @param state     the state
   */
  private static void addState(String hostName, Map<String, Set<HostComponentState>> stateMap, HostComponentState state) {
    Set<HostComponentState> states = stateMap.get(hostName);
    if (states == null) {
      states = new HashSet<HostComponentState>();
      stateMap.put(hostName, states);
    }
    states.add(state);
  }

  /**
   * Get a key from the given service name and component name.
   *
   * @param serviceName    the service name
   * @param componentName  the component name
   *
   * @return the key
   */
  private String getComponentKey(String serviceName, String componentName) {
    return serviceName + "." + componentName;
  }

  /**
   * Get a key from the given host name, service name and component name.
   *
   * @param hostName       the host name
   * @param serviceName    the service name
   * @param componentName  the component name
   *
   * @return the key
   */
  private String getHostComponentKey(String hostName, String serviceName, String componentName) {
    return hostName + "." + serviceName + "." + componentName;
  }

  /**
   * Determine whether or not the host components associated
   * with the given states are healthy.
   *
   * @param states  the states
   *
   * @return true if the associated host components are healthy
   */
  private boolean isHealthy(Set<HostComponentState> states) {
    if (states != null) {
      for (HostComponentState state : states) {
        if (!state.isHealthy()) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Determine whether or not the host component associated
   * with the given state is healthy.
   *
   * @param state  the state
   *
   * @return true if the associated host component is healthy
   */
  private boolean isHealthy(HostComponentState state) {
    return state == null || state.isHealthy();
  }


  // ----- inner classes -----------------------------------------------------

  /**
   * A state object used to check the health of a host component.
   */
  private class HostComponentState {
    private final String hostName;
    private final String componentName;
    private final int expiry;
    private boolean healthy = true;
    private long lastAccess;

    // ----- Constructor -----------------------------------------------------

    /**
     * Constructor.
     *
     * @param hostName       the host name
     * @param componentName  the component name
     */
    HostComponentState(String hostName, String componentName, int expiry) {
      this.hostName      = hostName;
      this.componentName = componentName;
      this.expiry        = expiry;
    }

    /**
     * Determine whether or not the associated host component is healthy.
     *
     * @return true if the associated host component is healthy
     */
    public boolean isHealthy() {
      if (System.currentTimeMillis() - lastAccess > expiry) {
        // health value has expired... get it again
        healthy = stateProvider.isHealthy(hostName, componentName);
        this.lastAccess = System.currentTimeMillis();
      }
      return healthy;
    }
  }
}
