package com.airlock.iam.common.application.configuration.context;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.application.configuration.gateway.AirlockMicrogatewayConfig;
import com.airlock.iam.common.application.configuration.gateway.extractor.ClientCertificateExtractionFormatConfig;
import com.airlock.iam.common.application.configuration.gateway.extractor.ClientCertificatePemExtractionFormatConfig;
import com.airlock.iam.common.application.configuration.gateway.extractor.ClientCertificateXfccExtractionFormatConfig;
import com.airlock.iam.common.application.configuration.gateway.extractor.RequestMtlsClientCertificateExtractorConfig;
import com.airlock.iam.common.domain.model.gateway.ClientCertificateExtractionFormat;
import com.airlock.iam.common.domain.model.gateway.ClientCertificatePemExtractionFormat;
import com.airlock.iam.common.domain.model.gateway.ClientCertificateXfccExtractionFormat;
import com.airlock.iam.common.domain.model.request.ClientTlsCertificate;
import com.airlock.iam.common.infrastructure.request.HttpRequestMtlsClientCertificateExtractorImpl;
import com.airlock.iam.core.application.configuration.waf.AirlockGatewayConfig;
import com.airlock.iam.core.application.configuration.waf.GatewayConfig;
import com.airlock.iam.core.misc.util.context.ContextExtractor;
import com.airlock.iam.core.misc.util.crypto.CertUtils;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.servlet.http.HttpServletRequest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

@Plugin(
   description = "<p>Context extractor that determines the context by matching configurable regular expressions\nagainst information in the client certificate extracted from the request.</p>\n<p>This extractor works in conjunction with client certificate authentication.</p>"
)
public final class ClientCertificateContextExtractor implements ContextExtractor {
   private static final ELogger LOG = ELoggerFactory.getLogger(ClientCertificateContextExtractor.class);
   @PluginListProperty(
      description = "<p>Defines mappings of regular expressions patterns to configuration contexts.</p>\n<p>Each pattern is matched in order against the issuer distinguished name (DN) of the extracted\nclient certificate.</p>\n<p>The first matching pattern determines the resulting configuration context.</p>",
      optional = false
   )
   private List<ContextPatternForClientCertificateContextExtractor> mappings;
   @BooleanProperty(
      description = "By default, the patterns are matched against the distinguished name (DN) of the certificate issuer.\nIf this property is enabled, the patterns are matched against the DN of the certificate subject (holder)\ninstead.",
      optional = true,
      displayName = "Match Against Subject DN",
      defaultValue = "false"
   )
   private boolean matchAgainstSubjectDn;
   @StringProperty(
      description = "Name of the context to be used if no pattern matches or no client certificate could be extracted from the request.<br>Leave empty to implicitly use the <i>default</i> context. If this plugin is used within a \"Combining Context Extractor\", use \"[DEFAULT]\" to explicitly return the default context if necessary.",
      optional = true,
      exampleValues = {"CTX1", "EXT", "[DEFAULT]"}
   )
   private String fallbackContext;
   @PluginProperty(
      description = "Settings regarding an Airlock Gateway or Airlock Microgateway reverse proxy placed in front of Airlock IAM.<p>The client certificate is extracted differently from the request based on this configuration:<ul><li>Airlock Gateway (WAF): certificate is extracted from the environment cookie</li><li>Airlock Microgateway: certificate is extracted from the configured header</li><li>When no gateway is configured, attempt to extract the client certificate from the <tt>jakarta.servlet.request.X509Certificate</tt> request attribute</li></ul></p>",
      optional = true
   )
   private GatewayConfig gateway;

   @Override
   public Optional<ConfigurationContext> getContext(HttpServletRequest request) {
      Optional<ConfigurationContext> fallbackConfigurationContext = Optional.ofNullable(this.fallbackContext).map(ConfigurationContext::configurationContext);
      if (request == null) {
         LOG.warn(
            LogTypes.ILLEGAL_ARGUMENT,
            "Returning %s because there is no request object to determine context.".formatted(fallbackContextDescription(fallbackConfigurationContext))
         );
         return fallbackConfigurationContext;
      }

      Optional<X509Certificate> cert;
      if (this.gateway instanceof AirlockMicrogatewayConfig airlockMicrogatewayConfig) {
         cert = determineCertificateFromMicrogatewayHeader(request, airlockMicrogatewayConfig);
      } else {
         try {
            cert = this.determineCertificateFromGatewayCookieOrRequest(request);
         } catch (CertificateException e) {
            LOG.warn(
               LogTypes.TRACE_MESSAGE,
               "Returning %s because the client certificate could not be extracted: %s."
                  .formatted(fallbackContextDescription(fallbackConfigurationContext), e.getMessage()),
               e
            );
            return fallbackConfigurationContext;
         }
      }

      if (cert.isEmpty()) {
         LOG.debug("Returning %s because there is no client certificate in the request.".formatted(fallbackContextDescription(fallbackConfigurationContext)));
         return fallbackConfigurationContext;
      } else {
         return this.configurationContextFromCertDnMapping(cert.get(), fallbackConfigurationContext);
      }
   }

   private Optional<ConfigurationContext> configurationContextFromCertDnMapping(
      X509Certificate certificate, Optional<ConfigurationContext> fallbackConfigurationContext
   ) {
      String dn = this.getCertificateDn(certificate);

      for (ContextPatternForClientCertificateContextExtractor p : this.mappings) {
         if (p.getPattern().matcher(dn).matches()) {
            ConfigurationContext matchingContext = ConfigurationContext.configurationContext(p.getConfigurationContext());
            LOG.debug(
               "Returning configuration context \"%s\" because the client certificate DN %s matches the pattern %s."
                  .formatted(matchingContext, LogUtil.logSafeQuote(dn), LogUtil.quote(p.getPattern()))
            );
            return Optional.of(matchingContext);
         }
      }

      LOG.debug(
         "Returning %s because the client certificate DN %s did not match any mapping."
            .formatted(fallbackContextDescription(fallbackConfigurationContext), LogUtil.logSafeQuote(dn))
      );
      return fallbackConfigurationContext;
   }

