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

import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.core.misc.token.KnownTokenTypes;
import com.airlock.iam.database.infrastructure.generated.schema.tables.Oauth2SessionAttributeTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.Oauth2SessionTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.TokenAssignmentTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.TokenTable;
import com.airlock.iam.oauth2.api.domain.model.AuthorizationServerId;
import com.airlock.iam.oauth2.domain.model.session.OAuth2Session;
import com.airlock.iam.oauth2.domain.model.session.OAuth2SessionId;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.Record1;
import org.jooq.Result;
import org.jooq.SelectFieldOrAsterisk;
import org.jooq.SelectLimitStep;
import org.jooq.SelectWhereStep;
import org.jooq.impl.DSL;

public final class OAuth2SessionFinder {
   private final DSLContext db;
   private final OAuth2SessionMaterializer materializer;
   private final boolean usePersistedClaims;

   public static OAuth2SessionFinder oAuth2SessionFinder(DSLContext db, boolean usePersistedClaims) {
      return new OAuth2SessionFinder(db, OAuth2SessionMaterializer.oAuth2SessionMaterializer(), usePersistedClaims);
   }

   public Optional<OAuth2Session> findOAuth2SessionById(OAuth2SessionId oauth2SessionId, AuthorizationServerId authorizationServerId) {
      OAuth2SessionId[] ids = this.mapSessionIds(
         this.selectSessionIdsFromTokenModel(
            false,
            Optional.of(1),
            OAuth2SessionRepositoryFields.TOKEN_AUTHORIZATION_SERVER_ID.eq(authorizationServerId.id()),
            OAuth2SessionRepositoryFields.TOKEN_SESSION_ID.eq(oauth2SessionId.getId())
         )
      );
      if (ids.length == 0) {
         return Optional.empty();
      }

      Result<Record> records = this.findOAuth2SessionRecordById(oauth2SessionId);
      return this.materializer.materializeOAuth2Session(records);
   }

   public OAuth2SessionId[] findAllOAuth2SessionsHavingTokens(UserId userId) {
      return this.mapSessionIds(this.selectSessionIdsFromTokenModelAndAssignment(TokenAssignmentTable.TOKEN_ASSIGNMENT.TA_USER.eq(userId.getValue())));
   }

   public OAuth2SessionId[] findAllOAuth2SessionsHavingTokens(UserId userId, AuthorizationServerId authorizationServerId) {
      return this.mapSessionIds(
         this.selectSessionIdsFromTokenModelAndAssignment(
            TokenAssignmentTable.TOKEN_ASSIGNMENT.TA_USER.eq(userId.getValue()),
            OAuth2SessionRepositoryFields.TOKEN_AUTHORIZATION_SERVER_ID.eq(authorizationServerId.id())
         )
      );
   }

   private Result<Record1<String>> selectSessionIdsFromTokenModelAndAssignment(Condition... conditions) {
      return this.selectSessionIdsFromTokenModel(true, Optional.empty(), conditions);
   }

   private Result<Record1<String>> selectSessionIdsFromTokenModel(boolean includeTokenAssignments, Optional<Integer> limit, Condition... conditions) {
      SelectWhereStep<Record1<String>> select;
      if (includeTokenAssignments) {
         select = this.db
            .selectDistinct(OAuth2SessionRepositoryFields.TOKEN_SESSION_ID)
            .from(TokenAssignmentTable.TOKEN_ASSIGNMENT)
            .join(TokenTable.TOKEN)
            .on(TokenTable.TOKEN.TOKEN_ID.eq(TokenAssignmentTable.TOKEN_ASSIGNMENT.TA_TOKEN_ID));
      } else {
         select = this.db.selectDistinct(OAuth2SessionRepositoryFields.TOKEN_SESSION_ID).from(TokenTable.TOKEN);
      }

      SelectLimitStep<Record1<String>> query = select.where(
            Stream.concat(Stream.of(TokenTable.TOKEN.TYPE.eq(DSL.inline(KnownTokenTypes.OAUTH2.getId()))), Stream.of(conditions)).toArray(Condition[]::new)
         )
         .orderBy(OAuth2SessionRepositoryFields.TOKEN_SESSION_ID);
      return limit.isPresent() ? query.limit(limit.get()).fetch() : query.fetch();
   }

   private OAuth2SessionId[] mapSessionIds(Result<Record1<String>> sessionIdResult) {
      return sessionIdResult.map(r -> (String)r.get(OAuth2SessionRepositoryFields.TOKEN_SESSION_ID))
         .stream()
         .map(OAuth2SessionId::oAuth2SessionId)
         .toArray(OAuth2SessionId[]::new);
   }

   private Result<Record> findOAuth2SessionRecordById(OAuth2SessionId oauth2SessionId) {
      return this.fetchOAuth2SessionsWhere(OAuth2SessionRepositoryConditions.matchingOAuth2SessionId(oauth2SessionId));
   }

   Result<Record> fetchOAuth2SessionsWhere(Condition condition) {
      return this.db
         .select(selectAvailableFields(this.usePersistedClaims))
         .from(Oauth2SessionTable.OAUTH2_SESSION)
         .leftJoin(Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE)
         .onKey()
         .where(condition)
         .orderBy(Oauth2SessionTable.OAUTH2_SESSION.ID, Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE.NAME)
         .fetch();
   }

   private static Collection<? extends SelectFieldOrAsterisk> selectAvailableFields(boolean usePersistedClaims) {
      return usePersistedClaims
         ? List.of(
            Oauth2SessionTable.OAUTH2_SESSION.PK,
            Oauth2SessionTable.OAUTH2_SESSION.ID,
            Oauth2SessionTable.OAUTH2_SESSION.CLAIMS,
            Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE.NAME,
            Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE.CONTENT
         )
         : List.of(
            Oauth2SessionTable.OAUTH2_SESSION.PK,
            Oauth2SessionTable.OAUTH2_SESSION.ID,
            Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE.NAME,
            Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE.CONTENT
         );
   }

   @SuppressFBWarnings(justification = "generated code")
   private OAuth2SessionFinder(DSLContext db, OAuth2SessionMaterializer materializer, boolean usePersistedClaims) {
      this.db = db;
      this.materializer = materializer;
      this.usePersistedClaims = usePersistedClaims;
   }
}
