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

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
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 java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import org.apache.ambari.server.AmbariService;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.events.AlertEvent;
import org.apache.ambari.server.notifications.DispatchCallback;
import org.apache.ambari.server.notifications.DispatchCredentials;
import org.apache.ambari.server.notifications.DispatchFactory;
import org.apache.ambari.server.notifications.DispatchRunnable;
import org.apache.ambari.server.notifications.Notification;
import org.apache.ambari.server.notifications.NotificationDispatcher;
import org.apache.ambari.server.notifications.Recipient;
import org.apache.ambari.server.orm.dao.AlertDispatchDAO;
import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
import org.apache.ambari.server.orm.entities.AlertHistoryEntity;
import org.apache.ambari.server.orm.entities.AlertNoticeEntity;
import org.apache.ambari.server.orm.entities.AlertTargetEntity;
import org.apache.ambari.server.state.AlertState;
import org.apache.ambari.server.state.NotificationState;
import org.apache.ambari.server.state.alert.AlertNotification;
import org.apache.commons.io.IOUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.util.concurrent.AbstractScheduledService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Provider;

/**
 * The {@link AlertNoticeDispatchService} is used to scan the database for
 * {@link AlertNoticeEntity} that are in the {@link NotificationState#PENDING}
 * state. It will then process them through the dispatch system.
 * <p/>
 * The dispatch system will then make a callback to
 * {@link AlertNoticeDispatchCallback} so that the {@link NotificationState} can
 * be updated to its final value.
 * <p/>
 * This class uses the templates that are defined via
 * {@link Configuration#getAlertTemplateFile()} or the fallback internal
 * template {@code alert-templates.xml}. These files are parsed during
 * {@link #startUp()}. If there is a problem parsing them, the service will
 * still startup normally, producing an error in logs. It will fall back to
 * simple string concatenation for {@link Notification} content in this case.
 */
@AmbariService
public class AlertNoticeDispatchService extends AbstractScheduledService {
  /**
   * Logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(AlertNoticeDispatchService.class);

  /**
   * The log tag to pass to Apache Velocity during rendering.
   */
  private static final String VELOCITY_LOG_TAG = "ambari-alerts";

  /**
   * The internal Ambari templates that ship.
   */
  private static final String AMBARI_ALERT_TEMPLATES = "alert-templates.xml";

  /**
   * The property containing the dispatch authentication username.
   */
  public static final String AMBARI_DISPATCH_CREDENTIAL_USERNAME = "ambari.dispatch.credential.username";

  /**
   * The property containing the dispatch authentication password.
   */
  public static final String AMBARI_DISPATCH_CREDENTIAL_PASSWORD = "ambari.dispatch.credential.password";

  /**
   * The property containing the dispatch recipients
   */
  public static final String AMBARI_DISPATCH_RECIPIENTS = "ambari.dispatch.recipients";

  /**
   * The context key for Ambari information to be passed to Velocity.
   */
  private static final String VELOCITY_AMBARI_KEY = "ambari";

  /**
   * The context key for alert summary information to be passed to Velocity.
   */
  private static final String VELOCITY_SUMMARY_KEY = "summary";

  /**
   * The context key for a single alert's information to be passed to Velocity.
   */
  private static final String VELOCITY_ALERT_KEY = "alert";

  /**
   * The context key for dispatch target information to be passed to Velocity.
   */
  private static final String VELOCITY_DISPATCH_KEY = "dispatch";

  /**
   * Gson used to convert JSON properties to a map.
   */
  private final Gson m_gson;

  /**
   * Dispatch DAO to query pending {@link AlertNoticeEntity} instances from.
   */
  @Inject
  private AlertDispatchDAO m_dao;

  /**
   * The factory used to get an {@link NotificationDispatcher} instance to
   * submit to the {@link #m_executor}.
   */
  @Inject
  private DispatchFactory m_dispatchFactory;

  /**
   * The alert templates to use when rendering content for a
   * {@link Notification}.
   */
  private AlertTemplates m_alertTemplates;

