package com.airlock.iam.common.application.configuration.jwt.signature;

import com.airlock.iam.common.domain.model.jwt.JwtClaimsSet;
import com.airlock.iam.common.domain.model.jwt.SignatureVerificationInformation;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.model.jwt.signature.SignatureAlgorithm;
import com.airlock.iam.common.domain.service.JwksPublicKeyRegistry;
import com.airlock.iam.common.domain.service.jwt.JwtSettingsValidationError;
import com.airlock.iam.common.domain.service.jwt.signature.CertificateThumbPrintConfig;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSigner;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignerException;
import com.airlock.iam.common.domain.service.jwt.signature.PrivateKeyJwtSigner;
import com.airlock.iam.common.infrastructure.service.JwksProvider;
import com.airlock.iam.core.misc.plugin.config.validation.KeystoreValidator;
import com.airlock.iam.core.misc.plugin.config.validation.OpenedKeystoreValidatorHelper;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.lifecycle.BeforePluginDeactivation;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.FileProperty;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
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.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.valuereader.BooleanPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.security.PrivateKey;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public abstract class PrivateKeyJwtTicketSignerSettings implements JwtSigner, JwksPublicKeyRegistry {
   static final String CERTIFICATE_THUMBPRINTS = "Certificate Thumbprints";
   @FileProperty(
      description = "Keystore that holds the private key for JWT token signing.",
      optional = false,
      assureExistence = true,
      assureReadable = true,
      isFile = true,
      exampleValues = {"keystore.p12", "/etc/airlock/keystore.jks"}
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private File keystorePath;
   static final String KEY_STORE_NAME = "keystorePath";
   @StringProperty(description = "Password for the keystore.", optional = true, isSensitive = true)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String keystorePassword;
   private static final String KEY_STORE_PASSWORD_NAME = "keystorePassword";
   @StringProperty(
      description = "Alias for the private key contained in the keystore that should be used for signing. This field can be omitted if the keystore only contains one EC private key with the configured password.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String privateKeyAlias;
   static final String PRIVATE_KEY_ALIAS_NAME = "privateKeyAlias";
   @StringProperty(description = "Password for the private key in the keystore.", optional = true, isSensitive = true)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String privateKeyPassword;
   static final String PRIVATE_KEY_PASSWORD_NAME = "privateKeyPassword";
   @BooleanProperty(
      description = "If enabled, the KID of the public key used to sign a JWT is added to the JWT header. Consumers of the JWT can\nuse the KID to identify the public key that is verifying the signature.",
      displayName = "Include KID",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private boolean includeKid;
   @BooleanProperty(
      description = "Indicates whether the x5t#S256 header parameter containing a SHA-256 certificate thumbprint should be added to the JWT header.<p> The 'Private Key Alias' is used to find the X.509 certificate in the keystore to compute the thumbprint for. I.e., it is assumed that the alias identifies a private key with the corresponding certificate. If no 'Private Key Alias' is configured, the keystore is assumed to contain exactly one certificate. </p>",
      optional = true,
      displayName = "Add x5t#S256 Header Parameter",
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Certificate Thumbprints")
   private boolean addX5tS256HeaderParameter;
   static final String ADD_X5TS256_HEADER_PARAMETER_NAME = "addX5tS256HeaderParameter";
   @BooleanProperty(
      description = "Indicates whether the x5t header parameter containing a SHA-1 certificate thumbprint should be added to the JWT header.<p> The 'Private Key Alias' is used to find the X.509 certificate in the keystore to compute the thumbprint for. I.e., it is assumed that the alias identifies a private key with the corresponding certificate. If no 'Private Key Alias' is configured, the keystore is assumed to contain exactly one certificate. </p><p><b>Security warning:</b> SHA-1 fingerprints should no longer be used as SHA-1 is considered broken. We recommend the x5t#S256 thumbprint instead.</p>",
      optional = true,
      displayName = "Add x5t Header Parameter",
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Certificate Thumbprints")
   private boolean addX5tHeaderParameter;
   static final String ADD_X5T_HEADER_PARAMETER_NAME = "addX5tHeaderParameter";
   private PrivateKeyJwtSigner ticketSigner;

   protected PrivateKeyJwtTicketSignerSettings(
      File keystorePath,
      String keystorePassword,
      String privateKeyAlias,
      String privateKeyPassword,
      boolean includeKid,
      boolean addX5tS256HeaderParameter,
      boolean addX5tHeaderParameter
   ) {
      this.keystorePath = keystorePath;
      this.keystorePassword = keystorePassword;
      this.privateKeyAlias = privateKeyAlias;
      this.privateKeyPassword = privateKeyPassword;
      this.includeKid = includeKid;
      this.addX5tS256HeaderParameter = addX5tS256HeaderParameter;
      this.addX5tHeaderParameter = addX5tHeaderParameter;
   }

   protected abstract SignatureAlgorithm signatureAlgorithm();

   @Override
   public JsonWebSignatureToken sign(JwtClaimsSet payload) throws JwtSignerException {
      return Objects.requireNonNull(this.ticketSigner, "AfterConfigActivation has not been called").sign(payload);
   }

   private CertificateThumbPrintConfig getCertificateThumbprintConfig() {
      return CertificateThumbPrintConfig.builder()
         .certificateAlias(Optional.ofNullable(this.privateKeyAlias))
         .addX5tThumbprint(this.addX5tHeaderParameter)
         .addX5tS256Thumbprint(this.addX5tS256HeaderParameter)
         .build();
   }

   protected static void validateConfig(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, SignatureAlgorithm signatureAlgorithm
   ) throws PropertyConfigurationException {
      OpenedKeystoreValidatorHelper validator = KeystoreValidator.getAutodetectingKeystoreValidator("keystorePath", "keystorePassword", pc, env, context)
         .validateKeystore();
      PrivateKey privateKey = validator.matchingPrivateKeyWithSpecificPasswordOrElseThrow(
         "privateKeyAlias", "privateKeyPassword", Optional.of(signatureAlgorithm.getPrivateKeyFilter())
      );
      Optional<JwtSettingsValidationError> error = PrivateKeyJwtSigner.validate(signatureAlgorithm, privateKey);
      if (error.isPresent()) {
         throw new PropertyConstraintViolationException("privateKeyAlias", pc, env, context, error.get().getMessage(), error.get().getCause().orElse(null));
      }

      validateThumbprintProperties(validator, pc, env, context);
   }

   private static void validateThumbprintProperties(
      OpenedKeystoreValidatorHelper validatorHelper, PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context
   ) throws PropertyConfigurationException {
      try {
         if (BooleanPropertyValueReader.effectiveBooleanPropertyValue(pc, "addX5tHeaderParameter", env, context)
            || BooleanPropertyValueReader.effectiveBooleanPropertyValue(pc, "addX5tS256HeaderParameter", env, context)) {
            validatorHelper.matchingX509CertificateOrElseThrow("privateKeyAlias", Optional.empty());
         }
      } catch (PropertyTypeMismatchException e) {
         throw new IllegalStateException("Error in config validation", e);
      }
   }

   @Override
   public List<SignatureVerificationInformation> getSigningCertificatesForJwks() {
      return Objects.requireNonNull(this.ticketSigner, "AfterConfigActivation has not been called").getSignatureVerificationInformation().stream().toList();
   }

   @AfterConfigActivation
   void afterConfigActivation() {
      this.ticketSigner = PrivateKeyJwtSigner.builder(this.signatureAlgorithm(), this.getKeystorePath())
         .keyStorePassword(this.getKeystorePassword())
         .privateKeyAlias(this.getPrivateKeyAlias())
         .privateKeyPassword(this.getPrivateKeyPassword())
         .certificateThumbPrintConfig(this.getCertificateThumbprintConfig())
         .includeKid(this.includeKid)
         .build();
      this.registerSigner();
   }

   private void registerSigner() {
      JwksProvider.register(this);
   }

   @BeforePluginDeactivation
   void deregisterSigner() {
      JwksProvider.deregister(this);
   }

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

   @SuppressFBWarnings(justification = "generated code")
   File getKeystorePath() {
      return this.keystorePath;
   }

   @SuppressFBWarnings(justification = "generated code")
   String getKeystorePassword() {
      return this.keystorePassword;
   }

   @SuppressFBWarnings(justification = "generated code")
   String getPrivateKeyAlias() {
      return this.privateKeyAlias;
   }

   @SuppressFBWarnings(justification = "generated code")
   String getPrivateKeyPassword() {
      return this.privateKeyPassword;
   }
}
