package com.airlock.iam.oauth2.domain.service.clientauthentication.certificate;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.domain.model.certificate.CertificateLogUtil;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2AuthClientId;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthentication;
import com.airlock.iam.oauth2.api.domain.service.clientauthentication.OAuth2ClientAuthenticationVerifier;
import com.airlock.iam.oauth2.domain.model.clientauthentication.OAuth2AuthenticationVerifierSource;
import com.airlock.iam.oauth2.domain.model.clientauthentication.OAuth2ClientAuthenticationData;
import com.airlock.iam.oauth2.domain.model.clientauthentication.OAuth2ClientCertificate;
import com.airlock.iam.oauth2.domain.model.clientauthentication.OAuth2ClientCertificateAuthenticationPayload;
import com.airlock.iam.security.certificate.CertificateStatusChecker;
import com.airlock.iam.security.certificate.CertificateStatusCheckerException;
import com.airlock.iam.security.util.SecureComparison;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import lombok.NonNull;

public final class OAuth2ClientCertificateAuthenticationVerifier implements OAuth2ClientAuthenticationVerifier<OAuth2ClientCertificateAuthenticationPayload> {
   private final ELogger logger;
   private final OAuth2AuthenticationVerifierSource verifierSource;
   private final Collection<CertificateStatusChecker> certStatusCheckers;
   private final boolean ignoreTemporalValidity;

   private OAuth2ClientCertificateAuthenticationVerifier(
      @NonNull OAuth2AuthenticationVerifierSource verifierSource,
      @NonNull Collection<CertificateStatusChecker> certStatusCheckers,
      boolean ignoreTemporalValidity,
      @NonNull ELogger logger
   ) {
      if (verifierSource == null) {
         throw new NullPointerException("verifierSource is marked non-null but is null");
      }

      if (certStatusCheckers == null) {
         throw new NullPointerException("certStatusCheckers is marked non-null but is null");
      }

      if (logger == null) {
         throw new NullPointerException("logger is marked non-null but is null");
      }

      this.verifierSource = verifierSource;
      this.certStatusCheckers = certStatusCheckers;
      this.ignoreTemporalValidity = ignoreTemporalValidity;
      this.logger = logger;
   }

   public Optional<OAuth2ClientAuthentication> verify(OAuth2ClientCertificateAuthenticationPayload authenticationPayload) {
      OAuth2AuthClientId authenticationClientID = authenticationPayload.clientId()
         .orElseThrow(() -> new IllegalStateException("Required Client ID missing in mTLS authentication payload"));
      Optional<OAuth2ClientAuthenticationData> verificationData = this.verifierSource.verificationData(authenticationClientID);
      if (verificationData.isEmpty()) {
         this.logger.info("No verification data found for client " + LogUtil.logSafeQuote(authenticationClientID));
         return Optional.empty();
      } else {
         return this.isMatchingClientId(authenticationClientID, verificationData.get())
               && this.isMatchingCertificate(verificationData.get().certificates(), authenticationPayload.certificate())
            ? Optional.of(OAuth2ClientAuthentication.clientAuthentication(authenticationPayload))
            : Optional.empty();
      }
   }

   private boolean isMatchingClientId(OAuth2AuthClientId authenticationClientID, OAuth2ClientAuthenticationData verificationData) {
      boolean clientIdMatches = SecureComparison.isEqual(authenticationClientID.id(), verificationData.clientId().id());
      if (!clientIdMatches) {
         this.logger
            .info(
               "Client ID from authentication (\""
                  + authenticationClientID
                  + "\") does not match the expected Client ID \""
                  + verificationData.clientId()
                  + "\""
            );
      }

      return clientIdMatches;
   }

   private boolean isMatchingCertificate(Set<OAuth2ClientCertificate> verificationCertificates, X509Certificate payloadCertificate) {
      if (verificationCertificates.isEmpty()) {
         this.logger.debug("Found no certificates to compare the certificate from the request against.");
         return false;
      } else if (!this.payloadMatchesCertInfo(verificationCertificates, payloadCertificate)) {
         return false;
      } else {
         return !this.certificateIsTemporarilyValid(payloadCertificate) ? false : this.certificateStatusIsOk(payloadCertificate);
      }
   }