  /**
   * The configuration instance to get Ambari properties.
   */
  @Inject
  private Configuration m_configuration;

  /**
   * Ambari meta information used fro alert {@link Notification}s.
   */
  @Inject
  private Provider<AmbariMetaInfo> m_metaInfo;

  /**
   * The executor responsible for dispatching.
   */
  private Executor m_executor;

  /**
   * Constructor.
   */
  public AlertNoticeDispatchService() {
    m_executor = new ThreadPoolExecutor(0, 2, 5L, TimeUnit.MINUTES,
      new LinkedBlockingQueue<>(), new AlertDispatchThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy());

    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(AlertTargetProperties.class,
        new AlertTargetPropertyDeserializer());

    m_gson = gsonBuilder.create();
  }

  /**
   * {@inheritDoc}
   * <p/>
   * Parse the XML template for {@link Notification} content. If there is a
   * problem parsing the content, the service will still startup normally but
   * the {@link Notification} content will fallback to plaintext.
   */
  @Override
  protected void startUp() throws Exception {
    super.startUp();

    InputStream inputStream = null;
    String alertTemplatesFile = null;

    try {
      alertTemplatesFile = m_configuration.getAlertTemplateFile();
      if (null != alertTemplatesFile) {
        File file = new File(alertTemplatesFile);
        inputStream = new FileInputStream(file);
      }
    } catch (Exception exception) {
      LOG.warn("Unable to load alert template file {}", alertTemplatesFile,
          exception);
    }

    try {
      JAXBContext context = JAXBContext.newInstance(AlertTemplates.class);
      Unmarshaller unmarshaller = context.createUnmarshaller();

      // if the file provided via the configuration is not available, use
      // the internal one
      if (null == inputStream) {
        inputStream = ClassLoader.getSystemResourceAsStream(AMBARI_ALERT_TEMPLATES);
      }

      m_alertTemplates = (AlertTemplates) unmarshaller.unmarshal(inputStream);
    } catch (Exception exception) {
      LOG.error(
          "Unable to load alert template file {}, outbound notifications will not be formatted",
          AMBARI_ALERT_TEMPLATES, exception);
    } finally {
      if (null != inputStream) {
        IOUtils.closeQuietly(inputStream);
      }
    }
  }

