/**
 * 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 distribut
 * ed 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.topology;

import com.google.inject.Inject;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.ObjectNotFoundException;
import org.apache.ambari.server.StackAccessException;
import org.apache.ambari.server.controller.AmbariManagementController;
import org.apache.ambari.server.controller.AmbariServer;
import org.apache.ambari.server.controller.internal.Stack;
import org.apache.ambari.server.controller.utilities.PropertyHelper;
import org.apache.ambari.server.orm.dao.BlueprintDAO;
import org.apache.ambari.server.orm.entities.BlueprintEntity;
import org.apache.ambari.server.stack.NoSuchStackException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Create a Blueprint instance.
 */
public class BlueprintFactory {

  // Blueprints
  protected static final String BLUEPRINT_NAME_PROPERTY_ID =
      PropertyHelper.getPropertyId("Blueprints", "blueprint_name");
  protected static final String STACK_NAME_PROPERTY_ID =
      PropertyHelper.getPropertyId("Blueprints", "stack_name");
  protected static final String STACK_VERSION_PROPERTY_ID =
      PropertyHelper.getPropertyId("Blueprints", "stack_version");

  // Host Groups
  protected static final String HOST_GROUP_PROPERTY_ID = "host_groups";
  protected static final String HOST_GROUP_NAME_PROPERTY_ID = "name";
  protected static final String HOST_GROUP_CARDINALITY_PROPERTY_ID = "cardinality";

  // Host Group Components
  protected static final String COMPONENT_PROPERTY_ID ="components";
  protected static final String COMPONENT_NAME_PROPERTY_ID ="name";

  // Configurations
  protected static final String CONFIGURATION_PROPERTY_ID = "configurations";
  protected static final String PROPERTIES_PROPERTY_ID = "properties";
  protected static final String PROPERTIES_ATTRIBUTES_PROPERTY_ID = "properties_attributes";

  private static BlueprintDAO blueprintDAO;
  private ConfigurationFactory configFactory = new ConfigurationFactory();

  private final StackFactory stackFactory;

  public BlueprintFactory() {
    this(new DefaultStackFactory());
  }

  protected BlueprintFactory(StackFactory stackFactory) {
    this.stackFactory = stackFactory;
  }

  public Blueprint getBlueprint(String blueprintName) throws NoSuchStackException {
    BlueprintEntity entity = blueprintDAO.findByName(blueprintName);
    //todo: just return null?
    return entity == null ? null : new BlueprintImpl(entity);
  }

  /**
   * Convert a map of properties to a blueprint entity.
   *
   * @param properties  property map
   * @return new blueprint entity
   */
  @SuppressWarnings("unchecked")
  public Blueprint createBlueprint(Map<String, Object> properties) throws NoSuchStackException {
    String name = String.valueOf(properties.get(BLUEPRINT_NAME_PROPERTY_ID));
    // String.valueOf() will return "null" if value is null
    if (name.equals("null") || name.isEmpty()) {
      //todo: should throw a checked exception from here
      throw new IllegalArgumentException("Blueprint name must be provided");
    }

    Stack stack = createStack(properties);
    Collection<HostGroup> hostGroups = processHostGroups(name, stack, properties);
    Configuration configuration = configFactory.getConfiguration((Collection<Map<String, String>>)
        properties.get(CONFIGURATION_PROPERTY_ID));

    return new BlueprintImpl(name, hostGroups, stack, configuration);
  }

  protected Stack createStack(Map<String, Object> properties) throws NoSuchStackException {
    String stackName = String.valueOf(properties.get(STACK_NAME_PROPERTY_ID));
    String stackVersion = String.valueOf(properties.get(STACK_VERSION_PROPERTY_ID));
    try {
      //todo: don't pass in controller
      return stackFactory.createStack(stackName, stackVersion, AmbariServer.getController());
    } catch (ObjectNotFoundException e) {
      throw new NoSuchStackException(stackName, stackVersion);
    } catch (AmbariException e) {
      //todo:
      throw new RuntimeException("An error occurred parsing the stack information.", e);
    }
  }

