package com.airlock.iam.oauth2.domain.model.clientauthentication;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.security.cert.X509Certificate;
import java.util.Optional;
import lombok.NonNull;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;

public final class OAuth2ClientCertificate {
   @NonNull
   private final X500Name subjectDn;
   private final Optional<X500Name> issuerDn;
   private final ELogger defaultLogger;

   private OAuth2ClientCertificate(@Nonnull X500Name subjectDn, @Nullable X500Name issuerDn, @Nullable ELogger logger) {
      this.subjectDn = subjectDn;
      this.issuerDn = Optional.ofNullable(issuerDn);
      this.defaultLogger = Optional.ofNullable(logger).orElse(ELoggerFactory.getLogger(OAuth2ClientCertificate.class));
   }

   @VisibleForTesting
   public OAuth2ClientCertificate(X509Certificate certificate) {
      this.subjectDn = X500Name.getInstance(certificate.getSubjectX500Principal().getEncoded());
      this.issuerDn = Optional.of(X500Name.getInstance(certificate.getIssuerX500Principal().getEncoded()));
      this.defaultLogger = ELoggerFactory.getLogger(OAuth2ClientCertificate.class);
   }

   public boolean matches(X509Certificate certificate, ELogger customLogger) {
      return this.subjectDnsMatch(X500Name.getInstance(certificate.getSubjectX500Principal().getEncoded()), customLogger)
         && this.issuerDnsMatch(X500Name.getInstance(certificate.getIssuerX500Principal().getEncoded()), customLogger);
   }

   public boolean matches(X509Certificate certificate) {
      return this.subjectDnsMatch(X500Name.getInstance(certificate.getSubjectX500Principal().getEncoded()), this.defaultLogger)
         && this.issuerDnsMatch(X500Name.getInstance(certificate.getIssuerX500Principal().getEncoded()), this.defaultLogger);
   }

   public boolean matches(OAuth2ClientCertificate other) {
      return this.subjectDnsMatch(other.subjectDn, this.defaultLogger)
         && this.issuerDn
            .<Boolean>map(issuer -> other.issuerDn.isPresent() && this.issuerDnsMatch(other.issuerDn.get(), this.defaultLogger))
            .orElse(other.issuerDn.isEmpty());
   }

   private boolean subjectDnsMatch(X500Name payloadSubject, ELogger logger) {
      String payloadSubjectLogName = LogUtil.quote(toBCStyle(payloadSubject));
      String expectedSubjectLogName = LogUtil.quote(toBCStyle(this.subjectDn));
      if (payloadSubject.equals(this.subjectDn)) {
         logger.debug("Certificate subjects did match: " + payloadSubjectLogName + " (from the request) was " + expectedSubjectLogName + " (expected)");
         return true;
      } else {
         logger.debug("Certificate subjects did not match: " + payloadSubjectLogName + " (from the request) was not " + expectedSubjectLogName + " (expected)");
         return false;
      }
   }

   private static X500Name toBCStyle(X500Name name) {
      return X500Name.getInstance(BCStyle.INSTANCE, name);
   }

   private boolean issuerDnsMatch(X500Name payloadIssuer, ELogger logger) {
      return this.issuerDn.<Boolean>map(issuer -> this.issuerDnsMatch(issuer, payloadIssuer, logger)).orElse(Boolean.TRUE);
   }

   private boolean issuerDnsMatch(X500Name expectedIssuer, X500Name payloadIssuer, ELogger logger) {
      String payloadIssuerLogName = LogUtil.quote(toBCStyle(payloadIssuer));
      String expectedIssuerLogName = LogUtil.quote(toBCStyle(expectedIssuer));
      if (expectedIssuer.equals(payloadIssuer)) {
         logger.debug("Certificate issuers did match: " + payloadIssuerLogName + " (from the request) was " + expectedIssuerLogName + " (expected)");
         return true;
      } else {
         logger.debug("Certificate issuers did not match: " + payloadIssuerLogName + " (from the request) was not " + expectedIssuerLogName + " (expected)");
         return false;
      }
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2ClientCertificateBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private X500Name subjectDn;
      @SuppressFBWarnings(justification = "generated code")
      private X500Name issuerDn;
      @SuppressFBWarnings(justification = "generated code")
      private ELogger logger;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificate.OAuth2ClientCertificateBuilder subjectDn(@Nonnull X500Name subjectDn) {
         if (subjectDn == null) {
            throw new NullPointerException("subjectDn is marked non-null but is null");
         }

         this.subjectDn = subjectDn;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificate.OAuth2ClientCertificateBuilder issuerDn(@Nullable X500Name issuerDn) {
         this.issuerDn = issuerDn;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificate.OAuth2ClientCertificateBuilder logger(@Nullable ELogger logger) {
         this.logger = logger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificate build() {
         return new OAuth2ClientCertificate(this.subjectDn, this.issuerDn, this.logger);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2ClientCertificate.OAuth2ClientCertificateBuilder(subjectDn="
            + this.subjectDn
            + ", issuerDn="
            + this.issuerDn
            + ", logger="
            + this.logger
            + ")";
      }
   }
}
