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

import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.metamodel.SingularAttribute;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.api.query.JpaPredicateVisitor;
import org.apache.ambari.server.api.query.JpaSortBuilder;
import org.apache.ambari.server.controller.AlertNoticeRequest;
import org.apache.ambari.server.controller.RootServiceResponseFactory.Services;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.utilities.PredicateHelper;
import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
import org.apache.ambari.server.orm.entities.AlertGroupEntity;
import org.apache.ambari.server.orm.entities.AlertNoticeEntity;
import org.apache.ambari.server.orm.entities.AlertNoticeEntity_;
import org.apache.ambari.server.orm.entities.AlertTargetEntity;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.NotificationState;
import org.apache.ambari.server.state.Service;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.persist.Transactional;

/**
 * The {@link AlertDispatchDAO} class manages the {@link AlertTargetEntity},
 * {@link AlertGroupEntity}, and the associations between them.
 */
@Singleton
public class AlertDispatchDAO {
  /**
   * JPA entity manager
   */
  @Inject
  private Provider<EntityManager> entityManagerProvider;

  /**
   * DAO utilities for dealing mostly with {@link TypedQuery} results.
   */
  @Inject
  private DaoUtils daoUtils;

  /**
   * Used to retrieve a cluster and its services when creating a default
   * {@link AlertGroupEntity} for a service.
   */
  @Inject
  private Provider<Clusters> m_clusters;

  /**
   * A lock that ensures that group writes are protected. This is useful since
   * groups can be created through different events/threads in the system.
   */
  private final Lock m_groupLock = new ReentrantLock();

  /**
   * Gets an alert group with the specified ID.
   *
   * @param groupId
   *          the ID of the group to retrieve.
   * @return the group or {@code null} if none exists.
   */
  public AlertGroupEntity findGroupById(long groupId) {
    return entityManagerProvider.get().find(AlertGroupEntity.class, groupId);
  }

  /**
   * Gets all of the alert groups for the list of IDs given.
   *
   * @param groupIds
   *          the IDs of the groups to retrieve.
   * @return the groups or an empty list (never {@code null}).
   */
  public List<AlertGroupEntity> findGroupsById(List<Long> groupIds) {
    TypedQuery<AlertGroupEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertGroupEntity.findByIds", AlertGroupEntity.class);

    query.setParameter("groupIds", groupIds);

    return daoUtils.selectList(query);
  }

  /**
   * Gets an alert target with the specified ID.
   *
   * @param targetId
   *          the ID of the target to retrieve.
   * @return the target or {@code null} if none exists.
   */
  public AlertTargetEntity findTargetById(long targetId) {
    return entityManagerProvider.get().find(AlertTargetEntity.class, targetId);
  }

  /**
   * Gets all of the alert targets for the list of IDs given.
   *
   * @param targetIds
   *          the IDs of the targets to retrieve.
   * @return the targets or an empty list (never {@code null}).
   */
  public List<AlertTargetEntity> findTargetsById(List<Long> targetIds) {
    TypedQuery<AlertTargetEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertTargetEntity.findByIds", AlertTargetEntity.class);

    query.setParameter("targetIds", targetIds);

    return daoUtils.selectList(query);
  }

  /**
   * Gets a notification with the specified ID.
   *
   * @param noticeId
   *          the ID of the notification to retrieve.
   * @return the notification or {@code null} if none exists.
   */
  public AlertNoticeEntity findNoticeById(long noticeId) {
    return entityManagerProvider.get().find(AlertNoticeEntity.class, noticeId);
  }

  /**
   * Gets a notification with the specified UUID.
   *
   * @param uuid
   *          the UUID of the notification to retrieve.
   * @return the notification or {@code null} if none exists.
   */
  public AlertNoticeEntity findNoticeByUuid(String uuid) {
    TypedQuery<AlertNoticeEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertNoticeEntity.findByUuid", AlertNoticeEntity.class);

    query.setParameter("uuid", uuid);

    return daoUtils.selectOne(query);
  }

  /**
   * Gets all {@link AlertNoticeEntity} instances that are
   * {@link NotificationState#PENDING} and not yet dispatched.
   *
   * @return the notices that are waiting to be dispatched, or an empty list
   *         (never {@code null}).
   */
  public List<AlertNoticeEntity> findPendingNotices() {
    TypedQuery<AlertNoticeEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertNoticeEntity.findByState", AlertNoticeEntity.class);

    query.setParameter("notifyState", NotificationState.PENDING);
    return daoUtils.selectList(query);
  }

