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.plugin.LogicForConfig;
import com.airlock.iam.base.infrastructure.db.DbInstantConverter;
import com.airlock.iam.base.misc.exception.Exceptions;
import com.airlock.iam.base.misc.log.LogUtil;
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.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.oauth2.api.domain.model.AuthorizationServerId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.application.configuration.par.OAuth2ParRequestRepositoryConfig;
import com.airlock.iam.oauth2.domain.model.par.OAuth2AuthorizationRequestQuery;
import com.airlock.iam.oauth2.domain.model.par.OAuth2ParRequestRepository;
import com.airlock.iam.oauth2.domain.model.par.OAuth2ParRequestUri;
import com.airlock.iam.security.util.crypto.random.AlphanumericRandomStringGenerator;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Instant;
import java.util.Optional;
import org.apache.commons.lang3.Strings;
import org.jooq.DSLContext;
import org.jooq.Record;

@LogicForConfig(OAuth2ParRequestRepositoryConfig.class)
public final class OAuth2ParRequestRepositoryImpl implements OAuth2ParRequestRepository {
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2ParRequestRepositoryImpl.class);
   private static final String REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
   private static final int REQUEST_URI_REFERENCE_LENGTH = 30;
   private final Provider<JooqConfig> jooqConfig;
   private final Provider<TenantId> tenantId;
   private final Provider<TimeProvider> timeProvider;

   @Inject
   public OAuth2ParRequestRepositoryImpl(Provider<JooqConfig> jooqConfig, Provider<TenantId> tenantId, Provider<TimeProvider> timeProvider) {
      this.jooqConfig = jooqConfig;
      this.tenantId = tenantId;
      this.timeProvider = timeProvider;
   }

   private DSLContext db() {
      return JooqProvider.jooqProvider((JooqConfig)this.jooqConfig.get()).db();
   }

   @Override
   public OAuth2ParRequestUri insert(
      OAuth2ClientId clientId, AuthorizationServerId authorizationServerId, OAuth2AuthorizationRequestQuery query, Instant validUntil
   ) {
      String requestUriReference = AlphanumericRandomStringGenerator.generateAlphanumericRandomString(30);
      Exceptions.wrapException(
         () -> JooqTransaction.jooqTransaction(this.db())
            .execute(
               db -> db.insertInto(Tables.OAUTH2_PAR_REQUEST)
                  .set(Tables.OAUTH2_PAR_REQUEST.REQUEST_URI_REFERENCE, requestUriReference)
                  .set(Tables.OAUTH2_PAR_REQUEST.CLIENT_ID, clientId.id())
                  .set(Tables.OAUTH2_PAR_REQUEST.AUTHORIZATION_SERVER_ID, authorizationServerId.id())
                  .set(Tables.OAUTH2_PAR_REQUEST.QUERY_STRING, query.queryString())
                  .set(Tables.OAUTH2_PAR_REQUEST.VALID_UNTIL, DbInstantConverter.toNanoSecondsSinceEpoch(validUntil))
                  .set(Tables.OAUTH2_PAR_REQUEST.TENANT_ID, ((TenantId)this.tenantId.get()).tenantId())
                  .execute()
            ),
         e -> new FailureException("Error inserting OAuth 2.0 Pushed Authorization Request.", e)
      );
      OAuth2ParRequestUri requestUri = new OAuth2ParRequestUri("urn:ietf:params:oauth:request_uri:" + requestUriReference);
      LOG.debug(
         "Stored OAuth 2.0 Pushed Authorization Request for client "
            + LogUtil.quote(clientId.id())
            + " and authorization server "
            + LogUtil.quote(authorizationServerId.id())
            + " with request URI: "
            + LogUtil.logSafeQuote(requestUri.requestUri())
      );
      return requestUri;
   }

   @Override
   public Optional<OAuth2AuthorizationRequestQuery> getAndDelete(
      OAuth2ClientId clientId, AuthorizationServerId authorizationServerId, OAuth2ParRequestUri requestUri
   ) {
      if (!this.validateRequestUri(requestUri)) {
         throw new ViolationException(
            String.format(
               "Pushed Authorization Request URI does not start with %s: %s",
               LogUtil.quote("urn:ietf:params:oauth:request_uri:"),
               LogUtil.logSafeQuote(requestUri.requestUri())
            )
         );
      } else {
         return Exceptions.wrapException(
            () -> (Optional<OAuth2AuthorizationRequestQuery>)JooqTransaction.jooqTransaction(this.db())
               .executeAndReturn(
                  db -> {
                     Optional<OAuth2ParRequestRepositoryImpl.OAuth2ParRequest> matchingRequest = db.select(
                           Tables.OAUTH2_PAR_REQUEST.QUERY_STRING, Tables.OAUTH2_PAR_REQUEST.VALID_UNTIL
                        )
                        .from(Tables.OAUTH2_PAR_REQUEST)
                        .where(Tables.OAUTH2_PAR_REQUEST.REQUEST_URI_REFERENCE.eq(this.extractRequestUriReference(requestUri)))
                        .and(Tables.OAUTH2_PAR_REQUEST.TENANT_ID.eq(((TenantId)this.tenantId.get()).tenantId()))
                        .and(Tables.OAUTH2_PAR_REQUEST.AUTHORIZATION_SERVER_ID.eq(authorizationServerId.id()))
                        .and(Tables.OAUTH2_PAR_REQUEST.CLIENT_ID.eq(clientId.id()))
                        .fetch(OAuth2ParRequestRepositoryImpl::mapParRequest)
                        .stream()
                        .findFirst();
                     if (matchingRequest.isEmpty()) {
                        return Optional.empty();
                     } else {
                        int numDeletedEntries = db.deleteFrom(Tables.OAUTH2_PAR_REQUEST)
                           .where(Tables.OAUTH2_PAR_REQUEST.REQUEST_URI_REFERENCE.eq(this.extractRequestUriReference(requestUri)))
                           .execute();
                        if (numDeletedEntries != 1) {
                           LOG.debug(
                              String.format(
                                 "Error while getting OAuth 2.0 PAR request for request URI %s: matching request was already removed, not returning any result.",
                                 LogUtil.logSafeQuote(requestUri.requestUri())
                              )
                           );
                           return Optional.empty();
                        } else {
                           return matchingRequest.filter(
                                 request -> matchingRequest.get().validUntil().isAfter(((TimeProvider)this.timeProvider.get()).getCurrentInstant())
                              )
                              .map(OAuth2ParRequestRepositoryImpl.OAuth2ParRequest::requestQuery);
                        }
                     }
                  }
               ),
            e -> new FailureException(String.format("Error finding OAuth 2.0 PAR request for request URI %s", LogUtil.logSafeQuote(requestUri.requestUri())), e)
         );
      }
   }

   private boolean validateRequestUri(OAuth2ParRequestUri requestUri) {
      return requestUri.requestUri().startsWith("urn:ietf:params:oauth:request_uri:");
   }

   private String extractRequestUriReference(OAuth2ParRequestUri requestUri) {
      return Strings.CS.removeStart(requestUri.requestUri(), "urn:ietf:params:oauth:request_uri:");
   }

   private static OAuth2ParRequestRepositoryImpl.OAuth2ParRequest mapParRequest(Record record) {
      return new OAuth2ParRequestRepositoryImpl.OAuth2ParRequest(
         new OAuth2AuthorizationRequestQuery((String)record.get(Tables.OAUTH2_PAR_REQUEST.QUERY_STRING)),
         DbInstantConverter.toInstant((Long)record.get(Tables.OAUTH2_PAR_REQUEST.VALID_UNTIL))
      );
   }

   private record OAuth2ParRequest(OAuth2AuthorizationRequestQuery requestQuery, Instant validUntil) {
   }
}
