package com.airlock.iam.oauth2.application.configuration.tokenexchange;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.common.domain.service.jwt.signature.HttpClientRemoteJWKSetLoader;
import com.airlock.iam.common.domain.service.jwt.signature.RemoteJWKSetLoader;
import com.airlock.iam.core.domain.model.http.client.HttpClient;
import com.airlock.iam.core.misc.util.httpclient.HttpClientConfig;
import com.airlock.iam.oauth2.application.configuration.OAuth2IssuerIdConfig;
import com.airlock.iam.oauth2.domain.service.tokenexchange.HttpClientRemoteDiscoveryLoader;
import com.airlock.iam.oauth2.domain.service.tokenexchange.RemoteDiscoveryLoader;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.license.PluginLicenseRestriction;
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.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.util.List;

@Plugin(
   description = "<p>Subject token validation based on OpenID Connect discovery.</p>\n<p>The subject token \"iss\" claim will be checked against the list of allowed token issuers and used to determine\nthe OIDC discovery endpoint (see the <a href=\"https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig\"\ntarget=\"_blank\">OIDC discovery specifications</a>). The JWKS keys obtained through OIDC discovery at that\nURL will then be used for the subject token signature validation.</p>",
   displayName = "OIDC Discovery Subject Token Validation"
)
@PluginLicenseRestriction(moduleAffiliation = "OAuthTokenExchange")
public final class OAuth2OIDCDiscoverySubjectTokenValidationConfig implements OAuth2SubjectTokenValidationConfig {
   @PluginListProperty(description = "Only tokens issued by these issuers can be exchanged at the endpoint.", optional = false, minElements = 1)
   private List<OAuth2IssuerIdConfig> allowedTokenIssuers;
   @PluginProperty(
      description = "The HTTP client used to fetch the JWKS data.",
      optional = true,
      defaultValue = HttpClientConfig.class,
      displayName = "HTTP Client"
   )
   private HttpClientConfig httpClient;
   @IntegerProperty(
      description = "Time in minutes after which cached data from the discovery endpoint URL and the JWKS URL 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 = {60, 1440, 2880, 10080, 302400},
      minValue = 1,
      optional = true,
      defaultValue = 2880
   )
   private int cacheRefreshTimeInMinutes;
   private RemoteJWKSetLoader remoteJwksLoader;
   private RemoteDiscoveryLoader remoteDiscoveryLoader;

   public OAuth2OIDCDiscoverySubjectTokenValidationConfig(
      List<OAuth2IssuerIdConfig> allowedTokenIssuers, int cacheRefreshTimeInMinutes, HttpClientConfig httpClient
   ) {
      this.allowedTokenIssuers = allowedTokenIssuers;
      this.cacheRefreshTimeInMinutes = cacheRefreshTimeInMinutes;
      this.httpClient = httpClient;
   }

   @AfterConfigActivation
   void initializeJwksLoader() {
      try {
         HttpClient httpClientImpl = this.httpClient.createHttpClientForDomainClasses();
         this.remoteJwksLoader = new HttpClientRemoteJWKSetLoader(httpClientImpl, Duration.ofMinutes(this.cacheRefreshTimeInMinutes));
         this.remoteDiscoveryLoader = new HttpClientRemoteDiscoveryLoader(httpClientImpl, Duration.ofMinutes(this.cacheRefreshTimeInMinutes));
      } catch (GeneralSecurityException | IOException e) {
         throw new FailureException("Could not initialize HTTP client: " + e.getMessage(), e);
      }
   }

   public List<OAuth2IssuerIdConfig> getAllowedTokenIssuers() {
      return this.allowedTokenIssuers;
   }

   public HttpClientConfig getHttpClient() {
      return this.httpClient;
   }

   public RemoteJWKSetLoader getRemoteJwksLoader() {
      return this.remoteJwksLoader;
   }

   public RemoteDiscoveryLoader getRemoteDiscoveryLoader() {
      return this.remoteDiscoveryLoader;
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2OIDCDiscoverySubjectTokenValidationConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<OAuth2IssuerIdConfig> allowedTokenIssuers;
      @SuppressFBWarnings(justification = "generated code")
      private int cacheRefreshTimeInMinutes;
      @SuppressFBWarnings(justification = "generated code")
      private HttpClientConfig httpClient;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2OIDCDiscoverySubjectTokenValidationConfig.OAuth2OIDCDiscoverySubjectTokenValidationConfigBuilder allowedTokenIssuers(
         List<OAuth2IssuerIdConfig> allowedTokenIssuers
      ) {
         this.allowedTokenIssuers = allowedTokenIssuers;
         return this;
      }

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2OIDCDiscoverySubjectTokenValidationConfig.OAuth2OIDCDiscoverySubjectTokenValidationConfigBuilder(allowedTokenIssuers="
            + this.allowedTokenIssuers
            + ", cacheRefreshTimeInMinutes="
            + this.cacheRefreshTimeInMinutes
            + ", httpClient="
            + this.httpClient
            + ")";
      }
   }
}
