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.ClientIp;
import com.airlock.iam.common.infrastructure.request.AirlockEnvCookiesInformationServiceImpl;
import com.airlock.iam.common.infrastructure.request.HttpRequestClientIpExtractorImpl;
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.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.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 = "IP Address Context Extractor",
   description = "<p>Configures client IP address to context mappings. If the IP of the client does not match, the fallback context is used.</p>To determine the client IP address 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 in Airlock Gateway and Airlock IAM is the same. The client IP is sent by the WAF in the <tt>REMOTE_ADDR</tt> cookie (with respect to the configured prefix)</li><li>Airlock Microgateway: Make sure to extract the client's IP address in Airlock Microgateway Settings.</li></ul>"
)
public final class IpAddressContextExtractor implements ContextExtractor {
   private static final ELogger LOG = ELoggerFactory.getLogger(IpAddressContextExtractor.class);
   @PluginListProperty(description = "Defines a list of IPv4 ranges matched against the client IP to determine the configuration context.", optional = false)
   private List<IpRangeContext> mappings;
   @StringProperty(
      description = "Name of the context to be used if none of the IP range 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 client IP address is extracted differently from the request based on this configuration:<ul><li>Airlock Gateway (WAF): client IP is extracted from the environment cookie</li><li>Airlock Microgateway: client IP is extracted from the configured header</li><li>When no gateway is configured, the request's remote address is used as client IP</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<ClientIp> clientIp = this.extractClientIpFromRequest(request);
         if (clientIp.isEmpty()) {
            LOG.debug("Returning %s because the client IP could not be determined.".formatted(fallbackContextDescription(fallbackConfigurationContext)));
            return fallbackConfigurationContext;
         } else {
            return this.configurationContextFromIpMapping(clientIp.get(), fallbackConfigurationContext);
         }
      }
   }

   private Optional<ClientIp> extractClientIpFromRequest(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 client IP - trying to extract client IP from Airlock Gateway environment cookie %s."
               .formatted(LogUtil.logSafeQuote(wafConfig.orElseThrow().getEnvironmentCookiePrefix() + "REMOTE_ADDR"))
         );
         return new AirlockEnvCookiesInformationServiceImpl(() -> wafConfig, () -> request).getClientIp();
      } else if (this.gateway instanceof AirlockMicrogatewayConfig microgatewayConfig) {
         LOG.debug(
            "Determine configuration context based on client IP - trying to extract client IP from Airlock Microgateway header %s."
               .formatted(LogUtil.logSafeQuote(microgatewayConfig.getClientIpExtractor().getHttpHeaderName()))
         );
         return new HttpRequestClientIpExtractorImpl(microgatewayConfig.getClientIpExtractor(), () -> request).getClientIp();
      } else {
         LOG.debug("Determine configuration context based on client IP - no gateway configuration - extract client IP from request.");
         return Optional.of(new ClientIp(request.getRemoteAddr()));
      }
   }

   private Optional<ConfigurationContext> configurationContextFromIpMapping(ClientIp clientIp, Optional<ConfigurationContext> fallbackConfigurationContext) {
      for (IpRangeContext ipRangeContext : this.mappings) {
         if (ipRangeContext.isIpInRange(clientIp.value())) {
            String currentContext = ipRangeContext.getConfigurationContext();
            LOG.debug(
               "Returning configuration context \"%s\" because client IP \"%s\" matches the configured IP range mapping."
                  .formatted(currentContext, clientIp.value())
            );
            return Optional.of(ConfigurationContext.configurationContext(currentContext));
         }
      }

      LOG.debug(
         "Returning %s because client IP \"%s\" did not match any mapping."
            .formatted(fallbackContextDescription(fallbackConfigurationContext), clientIp.value())
      );
      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 IpAddressContextExtractor.IpAddressContextExtractorBuilder builder() {
      return new IpAddressContextExtractor.IpAddressContextExtractorBuilder();
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class IpAddressContextExtractorBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<IpRangeContext> mappings;
      @SuppressFBWarnings(justification = "generated code")
      private String fallbackContext;
      @SuppressFBWarnings(justification = "generated code")
      private GatewayConfig gateway;

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

      @SuppressFBWarnings(justification = "generated code")
      public IpAddressContextExtractor.IpAddressContextExtractorBuilder mappings(List<IpRangeContext> mappings) {
         this.mappings = mappings;
         return this;
      }

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

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

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

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