/*-
 * -\-\-
 * Helios Services
 * --
 * Copyright (C) 2016 Spotify AB
 * --
 * 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 com.spotify.helios.servicescommon;

import com.google.cloud.ByteArray;
import com.google.cloud.pubsub.Message;
import com.google.cloud.pubsub.PubSub;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.JdkFutureAdapters;
import com.google.common.util.concurrent.MoreExecutors;
import io.dropwizard.lifecycle.Managed;
import java.time.Duration;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** An EventSender that publishes events to Google Cloud PubSub. */
public class GooglePubSubSender implements EventSender {

  private static final Logger log = LoggerFactory.getLogger(GooglePubSubSender.class);

  private final PubSub pubsub;
  private final String topicPrefix;
  private final HealthChecker healthchecker;

  public static GooglePubSubSender create(final PubSub pubSub,
                                          final String topicPrefix,
                                          final HealthChecker healthchecker) {
    return new GooglePubSubSender(pubSub, topicPrefix, healthchecker);
  }

  private GooglePubSubSender(final PubSub pubSub,
                             final String topicPrefix,
                             final HealthChecker healthchecker) {
    this.pubsub = pubSub;
    this.topicPrefix = topicPrefix;
    this.healthchecker = healthchecker;
  }

  @Override
  public void start() throws Exception {
    healthchecker.start();
  }

  @Override
  public void stop() throws Exception {
    healthchecker.stop();
  }

  @Override
  public void send(final String topic, final byte[] message) {
    final String combinedTopic = topicPrefix + topic;

    if (!healthchecker.isHealthy()) {
      log.warn("will not publish message to pubsub topic={} as the pubsub client "
               + "appears to be unhealthy", combinedTopic);
      return;
    }

    try {
      Futures.addCallback(
          JdkFutureAdapters.listenInPoolThread(
              pubsub.publishAsync(combinedTopic, Message.of(ByteArray.copyFrom(message)))),
          new FutureCallback<String>() {
            @Override
            public void onSuccess(@Nullable final String ackId) {
              log.debug("Sent an event to Google PubSub, topic: {}, ack: {}", combinedTopic, ackId);
            }

            @Override
            public void onFailure(final Throwable th) {
              log.warn("Unable to send an event to Google PubSub, topic: {}", combinedTopic, th);
            }
          }, MoreExecutors.directExecutor());
    } catch (Exception e) {
      log.warn("Failed to publish Google PubSub message, topic: {}", combinedTopic, e);
    }
  }

  public interface HealthChecker extends Managed {

    boolean isHealthy();
  }

  public static class DefaultHealthChecker implements HealthChecker {

    private final PubSub pubsub;
    private final String topic;
    private final ScheduledExecutorService executor;
    private final Duration healthcheckInterval;
    private AtomicBoolean healthy = new AtomicBoolean(false);

    public DefaultHealthChecker(final PubSub pubsub, final String topic,
                                final ScheduledExecutorService executor,
                                final Duration healthcheckInterval) {
      this.pubsub = pubsub;
      this.topic = topic;
      this.executor = executor;
      this.healthcheckInterval = healthcheckInterval;
    }

    @Override
    public void start() {
      final long millis = healthcheckInterval.toMillis();
      executor.scheduleWithFixedDelay(this::checkHealth, 0, millis, TimeUnit.MILLISECONDS);
    }

    @Override
    public void stop() throws Exception {
      executor.shutdown();
    }

    @Override
    public boolean isHealthy() {
      return healthy.get();
    }

    @VisibleForTesting
    void checkHealth() {
      final boolean currentHealth = doCheckHealth();
      final boolean oldHealth = healthy.getAndSet(currentHealth);
      // only log that it is healthy on a state change to avoid repeating this message in the logs
      // on every interval
      if (currentHealth && !oldHealth) {
        log.info("successfully checked if pubsub topic {} exists - this instance is now healthy",
            topic);
      }
    }

    private boolean doCheckHealth() {
      try {
        // perform a blocking call to see if we can connect to pubsub at all
        // if the topic does not exist, getTopic() returns null and does not throw an exception
        pubsub.getTopic(topic);
        return true;
      } catch (RuntimeException ex) {
        // PubSubException is an instance of RuntimeException, catch any other subtypes too
        log.warn("caught exception checking if pubsub topic {} exists. "
                 + "Publishing to pubsub will be disabled until connectivity is restored "
                 + "(next check is in {})",
            topic, healthcheckInterval, ex);
        return false;
      }
    }

  }
}
