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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.user.UserId;
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.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.oauth2.api.domain.model.AuthorizationServerId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.domain.model.consent.OAuth2Consent;
import com.airlock.iam.oauth2.domain.model.consent.OAuth2ConsentId;
import com.airlock.iam.oauth2.domain.model.consent.OAuth2ConsentRepository;
import com.airlock.iam.oauth2.domain.model.consent.OAuth2ConsentStatus;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.impl.DSL;

public final class OAuth2ConsentRepositoryImpl implements OAuth2ConsentRepository {
   private final Supplier<DSLContext> provider;
   private final TenantId tenantId;

   @Inject
   public OAuth2ConsentRepositoryImpl(JooqConfig jooqConfig) {
      this.provider = () -> JooqProvider.jooqProvider(jooqConfig).db();
      this.tenantId = jooqConfig.tenantId();
   }

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

   private OAuth2Consent mapConsent(Record record) {
      return new OAuth2Consent(
         OAuth2ConsentId.oAuth2ConsentId((String)record.get(Tables.OAUTH2_CONSENT.ID)),
         OAuth2ClientId.oAuth2ClientId((String)record.get(Tables.OAUTH2_CONSENT.CLIENT_ID)),
         AuthorizationServerId.authorizationServerId((String)record.get(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID)),
         UserId.userId((String)record.get(Tables.OAUTH2_CONSENT.USER_ID)),
         OAuth2Scope.of((String)record.get(Tables.OAUTH2_CONSENT.SCOPE_NAME)),
         OAuth2ConsentStatus.getStatusForIdentifier((String)record.get(Tables.OAUTH2_CONSENT.STATUS)).orElseThrow(),
         DbInstantConverter.toInstant((Long)record.get(Tables.OAUTH2_CONSENT.UPDATED_AT))
      );
   }

   private OAuth2ConsentRepository.AuthorizationServerAndClient mapAuthorizationServerAndClient(Record record) {
      return new OAuth2ConsentRepository.AuthorizationServerAndClient(
         AuthorizationServerId.authorizationServerId((String)record.get(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID)),
         OAuth2ClientId.oAuth2ClientId((String)record.get(Tables.OAUTH2_CONSENT.CLIENT_ID))
      );
   }

