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

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.application.configuration.gateway.AirlockMicrogatewayConfig;
import com.airlock.iam.common.domain.model.gateway.WafSettings;
import com.airlock.iam.common.domain.model.request.RequestUrl;
import com.airlock.iam.common.domain.model.request.VirtualHostName;
import com.airlock.iam.common.infrastructure.request.AirlockEnvCookiesInformationServiceImpl;
import com.airlock.iam.common.infrastructure.request.HttpRequestUrlExtractorImpl;
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.context.ContextPattern;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
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.util.List;
import java.util.Optional;

@Plugin(
   displayName = "URL Context Extractor",
   description = "<p>Configures request URI to context mappings. If the request URI does not match, the fallback context is used.</p><p>The matching is performed against the part of the request URI after the protocol or after the host (depending on the configuration).</p><p><b>Example:</b> If the request URL is \"https://host/some/path\" then the considered path is \"host/some/path\" (if \"Use Virtual Host\" is enabled) or \"/some/path\" otherwise.</p>To determine the request URI behind a gateway, Airlock IAM requires one of these gateway settings:<ul><li>Airlock Gateway (WAF): Verify that \"Environment Cookies\" are activated on all Loginapp mappings and that the environment cookie prefix is the same in the Airlock Gateway and Airlock IAM. The URL parts are sent by the WAF in the <code>HTTPS</code>, <code>SERVER_NAME</code>, <code>SERVER_REQUEST</code> and <code>SERVER_PORT</code> cookies (with respect to the configured prefix)</li><li>Airlock Microgateway: Make sure to extract the request URI in Airlock Microgateway Settings.</li></ul>"
)
public class URLContextExtractor implements ContextExtractor {
   private static final ELogger LOG = ELoggerFactory.getLogger(URLContextExtractor.class);
   @PluginListProperty(
      description = "Defines a list of regular expressions matched against the request to determine the configuration contexts.",
      optional = false
   )
   private List<ContextPattern> mappings;
   @BooleanProperty(
      description = "Also include the host name in the URL being matched.\n<p>If disabled, the Mappings only match against the path of the request URL, for example <code>/auth/</code><br>\nIf enabled, the Mappings match against the host name and path, for example <code>www.server.com/auth/</code></p>",
      optional = true,
      defaultValue = "false"
   )
   private boolean useVirtualHost;
   @StringProperty(
      description = "Name of the context to be used if none of the mappings match.<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 request URI is extracted differently from the request based on this configuration:<ul><li>Airlock Gateway (WAF): request URI is extracted from the environment cookie</li><li>Airlock Microgateway: request URI is extracted from the configured header</li><li>When no gateway is configured, the request URI is used</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.debug("Returning %s because the request is null.".formatted(fallbackContextDescription(fallbackConfigurationContext)));
         return fallbackConfigurationContext;
      } else {
         Optional<String> requestUrl = this.extractUrlFromRequest(request).getRequestUrl(this.useVirtualHost);
         if (requestUrl.isEmpty()) {
            LOG.debug("Returning %s because the request URI could not be determined.".formatted(fallbackContextDescription(fallbackConfigurationContext)));
            return fallbackConfigurationContext;
         } else {
            return this.configurationContextFromUrlMapping(requestUrl.get(), fallbackConfigurationContext);
         }
      }
   }

   private URLContextExtractor.RequestUri extractUrlFromRequest(HttpServletRequest request) {
      if (this.gateway instanceof AirlockGatewayConfig airlockGatewayConfig) {
         Optional<WafSettings> wafConfig = Optional.of(airlockGatewayConfig)
            .map(c -> WafSettings.builder(c.getControlCookieName(), c.getEnvironmentCookiePrefix()).build());
         LOG.debug("Determine configuration context based on request URI - trying to extract URI from Airlock Gateway environment cookies.");
         AirlockEnvCookiesInformationServiceImpl service = new AirlockEnvCookiesInformationServiceImpl(() -> wafConfig, () -> request);
         URLContextExtractor.RequestUri.RequestUriBuilder requestUriBuilder = URLContextExtractor.RequestUri.builder();
         service.getRequestUrl().ifPresent(uri -> requestUriBuilder.uriPath(uri.getUri().getPath()));
         service.getVirtualHostName().ifPresent(host -> requestUriBuilder.virtualHostName(new VirtualHostName(host.name())));
         return requestUriBuilder.build();
      } else if (this.gateway instanceof AirlockMicrogatewayConfig airlockMicrogatewayConfig) {
         LOG.debug(
            String.format(
               "Determine configuration context based on request URI - trying to extract URL from Airlock Microgateway header %s.",
               LogUtil.logSafeQuote(airlockMicrogatewayConfig.getRequestUrlExtractor().getHttpHeaderName())
            )
         );
         return new HttpRequestUrlExtractorImpl(airlockMicrogatewayConfig.getRequestUrlExtractor(), () -> request)
            .getRequestUrl()
            .map(URLContextExtractor::microgatewayToRequestUri)
            .orElse(URLContextExtractor.RequestUri.builder().build());
      } else {
         LOG.debug("Determine configuration context based on request URI - no gateway configuration - extract URI from request.");
         return noGatewayToRequestUri(request);
      }
   }

   private static URLContextExtractor.RequestUri microgatewayToRequestUri(RequestUrl request) {
      VirtualHostName virtualHostName = new VirtualHostName(request.getUri().getHost());
      String requestUri = request.getUri().getPath();
      return new URLContextExtractor.RequestUri(virtualHostName, requestUri);
   }

   private static URLContextExtractor.RequestUri noGatewayToRequestUri(HttpServletRequest request) {
      VirtualHostName virtualHostName = new VirtualHostName(request.getServerName());
      String requestUri = request.getRequestURI();
      return new URLContextExtractor.RequestUri(virtualHostName, requestUri);
   }

   private Optional<ConfigurationContext> configurationContextFromUrlMapping(String url, Optional<ConfigurationContext> fallbackConfigurationContext) {
      for (ContextPattern pattern : this.mappings) {
         if (pattern.getPattern().matcher(url).matches()) {
            String currentContext = pattern.getConfigurationContext();
            LOG.debug(
               "Returning configuration context \"%s\" because request URI %s matches the configured mapping."
                  .formatted(currentContext, LogUtil.logSafeQuote(url))
            );
            return Optional.of(ConfigurationContext.configurationContext(currentContext));
         }
      }

      LOG.debug(
         "Returning %s because the request URI %s did not match any mapping."
            .formatted(fallbackContextDescription(fallbackConfigurationContext), LogUtil.logSafeQuote(url))
      );
      return fallbackConfigurationContext;
   }

   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 URLContextExtractor.URLContextExtractorBuilder builder() {
      return new URLContextExtractor.URLContextExtractorBuilder();
   }

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

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

   static final class RequestUri {
      private final VirtualHostName virtualHostName;
      private final String uriPath;

      public Optional<VirtualHostName> virtualHostName() {
         return Optional.ofNullable(this.virtualHostName);
      }

      public Optional<String> uriPath() {
         return Optional.ofNullable(this.uriPath);
      }

      public Optional<String> getRequestUrl(boolean useVirtualHost) {
         Optional<String> path = this.uriPath();
         if (useVirtualHost) {
            Optional<VirtualHostName> vHostName = this.virtualHostName();
            return vHostName.isPresent() && path.isPresent()
               ? Optional.of(vHostName.get().name() + path.get())
               : vHostName.map(VirtualHostName::name).or(this::uriPath);
         } else {
            return path;
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      public static URLContextExtractor.RequestUri.RequestUriBuilder builder() {
         return new URLContextExtractor.RequestUri.RequestUriBuilder();
      }

      @SuppressFBWarnings(justification = "generated code")
      public RequestUri(VirtualHostName virtualHostName, String uriPath) {
         this.virtualHostName = virtualHostName;
         this.uriPath = uriPath;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static class RequestUriBuilder {
         @SuppressFBWarnings(justification = "generated code")
         private VirtualHostName virtualHostName;
         @SuppressFBWarnings(justification = "generated code")
         private String uriPath;

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

         @SuppressFBWarnings(justification = "generated code")
         public URLContextExtractor.RequestUri.RequestUriBuilder virtualHostName(VirtualHostName virtualHostName) {
            this.virtualHostName = virtualHostName;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public URLContextExtractor.RequestUri.RequestUriBuilder uriPath(String uriPath) {
            this.uriPath = uriPath;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public URLContextExtractor.RequestUri build() {
            return new URLContextExtractor.RequestUri(this.virtualHostName, this.uriPath);
         }

         @SuppressFBWarnings(justification = "generated code")
         @Override
         public String toString() {
            return "URLContextExtractor.RequestUri.RequestUriBuilder(virtualHostName=" + this.virtualHostName + ", uriPath=" + this.uriPath + ")";
         }
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class URLContextExtractorBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<ContextPattern> mappings;
      @SuppressFBWarnings(justification = "generated code")
      private boolean useVirtualHost;
      @SuppressFBWarnings(justification = "generated code")
      private String fallbackContext;
      @SuppressFBWarnings(justification = "generated code")
      private GatewayConfig gateway;

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

      @SuppressFBWarnings(justification = "generated code")
      public URLContextExtractor.URLContextExtractorBuilder mappings(List<ContextPattern> mappings) {
         this.mappings = mappings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public URLContextExtractor.URLContextExtractorBuilder useVirtualHost(boolean useVirtualHost) {
         this.useVirtualHost = useVirtualHost;
         return this;
      }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public URLContextExtractor build() {
         return new URLContextExtractor(this.mappings, this.useVirtualHost, this.fallbackContext, this.gateway);
      }

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