package com.airlock.iam.common.application.configuration.jwt.signature;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.service.jwt.jwks.JwksSignatureValidator;
import com.airlock.iam.common.domain.service.jwt.signature.HttpClientRemoteJWKSetLoader;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifier;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifierException;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifierImpl;
import com.airlock.iam.common.domain.service.jwt.signature.RemoteJWKSetLoader;
import com.airlock.iam.core.misc.plugin.config.validation.UriPropertyValidator;
import com.airlock.iam.core.misc.util.httpclient.HttpClientConfig;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
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 com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.util.Objects;

@Plugin(
   description = "<p>JWT Signature verification based on fetching key information from a URL providing a JWKS\n(see <a href=\"https://www.rfc-editor.org/rfc/rfc7517\" target=\"_blank\">RFC7517</a>).</p>\n",
   displayName = "JWKS Ticket Verifier Settings"
)
public final class JwksSignatureVerifierSettings implements JwtSignatureVerifier {
   @StringProperty(
      description = "<p>\nThe URL providing the JWKS. Must be an absolute URL with \"https\" scheme.\n</p>",
      optional = false,
      displayName = "JWKS URL"
   )
   private String url;
   private static final String JWKS_URL_FIELD = "url";
   private static final String JWKS_URL_DISPLAY_NAME = "JWKS URL";
   @PluginProperty(description = "<p>\nThe HTTP Client used to fetch the JWKS data\n</p>", optional = false, displayName = "HTTP Client")
   private HttpClientConfig httpClient;
   @IntegerProperty(
      description = "Time in minutes after which cached data from the JWKS endpoint is refreshed.\n<p>The refresh is asynchronous: the first request after this time still receives the previously cached data and triggers a reload in the background.</p>\n<p>Subsequent requests receive the refreshed data.</p>\n<p>Data not requested for twice this time expires and is reloaded synchronously on the next request, so served data is never older than twice this time.</p>\n<p>The JWKS data is additionally reloaded if a key is not yet known, or if a signature check with a known key fails.</p>\n<p>If a refresh fails, the previously fetched data is kept while the entry is still in use.</p>",
      displayName = "Cache Refresh Time [minutes]",
      exampleValues = {10, 60, 120},
      minValue = 1,
      optional = true,
      defaultValue = 10
   )
   private int cacheRefreshTimeInMinutes;
   private JwksSignatureValidator jwksSignatureValidator;

   public JwksSignatureValidator getJwksSignatureValidator() {
      return this.jwksSignatureValidator;
   }

   public JwksSignatureVerifierSettings(String url, HttpClientConfig httpClient, int cacheRefreshTimeInMinutes) {
      this.url = url;
      this.httpClient = httpClient;
      this.cacheRefreshTimeInMinutes = cacheRefreshTimeInMinutes;
   }

   @AfterConfigActivation
   void afterConfigActivation() {
      this.jwksSignatureValidator = new JwksSignatureValidator(this.getRemoteJwksLoader(), URI.create(this.url));
   }

   @ConfigurationValidator
   static void validateJwksUrl(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      UriPropertyValidator.uriPropertyValidator("url", "JWKS URL", config, env, context).validateIsAbsolute().validateHasHttpsScheme();
   }

   @Override
   public boolean isSignatureValid(JsonWebSignatureToken signedJwt) throws JwtSignatureVerifierException {
      return JwtSignatureVerifierImpl.jwksBuilder()
         .validator(Objects.requireNonNull(this.jwksSignatureValidator, "AfterConfigActivation has not been called"))
         .build()
         .isSignatureValid(signedJwt);
   }

   private RemoteJWKSetLoader getRemoteJwksLoader() {
      try {
         return new HttpClientRemoteJWKSetLoader(this.httpClient.createHttpClientForDomainClasses(), Duration.ofMinutes(this.cacheRefreshTimeInMinutes));
      } catch (GeneralSecurityException | IOException e) {
         throw new FailureException("Failed to initialize OAuth 2.0 HTTP client.", e);
      }
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class JwksSignatureVerifierSettingsBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String url;
      @SuppressFBWarnings(justification = "generated code")
      private HttpClientConfig httpClient;
      @SuppressFBWarnings(justification = "generated code")
      private int cacheRefreshTimeInMinutes;

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

      @SuppressFBWarnings(justification = "generated code")
      public JwksSignatureVerifierSettings.JwksSignatureVerifierSettingsBuilder url(String url) {
         this.url = url;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwksSignatureVerifierSettings.JwksSignatureVerifierSettingsBuilder httpClient(HttpClientConfig httpClient) {
         this.httpClient = httpClient;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwksSignatureVerifierSettings.JwksSignatureVerifierSettingsBuilder cacheRefreshTimeInMinutes(int cacheRefreshTimeInMinutes) {
         this.cacheRefreshTimeInMinutes = cacheRefreshTimeInMinutes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwksSignatureVerifierSettings build() {
         return new JwksSignatureVerifierSettings(this.url, this.httpClient, this.cacheRefreshTimeInMinutes);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "JwksSignatureVerifierSettings.JwksSignatureVerifierSettingsBuilder(url="
            + this.url
            + ", httpClient="
            + this.httpClient
            + ", cacheRefreshTimeInMinutes="
            + this.cacheRefreshTimeInMinutes
            + ")";
      }
   }
}
