/*
 * 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.util.Collections;
import java.util.List;
import java.util.Set;

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.agent.stomp.dto.AlertGroupUpdate;
import org.apache.ambari.server.controller.RootComponent;
import org.apache.ambari.server.controller.RootService;
import org.apache.ambari.server.controller.internal.AlertDefinitionResourceProvider;
import org.apache.ambari.server.events.AlertDefinitionChangedEvent;
import org.apache.ambari.server.events.AlertDefinitionDeleteEvent;
import org.apache.ambari.server.events.AlertDefinitionRegistrationEvent;
import org.apache.ambari.server.events.AlertGroupsUpdateEvent;
import org.apache.ambari.server.events.UpdateEventType;
import org.apache.ambari.server.events.publishers.AmbariEventPublisher;
import org.apache.ambari.server.events.publishers.STOMPUpdatePublisher;
import org.apache.ambari.server.orm.RequiresSession;
import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
import org.apache.ambari.server.orm.entities.AlertGroupEntity;
import org.apache.ambari.server.state.alert.AlertDefinition;
import org.apache.ambari.server.state.alert.AlertDefinitionFactory;
import org.apache.ambari.server.state.alert.Scope;
import org.apache.ambari.server.state.alert.SourceType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * The {@link AlertDefinitionDAO} class is used to manage the persistence and
 * retrieval of {@link AlertDefinitionEntity} instances.
 */
@Singleton
public class AlertDefinitionDAO {
  /**
   * Logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(AlertDefinitionDAO.class);

  /**
   * JPA entity manager
   */
  @Inject
  private Provider<EntityManager> entityManagerProvider;

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

  /**
   * Alert history DAO.
   */
  @Inject
  private AlertsDAO alertsDao;

  /**
   * Alert dispatch DAO.
   */
  @Inject
  private AlertDispatchDAO dispatchDao;

  /**
   * Publishes the following events:
   * <ul>
   * <li>{@link AlertDefinitionRegistrationEvent} when new alerts are merged
   * from the stack or created from the {@link AlertDefinitionResourceProvider}</li>
   * <li>{@link AlertDefinitionChangedEvent} when alerts are updated.</li>
   * <li>{@link AlertDefinitionDeleteEvent} when alerts are removed</li>
   * </ul>
   */
  @Inject
  private AmbariEventPublisher eventPublisher;

  /**
   * A factory that assists in the creation of {@link AlertDefinition} and
   * {@link AlertDefinitionEntity}.
   */
  @Inject
  private AlertDefinitionFactory alertDefinitionFactory;

  @Inject
  private STOMPUpdatePublisher STOMPUpdatePublisher;

  /**
   * Gets an alert definition with the specified ID.
   *
   * @param definitionId
   *          the ID of the definition to retrieve.
   * @return the alert definition or {@code null} if none exists.
   */
  @RequiresSession
  public AlertDefinitionEntity findById(long definitionId) {
    return entityManagerProvider.get().find(AlertDefinitionEntity.class,
        definitionId);
  }

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

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

