package com.airlock.iam.oauth2.domain.model.clientcredentialsgrant;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.time.RealTimeProvider;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.common.domain.model.jwt.JwtClaimsSet;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSigner;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignerException;
import com.airlock.iam.elog.api.ELogger;
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.OAuth2TokenRepresentation;
import com.airlock.iam.oauth2.domain.model.token.OAuth2ClientCredentialsGrantAccessToken;
import com.airlock.iam.oauth2.domain.service.GrantedScopeProcessorsService;
import com.airlock.iam.oauth2.domain.service.token.OAuth2TokenBinder;
import com.airlock.iam.security.util.crypto.random.AlphanumericRandomStringGenerator;
import com.nimbusds.jwt.JWTClaimsSet.Builder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import lombok.NonNull;

public final class DefaultOAuth2ClientCredentialsGrantTokenFactory implements OAuth2ClientCredentialsGrantTokenFactory {
   private static final String SCOPE_CLAIM = "scope";
   private static final String CONFIRMATION_CLAIM = "cnf";
   private final ELogger logger;
   private final OAuth2TokenRepresentation tokenRepresentation;
   private final GrantedScopeProcessorsService grantedScopeProcessorsService;
   private final JwtSigner signer;
   private final TimeProvider timeProvider;
   private final Duration accessTokenLifeTime;
   private final Optional<String> issuer;
   private final List<String> audience;
   private final OAuth2TokenBinder tokenBinder;

   private DefaultOAuth2ClientCredentialsGrantTokenFactory(
      @NonNull ELogger logger,
      @NonNull OAuth2TokenRepresentation tokenRepresentation,
      @NonNull GrantedScopeProcessorsService grantedScopeProcessorsService,
      @NonNull JwtSigner signer,
      @Nullable TimeProvider timeProvider,
      @NonNull Duration accessTokenLifeTime,
      @Nullable Optional<String> issuer,
      @Nullable List<String> audience,
      @NonNull OAuth2TokenBinder tokenBinder
   ) {
      if (logger == null) {
         throw new NullPointerException("logger is marked non-null but is null");
      }

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

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

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

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

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

      this.logger = logger;
      this.tokenRepresentation = tokenRepresentation;
      this.grantedScopeProcessorsService = grantedScopeProcessorsService;
      this.signer = signer;
      this.timeProvider = timeProvider == null ? new RealTimeProvider() : timeProvider;
      this.accessTokenLifeTime = accessTokenLifeTime;
      this.issuer = Objects.requireNonNullElseGet(issuer, Optional::empty);
      this.audience = Objects.requireNonNullElseGet(audience, List::of);
      this.tokenBinder = tokenBinder;
   }

   @Override
   public OAuth2ClientCredentialsGrantAccessToken issueAccessToken(
      OAuth2Client client, OAuth2ClientAuthentication authentication, Set<OAuth2Scope> requestedScopeValues
   ) {
      Set<OAuth2Scope> scopeValues = this.grantedScopeProcessorsService.processScopes(requestedScopeValues, client, authentication);
      return this.generateAccessToken(client, scopeValues, authentication);
   }

   private void logAccessToken(OAuth2ClientCredentialsGrantAccessToken accessToken, OAuth2Client client) {
      this.logger
         .info(
            "OAuth 2.0 Client Credentials Grant Access Token issued: \nclient: "
               + LogUtil.logSafeQuote(client.clientId())
               + "\ntoken: "
               + this.tokenRepresentation.apply(accessToken.getTokenString())
               + "\nexpiration date: "
               + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(ZonedDateTime.ofInstant(accessToken.getExpiresAt().toInstant(), ZoneId.systemDefault()))
         );
   }

