/**
 * Licensed 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.aurora.scheduler.events;

import java.time.Instant;
import java.util.concurrent.atomic.AtomicLong;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.eventbus.Subscribe;

import com.google.common.util.concurrent.AbstractIdleService;
import com.google.inject.Inject;

import org.apache.aurora.common.stats.StatsProvider;
import org.apache.aurora.gen.ScheduleStatus;
import org.apache.aurora.scheduler.events.PubsubEvent.EventSubscriber;
import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.BoundRequestBuilder;
import org.asynchttpclient.HttpResponseStatus;
import org.asynchttpclient.Response;
import org.asynchttpclient.util.HttpConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.util.Objects.requireNonNull;

/**
 * Watches TaskStateChanges and send events to configured endpoint.
 */
public class Webhook extends AbstractIdleService implements EventSubscriber {
  @VisibleForTesting
  static final String ATTEMPTS_STAT_NAME = "webhooks_attempts";
  @VisibleForTesting
  static final String SUCCESS_STAT_NAME = "webhooks_success";
  @VisibleForTesting
  static final String ERRORS_STAT_NAME = "webhooks_errors";
  @VisibleForTesting
  static final String USER_ERRORS_STAT_NAME = "webhooks_user_errors";

  private static final Logger LOG = LoggerFactory.getLogger(Webhook.class);

  private final WebhookInfo webhookInfo;
  private final AsyncHttpClient httpClient;
  private final Predicate<ScheduleStatus> isWhitelisted;

  private final AtomicLong attemptsCounter;
  private final AtomicLong successCounter;
  private final AtomicLong errorsCounter;
  private final AtomicLong userErrorsCounter;

  @Inject
  Webhook(AsyncHttpClient httpClient, WebhookInfo webhookInfo, StatsProvider statsProvider) {
    this.webhookInfo = requireNonNull(webhookInfo);
    this.httpClient = requireNonNull(httpClient);
    this.attemptsCounter = statsProvider.makeCounter(ATTEMPTS_STAT_NAME);
    this.successCounter = statsProvider.makeCounter(SUCCESS_STAT_NAME);
    this.errorsCounter = statsProvider.makeCounter(ERRORS_STAT_NAME);
    this.userErrorsCounter = statsProvider.makeCounter(USER_ERRORS_STAT_NAME);
    this.isWhitelisted = status -> !webhookInfo.getWhitelistedStatuses().isPresent()
        || webhookInfo.getWhitelistedStatuses().get().contains(status);
    LOG.info("Webhook enabled with info" + this.webhookInfo);
  }

  private BoundRequestBuilder createRequest(TaskStateChange stateChange) {
    return httpClient.preparePost(webhookInfo.getTargetURI().toString())
        .setBody(stateChange.toJson())
        .setSingleHeaders(webhookInfo.getHeaders())
        .addHeader("Timestamp", Long.toString(Instant.now().toEpochMilli()));
  }

  /**
   * Watches all TaskStateChanges and send them best effort to a configured endpoint.
   * <p>
   * This is used to expose an external event bus.
   *
   * @param stateChange State change notification.
   */
  @Subscribe
  public void taskChangedState(TaskStateChange stateChange) {
    LOG.debug("Got an event: {}", stateChange);
    // Ensure that this state change event is a transition, and not an event from when the scheduler
    // first initializes. In that case we do not want to resend the entire state. This check also
    // ensures that only whitelisted statuses will be sent to the configured endpoint.
    if (stateChange.isTransition() && isWhitelisted.apply(stateChange.getNewState())) {
      attemptsCounter.incrementAndGet();
      try {
        // We don't care about the response body, so only listen for the HTTP status code.
        createRequest(stateChange).execute(new AsyncCompletionHandler<Integer>() {
          @Override
          public void onThrowable(Throwable t) {
            errorsCounter.incrementAndGet();
            LOG.error("Error sending a Webhook event", t);
          }

          @Override
          public State onStatusReceived(HttpResponseStatus status) throws Exception {
            if (status.getStatusCode() == HttpConstants.ResponseStatusCodes.OK_200) {
              successCounter.incrementAndGet();
            } else {
              userErrorsCounter.incrementAndGet();
            }

            // Abort after we get the status because that is all we use for processing.
            return State.ABORT;
          }

          @Override
          public Integer onCompleted(Response response) throws Exception {
            // We do not care about the full response.
            return 0;
          }
        });
      } catch (Exception e) {
        LOG.error("Error making Webhook request", e);
        errorsCounter.incrementAndGet();
      }
    }
  }

  @Override
  protected void startUp() throws Exception {
    // No-op
  }

  @Override
  protected void shutDown() throws Exception {
    LOG.info("Shutting down async Webhook client.");
    httpClient.close();
  }
}