    return daoUtils.selectSingle(query);
  }

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

    return daoUtils.selectList(query);
  }

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

    query.setParameter("clusterId", clusterId);

    return daoUtils.selectList(query);
  }

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

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

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

    query.setParameter("definitionIds", definitionIds);

    return daoUtils.selectList(query);
  }

  /**
   * Gets all alert definitions for the given service in the specified cluster.
   *
   * @param clusterId
   *          the ID of the cluster.
   * @param serviceName
   *          the name of the service.
   *
   * @return all alert definitions for the service or empty list if none exist
   *         (never {@code null}).
   */
  @RequiresSession
  public List<AlertDefinitionEntity> findByService(long clusterId,
      String serviceName) {
    TypedQuery<AlertDefinitionEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertDefinitionEntity.findByService", AlertDefinitionEntity.class);

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

    return daoUtils.selectList(query);
  }

  /**
   * Gets all alert definitions for the specified services that do not have a
   * component and do not belong to AGGREGATE source type. These definitions are assumed to be run on the master hosts.
   *
   * @param clusterId
   *          the ID of the cluster.
   * @param services
   *          the services to match on.
   *
   * @return all alert definitions for the services or empty list if none exist
   *         (never {@code null}).
   */
  @RequiresSession
  public List<AlertDefinitionEntity> findByServiceMaster(long clusterId,
      Set<String> services) {
    if (null == services || services.size() == 0) {
      return Collections.emptyList();
    }

    TypedQuery<AlertDefinitionEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertDefinitionEntity.findByServiceMaster",
        AlertDefinitionEntity.class);

    query.setParameter("clusterId", clusterId);
    query.setParameter("services", services);
    query.setParameter("scope", Scope.SERVICE);

    return daoUtils.selectList(query);
  }

  /**
   * Gets all alert definitions that are not bound to a particular service. An
   * example of this type of definition is a host capacity alert.
   *
   * @param clusterId
   *          the ID of the cluster.
   * @param serviceName
   *          the name of the service (not {@code null}).
   * @param componentName
   *          the name of the service component (not {@code null}).
   * @return all alert definitions that are not bound to a service or an empty
   *         list (never {@code null}).
   */
  @RequiresSession
  public List<AlertDefinitionEntity> findByServiceComponent(long clusterId,
      String serviceName, String componentName) {
    if (null == serviceName || null == componentName) {
      return Collections.emptyList();
    }

    TypedQuery<AlertDefinitionEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertDefinitionEntity.findByServiceAndComponent",
        AlertDefinitionEntity.class);

    query.setParameter("clusterId", clusterId);
    query.setParameter("serviceName", serviceName);
    query.setParameter("componentName", componentName);

    return daoUtils.selectList(query);
  }

  /**
   * Gets all alert definitions that are not bound to a particular service. An
   * example of this type of definition is a host capacity alert.
   *
   * @param clusterId
   *          the ID of the cluster.
   * @return all alert definitions that are not bound to a service or an empty
   *         list (never {@code null}).
   */
  @RequiresSession
  public List<AlertDefinitionEntity> findAgentScoped(long clusterId) {
    TypedQuery<AlertDefinitionEntity> query = entityManagerProvider.get().createNamedQuery(
        "AlertDefinitionEntity.findByServiceAndComponent",
        AlertDefinitionEntity.class);

    query.setParameter("clusterId", clusterId);

    query.setParameter("serviceName",
        RootService.AMBARI.name());

    query.setParameter("componentName",
        RootComponent.AMBARI_AGENT.name());

    return daoUtils.selectList(query);
  }

  /**
   * @return all definitions with the given sourceType
   */
  @RequiresSession
  public List<AlertDefinitionEntity> findBySourceType(Long clusterId, SourceType sourceType) {
    return daoUtils.selectList(
      entityManagerProvider.get()
        .createNamedQuery("AlertDefinitionEntity.findBySourceType", AlertDefinitionEntity.class)
        .setParameter("clusterId", clusterId)
        .setParameter("sourceType", sourceType));
  }

  /**
   * Persists a new alert definition, also creating the associated
   * {@link AlertGroupEntity} relationship for the definition's service default
   * group. Fires an {@link AlertDefinitionRegistrationEvent}.
   *
   * @param alertDefinition
   *          the definition to persist (not {@code null}).
   */
  @Transactional
  public void create(AlertDefinitionEntity alertDefinition)
      throws AmbariException {
    EntityManager entityManager = entityManagerProvider.get();
    entityManager.persist(alertDefinition);

    AlertGroupEntity group = dispatchDao.findDefaultServiceGroup(alertDefinition.getClusterId(),
        alertDefinition.getServiceName());

    if (null == group) {
      // create the default alert group for the new service; this MUST be done
      // before adding definitions so that they are properly added to the
      // default group
      String serviceName = alertDefinition.getServiceName();
      group = dispatchDao.createDefaultGroup(alertDefinition.getClusterId(), serviceName);
    }

    group.addAlertDefinition(alertDefinition);
    AlertGroupsUpdateEvent alertGroupsUpdateEvent = new AlertGroupsUpdateEvent(Collections.singletonList(
        new AlertGroupUpdate(group)),
        UpdateEventType.UPDATE);
    STOMPUpdatePublisher.publish(alertGroupsUpdateEvent);
    dispatchDao.merge(group);

    // publish the alert definition registration
    AlertDefinition coerced = alertDefinitionFactory.coerce(alertDefinition);
    if (null != coerced) {
      AlertDefinitionRegistrationEvent event = new AlertDefinitionRegistrationEvent(
          alertDefinition.getClusterId(), coerced);
      eventPublisher.publish(event);
    } else {
      LOG.warn("Unable to broadcast alert registration event for {}",
          alertDefinition.getDefinitionName());
    }

    entityManager.refresh(alertDefinition);
  }

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

  /**
   * Merge the speicified alert definition with the existing definition in the
   * database. Fires an {@link AlertDefinitionChangedEvent}.
   *
   * @param alertDefinition
   *          the definition to merge (not {@code null}).
   * @return the updated definition with merged content (never {@code null}).
   */
  @Transactional
  public AlertDefinitionEntity merge(AlertDefinitionEntity alertDefinition) {
    AlertDefinitionEntity entity = entityManagerProvider.get().merge(alertDefinition);

    AlertDefinition definition = alertDefinitionFactory.coerce(entity);

    AlertDefinitionChangedEvent event = new AlertDefinitionChangedEvent(
        alertDefinition.getClusterId(), definition);

    eventPublisher.publish(event);

    return entity;
  }

  /**
   * Creates or updates the specified entity. This method will check
   * {@link AlertDefinitionEntity#getDefinitionId()} in order to determine
   * whether the entity should be created or merged.
   *
   * @param alertDefinition
   *          the definition to create or update (not {@code null}).
   */
  public void createOrUpdate(AlertDefinitionEntity alertDefinition)
      throws AmbariException {
    if (null == alertDefinition.getDefinitionId()) {
      create(alertDefinition);
    } else {
      merge(alertDefinition);
    }
  }

  /**
   * Removes the specified alert definition and all related history and
   * associations from the database. Fires an {@link AlertDefinitionDeleteEvent}
   * .
   *
   * @param alertDefinition
   *          the definition to remove.
   */
  @Transactional
  public void remove(AlertDefinitionEntity alertDefinition) {
    dispatchDao.removeNoticeByDefinitionId(alertDefinition.getDefinitionId());
    alertsDao.removeByDefinitionId(alertDefinition.getDefinitionId());

    EntityManager entityManager = entityManagerProvider.get();

    alertDefinition = findById(alertDefinition.getDefinitionId());
    if (null != alertDefinition) {
      entityManager.remove(alertDefinition);

      // publish the alert definition removal
      AlertDefinition coerced = alertDefinitionFactory.coerce(alertDefinition);
      if (null != coerced) {
        AlertDefinitionDeleteEvent event = new AlertDefinitionDeleteEvent(
                alertDefinition.getClusterId(), coerced);

        eventPublisher.publish(event);
      } else {
        LOG.warn("Unable to broadcast alert removal event for {}",
                alertDefinition.getDefinitionName());
      }
    }
  }

  /**
   * Removes all {@link AlertDefinitionEntity} that are associated with the
   * specified cluster ID.
   *
   * @param clusterId
   *          the cluster ID.
   */
  @Transactional
  public void removeAll(long clusterId) {
    List<AlertDefinitionEntity> definitions = findAll(clusterId);
    for (AlertDefinitionEntity definition : definitions) {
      remove(definition);
    }
  }
}
