package com.airlock.iam.oauth2.domain.service.codegrant;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthentication;
import com.airlock.iam.oauth2.domain.model.client.OAuth2Client;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2TokenLogMessageGenerator;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2TokenRepresentation;
import com.airlock.iam.oauth2.domain.model.session.PkceCodeChallenge;
import com.airlock.iam.oauth2.domain.model.token.AccessTokenInfo;
import com.airlock.iam.oauth2.domain.model.token.AuthorizationCodeInfo;
import com.airlock.iam.oauth2.domain.model.token.OAuth2AccessToken;
import com.airlock.iam.oauth2.domain.model.token.OAuth2RawAccessTokenContainer;
import com.airlock.iam.oauth2.domain.model.token.OAuth2RefreshToken;
import com.airlock.iam.oauth2.domain.model.token.OAuth2Token;
import com.airlock.iam.oauth2.domain.model.token.RawOAuth2TokenWithHash;
import com.airlock.iam.oauth2.domain.model.token.SessionInfo;
import com.airlock.iam.oauth2.domain.model.token.scope.OAuth2ScopeMatcher;
import com.airlock.iam.oauth2.domain.service.GrantedScopeProcessorsService;
import com.airlock.iam.oauth2.domain.service.persistence.OAuth2AccessAndRefreshTokenGenerator;
import com.airlock.iam.oauth2.domain.service.persistence.OAuth2AccessTokenGenerator;
import com.airlock.iam.oauth2.domain.service.persistence.OAuth2AuthorizationCodeGenerator;
import com.airlock.iam.oauth2.domain.service.persistence.OAuth2TokenDeleter;
import com.airlock.iam.oauth2.domain.service.persistence.OAuth2TokenReader;
import com.airlock.iam.oauth2.domain.service.persistence.OAuth2TokenUpdater;
import com.airlock.iam.oauth2.domain.service.persistence.OAuth2UserLoader;
import com.airlock.iam.oauth2.domain.service.token.OAuth2TokenBinder;
import com.airlock.iam.oauth2.domain.service.validation.OAuth2TokenValidation;
import com.airlock.iam.plugin.api.config.LogTypes;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;

public final class DefaultOAuth2CodeGrantTokenManager implements OAuth2CodeGrantTokenManager {
   @NonNull
   private final OAuth2TokenReader tokenReader;
   @NonNull
   private final OAuth2TokenUpdater tokenUpdater;
   @NonNull
   private final OAuth2TokenDeleter tokenDeleter;
   @NonNull
   private final OAuth2TokenValidation tokenValidation;
   @NonNull
   private final OAuth2TokenBinder tokenBinder;
   @NonNull
   private final OAuth2AuthorizationCodeGenerator authorizationCodeGenerator;
   @NonNull
   private final OAuth2AccessAndRefreshTokenGenerator accessAndRefreshTokenGenerator;
   @NonNull
   private final OAuth2AccessTokenGenerator accessTokenGenerator;
   @NonNull
   private final OAuth2TokenRepresentation tokenRepresentation;
   @NonNull
   private final Optional<Integer> gracePeriod;
   @NonNull
   private final GrantedScopeProcessorsService grantedScopeProcessorsService;
   @NonNull
   private final List<OAuth2ScopeMatcher> scopesToRemoveOnRefresh;
   private final boolean allowEmptyScope;
   private final boolean isGenerateRefreshToken;
   private final boolean isSingleUseRefreshTokens;
   private final boolean isInvalidateOldAccessTokensOnRefresh;
   @NonNull
   private final OAuth2UserLoader userLoader;
   @NonNull
   private final ELogger logger;