  /**
   * Gets an alert group with the specified name across all clusters. Alert
   * group names are unique within a cluster.
   *
   * @param groupName
   *          the name of the group (not {@code null}).
   * @return the alert group or {@code null} if none exists.
   */
  public AlertGroupEntity findGroupByName(String groupName) {
    TypedQuery<AlertGroupEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertGroupEntity.findByName", AlertGroupEntity.class);

    query.setParameter("groupName", groupName);

    return daoUtils.selectSingle(query);
  }

  /**
   * Gets an alert group with the specified name for the given cluster. Alert
   * group names are unique within a cluster.
   *
   * @param clusterId
   *          the ID of the cluster.
   * @param groupName
   *          the name of the group (not {@code null}).
   * @return the alert group or {@code null} if none exists.
   */
  public AlertGroupEntity findGroupByName(long clusterId, String groupName) {
    TypedQuery<AlertGroupEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertGroupEntity.findByNameInCluster", AlertGroupEntity.class);

    query.setParameter("clusterId", clusterId);
    query.setParameter("groupName", groupName);

    return daoUtils.selectSingle(query);
  }

  /**
   * Gets an alert target with the specified name. Alert target names are unique
   * across all clusters.
   *
   * @param targetName
   *          the name of the target (not {@code null}).
   * @return the alert target or {@code null} if none exists.
   */
  public AlertTargetEntity findTargetByName(String targetName) {
    TypedQuery<AlertTargetEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertTargetEntity.findByName", AlertTargetEntity.class);

    query.setParameter("targetName", targetName);

    return daoUtils.selectSingle(query);
  }

  /**
   * Gets all alert groups stored in the database across all clusters.
   *
   * @return all alert groups or empty list if none exist (never {@code null}).
   */
  public List<AlertGroupEntity> findAllGroups() {
    TypedQuery<AlertGroupEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertGroupEntity.findAll", AlertGroupEntity.class);

    return daoUtils.selectList(query);
  }

  /**
   * Gets all alert groups stored in the database for the specified cluster.
   *
   * @return all alert groups in the specified cluster or empty list if none
   *         exist (never {@code null}).
   */
  public List<AlertGroupEntity> findAllGroups(long clusterId) {
    TypedQuery<AlertGroupEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertGroupEntity.findAllInCluster", AlertGroupEntity.class);

    query.setParameter("clusterId", clusterId);

    return daoUtils.selectList(query);
  }

  /**
   * Gets all alert targets stored in the database.
   *
   * @return all alert targets or empty list if none exist (never {@code null}).
   */
  public List<AlertTargetEntity> findAllTargets() {
    TypedQuery<AlertTargetEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertTargetEntity.findAll", AlertTargetEntity.class);

    return daoUtils.selectList(query);
  }

  /**
   * Gets all global alert targets stored in the database.
   *
   * @return all global alert targets or empty list if none exist (never
   *         {@code null}).
   */
  public List<AlertTargetEntity> findAllGlobalTargets() {
    TypedQuery<AlertTargetEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertTargetEntity.findAllGlobal", AlertTargetEntity.class);

    return daoUtils.selectList(query);
  }

  /**
   * Gets all of the {@link AlertGroupEntity} instances that include the
   * specified alert definition. Service default groups will also be returned.
   *
   * @param definitionEntity
   *          the definition that the group must include (not {@code null}).
   * @return all alert groups that have an association with the specified
   *         definition and the definition's service default group or empty list
   *         if none exist (never {@code null}).
   */
  public List<AlertGroupEntity> findGroupsByDefinition(
      AlertDefinitionEntity definitionEntity) {

    TypedQuery<AlertGroupEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertGroupEntity.findByAssociatedDefinition", AlertGroupEntity.class);

    query.setParameter("alertDefinition", definitionEntity);

    return daoUtils.selectList(query);
  }

  /**
   * Gets the default group for the specified cluster and service.
   *
   * @param clusterId
   *          the cluster that the group belongs to
   * @param serviceName
   *          the name of the service (not {@code null}).
   * @return the default group, or {@code null} if the service name is not valid
   *         for an installed service; otherwise {@code null} should not be
   *         possible.
   */
  public AlertGroupEntity findDefaultServiceGroup(long clusterId,
      String serviceName) {
    TypedQuery<AlertGroupEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertGroupEntity.findServiceDefaultGroup", AlertGroupEntity.class);

    query.setParameter("clusterId", clusterId);
    query.setParameter("serviceName", serviceName);
    return daoUtils.selectSingle(query);
  }

  /**
   * Gets all alert notifications stored in the database.
   *
   * @return all alert notifications or empty list if none exist (never
   *         {@code null}).
   */
  public List<AlertNoticeEntity> findAllNotices() {
    TypedQuery<AlertNoticeEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertNoticeEntity.findAll", AlertNoticeEntity.class);

    return daoUtils.selectList(query);
  }