  //todo: Move logic to HostGroupImpl
  @SuppressWarnings("unchecked")
  private Collection<HostGroup> processHostGroups(String bpName, Stack stack, Map<String, Object> properties) {
    Set<HashMap<String, Object>> hostGroupProps = (HashSet<HashMap<String, Object>>)
        properties.get(HOST_GROUP_PROPERTY_ID);

    if (hostGroupProps == null || hostGroupProps.isEmpty()) {
      throw new IllegalArgumentException("At least one host group must be specified in a blueprint");
    }

    Collection<HostGroup> hostGroups = new ArrayList<HostGroup>();
    for (HashMap<String, Object> hostGroupProperties : hostGroupProps) {
      String hostGroupName = (String) hostGroupProperties.get(HOST_GROUP_NAME_PROPERTY_ID);
      if (hostGroupName == null || hostGroupName.isEmpty()) {
        throw new IllegalArgumentException("Every host group must include a non-null 'name' property");
      }

      HashSet<HashMap<String, String>> componentProps = (HashSet<HashMap<String, String>>)
          hostGroupProperties.get(COMPONENT_PROPERTY_ID);

      Collection<Map<String, String>> configProps = (Collection<Map<String, String>>)
          hostGroupProperties.get(CONFIGURATION_PROPERTY_ID);

      Collection<String> components = processHostGroupComponents(stack, hostGroupName, componentProps);
      Configuration configuration = configFactory.getConfiguration(configProps);
      String cardinality = String.valueOf(hostGroupProperties.get(HOST_GROUP_CARDINALITY_PROPERTY_ID));

      HostGroup group = new HostGroupImpl(hostGroupName, bpName, stack, components, configuration, cardinality);

      hostGroups.add(group);
    }
    return hostGroups;
  }

  private Collection<String> processHostGroupComponents(Stack stack, String groupName, HashSet<HashMap<String, String>>  componentProps) {
    if (componentProps == null || componentProps.isEmpty()) {
      throw new IllegalArgumentException("Host group '" + groupName + "' must contain at least one component");
    }

    Collection<String> stackComponentNames = getAllStackComponents(stack);
    Collection<String> components = new ArrayList<String>();

    for (HashMap<String, String> componentProperties : componentProps) {
      String componentName = componentProperties.get(COMPONENT_NAME_PROPERTY_ID);
      if (componentName == null || componentName.isEmpty()) {
        throw new IllegalArgumentException("Host group '" + groupName +
            "' contains a component with no 'name' property");
      }

      if (! stackComponentNames.contains(componentName)) {
        throw new IllegalArgumentException("The component '" + componentName + "' in host group '" +
            groupName + "' is not valid for the specified stack");
      }
      components.add(componentName);

    }
    return components;
  }

  /**
   * Obtain all component names for the specified stack.
   *
   * @return collection of component names for the specified stack
   * @throws IllegalArgumentException if the specified stack doesn't exist
   */
  private Collection<String> getAllStackComponents(Stack stack) {
    Collection<String> allComponents = new HashSet<String>();
    for (Collection<String> components: stack.getComponents().values()) {
      allComponents.addAll(components);
    }
    // currently ambari server is no a recognized component
    allComponents.add("AMBARI_SERVER");

    return allComponents;
  }


  /**
   * Static initialization.
   *
   * @param dao  blueprint data access object
   */
  @Inject
  public static void init(BlueprintDAO dao) {
    blueprintDAO   = dao;
  }

  /**
   * Internal interface used to abstract out the process of creating the Stack object.
   *
   * This is used to simplify unit testing, since a new Factory can be provided to
   * simulate various Stack or error conditions.
   */
  interface StackFactory {
      public Stack createStack(String stackName, String stackVersion, AmbariManagementController managementController) throws AmbariException;
  }

  /**
   * Default implementation of StackFactory.
   *
   * Calls the Stack constructor to create the Stack instance.
   *
   */
  private static class DefaultStackFactory implements StackFactory {
    @Override
    public Stack createStack(String stackName, String stackVersion, AmbariManagementController managementController) throws AmbariException {
      return new Stack(stackName, stackVersion, managementController);
    }
  }
}