   @Override
   public Optional<RawOAuth2TokenWithHash> generateAuthorizationCode(AuthorizationCodeInfo authCodeInfo, SessionInfo sessionInfo) throws PersisterException {
      if (!this.allowEmptyScope && authCodeInfo.scopes().isEmpty()) {
         this.logger
            .warn(
               LogTypes.PLUGIN_INFO,
               "OAuth2: Resulting scope is empty (user has no roles matching any requested scope), and this is not allowed by the current settings. Cannot issue Authorization Code."
            );
         return Optional.empty();
      } else {
         return Optional.of(this.authorizationCodeGenerator.generateAndAssignAuthzCodeToken(authCodeInfo, sessionInfo));
      }
   }

   @Override
   public Optional<OAuth2RawAccessTokenContainer> exchangeAuthorizationCodeForAccessToken(
      String authorizationCode,
      OAuth2Client client,
      OAuth2ClientAuthentication authentication,
      Optional<String> pkceCodeVerifier,
      Optional<String> redirectUriFromRequest
   ) throws PersisterException {
      if (StringUtils.isBlank(authorizationCode)) {
         return Optional.empty();
      } else {
         Optional<OAuth2Token> authzCode = this.tokenReader
            .getActiveOrInactiveTokenFromProviderWithFullTokenString(authorizationCode, OAuth2Token.OAuth2TokenType.AUTHORIZATION_CODE);
         if (authzCode.isEmpty()) {
            return Optional.empty();
         } else {
            com.airlock.iam.oauth2.domain.model.token.OAuth2AuthorizationCode code = (com.airlock.iam.oauth2.domain.model.token.OAuth2AuthorizationCode)authzCode.get();
            if (this.isReplayed(code)) {
               this.logger.warn(LogTypes.PLUGIN_INFO, "Trying to exchange an authorization code twice; purging all related tokens from database.");
               this.tokenDeleter.purgeRelatedTokensFromDatabase(code);
               return Optional.empty();
            } else if (!this.validateCodeAndAssignedUser(code, client.clientId())) {
               this.tokenDeleter.purgeRelatedTokensFromDatabase(code);
               return Optional.empty();
            } else if (!this.isPkceValid(code.getPkceChallenge(), pkceCodeVerifier)) {
               this.tokenDeleter.purgeRelatedTokensFromDatabase(code);
               return Optional.empty();
            } else if (!this.isValidRedirectUri(redirectUriFromRequest, code.getRedirectUri())) {
               this.tokenDeleter.purgeRelatedTokensFromDatabase(code);
               return Optional.empty();
            } else {
               this.tokenUpdater.invalidate(code);
               this.tokenUpdater.increaseTokenUsage(code);
               SessionInfo session = sessionInfo(code);
               Set<OAuth2Scope> finalScopes = this.grantedScopeProcessorsService.processScopes(code.getScopes().scopesAsSet(), client, authentication);
               if (!this.allowEmptyScope && finalScopes.isEmpty()) {
                  this.logger
                     .warn(
                        LogTypes.PLUGIN_INFO,
                        "OAuth2: Resulting access token scope is empty (due to the configured \"Granted Scope Processors\"), and this is not allowed by the current settings. Cannot issue Access Token."
                     );
                  return Optional.empty();
               } else {
                  AccessTokenInfo accessTokenInfo = new AccessTokenInfo(finalScopes, this.tokenBinder.bind(authentication));
                  return this.generateAccessAndRefreshToken(session, accessTokenInfo);
               }
            }
         }
      }
   }

   private static SessionInfo sessionInfo(OAuth2Token token) {
      return new SessionInfo(
         token.getSessionId(),
         token.getUserId().getValue(),
         token.getClientId(),
         token.getAuthorizationServerId(),
         Date.from(token.getUserAuthenticationInstant())
      );
   }

   private boolean isReplayed(com.airlock.iam.oauth2.domain.model.token.OAuth2AuthorizationCode code) {
      return !code.isActive();
   }

