package com.airlock.iam.oauth2.application.configuration.clientauthentication;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.oauth2.domain.model.clientauthentication.OAuth2ClientCertificate;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
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.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.asn1.x500.X500Name;

@Plugin(description = "Defines a certificate which can be used during the client authentication in OAuth 2.0.", displayName = "OAuth 2.0 Client Certificate")
@PluginLicenseRestriction(moduleAffiliation = "OAuthServer")
public final class OAuth2ClientCertificateConfig {
   private static final String SUBJECT_DN_DISPLAY_NAME = "Subject DN";
   private static final String ISSUER_DN_DISPLAY_NAME = "Issuer DN";
   private static final String FORMAT_DESCRIPTION_INTRO = "<p>For RDN types without a string representation defined, the OID format with values starting with a '#', followed by the hexadecimal representation, must be used (see <a href=\"https://tools.ietf.org/html/rfc2253#section-2.4\" target=\"_blank\">RFC 2253</a>. An example of such a value in OID notation would be 1.3.6.1.4.1.1466.0=#04024869 (OCTET String value \"Hi\" for OID 1.3.6.1.4.1.1466.0).</p>";
   private static final String OPEN_SSL_FOR_SUBJECT = "<p>OpenSSL can be used to extract the subject as follows:</p><pre><code>openssl x509 -in cert.pem -noout -subject -nameopt sep_comma_plus -nameopt dn_rev -nameopt utf8</code></pre><p>if this produces an error the subject should be configured with the RDNs in their OID's and hexadecimal values as mentioned above. This format can be generated by:</p><pre><code>openssl x509 -in cert.pem -noout -subject -nameopt dump_all -nameopt dump_der -nameopt oid -nameopt sep_comma_plus</code></pre>";
   private static final String OPEN_SSL_FOR_ISSUER = "<p>OpenSSL can be used to extract the issuer as follows:</p><pre><code>openssl x509 -in cert.pem -noout -issuer -nameopt sep_comma_plus -nameopt dn_rev -nameopt utf8</code></pre><p>if this produces an error the issuer should be configured with the RDNs in their OID's and hexadecimal values as mentioned above. This format can be generated by:</p><pre><code>openssl x509 -in cert.pem -noout -issuer -nameopt dump_all -nameopt dump_der -nameopt oid -nameopt sep_comma_plus</code></pre>";
   @StringProperty(
      displayName = "Subject DN",
      description = "The subject distinguished name of of the certificate in the comma separated format, e.g. \"CN=John Smith,OU=Marketing,O=Company,C=CH\". <p>For RDN types without a string representation defined, the OID format with values starting with a '#', followed by the hexadecimal representation, must be used (see <a href=\"https://tools.ietf.org/html/rfc2253#section-2.4\" target=\"_blank\">RFC 2253</a>. An example of such a value in OID notation would be 1.3.6.1.4.1.1466.0=#04024869 (OCTET String value \"Hi\" for OID 1.3.6.1.4.1.1466.0).</p><p>OpenSSL can be used to extract the subject as follows:</p><pre><code>openssl x509 -in cert.pem -noout -subject -nameopt sep_comma_plus -nameopt dn_rev -nameopt utf8</code></pre><p>if this produces an error the subject should be configured with the RDNs in their OID's and hexadecimal values as mentioned above. This format can be generated by:</p><pre><code>openssl x509 -in cert.pem -noout -subject -nameopt dump_all -nameopt dump_der -nameopt oid -nameopt sep_comma_plus</code></pre>",
      optional = false
   )
   private String subjectDN;
   static final String SUBJECT_DN_PROPERTY_NAME = "subjectDN";
   @StringProperty(
      displayName = "Issuer DN",
      description = "The optional issuer distinguished name of of the certificate in the comma separated format, e.g. \"CN=Company Trusted Root,O=Company,C=CH\". If defined, this value is also used to compare the certificates during the authentication. <p>For RDN types without a string representation defined, the OID format with values starting with a '#', followed by the hexadecimal representation, must be used (see <a href=\"https://tools.ietf.org/html/rfc2253#section-2.4\" target=\"_blank\">RFC 2253</a>. An example of such a value in OID notation would be 1.3.6.1.4.1.1466.0=#04024869 (OCTET String value \"Hi\" for OID 1.3.6.1.4.1.1466.0).</p><p>OpenSSL can be used to extract the issuer as follows:</p><pre><code>openssl x509 -in cert.pem -noout -issuer -nameopt sep_comma_plus -nameopt dn_rev -nameopt utf8</code></pre><p>if this produces an error the issuer should be configured with the RDNs in their OID's and hexadecimal values as mentioned above. This format can be generated by:</p><pre><code>openssl x509 -in cert.pem -noout -issuer -nameopt dump_all -nameopt dump_der -nameopt oid -nameopt sep_comma_plus</code></pre>",
      optional = true
   )
   private String issuerDN;
   static final String ISSUER_DN_PROPERTY_NAME = "issuerDN";

   public OAuth2ClientCertificate createClientCertificate() {
      OAuth2ClientCertificate.OAuth2ClientCertificateBuilder builder = OAuth2ClientCertificate.builder().subjectDn(new X500Name(this.subjectDN));
      Optional.ofNullable(this.issuerDN).<X500Name>map(X500Name::new).ifPresent(builder::issuerDn);
      return builder.build();
   }

   @ConfigurationValidator
   public static void validateDistinguishedNames(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      String subjectValue = StringPropertyValueReader.effectiveStringConfigValue(config, "subjectDN", env, context);
      checkX500PropertyValue(config, "subjectDN", "Subject DN", subjectValue, env, context);
      String issuerValue = StringPropertyValueReader.effectiveStringConfigValue(config, "issuerDN", env, context);
      if (StringUtils.isNotBlank(issuerValue)) {
         checkX500PropertyValue(config, "issuerDN", "Issuer DN", issuerValue, env, context);
      }
   }

   private static void checkX500PropertyValue(
      PluginConfiguration config, String propertyName, String displayName, String propertyValue, ConfigurationEnvironment env, ConfigurationContext context
   ) throws PropertyConstraintViolationException {
      try {
         new X500Name(propertyValue);
      } catch (RuntimeException e) {
         String description = "Invalid value for "
            + LogUtil.quote(displayName)
            + ": "
            + LogUtil.quote(propertyValue)
            + " must be a valid X500 name (see RFC 2253). For unknown (custom) RDNs try using oid / hexadecimal value representation.";
         throw new PropertyConstraintViolationException(propertyName, config, env, context, description, e);
      }
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public OAuth2ClientCertificateConfig(String subjectDN, String issuerDN) {
      this.subjectDN = subjectDN;
      this.issuerDN = issuerDN;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2ClientCertificateConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String subjectDN;
      @SuppressFBWarnings(justification = "generated code")
      private String issuerDN;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificateConfig.OAuth2ClientCertificateConfigBuilder subjectDN(String subjectDN) {
         this.subjectDN = subjectDN;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificateConfig.OAuth2ClientCertificateConfigBuilder issuerDN(String issuerDN) {
         this.issuerDN = issuerDN;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificateConfig build() {
         return new OAuth2ClientCertificateConfig(this.subjectDN, this.issuerDN);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2ClientCertificateConfig.OAuth2ClientCertificateConfigBuilder(subjectDN=" + this.subjectDN + ", issuerDN=" + this.issuerDN + ")";
      }
   }
}
