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

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.StaticallyInject;
import org.apache.ambari.server.api.resources.AlertTargetResourceDefinition;
import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
import org.apache.ambari.server.controller.spi.NoSuchResourceException;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.spi.Request;
import org.apache.ambari.server.controller.spi.RequestStatus;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException;
import org.apache.ambari.server.controller.spi.SystemException;
import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
import org.apache.ambari.server.controller.utilities.PropertyHelper;
import org.apache.ambari.server.notifications.DispatchFactory;
import org.apache.ambari.server.notifications.NotificationDispatcher;
import org.apache.ambari.server.notifications.TargetConfigurationResult;
import org.apache.ambari.server.orm.dao.AlertDispatchDAO;
import org.apache.ambari.server.orm.entities.AlertGroupEntity;
import org.apache.ambari.server.orm.entities.AlertTargetEntity;
import org.apache.ambari.server.security.authorization.RoleAuthorization;
import org.apache.ambari.server.state.AlertState;
import org.apache.ambari.server.state.alert.AlertGroup;
import org.apache.ambari.server.state.alert.AlertTarget;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.persist.Transactional;

/**
 * The {@link AlertTargetResourceProvider} class deals with managing the CRUD
 * operations for {@link AlertTarget}, including property coercion to and from
 * {@link AlertTargetEntity}.
 */