   private boolean validateCodeAndAssignedUser(com.airlock.iam.oauth2.domain.model.token.OAuth2AuthorizationCode code, OAuth2ClientId clientId) throws PersisterException {
      return this.tokenValidation.isValidTokenCheckAssignment(code, Optional.of(clientId)) && this.userLoader.getValidUser(code).isPresent();
   }

   @Override
   public Optional<OAuth2RawAccessTokenContainer> refreshAccessToken(
      String refreshTokenString, OAuth2Client client, OAuth2ClientAuthentication clientAuthentication
   ) throws PersisterException {
      OAuth2ClientId clientId = client.clientId();
      if (StringUtils.isBlank(refreshTokenString)) {
         this.logTokenMessage("No Refresh Token provided.", refreshTokenString, clientId);
         return Optional.empty();
      }

      Optional<OAuth2Token> optionalRefreshToken = this.findRefreshToken(refreshTokenString);
      if (optionalRefreshToken.isEmpty()) {
         this.logTokenMessage("Provided Refresh Token not found; cannot refresh.", refreshTokenString, clientId);
         return Optional.empty();
      }

      OAuth2RefreshToken refreshToken = (OAuth2RefreshToken)optionalRefreshToken.get();
      if (!this.validateRefreshTokenAndAssignedUser(refreshToken, clientId)) {
         this.logTokenMessage("Validation of Refresh Token or assigned user failed.", refreshTokenString, clientId);
         return Optional.empty();
      }

      this.tokenUpdater.increaseTokenUsage(refreshToken);
      if (this.isSingleUseRefreshTokens) {
         if (this.singleUseRefreshCleanUpWithReplayAttackDetection(refreshToken)) {
            return Optional.empty();
         }

         this.handleSingleUseRefresh(refreshToken);
      }

      if (this.isInvalidateOldAccessTokensOnRefresh) {
         Optional<OAuth2AccessToken> oldAccesstoken = this.tokenReader.getAccessTokenLinkedWithRefreshToken(refreshToken);
         if (oldAccesstoken.isPresent()) {
            this.logger.debug("Invalidating the previous Access Token after token refresh.");
            this.tokenUpdater.invalidate(oldAccesstoken.get());
         }
      }

      try {
         Set<OAuth2Scope> resultingScopes = refreshToken.getScopes()
            .scopesAsSet()
            .stream()
            .filter(isNonRefreshableScope(this.scopesToRemoveOnRefresh).negate())
            .collect(Collectors.toSet());
         AccessTokenInfo accessTokenInfo = new AccessTokenInfo(resultingScopes, this.tokenBinder.bind(clientAuthentication));
         Optional<OAuth2RawAccessTokenContainer> rawAccessToken = this.generateAccessAndRefreshToken(sessionInfo(refreshToken), accessTokenInfo);
         OAuth2TokenLogMessageGenerator messageGenerator = new OAuth2TokenLogMessageGenerator(this.tokenRepresentation);
         if (this.isGenerateRefreshToken) {
            this.logger
               .info(messageGenerator.getRefreshTokenLogMessage("Generated new Access and Refresh Tokens.", clientId, refreshTokenString, rawAccessToken));
         } else {
            this.logger.info(messageGenerator.getRefreshTokenLogMessage("Generated new Access Token.", clientId, refreshTokenString, rawAccessToken));
         }

         return rawAccessToken;
      } catch (PersisterException e) {
         this.logTokenMessage("Caught persister exception generating new Access and Refresh Token: " + e.getMessage(), refreshTokenString, clientId);
         throw e;
      }
   }

   private Optional<OAuth2Token> findRefreshToken(String refreshTokenString) throws PersisterException {
      Optional<OAuth2Token> token = this.tokenReader
         .getActiveOrInactiveTokenFromProviderWithFullTokenString(refreshTokenString, OAuth2Token.OAuth2TokenType.REFRESH_TOKEN);
      return token.isPresent() ? token : Optional.empty();
   }