   private boolean payloadMatchesCertInfo(Set<OAuth2ClientCertificate> verificationCertificates, X509Certificate payloadCertificate) {
      return verificationCertificates.stream().anyMatch(certInfo -> certInfo.matches(payloadCertificate, this.logger));
   }

   private boolean certificateStatusIsOk(X509Certificate certificate) {
      return this.certStatusCheckers.stream().noneMatch(checker -> this.isRevoked(checker, certificate));
   }

   private boolean isRevoked(CertificateStatusChecker checker, X509Certificate certificate) {
      try {
         if (checker.isRevoked(certificate)) {
            this.logger.info("The certificate " + CertificateLogUtil.certificateLogString(certificate) + "is revoked.");
            return true;
         } else {
            return false;
         }
      } catch (CertificateStatusCheckerException e) {
         this.logger.info("Could not check the status of " + CertificateLogUtil.certificateLogString(certificate) + ": ", e);
         return true;
      }
   }

   private boolean certificateIsTemporarilyValid(X509Certificate certificate) {
      if (this.ignoreTemporalValidity) {
         return true;
      }

      try {
         certificate.checkValidity();
         return true;
      } catch (CertificateExpiredException e) {
         this.logger.info("Certificate is expired: " + CertificateLogUtil.certificateLogString(certificate) + " not after: " + certificate.getNotAfter());
      } catch (CertificateNotYetValidException e) {
         this.logger
            .info("Certificate is not yet valid: " + CertificateLogUtil.certificateLogString(certificate) + " not before: " + certificate.getNotBefore());
      }

      return false;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2ClientCertificateAuthenticationVerifierBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2AuthenticationVerifierSource verifierSource;
      @SuppressFBWarnings(justification = "generated code")
      private Collection<CertificateStatusChecker> certStatusCheckers;
      @SuppressFBWarnings(justification = "generated code")
      private boolean ignoreTemporalValidity;
      @SuppressFBWarnings(justification = "generated code")
      private ELogger logger;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificateAuthenticationVerifier.OAuth2ClientCertificateAuthenticationVerifierBuilder verifierSource(
         @NonNull OAuth2AuthenticationVerifierSource verifierSource
      ) {
         if (verifierSource == null) {
            throw new NullPointerException("verifierSource is marked non-null but is null");
         }

         this.verifierSource = verifierSource;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificateAuthenticationVerifier.OAuth2ClientCertificateAuthenticationVerifierBuilder certStatusCheckers(
         @NonNull Collection<CertificateStatusChecker> certStatusCheckers
      ) {
         if (certStatusCheckers == null) {
            throw new NullPointerException("certStatusCheckers is marked non-null but is null");
         }

         this.certStatusCheckers = certStatusCheckers;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificateAuthenticationVerifier.OAuth2ClientCertificateAuthenticationVerifierBuilder ignoreTemporalValidity(
         boolean ignoreTemporalValidity
      ) {
         this.ignoreTemporalValidity = ignoreTemporalValidity;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificateAuthenticationVerifier.OAuth2ClientCertificateAuthenticationVerifierBuilder logger(@NonNull ELogger logger) {
         if (logger == null) {
            throw new NullPointerException("logger is marked non-null but is null");
         }

         this.logger = logger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2ClientCertificateAuthenticationVerifier build() {
         return new OAuth2ClientCertificateAuthenticationVerifier(this.verifierSource, this.certStatusCheckers, this.ignoreTemporalValidity, this.logger);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2ClientCertificateAuthenticationVerifier.OAuth2ClientCertificateAuthenticationVerifierBuilder(verifierSource="
            + this.verifierSource
            + ", certStatusCheckers="
            + this.certStatusCheckers
            + ", ignoreTemporalValidity="
            + this.ignoreTemporalValidity
            + ", logger="
            + this.logger
            + ")";
      }
   }
}
