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

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;

import jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.PreRemove;
import jakarta.persistence.QueryHint;
import jakarta.persistence.Table;
import jakarta.persistence.TableGenerator;
import jakarta.persistence.UniqueConstraint;

import org.apache.ambari.server.state.AlertState;
import org.apache.ambari.server.state.alert.Scope;
import org.apache.ambari.server.state.alert.SourceType;

/**
 * The {@link AlertDefinitionEntity} class is used to model an alert that needs
 * to run in the system. Each received alert from an agent will essentially be
 * an instance of this template.
 */
@Entity
@Table(name = "alert_definition", uniqueConstraints = @UniqueConstraint(columnNames = {
  "cluster_id", "definition_name"}))
@TableGenerator(name = "alert_definition_id_generator", table = "ambari_sequences", pkColumnName = "sequence_name", valueColumnName = "sequence_value", pkColumnValue = "alert_definition_id_seq", initialValue = 0)
@NamedQueries({
  @NamedQuery(name = "AlertDefinitionEntity.findAll", query = "SELECT ad FROM AlertDefinitionEntity ad"),
  @NamedQuery(name = "AlertDefinitionEntity.findAllInCluster", query = "SELECT ad FROM AlertDefinitionEntity ad WHERE ad.clusterId = :clusterId"),
  @NamedQuery(name = "AlertDefinitionEntity.findAllEnabledInCluster", query = "SELECT ad FROM AlertDefinitionEntity ad WHERE ad.clusterId = :clusterId AND ad.enabled = 1"),
  @NamedQuery(name = "AlertDefinitionEntity.findByName", query = "SELECT ad FROM AlertDefinitionEntity ad WHERE ad.definitionName = :definitionName AND ad.clusterId = :clusterId",
    hints = {
      @QueryHint(name = "eclipselink.query-results-cache", value = "true"),
      @QueryHint(name = "eclipselink.query-results-cache.ignore-null", value = "true"),
      @QueryHint(name = "eclipselink.query-results-cache.size", value = "5000")
    }),
  @NamedQuery(name = "AlertDefinitionEntity.findByService", query = "SELECT ad FROM AlertDefinitionEntity ad WHERE ad.serviceName = :serviceName AND ad.clusterId = :clusterId"),
  @NamedQuery(name = "AlertDefinitionEntity.findByServiceAndComponent", query = "SELECT ad FROM AlertDefinitionEntity ad WHERE ad.serviceName = :serviceName AND ad.componentName = :componentName AND ad.clusterId = :clusterId"),
  @NamedQuery(name = "AlertDefinitionEntity.findByServiceMaster", query = "SELECT ad FROM AlertDefinitionEntity ad WHERE ad.serviceName IN :services AND ad.scope = :scope AND ad.clusterId = :clusterId AND ad.componentName IS NULL" +
      " AND ad.sourceType <> org.apache.ambari.server.state.alert.SourceType.AGGREGATE"),
  @NamedQuery(name = "AlertDefinitionEntity.findByIds", query = "SELECT ad FROM AlertDefinitionEntity ad WHERE ad.definitionId IN :definitionIds"),
  @NamedQuery(name = "AlertDefinitionEntity.findBySourceType", query = "SELECT ad FROM AlertDefinitionEntity ad WHERE ad.clusterId = :clusterId AND ad.sourceType = :sourceType")})