  /**
   * Gets all alert notifications stored in the database that match the given
   * predicate, pagination, and sorting.
   *
   * @param request
   * @return all alert notifications or empty list if none exist (never
   *         {@code null}).
   */
  public List<AlertNoticeEntity> findAllNotices(AlertNoticeRequest request) {
    EntityManager entityManager = entityManagerProvider.get();

    // convert the Ambari predicate into a JPA predicate
    NoticePredicateVisitor visitor = new NoticePredicateVisitor();
    PredicateHelper.visit(request.Predicate, visitor);

    CriteriaQuery<AlertNoticeEntity> query = visitor.getCriteriaQuery();
    javax.persistence.criteria.Predicate jpaPredicate = visitor.getJpaPredicate();

    if (null != jpaPredicate) {
      query.where(jpaPredicate);
    }

    // sorting
    JpaSortBuilder<AlertNoticeEntity> sortBuilder = new JpaSortBuilder<AlertNoticeEntity>();
    List<Order> sortOrders = sortBuilder.buildSortOrders(request.Sort, visitor);
    query.orderBy(sortOrders);

    // pagination
    TypedQuery<AlertNoticeEntity> typedQuery = entityManager.createQuery(query);
    if (null != request.Pagination) {
      typedQuery.setFirstResult(request.Pagination.getOffset());
      typedQuery.setMaxResults(request.Pagination.getPageSize());
    }

    return daoUtils.selectList(typedQuery);
  }

  /**
   * Gets the total count of all {@link AlertNoticeEntity} rows that match the
   * specified {@link Predicate}.
   *
   * @param predicate
   *          the predicate to apply, or {@code null} for none.
   * @return the total count of rows that would be returned in a result set.
   */
  @Transactional
  public int getNoticesCount(Predicate predicate) {
    return 0;
  }

  /**
   * Persists new alert groups.
   *
   * @param entities
   *          the groups to persist (not {@code null}).
   */
  @Transactional
  public void createGroups(List<AlertGroupEntity> entities) {
    if (null == entities) {
      return;
    }

    for (AlertGroupEntity entity : entities) {
      create(entity);
    }
  }

  /**
   * Persists a new alert group.
   *
   * @param group
   *          the group to persist (not {@code null}).
   */
  @Transactional
  public void create(AlertGroupEntity group) {
    entityManagerProvider.get().persist(group);

    // associate the group with all alert targets
    List<AlertTargetEntity> targets = findAllGlobalTargets();
    if (!targets.isEmpty()) {
      for (AlertTargetEntity target : targets) {
        group.addAlertTarget(target);
      }
      entityManagerProvider.get().merge(group);
    }
  }

  /**
   * Creates a default group in the specified cluster and service. If the
   * service is not valid, then this will throw an {@link AmbariException}.
   *
   * @param clusterId
   *          the cluster that the group is in.
   * @param serviceName
   *          the name of the group which is also the service name.
   */
  public AlertGroupEntity createDefaultGroup(long clusterId, String serviceName)
      throws AmbariException {
    // AMBARI is a special service that we let through, otherwise we need to
    // verify that the service exists before we create the default group
    String ambariServiceName = Services.AMBARI.name();
    if (!ambariServiceName.equals(serviceName)) {
      Cluster cluster = m_clusters.get().getClusterById(clusterId);
      Map<String, Service> services = cluster.getServices();

      if (!services.containsKey(serviceName)) {
        String message = MessageFormat.format(
            "Unable to create a default alert group for unknown service {0} in cluster {1}",
            serviceName, cluster.getClusterName());
        throw new AmbariException(message);
      }
    }

    AlertGroupEntity group = new AlertGroupEntity();

    m_groupLock.lock();
    try {
      group.setClusterId(clusterId);
      group.setDefault(true);
      group.setGroupName(serviceName);
      group.setServiceName(serviceName);

      create(group);
    } finally {
      m_groupLock.unlock();
    }

    return group;
  }

  /**
   * Refresh the state of the alert group from the database.
   *
   * @param alertGroup
   *          the group to refresh (not {@code null}).
   */
  @Transactional
  public void refresh(AlertGroupEntity alertGroup) {
    entityManagerProvider.get().refresh(alertGroup);
  }

  /**
   * Merge the speicified alert group with the existing group in the database.
   *
   * @param alertGroup
   *          the group to merge (not {@code null}).
   * @return the updated group with merged content (never {@code null}).
   */
  @Transactional
  public AlertGroupEntity merge(AlertGroupEntity alertGroup) {
    return entityManagerProvider.get().merge(alertGroup);
  }

  /**
   * Removes the specified alert group from the database.
   *
   * @param alertGroup
   *          the group to remove.
   */
  @Transactional
  public void remove(AlertGroupEntity alertGroup) {
    entityManagerProvider.get().remove(merge(alertGroup));
  }