@StaticallyInject
public class AlertTargetResourceProvider extends
 AbstractAuthorizedResourceProvider {

  private static final Logger LOG = LoggerFactory.getLogger(AlertTargetResourceProvider.class);
    
  public static final String ALERT_TARGET = "AlertTarget";

  public static final String ID_PROPERTY_ID = "id";
  public static final String NAME_PROPERTY_ID = "name";
  public static final String DESCRIPTION_PROPERTY_ID = "description";
  public static final String NOTIFICATION_TYPE_PROPERTY_ID = "notification_type";
  public static final String PROPERTIES_PROPERTY_ID = "properties";
  public static final String GROUPS_PROPERTY_ID = "groups";
  public static final String STATES_PROPERTY_ID = "alert_states";
  public static final String GLOBAL_PROPERTY_ID = "global";
  public static final String ENABLED_PROPERTY_ID = "enabled";

  public static final String ALERT_TARGET_ID = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + ID_PROPERTY_ID;
  public static final String ALERT_TARGET_NAME = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + NAME_PROPERTY_ID;
  public static final String ALERT_TARGET_DESCRIPTION = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + DESCRIPTION_PROPERTY_ID;
  public static final String ALERT_TARGET_NOTIFICATION_TYPE = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + NOTIFICATION_TYPE_PROPERTY_ID;
  public static final String ALERT_TARGET_PROPERTIES = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + PROPERTIES_PROPERTY_ID;
  public static final String ALERT_TARGET_GROUPS = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + GROUPS_PROPERTY_ID;
  public static final String ALERT_TARGET_STATES = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + STATES_PROPERTY_ID;
  public static final String ALERT_TARGET_GLOBAL = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + GLOBAL_PROPERTY_ID;
  public static final String ALERT_TARGET_ENABLED = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + ENABLED_PROPERTY_ID;

  private static final Set<String> PK_PROPERTY_IDS = new HashSet<>(
      Arrays.asList(ALERT_TARGET_ID, ALERT_TARGET_NAME));

  /**
   * The property ids for an alert target resource.
   */
  private static final Set<String> PROPERTY_IDS = new HashSet<>();

  /**
   * The key property ids for an alert target resource.
   */
  private static final Map<Resource.Type, String> KEY_PROPERTY_IDS = new HashMap<>();

  static {
    // properties
    PROPERTY_IDS.add(ALERT_TARGET_ID);
    PROPERTY_IDS.add(ALERT_TARGET_NAME);
    PROPERTY_IDS.add(ALERT_TARGET_DESCRIPTION);
    PROPERTY_IDS.add(ALERT_TARGET_NOTIFICATION_TYPE);
    PROPERTY_IDS.add(ALERT_TARGET_PROPERTIES);
    PROPERTY_IDS.add(ALERT_TARGET_GROUPS);
    PROPERTY_IDS.add(ALERT_TARGET_STATES);
    PROPERTY_IDS.add(ALERT_TARGET_GLOBAL);
    PROPERTY_IDS.add(ALERT_TARGET_ENABLED);

    // keys
    KEY_PROPERTY_IDS.put(Resource.Type.AlertTarget, ALERT_TARGET_ID);
  }

  /**
   * Target DAO
   */
  @Inject
  private static AlertDispatchDAO s_dao;

  @Inject
  private static DispatchFactory dispatchFactory;

  /**
   * Used for serialization and deserialization of some fields.
   */
  private static final Gson s_gson = new Gson();

  /**
   * Constructor.
   */
  @Inject
  AlertTargetResourceProvider() {
    super(Resource.Type.AlertTarget, PROPERTY_IDS, KEY_PROPERTY_IDS);

    EnumSet<RoleAuthorization> requiredAuthorizations = EnumSet.of(RoleAuthorization.CLUSTER_MANAGE_ALERT_NOTIFICATIONS);
    setRequiredCreateAuthorizations(requiredAuthorizations);
    setRequiredUpdateAuthorizations(requiredAuthorizations);
    setRequiredDeleteAuthorizations(requiredAuthorizations);
  }

  @Override
  protected RequestStatus createResourcesAuthorized(final Request request)
      throws SystemException,
      UnsupportedPropertyException, ResourceAlreadyExistsException,
      NoSuchParentResourceException {

    createResources(new Command<Void>() {
      @Override
      public Void invoke() throws AmbariException {
        createAlertTargets(request.getProperties(), request.getRequestInfoProperties());
        return null;
      }
    });

    notifyCreate(Resource.Type.AlertTarget, request);
    return getRequestStatus(null);
  }

  @Override
  public Set<Resource> getResources(Request request, Predicate predicate)
      throws SystemException, UnsupportedPropertyException,
      NoSuchResourceException, NoSuchParentResourceException {

    Set<Resource> results = new HashSet<>();
    Set<String> requestPropertyIds = getRequestPropertyIds(request, predicate);

    if( null == predicate ){
      List<AlertTargetEntity> entities = s_dao.findAllTargets();
      for (AlertTargetEntity entity : entities) {
        results.add(toResource(entity, requestPropertyIds));
      }
    } else {
      for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) {
        String id = (String) propertyMap.get(ALERT_TARGET_ID);
        if (null == id) {
          continue;
        }

        AlertTargetEntity entity = s_dao.findTargetById(Long.parseLong(id));
        if (null != entity) {
          results.add(toResource(entity, requestPropertyIds));
        }
      }
    }

    return results;
  }

  @Override
  protected RequestStatus updateResourcesAuthorized(final Request request,
      Predicate predicate)
      throws SystemException, UnsupportedPropertyException,
      NoSuchResourceException, NoSuchParentResourceException {

    modifyResources(new Command<Void>() {
      @Override
      public Void invoke() throws AmbariException {
        Set<Map<String, Object>> requestMaps = request.getProperties();
        for (Map<String, Object> requestMap : requestMaps) {
          String stringId = (String) requestMap.get(ALERT_TARGET_ID);

          if (StringUtils.isEmpty(stringId)) {
            throw new IllegalArgumentException(
                "The ID of the alert target is required when updating an existing target");
          }

          long alertTargetId = Long.parseLong(stringId);
          updateAlertTargets(alertTargetId, requestMap);
        }
        return null;
      }
    });

    notifyUpdate(Resource.Type.AlertTarget, request, predicate);
    return getRequestStatus(null);
  }

  @Override
  protected RequestStatus deleteResourcesAuthorized(Request request, Predicate predicate)
      throws SystemException, UnsupportedPropertyException,
      NoSuchResourceException, NoSuchParentResourceException {

    Set<Resource> resources = getResources(new RequestImpl(null, null, null,
        null), predicate);

    Set<Long> targetIds = new HashSet<>();

    for (final Resource resource : resources) {
      Long id = (Long) resource.getPropertyValue(ALERT_TARGET_ID);
      targetIds.add(id);
    }

    for (Long targetId : targetIds) {
      LOG.info("Deleting alert target {}", targetId);

      final AlertTargetEntity entity = s_dao.findTargetById(targetId.longValue());

      modifyResources(new Command<Void>() {
        @Override
        public Void invoke() throws AmbariException {
          s_dao.remove(entity);
          return null;
        }
      });
    }

    notifyDelete(Resource.Type.AlertTarget, predicate);
    return getRequestStatus(null);
  }

  @Override
  protected Set<String> getPKPropertyIds() {
    return PK_PROPERTY_IDS;
  }

  /**
   * Create and persist {@link AlertTargetEntity} from the map of properties.
   *
   * @param requestMaps
   * @param requestInfoProps
   * @throws AmbariException
   */
  @SuppressWarnings("unchecked")
  private void createAlertTargets(Set<Map<String, Object>> requestMaps, Map<String, String> requestInfoProps)
      throws AmbariException {
    for (Map<String, Object> requestMap : requestMaps) {
      String name = StringEscapeUtils.escapeHtml4((String) requestMap.get(ALERT_TARGET_NAME));
      String description = (String) requestMap.get(ALERT_TARGET_DESCRIPTION);
      String notificationType = (String) requestMap.get(ALERT_TARGET_NOTIFICATION_TYPE);
      Collection<String> alertStates = (Collection<String>) requestMap.get(ALERT_TARGET_STATES);
      String globalProperty = (String) requestMap.get(ALERT_TARGET_GLOBAL);
      String enabledProperty = (String) requestMap.get(ALERT_TARGET_ENABLED);

      if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException(
            "The name of the alert target is required.");
      }

      if (StringUtils.isEmpty(notificationType)) {
        throw new IllegalArgumentException(
            "The type of the alert target is required.");
      }

      Map<String, Object> properties = extractProperties(requestMap);

      String propertiesJson = s_gson.toJson(properties);
      if (StringUtils.isEmpty(propertiesJson)) {
        throw new IllegalArgumentException(
            "Alert targets must be created with their connection properties");
      }

      String validationDirective = requestInfoProps.get(AlertTargetResourceDefinition.VALIDATE_CONFIG_DIRECTIVE);
      if (validationDirective != null
          && validationDirective.equalsIgnoreCase("true")) {
        validateTargetConfig(notificationType, properties);
      }

      boolean overwriteExisting = false;
      if (requestInfoProps.containsKey(AlertTargetResourceDefinition.OVERWRITE_DIRECTIVE)) {
        overwriteExisting = true;
      }

      // global not required
      boolean isGlobal = false;
      if (null != globalProperty) {
        isGlobal = Boolean.parseBoolean(globalProperty);
      }

      // enabled not required
      boolean isEnabled = true;
      if (null != enabledProperty) {
        isEnabled = Boolean.parseBoolean(enabledProperty);
      }

      // set the states that this alert target cares about
      final Set<AlertState> alertStateSet;
      if (null != alertStates) {
        alertStateSet = new HashSet<>(alertStates.size());
        for (String state : alertStates) {
          alertStateSet.add(AlertState.valueOf(state));
        }
      } else {
        alertStateSet = EnumSet.allOf(AlertState.class);
      }

      // if we are overwriting an existing, determine if one exists first
      AlertTargetEntity entity = s_dao.findTargetByName(name);

      if (null == entity) {
        entity = new AlertTargetEntity();
      } else {
        if( !overwriteExisting ) {
          throw new IllegalArgumentException(
              "Alert targets already exists and can't be created");
        }
      }

      // groups are not required on creation
      if (requestMap.containsKey(ALERT_TARGET_GROUPS)) {
        Collection<Long> groupIds = (Collection<Long>) requestMap.get(ALERT_TARGET_GROUPS);
        if( !groupIds.isEmpty() ){
          Set<AlertGroupEntity> groups = new HashSet<>();
          List<Long> ids = new ArrayList<>(groupIds);
          groups.addAll(s_dao.findGroupsById(ids));
          entity.setAlertGroups(groups);
        }
      }

      entity.setDescription(description);
      entity.setNotificationType(notificationType);
      entity.setProperties(propertiesJson);
      entity.setTargetName(name);
      entity.setAlertStates(alertStateSet);
      entity.setGlobal(isGlobal);
      entity.setEnabled(isEnabled);

      if (null == entity.getTargetId() || 0 == entity.getTargetId()) {
        s_dao.create(entity);
      } else {
        s_dao.merge(entity);
      }
    }
  }

  /**
   * Updates existing {@link AlertTargetEntity}s with the specified properties.
   *
   * @param requestMap
   *          a set of property maps, one map for each entity.
   * @throws AmbariException
   *           if the entity could not be found.
   */
  @Transactional
  @SuppressWarnings("unchecked")
  void updateAlertTargets(long alertTargetId,
      Map<String, Object> requestMap)
      throws AmbariException {

    AlertTargetEntity entity = s_dao.findTargetById(alertTargetId);

    if (null == entity) {
      String message = MessageFormat.format(
          "The alert target with ID {0} could not be found", alertTargetId);

      throw new AmbariException(message);
    }

    String name = (String) requestMap.get(ALERT_TARGET_NAME);
    String description = (String) requestMap.get(ALERT_TARGET_DESCRIPTION);
    String notificationType = (String) requestMap.get(ALERT_TARGET_NOTIFICATION_TYPE);
    Collection<String> alertStates = (Collection<String>) requestMap.get(ALERT_TARGET_STATES);
    Collection<Long> groupIds = (Collection<Long>) requestMap.get(ALERT_TARGET_GROUPS);
    String isGlobal = (String) requestMap.get(ALERT_TARGET_GLOBAL);
    String isEnabled = (String) requestMap.get(ALERT_TARGET_ENABLED);

    if(null != isGlobal){
      entity.setGlobal(Boolean.parseBoolean(isGlobal));
    }

    if (null != isEnabled) {
      entity.setEnabled(Boolean.parseBoolean(isEnabled));
    }

    if (!StringUtils.isBlank(name)) {
      entity.setTargetName(name);
    }

    if (null != description) {
      entity.setDescription(description);
    }

    if (!StringUtils.isBlank(notificationType)) {
      entity.setNotificationType(notificationType);
    }

    Map<String, Object> propertiesMap = extractProperties(requestMap);

    if (propertiesMap != null) {
      String properties = s_gson.toJson(propertiesMap);
      if (!StringUtils.isEmpty(properties)) {
        LOG.debug("Updating Alert Target properties map to: " + properties);
        entity.setProperties(properties);
      }
    }

    // a null alert state implies that the key was not set and no update
    // should occur for this field, while an empty list implies all alert
    // states should be set
    if (null != alertStates) {
      final Set<AlertState> alertStateSet;
      if (alertStates.isEmpty()) {
        alertStateSet = EnumSet.allOf(AlertState.class);
      } else {
        alertStateSet = new HashSet<>(alertStates.size());
        for (String state : alertStates) {
          alertStateSet.add(AlertState.valueOf(state));
        }
      }

      entity.setAlertStates(alertStateSet);
    }

    // if groups were supplied, replace existing
    if (null != groupIds) {
      Set<AlertGroupEntity> groups = new HashSet<>();
      List<Long> ids = new ArrayList<>(groupIds);

      if (ids.size() > 0) {
        groups.addAll(s_dao.findGroupsById(ids));
      }

      entity.setAlertGroups(groups);
    } else if (entity.isGlobal()){
      Set<AlertGroupEntity> groups = new HashSet<>(s_dao.findAllGroups());
      entity.setAlertGroups(groups);
    }

    // merge the entity, cascading the merge to other entities, like groups
    s_dao.merge(entity);
  }

  /**
   * Convert the given {@link AlertTargetEntity} to a {@link Resource}.
   *
   * @param entity
   *          the entity to convert.
   * @param requestedIds
   *          the properties that were requested or {@code null} for all.
   * @return the resource representation of the entity (never {@code null}).
   */
  private Resource toResource(AlertTargetEntity entity, Set<String> requestedIds) {

    Resource resource = new ResourceImpl(Resource.Type.AlertTarget);
    resource.setProperty(ALERT_TARGET_ID, entity.getTargetId());
    resource.setProperty(ALERT_TARGET_NAME, entity.getTargetName());

    resource.setProperty(ALERT_TARGET_DESCRIPTION, entity.getDescription());
    resource.setProperty(ALERT_TARGET_NOTIFICATION_TYPE,
        entity.getNotificationType());

    resource.setProperty(ALERT_TARGET_ENABLED, entity.isEnabled());

    // these are expensive to deserialize; only do it if asked for
    if (requestedIds.contains(ALERT_TARGET_PROPERTIES)) {
      String properties = entity.getProperties();
      Map<String, String> map = s_gson.<Map<String, String>> fromJson(
          properties,
          Map.class);

      resource.setProperty(ALERT_TARGET_PROPERTIES, map);
    }

    setResourceProperty(resource, ALERT_TARGET_STATES, entity.getAlertStates(),
        requestedIds);

    setResourceProperty(resource, ALERT_TARGET_GLOBAL, entity.isGlobal(),
        requestedIds);

    if (BaseProvider.isPropertyRequested(ALERT_TARGET_GROUPS, requestedIds)) {
      Set<AlertGroupEntity> groupEntities = entity.getAlertGroups();
      List<AlertGroup> groups = new ArrayList<>(
        groupEntities.size());

      for (AlertGroupEntity groupEntity : groupEntities) {
        AlertGroup group = new AlertGroup();
        group.setId(groupEntity.getGroupId());
        group.setName(groupEntity.getGroupName());
        group.setClusterName(groupEntity.getClusterId());
        group.setDefault(groupEntity.isDefault());
        groups.add(group);
      }

      resource.setProperty(ALERT_TARGET_GROUPS, groups);
    }

    return resource;
  }

  /**
   * Looks through the flat list of propery keys in the supplied map and builds
   * a JSON string of key:value pairs for the {@link #ALERT_TARGET_PROPERTIES}
   * key.
   *
   * @param requestMap
   *          the map of flattened properties (not {@code null}).
   * @return the JSON representing the key/value pairs of all properties, or
   *         {@code null} if none.
   */
  private Map<String, Object> extractProperties(Map<String, Object> requestMap) {
    Map<String, Object> normalizedMap = new HashMap<>(
      requestMap.size());
    boolean has_properties = false;

    for (Entry<String, Object> entry : requestMap.entrySet()) {
      String key = entry.getKey();
      String propCat = PropertyHelper.getPropertyCategory(key);

      if (propCat.equals(ALERT_TARGET_PROPERTIES)) {
        has_properties = true;
        String propKey = PropertyHelper.getPropertyName(key);
        normalizedMap.put(propKey, entry.getValue());
      }
    }

    if (!has_properties) {
      normalizedMap = null;
    }

    return normalizedMap;
  }

  /**
   * Finds dispatcher for given notification type and validates on it given alert target configuration properties.
   * @param notificationType type of dispatcher
   * @param properties alert target configuration properties
   */
  private void validateTargetConfig(String notificationType, Map<String, Object> properties) {
    NotificationDispatcher dispatcher = dispatchFactory.getDispatcher(notificationType);
    if (dispatcher == null) {
      throw new IllegalArgumentException("Dispatcher for given notification type doesn't exist");
    }
    TargetConfigurationResult validationResult = dispatcher.validateTargetConfig(properties);
    if (validationResult.getStatus() == TargetConfigurationResult.Status.INVALID) {
      throw new IllegalArgumentException(validationResult.getMessage());
    }
  }
}
