package com.airlock.iam.security.pkix;

import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.security.certificate.CertificateStatusChecker;
import com.airlock.iam.security.certificate.CertificateStatusCheckerException;
import com.airlock.iam.security.util.SecurityProvider;
import com.google.common.annotations.VisibleForTesting;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertPathBuilderResult;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
import java.security.cert.CertStoreParameters;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import lombok.NonNull;
import org.apache.commons.lang3.exception.ExceptionUtils;

public final class TrustedCertificateVerifier {
   private static final ELogger LOG = ELoggerFactory.getLogger(TrustedCertificateVerifier.class);
   @NonNull
   private final KeyStore trustStore;
   @NonNull
   private final Consumer<String> infoLog;
   private final Provider provider = SecurityProvider.getProvider();
   @NonNull
   private final List<CertificateStatusChecker> certificateStatusCheckers;

   public TrustedCertificateVerifier(@NonNull KeyStore trustStore, @NonNull List<CertificateStatusChecker> certificateStatusCheckers) {
      this(trustStore, certificateStatusCheckers, LOG::info);
      if (trustStore == null) {
         throw new NullPointerException("trustStore is marked non-null but is null");
      }

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

   @VisibleForTesting
   TrustedCertificateVerifier(@NonNull KeyStore trustStore, @NonNull List<CertificateStatusChecker> certificateStatusCheckers, Consumer<String> infoLog) {
      if (trustStore == null) {
         throw new NullPointerException("trustStore is marked non-null but is null");
      }

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

      this.trustStore = trustStore;
      this.certificateStatusCheckers = certificateStatusCheckers;
      this.infoLog = infoLog;
   }

   public boolean verify(X509Certificate certificate) {
      try {
         PKIXBuilderParameters parameters = this.buildParameters(certificate);
         CertPathBuilderResult pathBuilderResult = CertPathBuilder.getInstance("PKIX", this.provider).build(parameters);
         CertPathValidator validator = CertPathValidator.getInstance("PKIX", this.provider);
         validator.validate(pathBuilderResult.getCertPath(), parameters);
      } catch (InvalidAlgorithmParameterException | CertPathBuilderException | CertPathValidatorException e) {
         String message = "Certificate for subject " + quotedSubject(certificate) + " is not trusted: " + getMessages(e);
         this.infoLog(message);
         return false;
      } catch (KeyStoreException e) {
         this.logException(certificate, e);
         throw new IllegalArgumentException(e);
      } catch (NoSuchAlgorithmException e) {
         this.logException(certificate, e);
         throw new IllegalStateException(e);
      }

      return !this.isCertificateRevoked(certificate);
   }

   private void logException(X509Certificate certificate, Exception e) {
      this.infoLog("Could not validate certificate with subject" + quotedSubject(certificate) + ": " + e.getMessage());
   }

   private static String quotedSubject(X509Certificate certificate) {
      return "\"" + certificate.getSubjectX500Principal() + "\"";
   }

   private void infoLog(String message) {
      this.infoLog.accept(message);
   }

   private static String getMessages(Exception e) {
      return ExceptionUtils.getThrowableList(e).stream().map(Throwable::getMessage).collect(Collectors.joining(" -> "));
   }

   private PKIXBuilderParameters buildParameters(X509Certificate certificate) throws KeyStoreException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
      PKIXBuilderParameters parameters = this.parametersWithTarget(certificate);
      parameters.setMaxPathLength(0);
      this.addCertificateStore(certificate, parameters);
      parameters.setRevocationEnabled(false);
      return parameters;
   }

   private void addCertificateStore(X509Certificate certificate, PKIXBuilderParameters parameters) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
      CertStoreParameters certificateStore = new CollectionCertStoreParameters(List.of(certificate));
      CertStore intermediatesStore = CertStore.getInstance("Collection", certificateStore, this.provider);
      parameters.addCertStore(intermediatesStore);
   }

   private PKIXBuilderParameters parametersWithTarget(X509Certificate certificate) throws KeyStoreException, InvalidAlgorithmParameterException {
      X509CertSelector selector = new X509CertSelector();
      selector.setCertificate(certificate);
      return new PKIXBuilderParameters(this.trustStore, selector);
   }

   public boolean isCertificateRevoked(X509Certificate certificate) {
      for (CertificateStatusChecker certificateStatusChecker : this.certificateStatusCheckers) {
         if (this.isCertificateRevoked(certificate, certificateStatusChecker)) {
            return true;
         }
      }

      return false;
   }

   private boolean isCertificateRevoked(X509Certificate certificate, CertificateStatusChecker certificateStatusChecker) {
      try {
         if (certificateStatusChecker.isRevoked(certificate)) {
            this.infoLog("Certificate is revoked: " + quotedSubject(certificate));
            return true;
         } else {
            return false;
         }
      } catch (CertificateStatusCheckerException e) {
         this.infoLog(
            "Error while checking certificate status for certificate "
               + quotedSubject(certificate)
               + ". Certificate is considered revoked. Error information: "
               + getMessages(e)
         );
         return true;
      }
   }
}
