package com.airlock.iam.common.domain.service.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.JwsAlgorithm;
import com.airlock.iam.common.domain.model.jwt.signature.KeyId;
import com.airlock.iam.common.domain.model.jwt.signature.SignatureAlgorithm;
import com.airlock.iam.common.domain.service.jwt.JwtSettingsValidationError;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.security.util.crypto.KeyStoreUtils;
import com.airlock.iam.security.util.crypto.KeyStoreUtils.KeyStoreType;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSHeader.Builder;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.opts.AllowWeakRSAKey;
import com.nimbusds.jose.util.Base64URL;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.util.Collections;
import java.util.Optional;
import java.util.function.Predicate;
import org.apache.commons.lang3.StringUtils;

public class PrivateKeyJwtSigner implements JwtSigner {
   private static final ELogger LOG = ELoggerFactory.getLogger(PrivateKeyJwtSigner.class);
   private final JwsAlgorithm jwsAlgorithm;
   private final JwsSignerWrapper signer;
   private final Optional<KeyId> keyId;
   private final Optional<Base64URL> x5tThumbprint;
   private final Optional<Base64URL> x5tS256Thumbprint;
   private final Optional<SignatureVerificationInformation> signatureVerificationInformation;

   public static Optional<JwtSettingsValidationError> validate(SignatureAlgorithm signatureAlgorithm, PrivateKey privateKey) {
      Optional<JwtSettingsValidationError> jwtSettingsValidationError = trySignerCreation(signatureAlgorithm, privateKey);
      if (jwtSettingsValidationError.isPresent()) {
         return jwtSettingsValidationError;
      }

      JwsSignerWrapper signerWrapper = signatureSigner(signatureAlgorithm, privateKey);
      return signerWrapper.validateAlgorithmSupported(signatureAlgorithm);
   }

   private static Optional<JwtSettingsValidationError> trySignerCreation(SignatureAlgorithm signatureAlgorithm, PrivateKey privateKey) {
      try {
         signatureSigner(signatureAlgorithm, privateKey);
         return Optional.empty();
      } catch (IllegalArgumentException e) {
         return Optional.of(new JwtSettingsValidationError(e.getMessage(), Optional.of(e)));
      }
   }

   private static JwsSignerWrapper signatureSigner(SignatureAlgorithm signatureAlgorithm, PrivateKey privateKey) {
      JwtSignatureUtils.addBouncyCastleSecurityProviderIfAbsent();

      return switch (signatureAlgorithm.family()) {
         case RSA -> JwsSignerWrapper.signerWrapper(new RSASSASigner(privateKey, Collections.singleton(AllowWeakRSAKey.getInstance())));
         case EC -> createEcSigner(privateKey);
         case HMAC -> throw new IllegalArgumentException("The provided algorithm is no private key signature algorithm: " + signatureAlgorithm);
      };
   }

   private static JwsSignerWrapper createEcSigner(PrivateKey privateKey) {
      try {
         return JwsSignerWrapper.signerWrapper(new ECDSASigner((ECPrivateKey)privateKey));
      } catch (JOSEException | ClassCastException e) {
         throw new IllegalArgumentException("Illegal EC key", e);
      }
   }

