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

import com.airlock.iam.common.domain.model.jwt.signature.SignatureAlgorithm;
import com.airlock.iam.common.domain.service.jwt.JwtSettingsValidationError;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifier;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifierImpl;
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.property.FileProperty;
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.FilePropertyValueReader;
import com.airlock.iam.security.certificate.AsymmetricKeyPairUtils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Optional;

public abstract class JwtTicketVerifierSettings implements JwtSignatureVerifier {
   @FileProperty(
      description = "Keystore that holds the certificate containing the public key for JWT signature verification. Alternatively, it can be a file containing a PEM encoded public key.",
      optional = false,
      assureExistence = true,
      assureReadable = true,
      isFile = true,
      exampleValues = {"keystore.p12", "/etc/airlock/keystore.jks", "public_key.pem"}
   )
   private File keystorePath;
   private static final String KEY_STORE_NAME = "keystorePath";
   @StringProperty(description = "Password for the keystore.", optional = true, isSensitive = true)
   private String keystorePassword;
   private static final String KEY_STORE_PASSWORD_NAME = "keystorePassword";
   @StringProperty(
      description = "Alias for the X.509 certificate contained in the keystore that should be used for signature verification. This field can be omitted if the keystore only contains one X.509 certificate with a public key.",
      optional = true,
      displayName = "X.509 Certificate Alias"
   )
   private String x509CertificateAlias;
   private static final String CERTIFICATE_ALIAS_NAME = "x509CertificateAlias";

   protected static void validateConfig(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, SignatureAlgorithm signatureAlgorithm
   ) throws PropertyConfigurationException {
      Optional<JwtSettingsValidationError> publicKeyError = publicKeyValidation(pc, env, context, signatureAlgorithm);
      Optional<JwtSettingsValidationError> keyStoreError = keyStoreValidation(pc, env, context, signatureAlgorithm);
      if (keyStoreError.isPresent() && publicKeyError.isPresent()) {
         throw new PropertyConstraintViolationException(
            "x509CertificateAlias", pc, env, context, keyStoreError.get().getMessage(), keyStoreError.get().getCause().orElse(null)
         );
      }
   }

   protected static JwtSignatureVerifier buildVerifier(SignatureAlgorithm algorithm, File keyStore, String keyStorePassword, String certificateAlias) {
      return publicKeyJwtVerifier(readPublicKey(keyStore.getPath()), algorithm)
         .orElseGet(() -> keyStoreJwtVerifier(algorithm, keyStore, keyStorePassword, certificateAlias));
   }

   private static JwtSignatureVerifierImpl keyStoreJwtVerifier(SignatureAlgorithm algorithm, File keyStore, String keyStorePassword, String certificateAlias) {
      return JwtSignatureVerifierImpl.keystoreBuilder()
         .signatureAlgorithm(algorithm)
         .keyStorePath(keyStore)
         .keyStorePassword(keyStorePassword)
         .certificateAlias(certificateAlias)
         .build();
   }

   private static Optional<JwtSettingsValidationError> keyStoreValidation(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, SignatureAlgorithm signatureAlgorithm
   ) {
      try {
         OpenedKeystoreValidatorHelper validator = KeystoreValidator.getAutodetectingKeystoreValidator("keystorePath", "keystorePassword", pc, env, context)
            .validateKeystore();
         X509Certificate certificate = validator.matchingX509CertificateOrElseThrow(
            "x509CertificateAlias", Optional.of(signatureAlgorithm.getCertificateFilter())
         );
         return JwtSignatureVerifierImpl.validate(signatureAlgorithm, certificate.getPublicKey());
      } catch (PropertyConfigurationException e) {
         return Optional.of(new JwtSettingsValidationError(e.getMessage(), Optional.of(e)));
      }
   }

   private static Optional<JwtSettingsValidationError> publicKeyValidation(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, SignatureAlgorithm signatureAlgorithm
   ) throws PropertyConstraintViolationException {
      String publicKeyPath;
      try {
         publicKeyPath = FilePropertyValueReader.effectiveFilePropertyValue(pc, "keystorePath", env, context);
      } catch (PropertyTypeMismatchException e) {
         return Optional.of(new JwtSettingsValidationError(e.getMessage(), Optional.of(e)));
      }

      Optional<PublicKey> publicKey = readPublicKey(publicKeyPath);
      if (publicKey.isEmpty()) {
         return Optional.of(new JwtSettingsValidationError("Not a parsable public key", Optional.empty()));
      } else {
         Optional<JwtSettingsValidationError> validationError = publicKey.flatMap(pk -> JwtSignatureVerifierImpl.validate(signatureAlgorithm, pk));
         if (validationError.isPresent()) {
            throw new PropertyConstraintViolationException("keystorePath", pc, env, context, validationError.get().getMessage());
         } else {
            return Optional.empty();
         }
      }
   }

   private static Optional<PublicKey> readPublicKey(String publicKeyPath) {
      try {
         String rawPublicKey = Files.readString(Path.of(publicKeyPath), StandardCharsets.UTF_8);
         return Optional.of(AsymmetricKeyPairUtils.publicKeyFromPem(rawPublicKey));
      } catch (IOException | IllegalArgumentException e) {
         return Optional.empty();
      }
   }

   private static Optional<JwtSignatureVerifier> publicKeyJwtVerifier(Optional<PublicKey> publicKey, SignatureAlgorithm algorithm) {
      return publicKey.filter(pk -> JwtSignatureVerifierImpl.validate(algorithm, pk).isEmpty())
         .map(pk -> JwtSignatureVerifierImpl.publicKeyBuilder().publicKey(pk).signatureAlgorithm(algorithm).build());
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public JwtTicketVerifierSettings(File keystorePath, String keystorePassword, String x509CertificateAlias) {
      this.keystorePath = keystorePath;
      this.keystorePassword = keystorePassword;
      this.x509CertificateAlias = x509CertificateAlias;
   }

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

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

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