/**
   * Receives request for processing a {@link UnifiedPushMessage} and loads tokens for devices that
   * match requested parameters from database.
   *
   * <p>Device tokens are loaded in a stream and split to batches of configured size (see {@link
   * SenderConfiguration#batchSize()}). Once the pre-configured number of batches (see {@link
   * SenderConfiguration#batchesToLoad()}) is reached, this method resends message to the same queue
   * it took the request from, so that the transaction it worked in is split and further processing
   * may continue in next transaction.
   *
   * <p>Additionally it fires {@link BatchLoadedEvent} as CDI event (that is translated to JMS
   * event) that helps {@link MetricsCollector} to track how many batches were loaded. When all
   * batches were loaded for the given variant, it fires {@link AllBatchesLoadedEvent}.
   *
   * @param msg holder object containing the payload and info about the effected variants
   */
  public void loadAndQueueTokenBatch(@Observes @Dequeue MessageHolderWithVariants msg)
      throws IllegalStateException {
    final UnifiedPushMessage message = msg.getUnifiedPushMessage();
    final VariantType variantType = msg.getVariantType();
    final Collection<Variant> variants = msg.getVariants();
    final String lastTokenFromPreviousBatch = msg.getLastTokenFromPreviousBatch();
    final SenderConfiguration configuration =
        senderConfiguration.select(new SenderTypeLiteral(variantType)).get();
    final PushMessageInformation pushMessageInformation = msg.getPushMessageInformation();
    int serialId = msg.getLastSerialId();

    logger.debug("Received message from queue: " + message.getMessage().getAlert());

    final Criteria criteria = message.getCriteria();
    final List<String> categories = criteria.getCategories();
    final List<String> aliases = criteria.getAliases();
    final List<String> deviceTypes = criteria.getDeviceTypes();

    logger.info(
        String.format(
            "Preparing message delivery and loading tokens for the %s 3rd-party Push Network (for %d variants)",
            variantType, variants.size()));

    for (Variant variant : variants) {

      try {

        ResultsStream<String> tokenStream;
        final Set<String> topics = new TreeSet<>();
        final boolean isAndroid = variantType.equals(VariantType.ANDROID);

        // the entire batch size
        int batchesToLoad = configuration.batchesToLoad();

        // Some checks for GCM, because of GCM-3 topics
        boolean gcmTopicRequest = (isAndroid && TokenLoaderUtils.isGCMTopicRequest(criteria));
        if (gcmTopicRequest) {

          // If we are able to do push for GCM topics...

          // 1)
          // find all topics, BUT only on the very first round of batches
          // otherwise after 10 (or what ever the max. is) another request would be sent to that
          // topic
          if (serialId == 0) {
            topics.addAll(TokenLoaderUtils.extractGCMTopics(criteria, variant.getVariantID()));

            // topics are handled as a first extra batch,
            // therefore we have to adjust the number by adding this extra batch
            batchesToLoad = batchesToLoad + 1;
          }

          // 2) always load the legacy tokens, for all number of batch iterations
          tokenStream =
              clientInstallationService
                  .findAllOldGoogleCloudMessagingDeviceTokenForVariantIDByCriteria(
                      variant.getVariantID(),
                      categories,
                      aliases,
                      deviceTypes,
                      configuration.tokensToLoad(),
                      lastTokenFromPreviousBatch)
                  .fetchSize(configuration.batchSize())
                  .executeQuery();
        } else {
          tokenStream =
              clientInstallationService
                  .findAllDeviceTokenForVariantIDByCriteria(
                      variant.getVariantID(),
                      categories,
                      aliases,
                      deviceTypes,
                      configuration.tokensToLoad(),
                      lastTokenFromPreviousBatch)
                  .fetchSize(configuration.batchSize())
                  .executeQuery();
        }

        String lastTokenInBatch = null;
        int tokensLoaded = 0;
        for (int batchNumber = 0; batchNumber < batchesToLoad; batchNumber++) {

          // increasing the serial ID,
          // to make sure it's properly read from all block
          ++serialId;

          final Set<String> tokens = new TreeSet<>();

          // On Android, the first batch is for GCM3 topics
          // legacy tokens are submitted in the batch #2 and later
          if (isAndroid && batchNumber == 0 && !topics.isEmpty()) {
            tokens.addAll(topics);
          } else {
            for (int i = 0; i < configuration.batchSize() && tokenStream.next(); i++) {
              lastTokenInBatch = tokenStream.get();
              tokens.add(lastTokenInBatch);
              tokensLoaded += 1;
            }
          }

          if (tokens.size() > 0) {
            if (tryToDispatchTokens(
                new MessageHolderWithTokens(
                    msg.getPushMessageInformation(), message, variant, tokens, serialId))) {
              logger.info(
                  String.format(
                      "Loaded batch #%s, containing %d tokens, for %s variant (%s)",
                      serialId,
                      tokens.size(),
                      variant.getType().getTypeName(),
                      variant.getVariantID()));
            } else {
              logger.debug(
                  String.format(
                      "Failing token loading transaction for batch token #%s for %s variant (%s), since queue is full, will retry...",
                      serialId, variant.getType().getTypeName(), variant.getVariantID()));
              context.setRollbackOnly();
              return;
            }

            // using combined key of variant and PMI (AGPUSH-1585):
            batchLoaded.fire(
                new BatchLoadedEvent(
                    variant.getVariantID() + ":" + msg.getPushMessageInformation().getId()));
            if (serialId == MessageHolderWithVariants.INITIAL_SERIAL_ID) {
              triggerVariantMetricCollection.fire(
                  new TriggerVariantMetricCollectionEvent(
                      msg.getPushMessageInformation(), variant));
            }
          } else {
            logger.debug(
                String.format(
                    "Ending batch processing: No more tokens for batch #%s available", serialId));
            break;
          }
        }

        // should we trigger next transaction?
        if (tokensLoaded >= configuration.tokensToLoad()) {
          logger.debug(
              String.format(
                  "Ending token loading transaction for %s variant (%s)",
                  variant.getType().getTypeName(), variant.getVariantID()));
          nextBatchEvent.fire(
              new MessageHolderWithVariants(
                  msg.getPushMessageInformation(),
                  message,
                  msg.getVariantType(),
                  variants,
                  serialId,
                  lastTokenInBatch));
        } else {
          logger.debug(
              String.format(
                  "All batches for %s variant were loaded (%s)",
                  variant.getType().getTypeName(), pushMessageInformation.getId()));

          // using combined key of variant and PMI (AGPUSH-1585):
          allBatchesLoaded.fire(
              new AllBatchesLoadedEvent(
                  variant.getVariantID() + ":" + msg.getPushMessageInformation().getId()));
          triggerVariantMetricCollection.fire(
              new TriggerVariantMetricCollectionEvent(pushMessageInformation, variant));

          if (tokensLoaded == 0 && lastTokenFromPreviousBatch == null) {
            // no tokens were loaded at all!
            if (gcmTopicRequest) {
              logger.debug("No legacy(non-InstanceID) tokens found. Just pure GCM topic requests");
            } else {
              logger.warn("Check your push query: Not a single token was loaded from the DB!");
            }

            VariantMetricInformation variantMetricInformation = new VariantMetricInformation();
            variantMetricInformation.setPushMessageInformation(msg.getPushMessageInformation());
            variantMetricInformation.setVariantID(variant.getVariantID());
            variantMetricInformation.setDeliveryStatus(Boolean.TRUE);
            dispatchVariantMetricEvent.fire(variantMetricInformation);
          }
        }
      } catch (ResultStreamException e) {
        logger.error("Failed to load batch of tokens", e);
      }
    }
  }