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

import com.airlock.iam.common.api.domain.model.claims.Claim;
import com.airlock.iam.common.api.domain.model.claims.ClaimValue;
import com.airlock.iam.common.api.domain.model.store.user.UserStore;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.oauth2.api.application.configuration.claims.OAuth2UsernameTransformer;
import com.airlock.iam.oauth2.api.domain.model.claims.OAuth2TokenClaimData;
import com.airlock.iam.oauth2.api.domain.model.claims.OAuth2TokenInformation;
import com.airlock.iam.oauth2.api.domain.model.claims.UserClaimExtractor;
import com.airlock.iam.oauth2.domain.model.claims.DistributedClaims;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2LogTypes;
import com.airlock.iam.oauth2.domain.model.session.OAuth2SessionClaims;
import com.airlock.iam.oauth2.domain.model.session.OAuth2SessionId;
import com.airlock.iam.oauth2.domain.model.token.OAuth2ClientAccessTokenExtension;
import com.airlock.iam.oauth2.domain.model.token.OAuth2ClientAccessTokenExtensionProvider;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2RequestHandlingException;
import com.airlock.iam.oauth2.domain.service.OAuth2AccessTokenClaimsService;
import com.airlock.iam.oauth2.domain.service.OAuth2GrantsSessionService;
import com.google.common.collect.ImmutableList;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTClaimsSet.Builder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.stream.Stream;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;

