package com.airlock.iam.security.certificate;

import com.airlock.iam.security.util.SeededSecureRandom;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Date;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

public final class CertificateGenerator {
   private final PublicKey publicKey;
   private final ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
   private final X500NameBuilder subjectBuilder = new X500NameBuilder();
   private final X500NameBuilder issuerBuilder = new X500NameBuilder();
   private BigInteger serialNumber = BigInteger.valueOf(SeededSecureRandom.getInstance().nextLong(1L, Long.MAX_VALUE));
   private Duration validity = Duration.ofDays(30L);
   private Duration notBeforeSkewDuration = Duration.ZERO;
   private CertificateSignatureAlgorithm signatureAlgorithm = CertificateSignatureAlgorithm.SHA256_WITH_RSA_ENCRYPTION;

   public static CertificateGenerator fromPublicKey(PublicKey publicKey) {
      return new CertificateGenerator(publicKey);
   }

   private CertificateGenerator(PublicKey publicKey) {
      this.publicKey = publicKey;
   }

   public CertificateGenerator withSubjectCommonName(String subjectCN) {
      this.subjectBuilder.addRDN(X509ObjectIdentifiers.commonName, subjectCN);
      return this;
   }

   public CertificateGenerator withSubjectAlternativeName(String subjectCN) {
      GeneralNames subjectAltNamesExtension = new GeneralNames(new GeneralName[]{new GeneralName(2, subjectCN)});

      try {
         this.extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltNamesExtension);
         return this;
      } catch (IOException e) {
         throw new IllegalStateException(e);
      }
   }

   public CertificateGenerator withSubjectRDN(String oid, String value) {
      this.subjectBuilder.addRDN(new ASN1ObjectIdentifier(oid), value);
      return this;
   }

   public CertificateGenerator withMultivaluedSubjectRDN(String oid1, String value1, String oid2, String value2) {
      ASN1ObjectIdentifier[] oids = new ASN1ObjectIdentifier[]{new ASN1ObjectIdentifier(oid1), new ASN1ObjectIdentifier(oid2)};
      this.subjectBuilder.addMultiValuedRDN(oids, new String[]{value1, value2});
      return this;
   }

   public CertificateGenerator withIssuerCommonName(String issuerCommonName) {
      this.issuerBuilder.addRDN(X509ObjectIdentifiers.commonName, issuerCommonName);
      return this;
   }

   public CertificateGenerator withSerialNumber(BigInteger serial) {
      this.serialNumber = serial;
      return this;
   }

   public CertificateGenerator withSignatureAlgorithm(CertificateSignatureAlgorithm algorithm) {
      this.signatureAlgorithm = algorithm;
      return this;
   }

   public CertificateGenerator withValidity(Duration validityDuration) {
      this.validity = validityDuration;
      return this;
   }

   public CertificateGenerator withNotBeforeSkewTime(Duration notBeforeSkew) {
      this.notBeforeSkewDuration = notBeforeSkew;
      return this;
   }

   public CertificateGenerator withExtension(String oid, boolean critical, byte[] value) {
      this.extensionsGenerator.addExtension(new ASN1ObjectIdentifier(oid), critical, value);
      return this;
   }

   public X509Certificate build(PrivateKey issuerPrivateKey) throws GeneralSecurityException {
      X509v3CertificateBuilder v3CertGen = new JcaX509v3CertificateBuilder(
         this.principal(this.issuerBuilder),
         this.serialNumber,
         new Date(System.currentTimeMillis() - this.notBeforeSkewDuration.toMillis()),
         new Date(System.currentTimeMillis() + this.validity.toMillis()),
         this.principal(this.subjectBuilder),
         this.publicKey
      );
      return createCert(v3CertGen, this.extensionsGenerator, issuerPrivateKey, this.signatureAlgorithm);
   }

   private X500Name principal(X500NameBuilder x500Name) {
      X500Name subject = x500Name.build();
      if (subject.getRDNs().length == 0) {
         String defaultCommonName = "localhost";
         subject = x500Name.addRDN(X509ObjectIdentifiers.commonName, defaultCommonName).build();
      }

      return subject;
   }

   private static X509Certificate createCert(
      X509v3CertificateBuilder v3CertGen, ExtensionsGenerator extensions, PrivateKey issuerPrivateKey, CertificateSignatureAlgorithm signatureAlgorithm
   ) throws GeneralSecurityException {
      try {
         if (!extensions.isEmpty()) {
            for (ASN1ObjectIdentifier extensionOid : extensions.generate().getExtensionOIDs()) {
               v3CertGen.addExtension(extensions.getExtension(extensionOid));
            }
         }

         ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm.getAlgorithmName()).build(issuerPrivateKey);
         X509CertificateHolder cert = v3CertGen.build(signer);
         return new JcaX509CertificateConverter().getCertificate(cert);
      } catch (CertificateException | CertIOException | OperatorCreationException e) {
         throw new GeneralSecurityException(e);
      }
   }
}