   @Override
   public Optional<OAuth2Consent> get(OAuth2ConsentId id) {
      return Exceptions.wrapException(
         () -> this.db()
            .select(
               Tables.OAUTH2_CONSENT.ID,
               Tables.OAUTH2_CONSENT.CLIENT_ID,
               Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID,
               Tables.OAUTH2_CONSENT.USER_ID,
               Tables.OAUTH2_CONSENT.SCOPE_NAME,
               Tables.OAUTH2_CONSENT.STATUS,
               Tables.OAUTH2_CONSENT.UPDATED_AT
            )
            .from(Tables.OAUTH2_CONSENT)
            .where(Tables.OAUTH2_CONSENT.ID.eq(id.getId()).and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId())))
            .fetch(this::mapConsent)
            .stream()
            .findFirst(),
         e -> new FailureException(String.format("error finding OAuth2 consent records by Consent Id '%s'", id.getId()), e)
      );
   }

   @Override
   public Collection<OAuth2Consent> findByUser(UserId userId) {
      return Exceptions.wrapException(
         () -> this.db()
            .select(
               Tables.OAUTH2_CONSENT.ID,
               Tables.OAUTH2_CONSENT.CLIENT_ID,
               Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID,
               Tables.OAUTH2_CONSENT.USER_ID,
               Tables.OAUTH2_CONSENT.SCOPE_NAME,
               Tables.OAUTH2_CONSENT.STATUS,
               Tables.OAUTH2_CONSENT.UPDATED_AT
            )
            .from(Tables.OAUTH2_CONSENT)
            .where(Tables.OAUTH2_CONSENT.USER_ID.eq(userId.getValue()).and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId())))
            .fetch(this::mapConsent)
            .stream()
            .toList(),
         e -> new FailureException(String.format("error finding OAuth2 consent records by User Id '%s'", userId.getValue()), e)
      );
   }

   @Override
   public Collection<OAuth2Consent> findByUserAndAuthorizationServer(UserId userId, AuthorizationServerId authorizationServerId) {
      return Exceptions.wrapException(
         () -> this.db()
            .select(
               Tables.OAUTH2_CONSENT.ID,
               Tables.OAUTH2_CONSENT.CLIENT_ID,
               Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID,
               Tables.OAUTH2_CONSENT.USER_ID,
               Tables.OAUTH2_CONSENT.SCOPE_NAME,
               Tables.OAUTH2_CONSENT.STATUS,
               Tables.OAUTH2_CONSENT.UPDATED_AT
            )
            .from(Tables.OAUTH2_CONSENT)
            .where(
               Tables.OAUTH2_CONSENT
                  .USER_ID
                  .eq(userId.getValue())
                  .and(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID.eq(authorizationServerId.id()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
            )
            .fetch(this::mapConsent)
            .stream()
            .toList(),
         e -> new FailureException(
            String.format(
               "error finding OAuth2 consent records by User Id '%s' and Authorization Server ID '%s'", userId.getValue(), authorizationServerId.id()
            ),
            e
         )
      );
   }

   @Override
   public Collection<OAuth2Consent> findByUserAndAuthorizationServerAndClient(
      UserId userId, AuthorizationServerId authorizationServerId, OAuth2ClientId oAuth2ClientId
   ) {
      return Exceptions.wrapException(
         () -> this.db()
            .select(
               Tables.OAUTH2_CONSENT.ID,
               Tables.OAUTH2_CONSENT.CLIENT_ID,
               Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID,
               Tables.OAUTH2_CONSENT.USER_ID,
               Tables.OAUTH2_CONSENT.SCOPE_NAME,
               Tables.OAUTH2_CONSENT.STATUS,
               Tables.OAUTH2_CONSENT.UPDATED_AT
            )
            .from(Tables.OAUTH2_CONSENT)
            .where(
               Tables.OAUTH2_CONSENT
                  .USER_ID
                  .eq(userId.getValue())
                  .and(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID.eq(authorizationServerId.id()))
                  .and(Tables.OAUTH2_CONSENT.CLIENT_ID.eq(oAuth2ClientId.id()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
            )
            .fetch(this::mapConsent)
            .stream()
            .toList(),
         e -> new FailureException(
            String.format(
               "error finding OAuth2 consent records by User Id '%s' and Authorization Server ID '%s' and Client ID '%s'",
               userId.getValue(),
               authorizationServerId.id(),
               oAuth2ClientId.id()
            ),
            e
         )
      );
   }

   @Override
   public void insert(OAuth2Consent oauth2Consent) {
      Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(
               db -> db.insertInto(Tables.OAUTH2_CONSENT)
                  .set(Tables.OAUTH2_CONSENT.ID, oauth2Consent.getId().getId())
                  .set(Tables.OAUTH2_CONSENT.CLIENT_ID, oauth2Consent.getClientId().id())
                  .set(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID, oauth2Consent.getAuthorizationServerId().id())
                  .set(Tables.OAUTH2_CONSENT.USER_ID, oauth2Consent.getUserId().getValue())
                  .set(Tables.OAUTH2_CONSENT.SCOPE_NAME, oauth2Consent.getScope().name())
                  .set(Tables.OAUTH2_CONSENT.STATUS, oauth2Consent.getStatus().getStatus())
                  .set(Tables.OAUTH2_CONSENT.UPDATED_AT, DbInstantConverter.toNanoSecondsSinceEpoch(oauth2Consent.getUpdatedAt()))
                  .set(Tables.OAUTH2_CONSENT.TENANT_ID, this.tenantId.tenantId())
                  .execute()
            ),
         e -> new FailureException(String.format("error inserting OAuth2 consent with id '%s'.", oauth2Consent.getId().getId()), e)
      );
   }

   @Override
   public void insertOrUpdateConsents(List<OAuth2Consent> oauth2Consent) {
      if (!isFromSameClientUserAndAs(oauth2Consent)) {
         throw new ViolationException("Consents from different client, authorization server and user are not allowed");
      }

      Exceptions.wrapException(() -> JooqTransaction.jooqTransaction(this.db()).execute(db -> {
         this.deleteMultipleScopes(oauth2Consent, db);
         this.insertConsents(oauth2Consent, db);
      }), e -> new FailureException("Error inserting or updating OAuth2 consent records", e));
   }

   private static boolean isFromSameClientUserAndAs(List<OAuth2Consent> oauth2Consent) {
      return oauth2Consent.stream().map(OAuth2Consent::getAuthorizationServerId).distinct().count() <= 1L
         && oauth2Consent.stream().map(OAuth2Consent::getUserId).distinct().count() <= 1L
         && oauth2Consent.stream().map(OAuth2Consent::getClientId).distinct().count() <= 1L;
   }

   private void insertConsents(List<OAuth2Consent> oauth2Consent, DSLContext db) {
      db.insertInto(
            Tables.OAUTH2_CONSENT,
            Tables.OAUTH2_CONSENT.ID,
            Tables.OAUTH2_CONSENT.CLIENT_ID,
            Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID,
            Tables.OAUTH2_CONSENT.USER_ID,
            Tables.OAUTH2_CONSENT.SCOPE_NAME,
            Tables.OAUTH2_CONSENT.STATUS,
            Tables.OAUTH2_CONSENT.UPDATED_AT,
            Tables.OAUTH2_CONSENT.TENANT_ID
         )
         .valuesOfRows(
            oauth2Consent.stream()
               .map(
                  consent -> DSL.row(
                     consent.getId().getId(),
                     consent.getClientId().id(),
                     consent.getAuthorizationServerId().id(),
                     consent.getUserId().getValue(),
                     consent.getScope().name(),
                     consent.getStatus().getStatus(),
                     DbInstantConverter.toNanoSecondsSinceEpoch(consent.getUpdatedAt()),
                     this.tenantId.tenantId()
                  )
               )
               .toList()
         )
         .execute();
   }

   private void deleteMultipleScopes(List<OAuth2Consent> oauth2Consent, DSLContext db) {
      db.deleteFrom(Tables.OAUTH2_CONSENT)
         .where(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID.eq(oauth2Consent.getFirst().getAuthorizationServerId().id()))
         .and(Tables.OAUTH2_CONSENT.CLIENT_ID.eq(oauth2Consent.getFirst().getClientId().id()))
         .and(Tables.OAUTH2_CONSENT.USER_ID.eq(oauth2Consent.getFirst().getUserId().getValue()))
         .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
         .and(Tables.OAUTH2_CONSENT.SCOPE_NAME.in(oauth2Consent.stream().map(OAuth2Consent::getScope).map(OAuth2Scope::name).toList()))
         .execute();
   }

   @Override
   public int updateUserId(UserId oldUserId, UserId newUserId) {
      return Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(
               db -> db.update(Tables.OAUTH2_CONSENT)
                  .set(Tables.OAUTH2_CONSENT.USER_ID, newUserId.getValue())
                  .where(Tables.OAUTH2_CONSENT.USER_ID.eq(oldUserId.getValue()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
                  .execute()
            ),
         e -> new FailureException(
            String.format(
               "Error updating IAM user ID from '%s' to '%s' in table %s.", oldUserId.getValue(), newUserId.getValue(), Tables.OAUTH2_CONSENT.getName()
            ),
            e
         )
      );
   }

   @Override
   public void updateStatus(OAuth2ConsentId consentId, UserId userId, OAuth2ConsentStatus newStatus) {
      Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(
               db -> db.update(Tables.OAUTH2_CONSENT)
                  .set(Tables.OAUTH2_CONSENT.STATUS, newStatus.getStatus())
                  .set(Tables.OAUTH2_CONSENT.UPDATED_AT, DbInstantConverter.toNanoSecondsSinceEpoch(Instant.now()))
                  .where(Tables.OAUTH2_CONSENT.ID.eq(consentId.getId()))
                  .and(Tables.OAUTH2_CONSENT.USER_ID.eq(userId.getValue()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
                  .execute()
            ),
         e -> new FailureException(
            String.format("Error changing status of OAuth 2.0 Consent with ID '%s' to '%s'.", consentId.getId(), newStatus.getStatus()), e
         )
      );
   }

   @Override
   public void delete(OAuth2ConsentId id) {
      Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(
               db -> db.deleteFrom(Tables.OAUTH2_CONSENT)
                  .where(Tables.OAUTH2_CONSENT.ID.eq(id.getId()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
                  .execute()
            ),
         e -> new FailureException(String.format("error deleting OAuth2 consent with id '%s'.", id.getId()), e)
      );
   }

   @Override
   public int deleteByUser(UserId userId) {
      return Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(
               db -> db.deleteFrom(Tables.OAUTH2_CONSENT)
                  .where(Tables.OAUTH2_CONSENT.USER_ID.eq(userId.getValue()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
                  .execute()
            ),
         e -> new FailureException(String.format("error deleting all OAuth2 consents of user '%s'.", userId.getValue()), e)
      );
   }

   @Override
   public int deleteByUserAndAuthorizationServer(UserId userId, AuthorizationServerId authorizationServerId) {
      return Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(
               db -> db.deleteFrom(Tables.OAUTH2_CONSENT)
                  .where(Tables.OAUTH2_CONSENT.USER_ID.eq(userId.getValue()))
                  .and(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID.eq(authorizationServerId.id()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
                  .execute()
            ),
         e -> new FailureException(
            String.format("error deleting all OAuth2 consents of user '%s' on authorization server '%s'.", userId.getValue(), authorizationServerId.id()), e
         )
      );
   }

   @Override
   public int deleteByUserAndAuthorizationServerAndClient(UserId userId, AuthorizationServerId authorizationServerId, OAuth2ClientId oAuth2ClientId) {
      return Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(
               db -> db.deleteFrom(Tables.OAUTH2_CONSENT)
                  .where(Tables.OAUTH2_CONSENT.USER_ID.eq(userId.getValue()))
                  .and(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID.eq(authorizationServerId.id()))
                  .and(Tables.OAUTH2_CONSENT.CLIENT_ID.eq(oAuth2ClientId.id()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
                  .execute()
            ),
         e -> new FailureException(
            String.format(
               "error deleting all OAuth2 consents of user '%s' on authorization server '%s' and client '%s'.",
               userId.getValue(),
               authorizationServerId.id(),
               oAuth2ClientId.id()
            ),
            e
         )
      );
   }

   @Override
   public int deleteByUserAndAuthorizationServerAndClientAndScope(
      UserId userId, AuthorizationServerId authorizationServerId, OAuth2ClientId oAuth2ClientId, OAuth2Scope oAuth2Scope
   ) {
      return Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(
               db -> db.deleteFrom(Tables.OAUTH2_CONSENT)
                  .where(Tables.OAUTH2_CONSENT.USER_ID.eq(userId.getValue()))
                  .and(Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID.eq(authorizationServerId.id()))
                  .and(Tables.OAUTH2_CONSENT.CLIENT_ID.eq(oAuth2ClientId.id()))
                  .and(Tables.OAUTH2_CONSENT.SCOPE_NAME.eq(oAuth2Scope.name()))
                  .and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId()))
                  .execute()
            ),
         e -> new FailureException(
            String.format(
               "error deleting all OAuth2 consents of user '%s' on authorization server '%s' and client '%s' with scope '%s'.",
               userId.getValue(),
               authorizationServerId.id(),
               oAuth2ClientId.id(),
               oAuth2Scope.name()
            ),
            e
         )
      );
   }

   @Override
   public List<OAuth2ConsentRepository.AuthorizationServerAndClient> findAuthorizationServersAndClientsForUser(UserId userId) {
      return Exceptions.wrapException(
         () -> this.db()
            .selectDistinct(Tables.OAUTH2_CONSENT.CLIENT_ID, Tables.OAUTH2_CONSENT.AUTHORIZATION_SERVER_ID)
            .from(Tables.OAUTH2_CONSENT)
            .where(Tables.OAUTH2_CONSENT.USER_ID.eq(userId.getValue()).and(Tables.OAUTH2_CONSENT.TENANT_ID.eq(this.tenantId.tenantId())))
            .fetch(this::mapAuthorizationServerAndClient)
            .stream()
            .toList(),
         e -> new FailureException(String.format("error finding authorization server ids and client ids for user '%s'.", userId.getValue()), e)
      );
   }
}