public final class OAuth2AccessTokenClaimsServiceImpl implements OAuth2AccessTokenClaimsService {
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2AccessTokenClaimsServiceImpl.class);
   private static final Collection<String> ACCESS_TOKEN_FIXED_CLAIMS = ImmutableList.builder()
      .addAll(JWTClaimsSet.getRegisteredNames())
      .add("random")
      .add("scope")
      .build();
   @NonNull
   private final UserStore userStore;
   @NonNull
   private final List<UserClaimExtractor<?>> defaultUserClaimExtractors;
   @NonNull
   private final OAuth2ClientAccessTokenExtensionProvider clientExtensionProvider;
   @NonNull
   private final Optional<DistributedClaims> defaultDistributedClaims;
   @NonNull
   private final CommonTokenClaimService commonTokenClaimService;
   @NonNull
   private final Optional<String> issuer;
   private final boolean includeSubClaim;
   @NonNull
   private final List<String> defaultAudience;
   @NonNull
   private final OAuth2UsernameTransformer usernameTransformer;
   @NonNull
   private final OAuth2GrantsSessionService sessionService;
   private final boolean isUsePersistedClaims;

   public OAuth2AccessTokenClaimsServiceImpl(
      @NonNull UserStore userStore,
      @NonNull List<UserClaimExtractor<?>> defaultUserClaimExtractors,
      @NonNull OAuth2ClientAccessTokenExtensionProvider clientExtensionProvider,
      @Nullable DistributedClaims defaultDistributedClaims,
      @Nullable String issuer,
      boolean includeSubClaim,
      @NonNull List<String> defaultAudience,
      @NonNull OAuth2UsernameTransformer usernameTransformer,
      @NonNull OAuth2GrantsSessionService sessionService,
      boolean isUsePersistedClaims
   ) {
      if (userStore == null) {
         throw new NullPointerException("userStore is marked non-null but is null");
      }

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

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

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

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

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

      this.userStore = userStore;
      this.defaultUserClaimExtractors = defaultUserClaimExtractors;
      this.clientExtensionProvider = clientExtensionProvider;
      this.defaultDistributedClaims = Optional.ofNullable(defaultDistributedClaims);
      this.commonTokenClaimService = new CommonTokenClaimService(ACCESS_TOKEN_FIXED_CLAIMS);
      this.issuer = Optional.ofNullable(issuer);
      this.includeSubClaim = includeSubClaim;
      this.defaultAudience = defaultAudience;
      this.usernameTransformer = usernameTransformer;
      this.sessionService = sessionService;
      this.isUsePersistedClaims = isUsePersistedClaims;
   }

   @Override
   public List<Claim<?>> evaluateAccessTokenClaims(OAuth2TokenInformation accessTokenInformation) {
      Builder claims = new Builder();
      PersistentUser user = this.userStore.getByIdentity(accessTokenInformation.username());
      Optional<OAuth2ClientAccessTokenExtension> clientAccessTokenExtension = this.clientExtensionProvider
         .extensionPerClientId(accessTokenInformation.getClientId());
      this.addCustomClaims(claims, user, accessTokenInformation, clientAccessTokenExtension);
      this.addRegisteredClaims(user, claims, clientAccessTokenExtension);
      return claims.build().toJSONObject().entrySet().stream().map(OAuth2AccessTokenClaimsServiceImpl::wrap).toList();
   }

   @Override
   public List<Claim<?>> loadOrEvaluateAccessTokenClaims(OAuth2TokenInformation tokenInformation) {
      return this.isUsePersistedClaims
         ? this.loadAccessTokenClaims(OAuth2SessionId.oAuth2SessionId(tokenInformation.getSessionId().getId()))
         : this.evaluateAccessTokenClaims(tokenInformation);
   }

   private List<Claim<?>> loadAccessTokenClaims(OAuth2SessionId sessionId) {
      return this.sessionService
         .getSessionClaims(sessionId)
         .map(OAuth2SessionClaims::getAccessTokenSnapshot)
         .map(OAuth2SessionClaims::toClaims)
         .orElse(List.of());
   }

   private void addRegisteredClaims(PersistentUser user, Builder jwtClaimsSet, Optional<OAuth2ClientAccessTokenExtension> clientAccessTokenExtension) {
      if (this.includeSubClaim) {
         jwtClaimsSet.subject(this.usernameTransformer.apply(user));
      }

      this.issuer.filter(StringUtils::isNotBlank).ifPresent(jwtClaimsSet::issuer);
      this.addAudienceClaim(jwtClaimsSet, clientAccessTokenExtension);
      this.addDistributedClaims(jwtClaimsSet, clientAccessTokenExtension);
   }

   private void addDistributedClaims(Builder jwtClaimsSet, Optional<OAuth2ClientAccessTokenExtension> clientAccessTokenExtension) {
      Optional<DistributedClaims> clientSpecificClaims = clientAccessTokenExtension.flatMap(OAuth2ClientAccessTokenExtension::distributedClaims);
      if (clientSpecificClaims.isPresent()) {
         DistributedClaims combinedDistributedClaims = this.defaultDistributedClaims
            .orElse(DistributedClaims.emptyDistributedClaims())
            .combineWith(clientSpecificClaims.get());
         jwtClaimsSet.claim("_claim_names", combinedDistributedClaims.getClaimNames());
         jwtClaimsSet.claim("_claim_sources", combinedDistributedClaims.getClaimSources());
      } else {
         this.defaultDistributedClaims.ifPresent(claims -> {
            jwtClaimsSet.claim("_claim_names", claims.getClaimNames());
            jwtClaimsSet.claim("_claim_sources", claims.getClaimSources());
         });
      }
   }

   private void addAudienceClaim(Builder jwtClaimsSet, Optional<OAuth2ClientAccessTokenExtension> clientAccessTokenExtension) {
      List<String> audience = new ArrayList<>();
      Stream.concat(
            this.defaultAudience.stream(), clientAccessTokenExtension.map(OAuth2ClientAccessTokenExtension::audiences).orElse(Collections.emptyList()).stream()
         )
         .filter(Predicate.not(audience::contains))
         .forEach(audience::add);
      if (!audience.isEmpty()) {
         jwtClaimsSet.audience(audience);
      }
   }

   private void addCustomClaims(
      Builder jwtClaimsSet,
      PersistentUser persistentUser,
      OAuth2TokenClaimData tokenInfo,
      Optional<OAuth2ClientAccessTokenExtension> clientAccessTokenExtension
   ) {
      try {
         List<Claim<?>> defaultClaims = this.commonTokenClaimService.extractCustomClaimValues(this.defaultUserClaimExtractors, persistentUser, tokenInfo);
         List<Claim<?>> clientSpecificClaims = clientAccessTokenExtension.<List<Claim<?>>>map(
               extension -> this.commonTokenClaimService.extractCustomClaimValues(extension.userClaimExtractors(), persistentUser, tokenInfo)
            )
            .orElse(Collections.emptyList());
         this.commonTokenClaimService.combineDefaultAndClientSpecificClaims(jwtClaimsSet, defaultClaims, clientSpecificClaims);
      } catch (RuntimeException e) {
         LOG.warn(OAuth2LogTypes.OAUTH2_MESSAGE, "Exception occurred while retrieving access token custom claim: " + e.getMessage(), e);
         throw new OAuth2RequestHandlingException(e);
      }
   }

   static Claim<?> wrap(final Entry<String, Object> entry) {
      return new Claim<ClaimValue>() {
         @Override
         public String name() {
            return entry.getKey();
         }

         @Override
         public ClaimValue value() {
            return entry::getValue;
         }
      };
   }

   @SuppressFBWarnings(justification = "generated code")
   public static OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder builder() {
      return new OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2AccessTokenClaimsServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private UserStore userStore;
      @SuppressFBWarnings(justification = "generated code")
      private List<UserClaimExtractor<?>> defaultUserClaimExtractors;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2ClientAccessTokenExtensionProvider clientExtensionProvider;
      @SuppressFBWarnings(justification = "generated code")
      private DistributedClaims defaultDistributedClaims;
      @SuppressFBWarnings(justification = "generated code")
      private String issuer;
      @SuppressFBWarnings(justification = "generated code")
      private boolean includeSubClaim;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<String> defaultAudience;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2UsernameTransformer usernameTransformer;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2GrantsSessionService sessionService;
      @SuppressFBWarnings(justification = "generated code")
      private boolean isUsePersistedClaims;

      @SuppressFBWarnings(justification = "generated code")
      OAuth2AccessTokenClaimsServiceImplBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder userStore(@NonNull UserStore userStore) {
         if (userStore == null) {
            throw new NullPointerException("userStore is marked non-null but is null");
         }

         this.userStore = userStore;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder defaultUserClaimExtractors(
         @NonNull List<UserClaimExtractor<?>> defaultUserClaimExtractors
      ) {
         if (defaultUserClaimExtractors == null) {
            throw new NullPointerException("defaultUserClaimExtractors is marked non-null but is null");
         }

         this.defaultUserClaimExtractors = defaultUserClaimExtractors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder clientExtensionProvider(
         @NonNull OAuth2ClientAccessTokenExtensionProvider clientExtensionProvider
      ) {
         if (clientExtensionProvider == null) {
            throw new NullPointerException("clientExtensionProvider is marked non-null but is null");
         }

         this.clientExtensionProvider = clientExtensionProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder defaultDistributedClaims(
         @Nullable DistributedClaims defaultDistributedClaims
      ) {
         this.defaultDistributedClaims = defaultDistributedClaims;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder issuer(@Nullable String issuer) {
         this.issuer = issuer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder includeSubClaim(boolean includeSubClaim) {
         this.includeSubClaim = includeSubClaim;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder defaultAudience(String defaultAudience) {
         if (this.defaultAudience == null) {
            this.defaultAudience = new ArrayList<>();
         }

         this.defaultAudience.add(defaultAudience);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder defaultAudience(Collection<? extends String> defaultAudience) {
         if (defaultAudience == null) {
            throw new NullPointerException("defaultAudience cannot be null");
         }

         if (this.defaultAudience == null) {
            this.defaultAudience = new ArrayList<>();
         }

         this.defaultAudience.addAll(defaultAudience);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder clearDefaultAudience() {
         if (this.defaultAudience != null) {
            this.defaultAudience.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder usernameTransformer(
         @NonNull OAuth2UsernameTransformer usernameTransformer
      ) {
         if (usernameTransformer == null) {
            throw new NullPointerException("usernameTransformer is marked non-null but is null");
         }

         this.usernameTransformer = usernameTransformer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder sessionService(@NonNull OAuth2GrantsSessionService sessionService) {
         if (sessionService == null) {
            throw new NullPointerException("sessionService is marked non-null but is null");
         }

         this.sessionService = sessionService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder isUsePersistedClaims(boolean isUsePersistedClaims) {
         this.isUsePersistedClaims = isUsePersistedClaims;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenClaimsServiceImpl build() {
         return new OAuth2AccessTokenClaimsServiceImpl(
            this.userStore,
            this.defaultUserClaimExtractors,
            this.clientExtensionProvider,
            this.defaultDistributedClaims,
            this.issuer,
            this.includeSubClaim,
            switch (this.defaultAudience == null ? 0 : this.defaultAudience.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.defaultAudience.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.defaultAudience));
            },
            this.usernameTransformer,
            this.sessionService,
            this.isUsePersistedClaims
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2AccessTokenClaimsServiceImpl.OAuth2AccessTokenClaimsServiceImplBuilder(userStore="
            + this.userStore
            + ", defaultUserClaimExtractors="
            + this.defaultUserClaimExtractors
            + ", clientExtensionProvider="
            + this.clientExtensionProvider
            + ", defaultDistributedClaims="
            + this.defaultDistributedClaims
            + ", issuer="
            + this.issuer
            + ", includeSubClaim="
            + this.includeSubClaim
            + ", defaultAudience="
            + this.defaultAudience
            + ", usernameTransformer="
            + this.usernameTransformer
            + ", sessionService="
            + this.sessionService
            + ", isUsePersistedClaims="
            + this.isUsePersistedClaims
            + ")";
      }
   }
}