   private static Predicate<OAuth2Scope> isNonRefreshableScope(List<OAuth2ScopeMatcher> scopesToRemoveOnRefresh) {
      return scope -> scopesToRemoveOnRefresh.stream().anyMatch(matcher -> matcher.matches(scope));
   }

   private void logTokenMessage(String message, String refreshToken, OAuth2ClientId clientId) {
      OAuth2TokenLogMessageGenerator messageGenerator = new OAuth2TokenLogMessageGenerator(this.tokenRepresentation);
      this.logger.info(messageGenerator.getRefreshTokenLogMessage(message, clientId, refreshToken, Optional.empty()));
   }

   private boolean validateRefreshTokenAndAssignedUser(OAuth2RefreshToken refreshToken, OAuth2ClientId clientId) throws PersisterException {
      if (!this.tokenValidation.isValidTokenCheckAssignment(refreshToken, Optional.of(clientId))) {
         if (!refreshToken.isActive()) {
            this.tokenDeleter.purgeRelatedTokensFromDatabase(refreshToken);
         }

         return false;
      } else if (this.userLoader.getValidUser(refreshToken).isEmpty()) {
         this.tokenDeleter.purgeRelatedTokensFromDatabase(refreshToken);
         return false;
      } else if (!this.gracePeriod.isEmpty() && refreshToken.isGracePeriodExpired()) {
         this.logger.info("Invalidating the current Refresh Token because the grace period is expired.");
         this.tokenUpdater.invalidate(refreshToken);
         return false;
      } else {
         return true;
      }
   }

   private boolean singleUseRefreshCleanUpWithReplayAttackDetection(OAuth2RefreshToken refreshToken) throws PersisterException {
      List<OAuth2RefreshToken> otherRefreshTokens = this.tokenReader.getOtherActiveRefreshTokensWithSameSession(refreshToken);
      if (refreshToken.getGracePeriodEnd().isPresent()) {
         return this.gracePeriodCleanUpWithReplayAttackDetection(otherRefreshTokens);
      }

      otherRefreshTokens.forEach(this.tokenUpdater::invalidate);
      return false;
   }

   private boolean gracePeriodCleanUpWithReplayAttackDetection(List<OAuth2RefreshToken> otherActiveRefreshTokens) throws PersisterException {
      if (otherActiveRefreshTokens.isEmpty()) {
         return false;
      }

      OAuth2RefreshToken otherRefreshToken = otherActiveRefreshTokens.getFirst();
      if (otherActiveRefreshTokens.size() > 1) {
         this.logger
            .warn(
               LogTypes.PLUGIN_INFO,
               "Inconsistent state: too many ("
                  + otherActiveRefreshTokens.size()
                  + ") other, active Refresh Tokens found for this OAuth 2.0 session. Replay attack detected, deleting OAuth 2.0 session."
            );
         this.tokenDeleter.purgeRelatedTokensFromDatabase(otherRefreshToken);
         return true;
      }

      this.tokenUpdater.invalidate(otherRefreshToken);
      Optional<OAuth2AccessToken> maybeOtherAccessToken = this.tokenReader.getAccessTokenLinkedWithRefreshToken(otherRefreshToken);
      if (maybeOtherAccessToken.isPresent()) {
         OAuth2Token otherAccessToken = maybeOtherAccessToken.get();
         if (otherAccessToken.getTotalUsages() != 0) {
            this.logger
               .warn(
                  LogTypes.PLUGIN_INFO,
                  "Obsolete (due to another refresh within grace period) Refresh Token with used Access Token found. Replay attack detected, deleting OAuth 2.0 session."
               );
            this.tokenDeleter.purgeRelatedTokensFromDatabase(otherAccessToken);
            return true;
         }

         this.logger.info("Refresh within grace period. The tokens of the last refresh will be invalidated and a new one will be generated.");
         this.tokenUpdater.invalidate(otherAccessToken);
      }

      return false;
   }