public class AlertDefinitionEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.TABLE, generator = "alert_definition_id_generator")
  @Column(name = "definition_id", nullable = false, updatable = false)
  private Long definitionId;

  @Lob
  @Basic
  @Column(name = "alert_source", nullable = false, length = 32672)
  private String source;

  @Column(name = "cluster_id", nullable = false)
  private Long clusterId;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "cluster_id", referencedColumnName = "cluster_id", insertable = false, updatable = false)
  private ClusterEntity clusterEntity;

  @Column(name = "component_name", length = 255)
  private String componentName;

  @Column(name = "definition_name", nullable = false, length = 255)
  private String definitionName;

  @Column(name = "label", nullable = true, length = 255)
  private String label;

  @Column(name = "help_url", nullable = true, length = 512)
  private String helpURL;

  @Lob
  @Basic
  @Column(name = "description", nullable = true, length = 32672)
  private String description;

  @Column(name = "scope", length = 255)
  @Enumerated(value = EnumType.STRING)
  private Scope scope;

  @Column(nullable = false)
  private Integer enabled = Integer.valueOf(1);

  @Column(nullable = false, length = 64)
  private String hash;

  @Column(name = "schedule_interval", nullable = false)
  private Integer scheduleInterval;

  @Column(name = "service_name", nullable = false, length = 255)
  private String serviceName;

  @Column(name = "source_type", nullable = false, length = 255)
  @Enumerated(value = EnumType.STRING)
  private SourceType sourceType;

  @Column(name = "ignore_host", nullable = false)
  private Integer ignoreHost = Integer.valueOf(0);

  /**
   * Indicates how many sequential alerts must be received for a non-OK state
   * change to be considered correct. This value is meant to eliminate
   * false-positive notifications on alerts which are flaky.
   */
  @Column(name = "repeat_tolerance", nullable = false)
  private Integer repeatTolerance = Integer.valueOf(1);

  /**
   * If {@code 1}, then the value of {@link #repeatTolerance} is used to
   * override the global alert tolerance value.
   */
  @Column(name = "repeat_tolerance_enabled", nullable = false)
  private Short repeatToleranceEnabled = Short.valueOf((short) 0);

  /**
   * Bi-directional many-to-many association to {@link AlertGroupEntity}
   */
  @ManyToMany(mappedBy = "alertDefinitions", cascade = {CascadeType.PERSIST,
    CascadeType.MERGE, CascadeType.REFRESH})
  private Set<AlertGroupEntity> alertGroups;

  /**
   * Constructor.
   */
  public AlertDefinitionEntity() {
  }

  /**
   * Gets the unique identifier for this alert definition.
   *
   * @return the ID.
   */
  public Long getDefinitionId() {
    return definitionId;
  }

  /**
   * Sets the unique identifier for this alert definition.
   *
   * @param definitionId the ID (not {@code null}).
   */
  public void setDefinitionId(Long definitionId) {
    this.definitionId = definitionId;
  }

  /**
   * Gets the source that defines the type of alert and the alert properties.
   * This is typically a JSON structure that can be mapped to a first-class
   * object.
   *
   * @return the alert source (never {@code null}).
   */
  public String getSource() {
    return source;
  }

  /**
   * Sets the source of the alert, typically in JSON, that defines the type of
   * the alert and its properties.
   *
   * @param alertSource the alert source (not {@code null}).
   */
  public void setSource(String alertSource) {
    source = alertSource;
  }

  /**
   * Gets the ID of the cluster that this alert definition is created for. Each
   * cluster has their own set of alert definitions that are not shared with any
   * other cluster.
   *
   * @return the ID of the cluster (never {@code null}).
   */
  public Long getClusterId() {
    return clusterId;
  }

  /**
   * Sets the ID of the cluster that this alert definition is created for. Each
   * cluster has their own set of alert definitions that are not shared with any
   * other cluster.
   *
   * @param clusterId the ID of the cluster (not {@code null}).
   */
  public void setClusterId(Long clusterId) {
    this.clusterId = clusterId;
  }

  /**
   * Gets the cluster that this alert definition is a member of.
   *
   * @return
   */
  public ClusterEntity getCluster() {
    return clusterEntity;
  }

  /**
   * Sets the cluster that the alert definition is a member of.
   *
   * @param clusterEntity
   */
  public void setCluster(ClusterEntity clusterEntity) {
    this.clusterEntity = clusterEntity;
  }

  /**
   * Gets the component name that this alert is associated with, if any. Some
   * alerts are scoped at the service level and will not have a component name.
   *
   * @return the component name or {@code null} if none.
   */
  public String getComponentName() {
    return componentName;
  }

  /**
   * Sets the component name that this alert is associated with, if any. Some
   * alerts are scoped at the service level and will not have a component name.
   *
   * @param componentName the component name or {@code null} if none.
   */
  public void setComponentName(String componentName) {
    this.componentName = componentName;
  }

  /**
   * Gets the scope of the alert definition. The scope is defined as either
   * being for a {@link Scope#SERVICE} or {@link Scope#HOST}.
   *
   * @return the scope, or {@code null} if not defined.
   */
  public Scope getScope() {
    return scope;
  }

  /**
   * Sets the scope of the alert definition. The scope is defined as either
   * being for a {@link Scope#SERVICE} or {@link Scope#HOST}.
   *
   * @param scope the scope to set, or {@code null} for none.
   */
  public void setScope(Scope scope) {
    this.scope = scope;
  }

  /**
   * Gets the name of this alert definition. Alert definition names are unique
   * within a cluster.
   *
   * @return the name of the alert definition (never {@code null}).
   */
  public String getDefinitionName() {
    return definitionName;
  }

  /**
   * Sets the name of this alert definition. Alert definition names are unique
   * within a cluster.
   *
   * @param definitionName the name of the alert definition (not {@code null}).
   */
  public void setDefinitionName(String definitionName) {
    this.definitionName = definitionName;
  }

  /**
   * Gets whether this alert definition is enabled. Disabling an alert
   * definition will prevent agents from scheduling the alerts. No alerts will
   * be triggered and no alert data will be collected.
   *
   * @return {@code true} if this alert definition is enabled, {@code false}
   * otherwise.
   */
  public boolean getEnabled() {
    return !Objects.equals(enabled, 0);
  }

  /**
   * Sets whether this alert definition is enabled.
   *
   * @param enabled {@code true} if this alert definition is enabled, {@code false}
   *                otherwise.
   */
  public void setEnabled(boolean enabled) {
    this.enabled = enabled ? Integer.valueOf(1) : Integer.valueOf(0);
  }

  /**
   * Gets whether this alert definition will ignore the hosts reporting the
   * alert and combine them all into a single alert entry.
   *
   * @return {@code true} if this alert definition is to ignore hosts and
   * combine all alert instances into a single entry, {@code false}
   * otherwise.
   */
  public boolean isHostIgnored() {
    return !Objects.equals(ignoreHost, 0);
  }

  /**
   * Sets whether this alert definition will ignore the hosts reporting the
   * alert and combine them all into a single alert entry.
   *
   * @param ignoreHost {@code true} if this alert definition is to ignore hosts and
   *                   combine all alert instances into a single entry, {@code false}
   *                   otherwise.
   */
  public void setHostIgnored(boolean ignoreHost) {
    this.ignoreHost = ignoreHost ? Integer.valueOf(1) : Integer.valueOf(0);
  }

  /**
   * Gets the unique hash for the current state of this definition. If a
   * property of this definition changes, a new hash is calculated.
   *
   * @return the unique hash or {@code null} if there is none.
   */
  public String getHash() {
    return hash;
  }

  /**
   * Gets the unique hash for the current state of this definition. If a
   * property of this definition changes, a new hash is calculated.
   *
   * @param hash the unique hash to set or {@code null} for none.
   */
  public void setHash(String hash) {
    this.hash = hash;
  }

  /**
   * Gets the alert trigger interval, in seconds.
   *
   * @return the interval, in seconds.
   */
  public Integer getScheduleInterval() {
    return scheduleInterval;
  }

  /**
   * Sets the alert trigger interval, in seconds.
   *
   * @param scheduleInterval the interval, in seconds.
   */
  public void setScheduleInterval(Integer scheduleInterval) {
    this.scheduleInterval = scheduleInterval;
  }

  /**
   * Gets the name of the service that this alert definition is associated with.
   * Every alert definition is associated with exactly one service.
   *
   * @return the name of the service (never {@code null}).
   */
  public String getServiceName() {
    return serviceName;
  }

  /**
   * Gets the name of the service that this alert definition is associated with.
   * Every alert definition is associated with exactly one service.
   *
   * @param serviceName the name of the service (not {@code null}).
   */
  public void setServiceName(String serviceName) {
    this.serviceName = serviceName;
  }

  /**
   * @return
   */
  public SourceType getSourceType() {
    return sourceType;
  }

  /**
   * @param sourceType
   */
  public void setSourceType(SourceType sourceType) {
    this.sourceType = sourceType;
  }

  /**
   * Gets the alert groups that this alert definition is associated with.
   *
   * @return the groups, or {@code null} if none.
   */
  public Set<AlertGroupEntity> getAlertGroups() {
    return Collections.unmodifiableSet(alertGroups);
  }

  /**
   * Sets the alert groups that this alert definition is associated with.
   *
   * @param alertGroups the groups, or {@code null} for none.
   */
  public void setAlertGroups(Set<AlertGroupEntity> alertGroups) {
    this.alertGroups = alertGroups;
  }

  /**
   * Sets a human readable label for this alert definition.
   *
   * @param label the label or {@code null} if none.
   */
  public void setLabel(String label) {
    this.label = label;
  }

  /**
   * Gets the label for this alert definition.
   *
   * @return the label or {@code null} if none.
   */
  public String getLabel() {
    return label;
  }

  /**
   * Gets the help url for this alert.
   *
   * @return the helpURL or {@code null} if none.
   */
  public String getHelpURL() {
    return helpURL;
  }

  /**
   * Sets a help url for this alert.
   *
   * @param helpURL the helpURL or {@code null} if none.
   */
  public void setHelpURL(String helpURL) {
    this.helpURL = helpURL;
  }

  /**
   * Gets the optional description for this alert definition.
   *
   * @return the description, or {@code null} if none.
   */
  public String getDescription() {
    return description;
  }

  /**
   * Gets the optional description for this alert definition.
   *
   * @param description the description to set or {@code null} for none.
   */
  public void setDescription(String description) {
    this.description = description;
  }

  /**
   * Gets the number of sequential instances of an non-OK {@link AlertState}
   * must be received in order to consider the alert instance as truly being in
   * that {@link AlertState}.
   * <p/>
   * A value of 1 or less indicates that there is no tolerance and a single
   * alert will cause dispatching of alert notifications.
   *
   * @return the repeatTolerance
   */
  public int getRepeatTolerance() {
    return repeatTolerance;
  }

  /**
   * Sets the number of sequential instances of an non-OK {@link AlertState}
   * must be received in order to consider the alert instance as truly being in
   * that {@link AlertState}.
   * <p/>
   * A value of 1 or less indicates that there is no tolerance and a single
   * alert will cause dispatching of alert notifications.
   *
   * @param repeatTolerance
   *          the tolerance to set
   */
  public void setRepeatTolerance(int repeatTolerance) {
    this.repeatTolerance = repeatTolerance;
  }

  /**
   * Gets whether this definition overrides the default global tolerance value
   * specified in {@code cluster-env/alerts.repeat.tolerance}. If enabled, the
   * value from {@link #getRepeatTolerance()} should be used to calculate retry
   * tolerance.
   *
   * @return the repeatToleranceEnabled {@code true} to override the global
   *         value.
   */
  public boolean isRepeatToleranceEnabled() {
    return !Objects.equals(repeatToleranceEnabled, (short) 0);
  }

  /**
   * Sets whether this definition overrides the default global tolerance value
   * specified in {@code cluster-env/alerts.repeat.tolerance}. If enabled, the
   * value from {@link #getRepeatTolerance()} should be used to calculate retry
   * tolerance.
   *
   * @param enabled
   *          {@code true} to override the defautlt value and use the value
   *          returned from {@link #getRepeatTolerance()}.
   */
  public void setRepeatToleranceEnabled(boolean enabled) {
    repeatToleranceEnabled = enabled ? Short.valueOf((short) 1) : 0;
  }

  /**
   * Adds the specified alert group to the groups that this definition is
   * associated with. This is used to complement the JPA bidirectional
   * association.
   *
   * @param alertGroup
   */
  protected void addAlertGroup(AlertGroupEntity alertGroup) {
    if (null == alertGroups) {
      alertGroups = new HashSet<>();
    }

    alertGroups.add(alertGroup);
  }

  /**
   * Removes the specified alert group to the groups that this definition is
   * associated with. This is used to complement the JPA bidirectional
   * association.
   *
   * @param alertGroup
   */
  protected void removeAlertGroup(AlertGroupEntity alertGroup) {
    if (null != alertGroups && alertGroups.contains(alertGroup)) {
      alertGroups.remove(alertGroup);
    }
  }

  /**
   * Called before {@link EntityManager#remove(Object)} for this entity, removes
   * the non-owning relationship between definitions and groups.
   */
  @PreRemove
  public void preRemove() {
    if (null == alertGroups || alertGroups.size() == 0) {
      return;
    }

    Iterator<AlertGroupEntity> iterator = alertGroups.iterator();
    while (iterator.hasNext()) {
      AlertGroupEntity group = iterator.next();
      iterator.remove();

      group.removeAlertDefinition(this);
    }
  }

  /**
   * Gets the equality to another historical alert entry based on the following criteria:
   * <ul>
   * <li>{@link #definitionId}
   * <li>{@link #clusterId}
   * <li>{@link #definitionName}
   * </ul>
   * <p/>
   * However, since we're guaranteed that {@link #definitionId} is unique among persisted entities, we
   * can return the hashCode based on this value if it is set.
   * <p/>
   * {@inheritDoc}
   */
  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }

    if (object == null || getClass() != object.getClass()) {
      return false;
    }

    AlertDefinitionEntity that = (AlertDefinitionEntity) object;

    // use the unique ID if it exists
    if( null != definitionId ){
      return Objects.equals(definitionId, that.definitionId);
    }

    return Objects.equals(definitionId, that.definitionId) &&
        Objects.equals(clusterId, that.clusterId) &&
        Objects.equals(definitionName, that.definitionName);
  }

  /**
   * Gets a hash to uniquely identify this alert definition. Since we're
   * guaranteed that {@link #definitionId} is unique among persisted entities,
   * we can return the hashCode based on this value if it is set.
   * <p/>
   * {@inheritDoc}
   */
  @Override
  public int hashCode() {
    // use the unique ID if it exists
    if( null != definitionId ){
      return definitionId.hashCode();
    }

    return Objects.hash(definitionId, clusterId, definitionName);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    StringBuilder buffer = new StringBuilder();
    buffer.append(getClass().getSimpleName());
    buffer.append("{");
    buffer.append("id=").append(definitionId);
    buffer.append(", name=").append(definitionName);
    buffer.append(", serviceName=").append(serviceName);
    buffer.append(", componentName=").append(componentName);
    buffer.append(", enabled=").append(enabled);
    buffer.append(", hash=").append(hash);
    buffer.append("}");
    return buffer.toString();
  }
}