   public PrivateKeyJwtSigner(
      SignatureAlgorithm signatureAlgorithm,
      File keyStorePath,
      String keyStorePassword,
      String privateKeyAlias,
      String privateKeyPassword,
      boolean includeKid,
      CertificateThumbPrintConfig certificateThumbPrintConfig
   ) {
      this.jwsAlgorithm = signatureAlgorithm;
      KeyStore keyStore = loadKeyStore(keyStorePath, keyStorePassword);
      PrivateKey privateKey = loadPrivateKey(keyStore, privateKeyAlias, privateKeyPassword, signatureAlgorithm.getPrivateKeyFilter());
      Optional<X509Certificate> certificate = this.getCertificate(privateKeyAlias, keyStore, signatureAlgorithm.getCertificateFilter());
      this.signatureVerificationInformation = certificate.map(c -> SignatureVerificationInformation.signatureVerificationInformation(signatureAlgorithm, c));
      this.keyId = certificate.filter(kid -> includeKid).map(KeyId::keyId);
      this.signer = signatureSigner(signatureAlgorithm, privateKey);
      this.assertSignatureAlgorithmSupported(signatureAlgorithm);
      Optional<CertificateThumbPrints> thumbPrints = this.getCertificateThumbprints(certificateThumbPrintConfig, certificate);
      this.x5tThumbprint = this.x5tThumbprint(certificateThumbPrintConfig, thumbPrints);
      this.x5tS256Thumbprint = this.x5tS256Thumbprint(certificateThumbPrintConfig, thumbPrints);
   }

   public static PrivateKeyJwtSigner.PrivateKeyJwtSignerBuilder builder(SignatureAlgorithm signatureAlgorithm, File keyStorePath) {
      return new PrivateKeyJwtSigner.PrivateKeyJwtSignerBuilder()
         .signatureAlgorithm(signatureAlgorithm)
         .keyStorePath(keyStorePath)
         .certificateThumbPrintConfig(CertificateThumbPrintConfig.noCertificateThumbprints());
   }

   private Optional<Base64URL> x5tThumbprint(CertificateThumbPrintConfig certificateThumbPrintConfig, Optional<CertificateThumbPrints> thumbPrints) {
      return certificateThumbPrintConfig.isAddX5tThumbprint() ? thumbPrints.map(CertificateThumbPrints::computeX5tThumbprint) : Optional.empty();
   }

   private Optional<Base64URL> x5tS256Thumbprint(CertificateThumbPrintConfig certificateThumbPrintConfig, Optional<CertificateThumbPrints> thumbPrints) {
      return certificateThumbPrintConfig.isAddX5tS256Thumbprint() ? thumbPrints.map(CertificateThumbPrints::computeX5tS256Thumbprint) : Optional.empty();
   }

   private Optional<CertificateThumbPrints> getCertificateThumbprints(CertificateThumbPrintConfig config, Optional<X509Certificate> certificate) {
      Optional<CertificateThumbPrints> thumbPrints = certificate.map(CertificateThumbPrints::thumbPrints);
      if (config.isAnyThumbprintRequired() && thumbPrints.isEmpty()) {
         throw new IllegalArgumentException(
            String.format("Cannot load certificate with alias '%s'. Not found or alias not unique", config.getCertificateAlias().orElse("<no alias provided>"))
         );
      } else {
         return thumbPrints;
      }
   }

   private Optional<X509Certificate> getCertificate(String certificateAlias, KeyStore keyStore, Predicate<X509Certificate> certificateFilter) {
      try {
         return KeyStoreUtils.getUniqueX509Certificate(keyStore, Optional.ofNullable(certificateAlias), Optional.of(certificateFilter));
      } catch (KeyStoreException e) {
         throw new IllegalArgumentException("Could not load certificate from keystore", e);
      }
   }

   private void assertSignatureAlgorithmSupported(SignatureAlgorithm signatureAlgorithm) {
      this.signer.validateAlgorithmSupported(signatureAlgorithm).ifPresent(error -> {
         throw new IllegalArgumentException(error.getMessage());
      });
   }

   private static KeyStore loadKeyStore(File keyStorePath, String keyStorePassword) {
      try {
         return KeyStoreUtils.createKeyStoreFromFile(keyStorePath, optional(keyStorePassword), KeyStoreType.AUTO);
      } catch (KeyStoreException e) {
         throw new IllegalArgumentException("Could not load keystore", e);
      }
   }