   private void handleSingleUseRefresh(OAuth2RefreshToken refreshToken) {
      if (this.gracePeriod.isPresent()) {
         this.setGracePeriodIfNecessary(refreshToken, this.gracePeriod.get());
      } else {
         this.logger.debug("Invalidating the previous Refresh Token after using it because the grace period is disabled.");
         this.tokenUpdater.invalidate(refreshToken);
      }
   }

   private void setGracePeriodIfNecessary(OAuth2RefreshToken refreshToken, int gracePeriodSeconds) {
      if (!refreshToken.getGracePeriodEnd().isPresent()) {
         Date currentGracePeriodEndDate = this.calculateGracePeriodEndDate(refreshToken, gracePeriodSeconds);
         this.logger.debug("Grace period enabled for Refresh Token: setting the grace period end to '" + currentGracePeriodEndDate + "'.");
         this.tokenUpdater.setGracePeriod(refreshToken, currentGracePeriodEndDate);
      }
   }

   private Date calculateGracePeriodEndDate(OAuth2RefreshToken refreshToken, int gracePeriodSeconds) {
      Date currentGracePeriodEndDate = new Date(System.currentTimeMillis() + gracePeriodSeconds * 1000L);
      Optional<Date> validityEndDate = refreshToken.getExpiry().map(Date::from);
      if (validityEndDate.isPresent() && currentGracePeriodEndDate.after(validityEndDate.get())) {
         this.logger
            .debug(
               "The calculated grace period end ("
                  + currentGracePeriodEndDate
                  + ") is after the Refresh Token validity end ("
                  + validityEndDate.get()
                  + "). The grace period will be capped to the validity end date."
            );
         currentGracePeriodEndDate = validityEndDate.get();
      }

      return currentGracePeriodEndDate;
   }

   private Optional<OAuth2RawAccessTokenContainer> generateAccessAndRefreshToken(SessionInfo session, AccessTokenInfo accessTokenInfo) throws PersisterException {
      return this.isGenerateRefreshToken
         ? Optional.of(this.accessAndRefreshTokenGenerator.generateAndAssignAccessAndRefreshToken(session, accessTokenInfo))
         : Optional.of(this.accessTokenGenerator.generateAndAssignAccessToken(session, accessTokenInfo));
   }

   private boolean isPkceValid(Optional<PkceCodeChallenge> pkceChallenge, Optional<String> pkceVerifier) {
      if (pkceChallenge.isEmpty()) {
         return true;
      } else if (pkceVerifier.isEmpty()) {
         this.logger.warn(LogTypes.PLUGIN_INFO, "PKCE activated, but token request does not contain a PKCE verifier");
         return false;
      } else if (!pkceChallenge.get().verify(pkceVerifier.get())) {
         this.logger.warn(LogTypes.PLUGIN_INFO, "PKCE verifier does not match the PKCE challenge");
         return false;
      } else {
         this.logger.debug("PKCE " + pkceChallenge.get().getChallengeMethod() + " challenge verified successfully.");
         return true;
      }
   }