  /**
   * Removes all {@link AlertDefinitionEntity} that are associated with the
   * specified cluster ID.
   *
   * @param clusterId
   *          the cluster ID.
   */
  @Transactional
  public void removeAllGroups(long clusterId) {
    List<AlertGroupEntity> groups = findAllGroups(clusterId);
    for (AlertGroupEntity group : groups) {
      remove(group);
    }
  }

  /**
   * Persists new alert targets.
   *
   * @param entities
   *          the targets to persist (not {@code null}).
   */
  @Transactional
  public void createTargets(List<AlertTargetEntity> entities) {
    if (null == entities) {
      return;
    }

    for (AlertTargetEntity entity : entities) {
      create(entity);
    }
  }

  /**
   * Persists a new alert target.
   *
   * @param alertTarget
   *          the target to persist (not {@code null}).
   */
  @Transactional
  public void create(AlertTargetEntity alertTarget) {
    entityManagerProvider.get().persist(alertTarget);

    if (alertTarget.isGlobal()) {
      List<AlertGroupEntity> groups = findAllGroups();
      for (AlertGroupEntity group : groups) {
        group.addAlertTarget(alertTarget);
        merge(group);
      }
    }
  }

  /**
   * Refresh the state of the alert target from the database.
   *
   * @param alertTarget
   *          the target to refresh (not {@code null}).
   */
  @Transactional
  public void refresh(AlertTargetEntity alertTarget) {
    entityManagerProvider.get().refresh(alertTarget);
  }

  /**
   * Merge the speicified alert target with the existing target in the database.
   *
   * @param alertTarget
   *          the target to merge (not {@code null}).
   * @return the updated target with merged content (never {@code null}).
   */
  @Transactional
  public AlertTargetEntity merge(AlertTargetEntity alertTarget) {
    return entityManagerProvider.get().merge(alertTarget);
  }

  /**
   * Removes the specified alert target from the database.
   *
   * @param alertTarget
   *          the target to remove.
   */
  @Transactional
  public void remove(AlertTargetEntity alertTarget) {
    entityManagerProvider.get().remove(merge(alertTarget));
  }

  /**
   * Persists a new notification.
   *
   * @param alertNotice
   *          the notification to persist (not {@code null}).
   */
  @Transactional
  public void create(AlertNoticeEntity alertNotice) {
    entityManagerProvider.get().persist(alertNotice);
  }

  /**
   * Refresh the state of the notification from the database.
   *
   * @param alertNotice
   *          the notification to refresh (not {@code null}).
   */
  @Transactional
  public void refresh(AlertNoticeEntity alertNotice) {
    entityManagerProvider.get().refresh(alertNotice);
  }

  /**
   * Merge the specified notification with the existing target in the database.
   *
   * @param alertNotice
   *          the notification to merge (not {@code null}).
   * @return the updated notification with merged content (never {@code null}).
   */
  @Transactional
  public AlertNoticeEntity merge(AlertNoticeEntity alertNotice) {
    return entityManagerProvider.get().merge(alertNotice);
  }

  /**
   * Removes the specified notification from the database.
   *
   * @param alertNotice
   *          the notification to remove.
   */
  @Transactional
  public void remove(AlertNoticeEntity alertNotice) {
    entityManagerProvider.get().remove(merge(alertNotice));
  }

  /**
   * Removes notifications for the specified alert definition ID. This will
   * invoke {@link EntityManager#clear()} when completed since the JPQL
   * statement will remove entries without going through the EM.
   *
   * @param definitionId
   *          the ID of the definition to remove.
   */
  @Transactional
  public void removeNoticeByDefinitionId(long definitionId) {
    EntityManager entityManager = entityManagerProvider.get();
    TypedQuery<AlertNoticeEntity> currentQuery = entityManager.createNamedQuery(
        "AlertNoticeEntity.removeByDefinitionId", AlertNoticeEntity.class);

    currentQuery.setParameter("definitionId", definitionId);
    currentQuery.executeUpdate();
    entityManager.clear();
  }

  /**
   * The {@link NoticePredicateVisitor} is used to convert an Ambari
   * {@link Predicate} into a JPA {@link javax.persistence.criteria.Predicate}.
   */
  private final class NoticePredicateVisitor extends
      JpaPredicateVisitor<AlertNoticeEntity> {

    /**
     * Constructor.
     *
     */
    public NoticePredicateVisitor() {
      super(entityManagerProvider.get(), AlertNoticeEntity.class);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Class<AlertNoticeEntity> getEntityClass() {
      return AlertNoticeEntity.class;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<? extends SingularAttribute<?, ?>> getPredicateMapping(
        String propertyId) {
      return AlertNoticeEntity_.getPredicateMapping().get(propertyId);
    }
  }
}