   private String getCertificateDn(X509Certificate certificate) {
      return this.matchAgainstSubjectDn ? certificate.getSubjectDN().getName() : certificate.getIssuerDN().getName();
   }

   private Optional<X509Certificate> determineCertificateFromGatewayCookieOrRequest(HttpServletRequest request) throws CertificateException {
      X509Certificate certFromEnvCookieOrRequest = CertUtils.extractClientCertificateFromRequest(request, this.getEnvCookiePrefix().orElse(null));
      return Optional.ofNullable(certFromEnvCookieOrRequest);
   }

   private Optional<String> getEnvCookiePrefix() {
      return this.gateway instanceof AirlockGatewayConfig airlockGatewayConfig
         ? Optional.ofNullable(airlockGatewayConfig.getEnvironmentCookiePrefix())
         : Optional.empty();
   }

   private static Optional<X509Certificate> determineCertificateFromMicrogatewayHeader(
      HttpServletRequest request, AirlockMicrogatewayConfig airlockMicrogatewayConfig
   ) {
      RequestMtlsClientCertificateExtractorConfig extractorConfig = airlockMicrogatewayConfig.getRequestMtlsClientCertificateExtractor();
      ClientCertificateExtractionFormat format = getClientCertificateExtractionFormat(extractorConfig);
      HttpRequestMtlsClientCertificateExtractorImpl extractor = new HttpRequestMtlsClientCertificateExtractorImpl(extractorConfig, () -> request, () -> format);
      return extractor.getMtlsClientCertificate().flatMap(ClientTlsCertificate::getX509Certificate);
   }

   private static ClientCertificateExtractionFormat getClientCertificateExtractionFormat(RequestMtlsClientCertificateExtractorConfig extractorConfig) {
      ClientCertificateExtractionFormatConfig formatConfig = extractorConfig.getExtractionFormat();
      if (formatConfig instanceof ClientCertificatePemExtractionFormatConfig) {
         return new ClientCertificatePemExtractionFormat();
      } else if (formatConfig instanceof ClientCertificateXfccExtractionFormatConfig) {
         return new ClientCertificateXfccExtractionFormat();
      } else {
         throw new FailureException("Unsupported client certificate extraction format: " + formatConfig.getClass().getSimpleName());
      }
   }

   private static String fallbackContextDescription(Optional<ConfigurationContext> fallbackContext) {
      return fallbackContext.isEmpty() ? "no configuration context" : "fallback configuration context " + LogUtil.quote(fallbackContext.get().toString());
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public ClientCertificateContextExtractor(
      List<ContextPatternForClientCertificateContextExtractor> mappings, boolean matchAgainstSubjectDn, String fallbackContext, GatewayConfig gateway
   ) {
      this.mappings = mappings;
      this.matchAgainstSubjectDn = matchAgainstSubjectDn;
      this.fallbackContext = fallbackContext;
      this.gateway = gateway;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class ClientCertificateContextExtractorBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<ContextPatternForClientCertificateContextExtractor> mappings;
      @SuppressFBWarnings(justification = "generated code")
      private boolean matchAgainstSubjectDn;
      @SuppressFBWarnings(justification = "generated code")
      private String fallbackContext;
      @SuppressFBWarnings(justification = "generated code")
      private GatewayConfig gateway;

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

      @SuppressFBWarnings(justification = "generated code")
      public ClientCertificateContextExtractor.ClientCertificateContextExtractorBuilder mapping(ContextPatternForClientCertificateContextExtractor mapping) {
         if (this.mappings == null) {
            this.mappings = new ArrayList<>();
         }

         this.mappings.add(mapping);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ClientCertificateContextExtractor.ClientCertificateContextExtractorBuilder mappings(
         Collection<? extends ContextPatternForClientCertificateContextExtractor> mappings
      ) {
         if (mappings == null) {
            throw new NullPointerException("mappings cannot be null");
         }

         if (this.mappings == null) {
            this.mappings = new ArrayList<>();
         }

         this.mappings.addAll(mappings);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ClientCertificateContextExtractor.ClientCertificateContextExtractorBuilder clearMappings() {
         if (this.mappings != null) {
            this.mappings.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ClientCertificateContextExtractor.ClientCertificateContextExtractorBuilder matchAgainstSubjectDn(boolean matchAgainstSubjectDn) {
         this.matchAgainstSubjectDn = matchAgainstSubjectDn;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ClientCertificateContextExtractor.ClientCertificateContextExtractorBuilder fallbackContext(String fallbackContext) {
         this.fallbackContext = fallbackContext;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ClientCertificateContextExtractor.ClientCertificateContextExtractorBuilder gateway(GatewayConfig gateway) {
         this.gateway = gateway;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ClientCertificateContextExtractor build() {
         return new ClientCertificateContextExtractor(switch (this.mappings == null ? 0 : this.mappings.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.mappings.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.mappings));
         }, this.matchAgainstSubjectDn, this.fallbackContext, this.gateway);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "ClientCertificateContextExtractor.ClientCertificateContextExtractorBuilder(mappings="
            + this.mappings
            + ", matchAgainstSubjectDn="
            + this.matchAgainstSubjectDn
            + ", fallbackContext="
            + this.fallbackContext
            + ", gateway="
            + this.gateway
            + ")";
      }
   }
}
