package com.airlock.iam.oauth2.application.configuration.openid.signature;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.common.domain.model.jwt.SignatureVerificationInformation;
import com.airlock.iam.common.domain.model.jwt.signature.JwsAlgorithmHmac;
import com.airlock.iam.common.domain.service.jwt.signature.HmacJwtSigner;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientSecret;
import com.airlock.iam.oauth2.domain.model.client.OAuth2Client;
import com.airlock.iam.oauth2.domain.model.token.IdToken;
import com.airlock.iam.oauth2.domain.model.token.IdTokenBuilder;
import com.airlock.iam.oauth2.domain.model.token.OpenIDConnectIDTokenSigner;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.license.PluginLicenseRestriction;
import com.airlock.iam.plugin.api.annotation.property.EnumProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.valuereader.EnumPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

@Plugin(
   description = "ID tokens will be signed using a Keyed-Hash Message Authentication Code (HMAC) using the client secret as key.<br>Notice that the client secret must have a minimum length depending on the selected algorithm (see property description).",
   displayName = "OIDC ID Token HMAC"
)
@PluginLicenseRestriction(moduleAffiliation = "OAuthServer")
public class OpenIDConnectIDTokenMACSignatureSigner implements OpenIDConnectIDTokenSigner {
   @EnumProperty(
      description = "<p>MAC based signature algorithm to use.</p><p><b>Important:</b> The ID Token is signed using the client secret. Therefore, the client must have registered such a secret and it must be long enough for the selected algorithm:</p><ul><li>HS256: 32 characters</li><li>HS384: 48 characters</li><li>HS512: 64 characters</li></ul>",
      optional = true,
      defaultValue = "HS256"
   )
   private OpenIDConnectIDTokenMACSignatureSigner.Algorithm algorithm;
   public static final String ALGORITHM_FIELD = "algorithm";

   @Override
   public IdToken signed(IdTokenBuilder idTokenBuilder, OAuth2Client client) throws IdTokenBuilder.IdTokenException {
      return idTokenBuilder.sign(
         HmacJwtSigner.builder()
            .hmacAlgorithm(this.algorithm.getAlgorithm())
            .hmacKey(
               client.clientSecret()
                  .map(OAuth2ClientSecret::getSecret)
                  .orElseThrow(() -> new ViolationException("No client secret present"))
                  .getBytes(StandardCharsets.UTF_8)
            )
            .build()
      );
   }

   @Override
   public Optional<SignatureVerificationInformation> signatureVerificationInformation() {
      return Optional.empty();
   }

   @Override
   public Optional<String> algorithm() {
      return Optional.of(this.algorithm.getAlgorithm().name());
   }

   public static void validateKeyLength(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, String key) throws PropertyTypeMismatchException, OpenIDConnectIDTokenMACSignatureSigner.KeyLengthValidationException {
      OpenIDConnectIDTokenMACSignatureSigner.Algorithm algorithmValue = (OpenIDConnectIDTokenMACSignatureSigner.Algorithm)EnumPropertyValueReader.effectiveConfigEnum(
         config, "algorithm", env, context
      );
      if (key.length() < algorithmValue.getMinSecretLengthInBytes()) {
         throw new OpenIDConnectIDTokenMACSignatureSigner.KeyLengthValidationException(
            "The client secret must be at least "
               + algorithmValue.getMinSecretLengthInBytes()
               + " characters for the signature algorithm "
               + algorithmValue.name()
         );
      }
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public OpenIDConnectIDTokenMACSignatureSigner(OpenIDConnectIDTokenMACSignatureSigner.Algorithm algorithm) {
      this.algorithm = algorithm;
   }

   @SuppressFBWarnings(justification = "generated code")
   public OpenIDConnectIDTokenMACSignatureSigner() {
   }

   public enum Algorithm {
      HS256(JwsAlgorithmHmac.HS256, 32),
      HS384(JwsAlgorithmHmac.HS384, 48),
      HS512(JwsAlgorithmHmac.HS512, 64);

      private final JwsAlgorithmHmac algorithm;
      private final int minSecretLengthInBytes;

      public JwsAlgorithmHmac getAlgorithm() {
         return this.algorithm;
      }

      public int getMinSecretLengthInBytes() {
         return this.minSecretLengthInBytes;
      }

      @SuppressFBWarnings(justification = "generated code")
      Algorithm(final JwsAlgorithmHmac algorithm, final int minSecretLengthInBytes) {
         this.algorithm = algorithm;
         this.minSecretLengthInBytes = minSecretLengthInBytes;
      }
   }

   public static class KeyLengthValidationException extends Exception {
      public KeyLengthValidationException(String message) {
         super(message);
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class OpenIDConnectIDTokenMACSignatureSignerBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private OpenIDConnectIDTokenMACSignatureSigner.Algorithm algorithm;

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

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectIDTokenMACSignatureSigner.OpenIDConnectIDTokenMACSignatureSignerBuilder algorithm(
         OpenIDConnectIDTokenMACSignatureSigner.Algorithm algorithm
      ) {
         this.algorithm = algorithm;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectIDTokenMACSignatureSigner build() {
         return new OpenIDConnectIDTokenMACSignatureSigner(this.algorithm);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OpenIDConnectIDTokenMACSignatureSigner.OpenIDConnectIDTokenMACSignatureSignerBuilder(algorithm=" + this.algorithm + ")";
      }
   }
}