   private static PrivateKey loadPrivateKey(KeyStore keyStore, String privateKeyAlias, String privateKeyPassword, Predicate<PrivateKey> privateKeyFilter) {
      try {
         return (PrivateKey)KeyStoreUtils.getUniquePrivateKey(keyStore, optional(privateKeyAlias), optional(privateKeyPassword), Optional.of(privateKeyFilter))
            .orElseThrow(() -> new IllegalArgumentException(String.format("Could not find private key with alias '%s' or key is not unique", privateKeyAlias)));
      } catch (KeyStoreException | UnrecoverableKeyException e) {
         throw new IllegalArgumentException("Could not load private key from keystore", e);
      }
   }

   @Override
   public JsonWebSignatureToken sign(JwtClaimsSet payload) throws JwtSignerException {
      JsonWebSignatureToken token = this.signer.sign(this.jwtHeader(), payload);
      LOG.debug("Successfully signed the JWT.");
      return token;
   }

   private static Optional<String> optional(String val) {
      return StringUtils.isEmpty(val) ? Optional.empty() : Optional.of(val);
   }

   private JWSHeader jwtHeader() {
      return new Builder(JWSAlgorithm.parse(this.jwsAlgorithm.name()))
         .type(JOSEObjectType.JWT)
         .keyID(this.keyId())
         .x509CertThumbprint(this.x5tThumbprint.orElse(null))
         .x509CertSHA256Thumbprint(this.x5tS256Thumbprint.orElse(null))
         .build();
   }

   public String keyId() {
      return this.keyId.<String>map(KeyId::keyId).orElse(null);
   }

   public Optional<SignatureVerificationInformation> getSignatureVerificationInformation() {
      return this.signatureVerificationInformation;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class PrivateKeyJwtSignerBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private SignatureAlgorithm signatureAlgorithm;
      @SuppressFBWarnings(justification = "generated code")
      private File keyStorePath;
      @SuppressFBWarnings(justification = "generated code")
      private String keyStorePassword;
      @SuppressFBWarnings(justification = "generated code")
      private String privateKeyAlias;
      @SuppressFBWarnings(justification = "generated code")
      private String privateKeyPassword;
      @SuppressFBWarnings(justification = "generated code")
      private boolean includeKid;
      @SuppressFBWarnings(justification = "generated code")
      private CertificateThumbPrintConfig certificateThumbPrintConfig;

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

      @SuppressFBWarnings(justification = "generated code")
      public PrivateKeyJwtSigner.PrivateKeyJwtSignerBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
         this.signatureAlgorithm = signatureAlgorithm;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PrivateKeyJwtSigner.PrivateKeyJwtSignerBuilder keyStorePath(File keyStorePath) {
         this.keyStorePath = keyStorePath;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PrivateKeyJwtSigner.PrivateKeyJwtSignerBuilder keyStorePassword(String keyStorePassword) {
         this.keyStorePassword = keyStorePassword;
         return this;
      }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public PrivateKeyJwtSigner.PrivateKeyJwtSignerBuilder includeKid(boolean includeKid) {
         this.includeKid = includeKid;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PrivateKeyJwtSigner.PrivateKeyJwtSignerBuilder certificateThumbPrintConfig(CertificateThumbPrintConfig certificateThumbPrintConfig) {
         this.certificateThumbPrintConfig = certificateThumbPrintConfig;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PrivateKeyJwtSigner build() {
         return new PrivateKeyJwtSigner(
            this.signatureAlgorithm,
            this.keyStorePath,
            this.keyStorePassword,
            this.privateKeyAlias,
            this.privateKeyPassword,
            this.includeKid,
            this.certificateThumbPrintConfig
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "PrivateKeyJwtSigner.PrivateKeyJwtSignerBuilder(signatureAlgorithm="
            + this.signatureAlgorithm
            + ", keyStorePath="
            + this.keyStorePath
            + ", keyStorePassword="
            + this.keyStorePassword
            + ", privateKeyAlias="
            + this.privateKeyAlias
            + ", privateKeyPassword="
            + this.privateKeyPassword
            + ", includeKid="
            + this.includeKid
            + ", certificateThumbPrintConfig="
            + this.certificateThumbPrintConfig
            + ")";
      }
   }
}
