package com.airlock.iam.base.infrastructure.db;

import com.google.common.base.Preconditions;
import java.time.Instant;
import java.util.Optional;

public final class DbInstantConverter {
   private static final long NANOS_PER_SECOND = 1000000000L;
   static final Instant MAX_CONVERTIBLE_INSTANT = Instant.ofEpochSecond(Math.floorDiv(Long.MAX_VALUE, 1000000000L), Math.floorMod(Long.MAX_VALUE, 1000000000L));
   static final Instant MIN_CONVERTIBLE_INSTANT = Instant.ofEpochSecond(Math.floorDiv(Long.MIN_VALUE, 1000000000L), Math.floorMod(Long.MIN_VALUE, 1000000000L));

   private DbInstantConverter() {
      throw new AssertionError("factory class");
   }

   public static Instant toInstant(long nanoSecondsSinceEpoch) {
      long epochSeconds = Math.floorDiv(nanoSecondsSinceEpoch, 1000000000L);
      long nanoAdjustment = Math.floorMod(nanoSecondsSinceEpoch, 1000000000L);
      return Instant.ofEpochSecond(epochSeconds, nanoAdjustment);
   }

   public static Optional<Instant> toInstant(Optional<Long> nanoSecondsSinceEpoch) {
      return nanoSecondsSinceEpoch.map(DbInstantConverter::toInstant);
   }

   public static long toNanoSecondsSinceEpoch(Instant instant) {
      Preconditions.checkArgument(
         !instant.isBefore(MIN_CONVERTIBLE_INSTANT) && !instant.isAfter(MAX_CONVERTIBLE_INSTANT),
         String.format(
            "The instant '%s' cannot be converted to a long, since its number of nano-seconds since EPOCH would overflow. The instant to be converted needs to be within (inclusive) '%s' and '%s'. ",
            instant,
            MIN_CONVERTIBLE_INSTANT,
            MAX_CONVERTIBLE_INSTANT
         )
      );
      return instant.getEpochSecond() >= 0L
         ? toNanos(instant.getEpochSecond(), instant.getNano())
         : toNanos(instant.getEpochSecond() + 1L, instant.getNano() - 1000000000L);
   }

   private static long toNanos(long numberOfSeconds, long numberOfNanoSeconds) {
      return Math.addExact(Math.multiplyExact(numberOfSeconds, 1000000000L), numberOfNanoSeconds);
   }

   public static Optional<Long> toNanoSecondsSinceEpoch(Optional<Instant> instant) {
      return instant.map(DbInstantConverter::toNanoSecondsSinceEpoch);
   }
}
