package com.airlock.iam.servicecontainer.app.infrastructure.rememberme;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.infrastructure.db.DbInstantConverter;
import com.airlock.iam.base.misc.exception.Exceptions;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.core.infrastructure.db.JooqProvider;
import com.airlock.iam.database.infrastructure.JooqConfig;
import com.airlock.iam.database.infrastructure.JooqTransaction;
import com.airlock.iam.database.infrastructure.TenantId;
import com.airlock.iam.database.infrastructure.generated.schema.Tables;
import com.airlock.iam.servicecontainer.app.domain.model.task.rememberme.RememberMeTokenCleanupRepository;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.function.Supplier;
import lombok.NonNull;
import org.jooq.Condition;
import org.jooq.DSLContext;

public class RememberMeTokenCleanupRepositoryImpl implements RememberMeTokenCleanupRepository {
   private final Supplier<DSLContext> jooqDslProvider;
   private final TenantId tenantId;
   private final TimeProvider timeProvider;
   private final Duration lifeTimeExpiry;
   private final Optional<Duration> idleTimeExpiry;

   public RememberMeTokenCleanupRepositoryImpl(
      @NonNull JooqConfig jooqConfig, @NonNull TimeProvider timeProvider, @NonNull Duration lifeTimeExpiry, @NonNull Optional<Duration> idleTimeExpiry
   ) {
      if (jooqConfig == null) {
         throw new NullPointerException("jooqConfig is marked non-null but is null");
      }

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

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

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

      this.jooqDslProvider = () -> JooqProvider.jooqProvider(jooqConfig).db();
      this.tenantId = jooqConfig.tenantId();
      this.timeProvider = timeProvider;
      this.lifeTimeExpiry = lifeTimeExpiry;
      this.idleTimeExpiry = idleTimeExpiry;
   }

   @Override
   public int deleteExpiredTokens(int batchSize) {
      return (Integer)Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db()).executeAndReturn(db -> this.deleteExpiredTokensInternal(db, batchSize)),
         e -> new FailureException(String.format("Error deleting expired Remember-Me tokens: %s", e.getMessage()), e)
      );
   }

   @Override
   public int getExpiredTokensCount() {
      return (Integer)Exceptions.wrapException(
         () -> this.fetchExpiredTokenCount(this.db()),
         e -> new FailureException(String.format("Error fetching expired Remember-Me token count: %s", e.getMessage()), e)
      );
   }

   private int deleteExpiredTokensInternal(DSLContext db, int batchSize) {
      return db.deleteFrom(Tables.REMEMBER_ME_TOKEN).where(this.tokenIsExpired()).and(this.tenantMatches()).limit(batchSize).execute();
   }

   private int fetchExpiredTokenCount(DSLContext db) {
      return db.fetchCount(Tables.REMEMBER_ME_TOKEN, this.tokenIsExpired().and(this.tenantMatches()));
   }

   private Condition tenantMatches() {
      return Tables.REMEMBER_ME_TOKEN.TENANT_ID.eq(this.tenantId.tenantId());
   }

   private Condition tokenIsExpired() {
      return this.idleTimeExpiry
         .<Condition>map(duration -> this.tokenLifetimeIsExpired().or(this.tokenIdleTimeIsExpired(duration)))
         .orElseGet(this::tokenLifetimeIsExpired);
   }

   private Condition tokenLifetimeIsExpired() {
      Instant expiredIfCreatedBefore = this.timeProvider.getCurrentInstant().minusSeconds(this.lifeTimeExpiry.getSeconds());
      return Tables.REMEMBER_ME_TOKEN.CREATED_AT.lessThan(DbInstantConverter.toNanoSecondsSinceEpoch(expiredIfCreatedBefore));
   }

   private Condition tokenIdleTimeIsExpired(Duration idleExpirationTime) {
      Instant expiredIfRenewedBefore = this.timeProvider.getCurrentInstant().minusSeconds(idleExpirationTime.getSeconds());
      return Tables.REMEMBER_ME_TOKEN.RENEWED_AT.lessThan(DbInstantConverter.toNanoSecondsSinceEpoch(expiredIfRenewedBefore));
   }

   private DSLContext db() {
      return this.jooqDslProvider.get();
   }
}