   private boolean isValidRedirectUri(Optional<String> redirectUriFromRequest, Optional<String> expectedRedirectUri) {
      if (!redirectUriFromRequest.equals(expectedRedirectUri)) {
         this.logger
            .warn(
               LogTypes.PLUGIN_INFO,
               "Redirect URI from token exchange request: "
                  + LogUtil.logSafeQuote(redirectUriFromRequest.orElse("N/A"))
                  + " does not match redirect_uri from previous authorization request: "
                  + LogUtil.logSafeQuote(expectedRedirectUri.orElse("N/A"))
            );
         return false;
      } else {
         return true;
      }
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder toBuilder() {
      return new DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder()
         .tokenReader(this.tokenReader)
         .tokenUpdater(this.tokenUpdater)
         .tokenDeleter(this.tokenDeleter)
         .tokenValidation(this.tokenValidation)
         .tokenBinder(this.tokenBinder)
         .authorizationCodeGenerator(this.authorizationCodeGenerator)
         .accessAndRefreshTokenGenerator(this.accessAndRefreshTokenGenerator)
         .accessTokenGenerator(this.accessTokenGenerator)
         .tokenRepresentation(this.tokenRepresentation)
         .gracePeriod(this.gracePeriod)
         .grantedScopeProcessorsService(this.grantedScopeProcessorsService)
         .scopesToRemoveOnRefresh(this.scopesToRemoveOnRefresh)
         .allowEmptyScope(this.allowEmptyScope)
         .isGenerateRefreshToken(this.isGenerateRefreshToken)
         .isSingleUseRefreshTokens(this.isSingleUseRefreshTokens)
         .isInvalidateOldAccessTokensOnRefresh(this.isInvalidateOldAccessTokensOnRefresh)
         .userLoader(this.userLoader)
         .logger(this.logger);
   }

   @SuppressFBWarnings(justification = "generated code")
   public DefaultOAuth2CodeGrantTokenManager(
      @NonNull OAuth2TokenReader tokenReader,
      @NonNull OAuth2TokenUpdater tokenUpdater,
      @NonNull OAuth2TokenDeleter tokenDeleter,
      @NonNull OAuth2TokenValidation tokenValidation,
      @NonNull OAuth2TokenBinder tokenBinder,
      @NonNull OAuth2AuthorizationCodeGenerator authorizationCodeGenerator,
      @NonNull OAuth2AccessAndRefreshTokenGenerator accessAndRefreshTokenGenerator,
      @NonNull OAuth2AccessTokenGenerator accessTokenGenerator,
      @NonNull OAuth2TokenRepresentation tokenRepresentation,
      @NonNull Optional<Integer> gracePeriod,
      @NonNull GrantedScopeProcessorsService grantedScopeProcessorsService,
      @NonNull List<OAuth2ScopeMatcher> scopesToRemoveOnRefresh,
      boolean allowEmptyScope,
      boolean isGenerateRefreshToken,
      boolean isSingleUseRefreshTokens,
      boolean isInvalidateOldAccessTokensOnRefresh,
      @NonNull OAuth2UserLoader userLoader,
      @NonNull ELogger logger
   ) {
      if (tokenReader == null) {
         throw new NullPointerException("tokenReader is marked non-null but is null");
      }

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

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

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

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

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

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

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

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

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

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

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

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

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

      this.tokenReader = tokenReader;
      this.tokenUpdater = tokenUpdater;
      this.tokenDeleter = tokenDeleter;
      this.tokenValidation = tokenValidation;
      this.tokenBinder = tokenBinder;
      this.authorizationCodeGenerator = authorizationCodeGenerator;
      this.accessAndRefreshTokenGenerator = accessAndRefreshTokenGenerator;
      this.accessTokenGenerator = accessTokenGenerator;
      this.tokenRepresentation = tokenRepresentation;
      this.gracePeriod = gracePeriod;
      this.grantedScopeProcessorsService = grantedScopeProcessorsService;
      this.scopesToRemoveOnRefresh = scopesToRemoveOnRefresh;
      this.allowEmptyScope = allowEmptyScope;
      this.isGenerateRefreshToken = isGenerateRefreshToken;
      this.isSingleUseRefreshTokens = isSingleUseRefreshTokens;
      this.isInvalidateOldAccessTokensOnRefresh = isInvalidateOldAccessTokensOnRefresh;
      this.userLoader = userLoader;
      this.logger = logger;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class DefaultOAuth2CodeGrantTokenManagerBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenReader tokenReader;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenUpdater tokenUpdater;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenDeleter tokenDeleter;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenValidation tokenValidation;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenBinder tokenBinder;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2AuthorizationCodeGenerator authorizationCodeGenerator;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2AccessAndRefreshTokenGenerator accessAndRefreshTokenGenerator;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2AccessTokenGenerator accessTokenGenerator;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenRepresentation tokenRepresentation;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<Integer> gracePeriod;
      @SuppressFBWarnings(justification = "generated code")
      private GrantedScopeProcessorsService grantedScopeProcessorsService;
      @SuppressFBWarnings(justification = "generated code")
      private List<OAuth2ScopeMatcher> scopesToRemoveOnRefresh;
      @SuppressFBWarnings(justification = "generated code")
      private boolean allowEmptyScope;
      @SuppressFBWarnings(justification = "generated code")
      private boolean isGenerateRefreshToken;
      @SuppressFBWarnings(justification = "generated code")
      private boolean isSingleUseRefreshTokens;
      @SuppressFBWarnings(justification = "generated code")
      private boolean isInvalidateOldAccessTokensOnRefresh;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2UserLoader userLoader;
      @SuppressFBWarnings(justification = "generated code")
      private ELogger logger;

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

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder tokenReader(@NonNull OAuth2TokenReader tokenReader) {
         if (tokenReader == null) {
            throw new NullPointerException("tokenReader is marked non-null but is null");
         }

         this.tokenReader = tokenReader;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder tokenUpdater(@NonNull OAuth2TokenUpdater tokenUpdater) {
         if (tokenUpdater == null) {
            throw new NullPointerException("tokenUpdater is marked non-null but is null");
         }

         this.tokenUpdater = tokenUpdater;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder tokenDeleter(@NonNull OAuth2TokenDeleter tokenDeleter) {
         if (tokenDeleter == null) {
            throw new NullPointerException("tokenDeleter is marked non-null but is null");
         }

         this.tokenDeleter = tokenDeleter;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder tokenValidation(@NonNull OAuth2TokenValidation tokenValidation) {
         if (tokenValidation == null) {
            throw new NullPointerException("tokenValidation is marked non-null but is null");
         }

         this.tokenValidation = tokenValidation;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder tokenBinder(@NonNull OAuth2TokenBinder tokenBinder) {
         if (tokenBinder == null) {
            throw new NullPointerException("tokenBinder is marked non-null but is null");
         }

         this.tokenBinder = tokenBinder;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder authorizationCodeGenerator(
         @NonNull OAuth2AuthorizationCodeGenerator authorizationCodeGenerator
      ) {
         if (authorizationCodeGenerator == null) {
            throw new NullPointerException("authorizationCodeGenerator is marked non-null but is null");
         }

         this.authorizationCodeGenerator = authorizationCodeGenerator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder accessAndRefreshTokenGenerator(
         @NonNull OAuth2AccessAndRefreshTokenGenerator accessAndRefreshTokenGenerator
      ) {
         if (accessAndRefreshTokenGenerator == null) {
            throw new NullPointerException("accessAndRefreshTokenGenerator is marked non-null but is null");
         }

         this.accessAndRefreshTokenGenerator = accessAndRefreshTokenGenerator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder accessTokenGenerator(
         @NonNull OAuth2AccessTokenGenerator accessTokenGenerator
      ) {
         if (accessTokenGenerator == null) {
            throw new NullPointerException("accessTokenGenerator is marked non-null but is null");
         }

         this.accessTokenGenerator = accessTokenGenerator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder tokenRepresentation(
         @NonNull OAuth2TokenRepresentation tokenRepresentation
      ) {
         if (tokenRepresentation == null) {
            throw new NullPointerException("tokenRepresentation is marked non-null but is null");
         }

         this.tokenRepresentation = tokenRepresentation;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder gracePeriod(@NonNull Optional<Integer> gracePeriod) {
         if (gracePeriod == null) {
            throw new NullPointerException("gracePeriod is marked non-null but is null");
         }

         this.gracePeriod = gracePeriod;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder grantedScopeProcessorsService(
         @NonNull GrantedScopeProcessorsService grantedScopeProcessorsService
      ) {
         if (grantedScopeProcessorsService == null) {
            throw new NullPointerException("grantedScopeProcessorsService is marked non-null but is null");
         }

         this.grantedScopeProcessorsService = grantedScopeProcessorsService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder scopesToRemoveOnRefresh(
         @NonNull List<OAuth2ScopeMatcher> scopesToRemoveOnRefresh
      ) {
         if (scopesToRemoveOnRefresh == null) {
            throw new NullPointerException("scopesToRemoveOnRefresh is marked non-null but is null");
         }

         this.scopesToRemoveOnRefresh = scopesToRemoveOnRefresh;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder allowEmptyScope(boolean allowEmptyScope) {
         this.allowEmptyScope = allowEmptyScope;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder isGenerateRefreshToken(boolean isGenerateRefreshToken) {
         this.isGenerateRefreshToken = isGenerateRefreshToken;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder isSingleUseRefreshTokens(boolean isSingleUseRefreshTokens) {
         this.isSingleUseRefreshTokens = isSingleUseRefreshTokens;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder isInvalidateOldAccessTokensOnRefresh(
         boolean isInvalidateOldAccessTokensOnRefresh
      ) {
         this.isInvalidateOldAccessTokensOnRefresh = isInvalidateOldAccessTokensOnRefresh;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder userLoader(@NonNull OAuth2UserLoader userLoader) {
         if (userLoader == null) {
            throw new NullPointerException("userLoader is marked non-null but is null");
         }

         this.userLoader = userLoader;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder logger(@NonNull ELogger logger) {
         if (logger == null) {
            throw new NullPointerException("logger is marked non-null but is null");
         }

         this.logger = logger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2CodeGrantTokenManager build() {
         return new DefaultOAuth2CodeGrantTokenManager(
            this.tokenReader,
            this.tokenUpdater,
            this.tokenDeleter,
            this.tokenValidation,
            this.tokenBinder,
            this.authorizationCodeGenerator,
            this.accessAndRefreshTokenGenerator,
            this.accessTokenGenerator,
            this.tokenRepresentation,
            this.gracePeriod,
            this.grantedScopeProcessorsService,
            this.scopesToRemoveOnRefresh,
            this.allowEmptyScope,
            this.isGenerateRefreshToken,
            this.isSingleUseRefreshTokens,
            this.isInvalidateOldAccessTokensOnRefresh,
            this.userLoader,
            this.logger
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "DefaultOAuth2CodeGrantTokenManager.DefaultOAuth2CodeGrantTokenManagerBuilder(tokenReader="
            + this.tokenReader
            + ", tokenUpdater="
            + this.tokenUpdater
            + ", tokenDeleter="
            + this.tokenDeleter
            + ", tokenValidation="
            + this.tokenValidation
            + ", tokenBinder="
            + this.tokenBinder
            + ", authorizationCodeGenerator="
            + this.authorizationCodeGenerator
            + ", accessAndRefreshTokenGenerator="
            + this.accessAndRefreshTokenGenerator
            + ", accessTokenGenerator="
            + this.accessTokenGenerator
            + ", tokenRepresentation="
            + this.tokenRepresentation
            + ", gracePeriod="
            + this.gracePeriod
            + ", grantedScopeProcessorsService="
            + this.grantedScopeProcessorsService
            + ", scopesToRemoveOnRefresh="
            + this.scopesToRemoveOnRefresh
            + ", allowEmptyScope="
            + this.allowEmptyScope
            + ", isGenerateRefreshToken="
            + this.isGenerateRefreshToken
            + ", isSingleUseRefreshTokens="
            + this.isSingleUseRefreshTokens
            + ", isInvalidateOldAccessTokensOnRefresh="
            + this.isInvalidateOldAccessTokensOnRefresh
            + ", userLoader="
            + this.userLoader
            + ", logger="
            + this.logger
            + ")";
      }
   }
}
