package com.airlock.iam.servicecontainer.app.application.service.event;

import com.airlock.iam.base.misc.concurrent.ThreadUtils;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.servicecontainer.app.domain.model.event.ProcessedEvents;
import com.airlock.iam.servicecontainer.app.domain.service.event.OutboxProcessorService;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Supplier;
import lombok.NonNull;

public final class EventOutboxService {
   private static final ELogger LOG = ELoggerFactory.getLogger(EventOutboxService.class);
   private static final Duration METRICS_LOG_INTERVAL = Duration.ofSeconds(60L);
   private final Duration minInterval;
   private final Duration maxInterval;
   private final OutboxProcessorService processor;
   private final Supplier<Boolean> shouldStop;
   private Duration waitInterval;
   private Instant lastMetricsLog = Instant.now();
   private int sentSinceLastLog;
   private int failuresSinceLastLog;

   public EventOutboxService(
      @NonNull Duration minInterval, @NonNull Duration maxInterval, @NonNull Supplier<Boolean> shouldStop, @NonNull OutboxProcessorService processor
   ) {
      if (minInterval == null) {
         throw new NullPointerException("minInterval is marked non-null but is null");
      }

      if (maxInterval == null) {
         throw new NullPointerException("maxInterval is marked non-null but is null");
      }

      if (shouldStop == null) {
         throw new NullPointerException("shouldStop is marked non-null but is null");
      }

      if (processor == null) {
         throw new NullPointerException("processor is marked non-null but is null");
      }

      this.minInterval = minInterval;
      this.maxInterval = maxInterval;
      this.shouldStop = shouldStop;
      this.processor = processor;
      this.waitInterval = minInterval;
   }

   public void processOutbox() {
      Instant start = Instant.now();

      try {
         ProcessedEvents result = this.processor.processOutbox();
         this.updateInterval(result);
         this.logMetrics(result);
      } catch (Exception e) {
         LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, "Exception while processing outbox: ", e);
         this.updateInterval(new ProcessedEvents(0, 1, 0));
      } finally {
         for (Duration remaining = this.waitInterval.minus(Duration.between(start, Instant.now()));
            remaining.isPositive() && !this.shouldStop.get();
            remaining = remaining.minusMillis(500L)
         ) {
            ThreadUtils.sleepInterruptibly(Duration.ofMillis(Math.min(500L, remaining.toMillis())));
         }
      }
   }

   private void logMetrics(ProcessedEvents result) {
      this.sentSinceLastLog = this.sentSinceLastLog + result.sent();
      this.failuresSinceLastLog = this.failuresSinceLastLog + result.failed();
      if (Duration.between(this.lastMetricsLog, Instant.now()).compareTo(METRICS_LOG_INTERVAL) >= 0) {
         LOG.info(
            LogTypes.SERVER_INFO_SYS,
            String.format(
               "Event outbox metrics since last log: %d sent, %d failures, %d remaining",
               this.sentSinceLastLog,
               this.failuresSinceLastLog,
               this.processor.getUnprocessedEventsCount()
            )
         );
         this.sentSinceLastLog = 0;
         this.failuresSinceLastLog = 0;
         this.lastMetricsLog = Instant.now();
      }
   }

   public void resetMetrics() {
      this.lastMetricsLog = Instant.now();
      this.sentSinceLastLog = 0;
      this.failuresSinceLastLog = 0;
      this.waitInterval = this.minInterval;
   }

   @VisibleForTesting
   Duration getWaitInterval() {
      return this.waitInterval;
   }

   private void updateInterval(ProcessedEvents result) {
      if (result.sent() > 0) {
         this.waitInterval = this.minInterval;
      } else if (result.failed() > 0) {
         this.waitInterval = this.waitInterval.multipliedBy(2L);
         if (this.waitInterval.compareTo(this.maxInterval) > 0) {
            this.waitInterval = this.maxInterval;
         }
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static EventOutboxService.EventOutboxServiceBuilder builder() {
      return new EventOutboxService.EventOutboxServiceBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class EventOutboxServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Duration minInterval;
      @SuppressFBWarnings(justification = "generated code")
      private Duration maxInterval;
      @SuppressFBWarnings(justification = "generated code")
      private Supplier<Boolean> shouldStop;
      @SuppressFBWarnings(justification = "generated code")
      private OutboxProcessorService processor;

      @SuppressFBWarnings(justification = "generated code")
      EventOutboxServiceBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public EventOutboxService.EventOutboxServiceBuilder minInterval(@NonNull Duration minInterval) {
         if (minInterval == null) {
            throw new NullPointerException("minInterval is marked non-null but is null");
         }

         this.minInterval = minInterval;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public EventOutboxService.EventOutboxServiceBuilder maxInterval(@NonNull Duration maxInterval) {
         if (maxInterval == null) {
            throw new NullPointerException("maxInterval is marked non-null but is null");
         }

         this.maxInterval = maxInterval;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public EventOutboxService.EventOutboxServiceBuilder shouldStop(@NonNull Supplier<Boolean> shouldStop) {
         if (shouldStop == null) {
            throw new NullPointerException("shouldStop is marked non-null but is null");
         }

         this.shouldStop = shouldStop;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public EventOutboxService.EventOutboxServiceBuilder processor(@NonNull OutboxProcessorService processor) {
         if (processor == null) {
            throw new NullPointerException("processor is marked non-null but is null");
         }

         this.processor = processor;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public EventOutboxService build() {
         return new EventOutboxService(this.minInterval, this.maxInterval, this.shouldStop, this.processor);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "EventOutboxService.EventOutboxServiceBuilder(minInterval="
            + this.minInterval
            + ", maxInterval="
            + this.maxInterval
            + ", shouldStop="
            + this.shouldStop
            + ", processor="
            + this.processor
            + ")";
      }
   }
}