  /**
   * Sets the {@link Executor} to use when dispatching {@link Notification}s.
   * This should only be used by unit tests to provide a mock executor.
   *
   * @param executor
   *          the executor to use (not {@code null).

   */
  protected void setExecutor(Executor executor) {
    m_executor = executor;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void runOneIteration() throws Exception {
    Map<AlertTargetEntity, List<AlertNoticeEntity>> aggregateMap;
    try {
      aggregateMap = getGroupedNotices();
    } catch (Exception e) {
      LOG.error("Caught exception during alert notices preparing.", e);
      return;
    }

    // now that all of the notices are grouped by target, dispatch them
    Set<AlertTargetEntity> targets = aggregateMap.keySet();
    for (AlertTargetEntity target : targets) {
      List<AlertNoticeEntity> notices = aggregateMap.get(target);
      if (null == notices || notices.size() == 0) {
        continue;
      }
      try {
        String targetType = target.getNotificationType();
        NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(targetType);

        // create a single digest notification if supported
        if (dispatcher.isDigestSupported()) {
          createSingleNotice(dispatcher, target, notices);
        } else {
          createSeparateNotices(dispatcher, target, notices);
        }
      } catch (Exception e) {
        LOG.error("Caught exception during Alert Notice dispatching.", e);
      }
    }
  }

  /**
   * Retrieves all pending notices to dispatch and groups them by target.
   * @return grouped notices.
   */
  private Map<AlertTargetEntity, List<AlertNoticeEntity>> getGroupedNotices() {
    List<AlertNoticeEntity> pending = m_dao.findPendingNotices();
    if (pending.size() == 0) {
      return Collections.EMPTY_MAP;
    }

    LOG.info("There are {} pending alert notices about to be dispatched...",
        pending.size());

    Map<AlertTargetEntity, List<AlertNoticeEntity>> aggregateMap = new HashMap<>(pending.size());

    // combine all histories by target
    for (AlertNoticeEntity notice : pending) {
      AlertTargetEntity target = notice.getAlertTarget();

      List<AlertNoticeEntity> notices = aggregateMap.get(target);
      if (null == notices) {
        notices = new ArrayList<>();
        aggregateMap.put(target, notices);
      }

      // at this point, notices have been processed but not yet delivered
      notice.setNotifyState(NotificationState.DISPATCHED);
      notice = m_dao.merge(notice);

      notices.add(notice);
    }
    return aggregateMap;
  }

  /**
   * Creates a single digest notification. Should be used when dispatcher supports digest.
   * @param dispatcher dispatches the created notifications.
   * @param target where notifications will be sent.
   * @param notices notices should be dispatched.
   */
  private void createSingleNotice(NotificationDispatcher dispatcher, AlertTargetEntity target,
                                           List<AlertNoticeEntity> notices) {
    AlertNotification notification = buildNotificationFromTarget(target);
    notification.CallbackIds = new ArrayList<>(notices.size());
    List<AlertHistoryEntity> histories = new ArrayList<>(
        notices.size());

    // add callback IDs so that the notices can be marked as DELIVERED or
    // FAILED, and create a list of just the alert histories
    for (AlertNoticeEntity notice : notices) {
      AlertHistoryEntity history = notice.getAlertHistory();
      histories.add(history);

      notification.CallbackIds.add(notice.getUuid());
    }


    // populate the subject and body fields; if there is a problem
    // generating the content, then mark the notices as FAILED
    try {
      renderDigestNotificationContent(dispatcher, notification, histories, target);

      // dispatch
      DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
      m_executor.execute(runnable);
    } catch (Exception exception) {
      LOG.error("Unable to create notification for alerts", exception);

      // there was a problem generating content for the target; mark all
      // notices as FAILED and skip this target
      // mark these as failed
      notification.Callback.onFailure(notification.CallbackIds);
    }
  }

  /**
   * If the dispatcher does not support digest, each notice must have a 1:1 notification created for it.
   * @param dispatcher dispatches the created notifications.
   * @param target where notifications will be sent.
   * @param notices notices should be dispatched.
   */
  private void createSeparateNotices(NotificationDispatcher dispatcher, AlertTargetEntity target,
                                      List<AlertNoticeEntity> notices) {
    for (AlertNoticeEntity notice : notices) {
      AlertNotification notification = buildNotificationFromTarget(target);
      AlertHistoryEntity history = notice.getAlertHistory();
      notification.CallbackIds = Collections.singletonList(notice.getUuid());

      // populate the subject and body fields; if there is a problem
      // generating the content, then mark the notices as FAILED
      try {
        renderNotificationContent(dispatcher, notification, history, target);

        // dispatch
        DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
        m_executor.execute(runnable);
      } catch (Exception exception) {
        LOG.error("Unable to create notification for alert", exception);

        // mark these as failed
        notification.Callback.onFailure(notification.CallbackIds);
      }
    }
  }

  /**
   * {@inheritDoc}
   * <p/>
   * Returns a schedule that starts after 2 minute and runs every 2 minutes
   * after {@link #runOneIteration()} completes.
   */
  @Override
  protected Scheduler scheduler() {
    return Scheduler.newFixedDelaySchedule(2, 2, TimeUnit.MINUTES);
  }

  /**
   * Initializes a {@link Notification} instance from an
   * {@link AlertTargetEntity}. This method does most of the boilerplate work to
   * get a {@link Notification} that is almost ready to send.
   * <p/>
   * The {@link Notification} will not have any of the callback IDs or content
   * set.
   *
   * @param target
   *          the alert target
   * @return the initialized notification
   */
  private AlertNotification buildNotificationFromTarget(AlertTargetEntity target) {
    String propertiesJson = target.getProperties();

    AlertTargetProperties targetProperties = m_gson.fromJson(propertiesJson,
        AlertTargetProperties.class);

    Map<String, String> properties = targetProperties.Properties;

    // create an initialize the notification
    AlertNotification notification = new AlertNotification();
    notification.Callback = new AlertNoticeDispatchCallback();
    notification.DispatchProperties = properties;

    // set dispatch credentials
    if (properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_USERNAME)
        && properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_PASSWORD)) {
      DispatchCredentials credentials = new DispatchCredentials();
      credentials.UserName = properties.get(AMBARI_DISPATCH_CREDENTIAL_USERNAME);
      credentials.Password = properties.get(AMBARI_DISPATCH_CREDENTIAL_PASSWORD);
      notification.Credentials = credentials;
    }