   private OAuth2ClientCredentialsGrantAccessToken generateAccessToken(
      OAuth2Client client, Set<OAuth2Scope> scopeValues, OAuth2ClientAuthentication authentication
   ) {
      Instant now = this.timeProvider.getCurrentInstant();
      JwtClaimsSet jwtClaimsSet = this.generateJWT(client, scopeValues, authentication, now);

      try {
         JsonWebSignatureToken jws = this.signer.sign(jwtClaimsSet);
         OAuth2ClientCredentialsGrantAccessToken accessToken = OAuth2ClientCredentialsGrantAccessToken.builder()
            .issuedTo(client.clientId())
            .issuedAt(Date.from(now))
            .expiresAt(Date.from(now.plus(this.accessTokenLifeTime)))
            .scopeValues(scopeValues)
            .tokenString(jws.tokenString())
            .build();
         this.logAccessToken(accessToken, client);
         return accessToken;
      } catch (JwtSignerException e) {
         throw new FailureException(e);
      }
   }

   private JwtClaimsSet generateJWT(OAuth2Client client, Set<OAuth2Scope> scopeValues, OAuth2ClientAuthentication authentication, Instant now) {
      Builder jwtClaimSetBuilder = new Builder()
         .jwtID(AlphanumericRandomStringGenerator.generateAlphanumericRandomString(27))
         .subject(client.clientId().id())
         .issueTime(Date.from(now))
         .expirationTime(Date.from(now.plus(this.accessTokenLifeTime)))
         .issuer(this.issuer.orElse(null))
         .audience(this.audience)
         .claim("scope", OAuth2Scope.convertToScopeNames(scopeValues));
      this.tokenBinder.bind(authentication).ifPresent(binding -> jwtClaimSetBuilder.claim("cnf", Map.of(binding.getMethod(), binding.getProof())));
      String claims = jwtClaimSetBuilder.build().toString();
      return JwtClaimsSet.of(claims);
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ELogger logger;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenRepresentation tokenRepresentation;
      @SuppressFBWarnings(justification = "generated code")
      private GrantedScopeProcessorsService grantedScopeProcessorsService;
      @SuppressFBWarnings(justification = "generated code")
      private JwtSigner signer;
      @SuppressFBWarnings(justification = "generated code")
      private TimeProvider timeProvider;
      @SuppressFBWarnings(justification = "generated code")
      private Duration accessTokenLifeTime;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<String> issuer;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> audience;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenBinder tokenBinder;

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

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder 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 DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder 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 DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder 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 DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder signer(@NonNull JwtSigner signer) {
         if (signer == null) {
            throw new NullPointerException("signer is marked non-null but is null");
         }

         this.signer = signer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder timeProvider(
         @Nullable TimeProvider timeProvider
      ) {
         this.timeProvider = timeProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder accessTokenLifeTime(
         @NonNull Duration accessTokenLifeTime
      ) {
         if (accessTokenLifeTime == null) {
            throw new NullPointerException("accessTokenLifeTime is marked non-null but is null");
         }

         this.accessTokenLifeTime = accessTokenLifeTime;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder issuer(@Nullable Optional<String> issuer) {
         this.issuer = issuer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder audience(@Nullable List<String> audience) {
         this.audience = audience;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder 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 DefaultOAuth2ClientCredentialsGrantTokenFactory build() {
         return new DefaultOAuth2ClientCredentialsGrantTokenFactory(
            this.logger,
            this.tokenRepresentation,
            this.grantedScopeProcessorsService,
            this.signer,
            this.timeProvider,
            this.accessTokenLifeTime,
            this.issuer,
            this.audience,
            this.tokenBinder
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "DefaultOAuth2ClientCredentialsGrantTokenFactory.DefaultOAuth2ClientCredentialsGrantTokenFactoryBuilder(logger="
            + this.logger
            + ", tokenRepresentation="
            + this.tokenRepresentation
            + ", grantedScopeProcessorsService="
            + this.grantedScopeProcessorsService
            + ", signer="
            + this.signer
            + ", timeProvider="
            + this.timeProvider
            + ", accessTokenLifeTime="
            + this.accessTokenLifeTime
            + ", issuer="
            + this.issuer
            + ", audience="
            + this.audience
            + ", tokenBinder="
            + this.tokenBinder
            + ")";
      }
   }
}
