package com.airlock.iam.oauth2.infrastructure.repository;

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.core.infrastructure.db.JooqProvider;
import com.airlock.iam.core.misc.token.KnownTokenTypes;
import com.airlock.iam.database.infrastructure.JooqConfig;
import com.airlock.iam.database.infrastructure.JooqTransaction;
import com.airlock.iam.database.infrastructure.generated.schema.Tables;
import com.airlock.iam.database.infrastructure.generated.schema.tables.Oauth2SessionTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.TokenTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.records.Oauth2AcceptedClientAssertionsRecord;
import com.airlock.iam.database.infrastructure.generated.schema.tables.records.Oauth2ParRequestRecord;
import com.airlock.iam.database.infrastructure.generated.schema.tables.records.TokenRecord;
import com.airlock.iam.oauth2.domain.service.repository.OAuth2CleanupRepository;
import java.time.Instant;
import java.util.Objects;
import java.util.function.Supplier;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.DeleteReturningStep;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;

public final class OAuth2CleanupRepositoryImpl implements OAuth2CleanupRepository {
   private final Supplier<DSLContext> provider;

   public OAuth2CleanupRepositoryImpl(JooqConfig jooqConfig) {
      this.provider = () -> JooqProvider.jooqProvider(jooqConfig).db();
   }

   @Override
   public OAuth2CleanupRepository.RemovalResult removeExpiredTokensBatch(int batchSize, Instant expiredAt) {
      return Exceptions.wrapException(
         () -> (OAuth2CleanupRepository.RemovalResult)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(db -> this.removeExpiredOAuth2Tokens(db, batchSize, expiredAt)),
         e -> new FailureException(String.format("Error deleting expired OAuth 2.0 tokens: %s", e.getMessage()), e)
      );
   }

   @Override
   public OAuth2CleanupRepository.TokensToCleanupCounts getNumberOfTokensToDelete(Instant expiredAt) {
      return Exceptions.wrapException(
         () -> this.fetchNumberOfTokensToDelete(this.db(), expiredAt),
         e -> new FailureException(String.format("Error getting number of expired OAuth 2.0 tokens: %s", e.getMessage()), e)
      );
   }

   @Override
   public OAuth2CleanupRepository.RemovalResult removeExpiredPushedAuthorizationRequestsBatch(int batchSize, Instant expiredAt) {
      return Exceptions.wrapException(
         () -> (OAuth2CleanupRepository.RemovalResult)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(db -> this.removeExpiredPushedAuthorizationRequests(db, batchSize, expiredAt)),
         e -> new FailureException(String.format("Error deleting expired OAuth 2.0 Pushed Authorization Requests: %s", e.getMessage()), e)
      );
   }

   @Override
   public int getNumberOfPushedAuthorizationRequestsToDelete(Instant expiredAt) {
      return Exceptions.wrapException(
         () -> this.fetchNumberOfPushedAuthorizationRequestsToDelete(this.db(), expiredAt),
         e -> new FailureException(String.format("Error getting number of expired OAuth 2.0 Pushed Authorization Requests: %s", e.getMessage()), e)
      );
   }

   @Override
   public OAuth2CleanupRepository.RemovalResult removeExpiredAcceptedClientAssertionsBatch(int batchSize, Instant expiredAt) {
      return Exceptions.wrapException(
         () -> (OAuth2CleanupRepository.RemovalResult)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(db -> this.removeExpiredAcceptedClientAssertions(db, batchSize, expiredAt)),
         e -> new FailureException(String.format("Error deleting expired OAuth 2.0 accepted client assertions: %s", e.getMessage()), e)
      );
   }

   @Override
   public int getNumberOfAcceptedClientAssertionsToDelete(Instant expiredAt) {
      return Exceptions.wrapException(
         () -> this.fetchNumberOfAcceptedClientAssertionsToDelete(this.db(), expiredAt),
         e -> new FailureException(String.format("Error getting number of expired OAuth 2.0 accepted client assertions: %s", e.getMessage()), e)
      );
   }

   @Override
   public OAuth2CleanupRepository.RemovalResult removeOrphanedSessionsBatch(OAuth2CleanupRepository.OrphanedSessionsProvider orphanedSessionsProvider) {
      return Exceptions.wrapException(
         () -> (OAuth2CleanupRepository.RemovalResult)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(db -> this.removeOrphanedOAuth2Sessions(db, orphanedSessionsProvider)),
         e -> new FailureException(String.format("Error deleting expired OAuth 2.0 sessions: %s", e.getMessage()), e)
      );
   }

   @Override
   public Long[] getOrphanSessionPks() {
      return Exceptions.wrapException(
         () -> (Long[])JooqTransaction.jooqTransaction(this.db()).executeAndReturn(OAuth2CleanupRepositoryImpl::fetchAllOrphanedSessionIds),
         e -> new FailureException(String.format("Error fetching orphaned OAuth 2.0 sessions: %s", e.getMessage()), e)
      );
   }

   private static Long[] fetchAllOrphanedSessionIds(DSLContext db) {
      return (Long[])db.select(Oauth2SessionTable.OAUTH2_SESSION.PK)
         .from(Oauth2SessionTable.OAUTH2_SESSION)
         .leftJoin(TokenTable.TOKEN)
         .on(
            TokenTable.TOKEN
               .TYPE
               .eq(DSL.inline(KnownTokenTypes.OAUTH2.getId()))
               .and(OAuth2SessionRepositoryFields.TOKEN_SESSION_ID.eq(Oauth2SessionTable.OAUTH2_SESSION.ID))
         )
         .where(TokenTable.TOKEN.TOKEN_ID.isNull())
         .limit(1000000)
         .fetchArray(Oauth2SessionTable.OAUTH2_SESSION.PK);
   }