    // create recipients
    if (null != targetProperties.Recipients) {
      List<Recipient> recipients = new ArrayList<>(
        targetProperties.Recipients.size());

      for (String stringRecipient : targetProperties.Recipients) {
        Recipient recipient = new Recipient();
        recipient.Identifier = stringRecipient;
        recipients.add(recipient);
      }

      notification.Recipients = recipients;
    }

    return notification;
  }

  /**
   * Generates digest content for the {@link Notification} using the
   * {@link #m_alertTemplates} and the list of alerts passed in. If there is a
   * problem with the templates, this will fallback to non-formatted content.
   *
   * @param dispatcher
   *          the dispatcher for this notification type (not {@code null}).
   * @param notification
   *          the notification (not {@code null}).
   * @param histories
   *          the alerts to generate the content from (not {@code null}.
   * @param target
   *          the target of the {@link Notification}.
   */
  private void renderDigestNotificationContent(NotificationDispatcher dispatcher,
      AlertNotification notification, List<AlertHistoryEntity> histories, AlertTargetEntity target)
      throws IOException {
    String targetType = target.getNotificationType();

    // build the velocity objects for template rendering
    AmbariInfo ambari = new AmbariInfo(m_metaInfo.get(), m_configuration);
    AlertSummaryInfo summary = new AlertSummaryInfo(histories);
    DispatchInfo dispatch = new DispatchInfo(target);

    // get the template for this target type
    final Writer subjectWriter = new StringWriter();
    final Writer bodyWriter = new StringWriter();
    final AlertTemplate template = m_alertTemplates.getTemplate(targetType);

    if (dispatcher.isNotificationContentGenerationRequired()) {
      if (null != template) {
        // create the velocity context for template rendering
        VelocityContext velocityContext = new VelocityContext();
        velocityContext.put(VELOCITY_AMBARI_KEY, ambari);
        velocityContext.put(VELOCITY_SUMMARY_KEY, summary);
        velocityContext.put(VELOCITY_DISPATCH_KEY, dispatch);

        // render the template and assign the content to the notification
        String subjectTemplate = template.getSubject();
        String bodyTemplate = template.getBody();

        // render the subject
        Velocity.evaluate(velocityContext, subjectWriter, VELOCITY_LOG_TAG, subjectTemplate);

        // render the body
        Velocity.evaluate(velocityContext, bodyWriter, VELOCITY_LOG_TAG, bodyTemplate);
      } else {
        // a null template is possible from parsing incorrectly or not
        // having the correct type defined for the target
        for (AlertHistoryEntity alert : histories) {
          subjectWriter.write("Apache Ambari Alert Summary");
          bodyWriter.write(alert.getAlertState().name());
          bodyWriter.write(" ");
          bodyWriter.write(alert.getAlertDefinition().getLabel());
          bodyWriter.write(" ");
          bodyWriter.write(alert.getAlertText());
          bodyWriter.write("\n");
        }
      }
    }

    notification.Subject = subjectWriter.toString();
    notification.Body = bodyWriter.toString();
  }

  /**
   * Generates the content for the {@link Notification} using the
   * {@link #m_alertTemplates} and the single alert passed in. If there is a
   * problem with the templates, this will fallback to non-formatted content.
   *
   * @param dispatcher
   *          the dispatcher for this notification type (not {@code null}).
   * @param notification
   *          the notification (not {@code null}).
   * @param history
   *          the alert to generate the content from (not {@code null}.
   * @param target
   *          the target of the {@link Notification}.
   */
  private void renderNotificationContent(NotificationDispatcher dispatcher,
      AlertNotification notification, AlertHistoryEntity history, AlertTargetEntity target)
      throws IOException {
    String targetType = target.getNotificationType();

    // build the velocity objects for template rendering
    AmbariInfo ambari = new AmbariInfo(m_metaInfo.get(), m_configuration);
    AlertInfo alert = new AlertInfo(history);
    DispatchInfo dispatch = new DispatchInfo(target);

    // set the alert info on the notification subclass so that dispatchers
    // can use it directly
    notification.setAlertInfo(alert);

    // get the template for this target type
    final Writer subjectWriter = new StringWriter();
    final Writer bodyWriter = new StringWriter();
    final AlertTemplate template = m_alertTemplates.getTemplate(targetType);

    if (dispatcher.isNotificationContentGenerationRequired()) {
      if (null != template) {
        // create the velocity context for template rendering
        VelocityContext velocityContext = new VelocityContext();
        velocityContext.put(VELOCITY_AMBARI_KEY, ambari);
        velocityContext.put(VELOCITY_ALERT_KEY, alert);
        velocityContext.put(VELOCITY_DISPATCH_KEY, dispatch);

        // render the template and assign the content to the notification
        String subjectTemplate = template.getSubject();
        String bodyTemplate = template.getBody();

        // render the subject
        Velocity.evaluate(velocityContext, subjectWriter, VELOCITY_LOG_TAG, subjectTemplate);

        // render the body
        Velocity.evaluate(velocityContext, bodyWriter, VELOCITY_LOG_TAG, bodyTemplate);
      } else {
        // a null template is possible from parsing incorrectly or not
        // having the correct type defined for the target
        subjectWriter.write(alert.getAlertState().name());
        subjectWriter.write(" ");
        subjectWriter.write(alert.getAlertName());

        bodyWriter.write(alert.getAlertState().name());
        bodyWriter.write(" ");
        bodyWriter.write(alert.getAlertName());
        bodyWriter.write(" ");
        bodyWriter.write(alert.getAlertText());
        if (alert.hasHostName()) {
          bodyWriter.write(" ");
          bodyWriter.append(alert.getHostName());
        }
        bodyWriter.write("\n");
      }
    }

    notification.Subject = subjectWriter.toString();
    notification.Body = bodyWriter.toString();
  }

  /**
   * The {@link AlertTargetProperties} separates out the dispatcher properties
   * from the list of recipients which is a JSON array and not a String.
   */
  private static final class AlertTargetProperties {
    /**
     * The properties to pass to the concrete dispatcher.
     */
    public Map<String, String> Properties;

    /**
     * The recipients of the notice.
     */
    public List<String> Recipients;
  }

  /**
   * The {@link AlertTargetPropertyDeserializer} is used to dump the majority of
   * JSON serialized properties into a {@link Map} of {@link String} while at
   * the same time, converting
   * {@link AlertNoticeDispatchService#AMBARI_DISPATCH_RECIPIENTS} into a list.
   */
  private static final class AlertTargetPropertyDeserializer implements
      JsonDeserializer<AlertTargetProperties> {

    /**
     * {@inheritDoc}
     */
    @Override
    public AlertTargetProperties deserialize(JsonElement json, Type typeOfT,
        JsonDeserializationContext context) throws JsonParseException {

      AlertTargetProperties properties = new AlertTargetProperties();
      properties.Properties = new HashMap<>();

      final JsonObject jsonObject = json.getAsJsonObject();
      Set<Entry<String, JsonElement>> entrySet = jsonObject.entrySet();

      for (Entry<String, JsonElement> entry : entrySet) {
        String entryKey = entry.getKey();
        JsonElement entryValue = entry.getValue();

        if (entryKey.equals(AMBARI_DISPATCH_RECIPIENTS)) {
          Type listType = new TypeToken<List<String>>() {
          }.getType();
          JsonArray jsonArray = entryValue.getAsJsonArray();
          properties.Recipients = context.deserialize(jsonArray, listType);
        } else {
          properties.Properties.put(entryKey, entryValue.getAsString());
        }
      }

      return properties;
    }
  }

  /**
   * A custom {@link ThreadFactory} for the threads that will handle dispatching
   * {@link AlertNoticeEntity} instances. Threads created will have slightly
   * reduced priority since {@link AlertEvent} instances are not critical to the
   * system.
   */
  private static final class AlertDispatchThreadFactory implements
      ThreadFactory {

    private static final AtomicInteger s_threadIdPool = new AtomicInteger(1);

    /**
     * {@inheritDoc}
     */
    @Override
    public Thread newThread(Runnable r) {
      Thread thread = new Thread(r, "alert-dispatch-"
          + s_threadIdPool.getAndIncrement());

      thread.setDaemon(false);
      thread.setPriority(Thread.NORM_PRIORITY - 1);

      return thread;
    }
  }

  /**
   * The {@link AlertNoticeDispatchCallback} is used to receive a callback from
   * the dispatch framework and then update the {@link AlertNoticeEntity}
   * {@link NotificationState}.
   */
  private final class AlertNoticeDispatchCallback implements DispatchCallback {

    /**
     * {@inheritDoc}
     */
    @Override
    public void onSuccess(List<String> callbackIds) {
      for (String callbackId : callbackIds) {
        updateAlertNotice(callbackId, NotificationState.DELIVERED);
      }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onFailure(List<String> callbackIds) {
      for (String callbackId : callbackIds) {
        updateAlertNotice(callbackId, NotificationState.FAILED);
      }
    }

    /**
     * Updates the {@link AlertNoticeEntity} matching the given UUID with the
     * specified state.
     *
     * @param uuid
     * @param state
     */
    private void updateAlertNotice(String uuid, NotificationState state) {
      try {
        AlertNoticeEntity entity = m_dao.findNoticeByUuid(uuid);
        if (null == entity) {
          LOG.warn("Unable to find an alert notice with UUID {}", uuid);
          return;
        }

        entity.setNotifyState(state);
        m_dao.merge(entity);
      } catch (Exception exception) {
        LOG.error(
            "Unable to update the alert notice with UUID {} to {}, notifications will continue to be sent",
            uuid, state, exception);
      }
    }
  }

  /**
   * The {@link AlertInfo} class encapsulates all information about a single
   * alert for a single outbound {@link Notification}.
   */
  public final static class AlertInfo {
    private final AlertHistoryEntity m_history;

    /**
     * Constructor.
     *
     * @param history
     */
    public AlertInfo(AlertHistoryEntity history) {
      m_history = history;
    }

    /**
     * Gets the host name or {@code null} if none.
     *
     * @return
     */
    public String getHostName() {
      return m_history.getHostName();
    }

    /**
     * Gets whether there is a host associated with the alert. Some alerts like
     * aggregate alerts don't have hosts.
     *
     * @return
     */
    public boolean hasHostName() {
      return m_history.getHostName() != null;
    }

    /**
     * Gets the service name or {@code null} if none.
     *
     * @return
     */
    public String getServiceName() {
      return m_history.getServiceName();
    }

    /**
     * Gets the service component name, or {@code null} if none.
     *
     * @return
     */
    public String getComponentName() {
      return m_history.getComponentName();
    }

    /**
     * Gets whether there is a component associated with an alert. Some alerts
     * don't have an associated component.
     *
     * @return
     */
    public boolean hasComponentName() {
      return m_history.getComponentName() != null;
    }

    /**
     *  Gets the time that the alert was received
     * @return
       */
    public long getAlertTimestamp() {
      return m_history.getAlertTimestamp();
    }

    /**
     * Gets the state of the alert.
     *
     * @return
     */
    public AlertState getAlertState() {
      return m_history.getAlertState();
    }

    /**
     * Gets the definition id of the alert.
     *
     * @return
     */
    public Long getAlertDefinitionId() {
      return m_history.getAlertDefinitionId();
    }

    /**
     * Gets the hash of alert definition entity.
     *
     * @return
     */
    public int getAlertDefinitionHash() {
      return m_history.getAlertDefinitionHash();
    }


    /**
     * Gets the descriptive name of the alert.
     *
     * @return
     */
    public String getAlertName() {
      return m_history.getAlertDefinition().getLabel();
    }

    /**
     * Gets the alert definition for this alert.
     *
     * @return the alert definition.
     */
    public AlertDefinitionEntity getAlertDefinition() {
      return m_history.getAlertDefinition();
    }

    /**
     * Gets the text of the alert.
     *
     * @return
     */
    public String getAlertText() {
      return m_history.getAlertText();
    }
  }

  /**
   * The {@link AlertSummaryInfo} class encapsulates all of the alert
   * information for the {@link Notification}. This includes customized
   * structures to better organize information about each of the services,
   * hosts, and alert states.
   */
  public final static class AlertSummaryInfo {
    private int m_okCount = 0;
    private int m_warningCount = 0;
    private int m_criticalCount = 0;
    private int m_unknownCount = 0;

    /**
     * The hosts that have at least 1 alert reported.
     */
    private final Set<String> m_hosts = new HashSet<>();

    /**
     * The services that have at least 1 alert reported.
     */
    private final Set<String> m_services = new HashSet<>();

    /**
     * All of the alerts for the {@link Notification}.
     */
    private final List<AlertHistoryEntity> m_alerts;

    /**
     * A mapping of service to alerts where the alerts are also grouped by state
     * for that service.
     */
    private final Map<String, Map<AlertState, List<AlertHistoryEntity>>> m_alertsByServiceAndState = new HashMap<>();

    /**
     * A mapping of all services by state.
     */
    private final Map<String, Set<String>> m_servicesByState = new HashMap<>();

    /**
     * A mapping of all alerts by the service that owns them.
     */
    private final Map<String, List<AlertHistoryEntity>> m_alertsByService = new HashMap<>();

    /**
     * Constructor.
     *
     * @param histories
     */
    protected AlertSummaryInfo(List<AlertHistoryEntity> histories) {
      m_alerts = histories;

      // group all alerts by their service and severity
      for (AlertHistoryEntity history : m_alerts) {
        AlertState alertState = history.getAlertState();
        String serviceName = history.getServiceName();
        String hostName = history.getHostName();

        if (null != hostName) {
          m_hosts.add(hostName);
        }

        if (null != serviceName) {
          m_services.add(serviceName);
        }

        // group alerts by service name & state
        Map<AlertState, List<AlertHistoryEntity>> service = m_alertsByServiceAndState.get(serviceName);
        if (null == service) {
          service = new HashMap<>();
          m_alertsByServiceAndState.put(serviceName, service);
        }

        List<AlertHistoryEntity> alertList = service.get(alertState);
        if (null == alertList) {
          alertList = new ArrayList<>();
          service.put(alertState, alertList);
        }

        alertList.add(history);

        // group services by alert states
        Set<String> services = m_servicesByState.get(alertState.name());
        if (null == services) {
          services = new HashSet<>();
          m_servicesByState.put(alertState.name(), services);
        }

        services.add(serviceName);

        // group alerts by service
        List<AlertHistoryEntity> alertsByService = m_alertsByService.get(serviceName);
        if (null == alertsByService) {
          alertsByService = new ArrayList<>();
          m_alertsByService.put(serviceName, alertsByService);
        }

        alertsByService.add(history);

        // keep track of totals
        switch (alertState) {
          case CRITICAL:
            m_criticalCount++;
            break;
          case OK:
            m_okCount++;
            break;
          case UNKNOWN:
            m_unknownCount++;
            break;
          case WARNING:
            m_warningCount++;