   private OAuth2CleanupRepository.TokensToCleanupCounts fetchNumberOfTokensToDelete(DSLContext db, Instant expiredAt) {
      return OAuth2CleanupRepository.TokensToCleanupCounts.genericTokens(
         (Integer)Objects.requireNonNullElse(
            (Integer)db.selectCount()
               .from(TokenTable.TOKEN)
               .where(
                  new Condition[]{
                     TokenTable.TOKEN.TYPE.eq(DSL.inline(KnownTokenTypes.OAUTH2.getId())),
                     TokenTable.TOKEN.VALIDITY_RANGE_UPPER.lessThan(DbInstantConverter.toNanoSecondsSinceEpoch(expiredAt))
                  }
               )
               .fetchOne(DSL.count()),
            0
         )
      );
   }

   private int fetchNumberOfPushedAuthorizationRequestsToDelete(DSLContext db, Instant expiredAt) {
      return (Integer)Objects.requireNonNullElse(
         (Integer)db.selectCount()
            .from(Tables.OAUTH2_PAR_REQUEST)
            .where(Tables.OAUTH2_PAR_REQUEST.VALID_UNTIL.lessThan(DbInstantConverter.toNanoSecondsSinceEpoch(expiredAt)))
            .fetchOne(DSL.count()),
         0
      );
   }

   private int fetchNumberOfAcceptedClientAssertionsToDelete(DSLContext db, Instant expiredAt) {
      return (Integer)Objects.requireNonNullElse(
         (Integer)db.selectCount()
            .from(Tables.OAUTH2_ACCEPTED_CLIENT_ASSERTIONS)
            .where(Tables.OAUTH2_ACCEPTED_CLIENT_ASSERTIONS.EXPIRES_AT.lessOrEqual(DbInstantConverter.toNanoSecondsSinceEpoch(expiredAt)))
            .fetchOne(DSL.count()),
         0
      );
   }

   private OAuth2CleanupRepository.RemovalResult removeExpiredOAuth2Tokens(DSLContext db, int batchSize, Instant expiredAt) {
      DeleteConditionStep<TokenRecord> query = db.deleteFrom(TokenTable.TOKEN)
         .where(
            new Condition[]{
               TokenTable.TOKEN.TYPE.eq(DSL.inline(KnownTokenTypes.OAUTH2.getId())),
               TokenTable.TOKEN.VALIDITY_RANGE_UPPER.lessThan(DbInstantConverter.toNanoSecondsSinceEpoch(expiredAt))
            }
         );
      int deleted = this.addLimit(query, batchSize, db).execute();
      return new OAuth2CleanupRepository.RemovalResult(deleted, batchSize == deleted);
   }

   private OAuth2CleanupRepository.RemovalResult removeExpiredPushedAuthorizationRequests(DSLContext db, int batchSize, Instant expiredAt) {
      DeleteConditionStep<Oauth2ParRequestRecord> query = db.deleteFrom(Tables.OAUTH2_PAR_REQUEST)
         .where(Tables.OAUTH2_PAR_REQUEST.VALID_UNTIL.lessThan(DbInstantConverter.toNanoSecondsSinceEpoch(expiredAt)));
      int deleted = this.addLimit(query, batchSize, db).execute();
      return new OAuth2CleanupRepository.RemovalResult(deleted, batchSize == deleted);
   }

   private OAuth2CleanupRepository.RemovalResult removeExpiredAcceptedClientAssertions(DSLContext db, int batchSize, Instant expiredAt) {
      DeleteConditionStep<Oauth2AcceptedClientAssertionsRecord> query = db.deleteFrom(Tables.OAUTH2_ACCEPTED_CLIENT_ASSERTIONS)
         .where(Tables.OAUTH2_ACCEPTED_CLIENT_ASSERTIONS.EXPIRES_AT.lessOrEqual(DbInstantConverter.toNanoSecondsSinceEpoch(expiredAt)));
      int deleted = this.addLimit(query, batchSize, db).execute();
      return new OAuth2CleanupRepository.RemovalResult(deleted, batchSize == deleted);
   }

   private DeleteReturningStep<?> addLimit(DeleteConditionStep<?> query, int batchSize, DSLContext db) {
      return (DeleteReturningStep<?>)(db.dialect().family() == SQLDialect.ORACLE ? query.and(DSL.rownum().lessOrEqual(batchSize)) : query.limit(batchSize));
   }

   private OAuth2CleanupRepository.RemovalResult removeOrphanedOAuth2Sessions(
      DSLContext db, OAuth2CleanupRepository.OrphanedSessionsProvider orphanedSessionsProvider
   ) {
      int deleted = db.deleteFrom(Oauth2SessionTable.OAUTH2_SESSION)
         .where(Oauth2SessionTable.OAUTH2_SESSION.PK.in(orphanedSessionsProvider.getNextOrphanedSessionPksBatch()))
         .execute();
      return new OAuth2CleanupRepository.RemovalResult(deleted, orphanedSessionsProvider.hasMoreSessions());
   }

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