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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.common.api.application.configuration.valueprovider.StringValueProviderConfig;
import com.airlock.iam.core.domain.service.OAuth2ClientCredentialsGrantAccessTokenLoader;
import com.airlock.iam.core.misc.impl.email.ClientCredentialsGrantTokenProvider;
import com.airlock.iam.core.misc.util.httpclient.HttpClientConfig;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.application.configuration.client.OAuth2PostRequestBuilderConfig;
import com.airlock.iam.oauth2.application.configuration.client.OAuth2RequestBuilderConfig;
import com.airlock.iam.oauth2.application.configuration.clientauthentication.OAuth2BasicAuthClientSecretConfig;
import com.airlock.iam.oauth2.domain.model.client.OAuth2ClientSideCredentials;
import com.airlock.iam.oauth2.domain.model.client.communication.OAuth2TokenRequestClientSecretProcessor;
import com.airlock.iam.oauth2.infrastructure.repository.HttpClientOAuth2ClientCredentialsGrantAccessTokenLoader;
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.LongProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.StringListProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
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.List;
import java.util.Optional;

@Plugin(
   description = "Retrieves, caches and provides an access token.\n\tWhen this provider is used for the first time, an access token is obtained from the token endpoint using the client credentials grant and then cached. \n\tFor subsequent requests, the token is retrieved from the cache until it expires.",
   displayName = "OAuth2 Access Token String Value Provider"
)
public final class OAuth2AccessTokenStringValueProviderConfig implements StringValueProviderConfig, ClientCredentialsGrantTokenProvider {
   @StringProperty(
      description = "Client ID identifying Airlock IAM at the authorization / token and resource endpoint of the OAuth 2.0 provider.<br/>Only alphanumeric characters and '-_.' are allowed.",
      exampleValues = {"example-app", "crypticyButUniqueAppId01953utjhu91823rih"},
      displayName = "Client ID",
      optional = false,
      validationRegex = "[a-zA-Z0-9-_.]+"
   )
   private String clientId;
   @StringProperty(description = "Client secret used to verify the client.", isSensitive = true, optional = false)
   private String clientSecret;
   @PluginProperty(description = "HTTP method to use for Access Token requests.", optional = true, defaultValue = OAuth2PostRequestBuilderConfig.class)
   private OAuth2RequestBuilderConfig accessTokenRequestMethod;
   @PluginProperty(description = "HTTP client used for token endpoint requests.", displayName = "HTTP Client", optional = false)
   private HttpClientConfig httpClient;
   @PluginProperty(
      description = "Specifies how the client secret is included in requests to the token endpoint.<p/><a target=\"_blank\" href=\"https://tools.ietf.org/html/rfc6749\">RFC 6749</a> suggests to use the HTTP Basic authentication scheme ('OAuth 2.0 Basic Auth Client Secret').",
      optional = true,
      defaultValue = OAuth2BasicAuthClientSecretConfig.class,
      displayName = "Token Endpoint Authentication"
   )
   private OAuth2TokenRequestClientSecretProcessor tokenEndpointAuthentication;
   @StringProperty(
      description = "Token endpoint URL to get Access Tokens.",
      exampleValues = {
            "https://airlock.com/auth/rest/oauth2/authorization-servers/asId/token",
            "https://accounts.google.com/o/oauth2/token",
            "https://login.live.com/oauth20_token.srf",
            "https://login.microsoftonline.com/tenantid/oauth2/v2.0/token"
      },
      optional = false
   )
   private String tokenEndpointURL;
   @StringListProperty(
      description = "Scopes to request from the token endpoint.<p>Scopes may only contain the following characters: <code>0-9, A-Z, a-z, !, #, $, %, &, ', (, ), *, +, ',', -, ., /, :, ;, &lt;, &gt;, =, ?, @, [, ], ^, _, `, {, }, |, ~</code></p>",
      validationRegex = "[\\x21\\x23-\\x5B\\x5D-\\x7E]+",
      exampleElements = {"userinfo", "https://outlook.office365.com/.default"},
      optional = true
   )
   private List<String> scopesToRequest;
   @LongProperty(
      description = "Defines the duration in seconds an access token should be reused from the cache before it is refreshed.\nWhen configured, this value should be lower than the access token's expiry as otherwise, expired access tokens could be returned.\nIf no specific Time to Live is configured, the IAM will use the access token's <code>expires_in</code> value minus 5 seconds.\nThis ensures that the token remains valid for at least five more seconds when it's retrieved from the cache.",
      displayName = "Access Token Time To Live [s]",
      minValue = 1L,
      optional = true
   )
   private Long timeToLive;
   private OAuth2ClientCredentialsGrantAccessTokenLoader remoteAccessTokenLoader;

   public OAuth2ClientSideCredentials getClientCredentials() {
      return new OAuth2ClientSideCredentials(OAuth2ClientId.oAuth2ClientId(this.clientId), Optional.of(this.clientSecret));
   }

   @AfterConfigActivation
   void initRemoteAccessTokenLoader() {
      try {
         this.remoteAccessTokenLoader = HttpClientOAuth2ClientCredentialsGrantAccessTokenLoader.builder()
            .clientCredentials(this.getClientCredentials())
            .tokenEndpointAuthentication(this.tokenEndpointAuthentication)
            .accessTokenRequestMethod(this.accessTokenRequestMethod.logic())
            .httpClient(this.httpClient.createHttpClientForDomainClasses())
            .scopesToRequest(this.scopesToRequest)
            .expiresIn(this.timeToLive != null ? Duration.ofSeconds(this.timeToLive) : null)
            .build();
      } catch (GeneralSecurityException | IOException e) {
         throw new FailureException("Failed to initialize OAuth 2.0 HTTP client:" + e.getMessage(), e);
      }
   }

   @Override
   public OAuth2ClientCredentialsGrantAccessTokenLoader getRemoteAccessTokenLoader() {
      return this.remoteAccessTokenLoader;
   }

   @Override
   public URI getTokenEndpointURL() {
      return URI.create(this.tokenEndpointURL);
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public OAuth2AccessTokenStringValueProviderConfig(
      String clientId,
      String clientSecret,
      OAuth2RequestBuilderConfig accessTokenRequestMethod,
      HttpClientConfig httpClient,
      OAuth2TokenRequestClientSecretProcessor tokenEndpointAuthentication,
      String tokenEndpointURL,
      List<String> scopesToRequest,
      Long timeToLive,
      OAuth2ClientCredentialsGrantAccessTokenLoader remoteAccessTokenLoader
   ) {
      this.clientId = clientId;
      this.clientSecret = clientSecret;
      this.accessTokenRequestMethod = accessTokenRequestMethod;
      this.httpClient = httpClient;
      this.tokenEndpointAuthentication = tokenEndpointAuthentication;
      this.tokenEndpointURL = tokenEndpointURL;
      this.scopesToRequest = scopesToRequest;
      this.timeToLive = timeToLive;
      this.remoteAccessTokenLoader = remoteAccessTokenLoader;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2AccessTokenStringValueProviderConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String clientId;
      @SuppressFBWarnings(justification = "generated code")
      private String clientSecret;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2RequestBuilderConfig accessTokenRequestMethod;
      @SuppressFBWarnings(justification = "generated code")
      private HttpClientConfig httpClient;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2TokenRequestClientSecretProcessor tokenEndpointAuthentication;
      @SuppressFBWarnings(justification = "generated code")
      private String tokenEndpointURL;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> scopesToRequest;
      @SuppressFBWarnings(justification = "generated code")
      private Long timeToLive;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2ClientCredentialsGrantAccessTokenLoader remoteAccessTokenLoader;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder clientId(String clientId) {
         this.clientId = clientId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder clientSecret(String clientSecret) {
         this.clientSecret = clientSecret;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder accessTokenRequestMethod(
         OAuth2RequestBuilderConfig accessTokenRequestMethod
      ) {
         this.accessTokenRequestMethod = accessTokenRequestMethod;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder tokenEndpointAuthentication(
         OAuth2TokenRequestClientSecretProcessor tokenEndpointAuthentication
      ) {
         this.tokenEndpointAuthentication = tokenEndpointAuthentication;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder tokenEndpointURL(String tokenEndpointURL) {
         this.tokenEndpointURL = tokenEndpointURL;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder scopesToRequest(List<String> scopesToRequest) {
         this.scopesToRequest = scopesToRequest;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder timeToLive(Long timeToLive) {
         this.timeToLive = timeToLive;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder remoteAccessTokenLoader(
         OAuth2ClientCredentialsGrantAccessTokenLoader remoteAccessTokenLoader
      ) {
         this.remoteAccessTokenLoader = remoteAccessTokenLoader;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AccessTokenStringValueProviderConfig build() {
         return new OAuth2AccessTokenStringValueProviderConfig(
            this.clientId,
            this.clientSecret,
            this.accessTokenRequestMethod,
            this.httpClient,
            this.tokenEndpointAuthentication,
            this.tokenEndpointURL,
            this.scopesToRequest,
            this.timeToLive,
            this.remoteAccessTokenLoader
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2AccessTokenStringValueProviderConfig.OAuth2AccessTokenStringValueProviderConfigBuilder(clientId="
            + this.clientId
            + ", clientSecret="
            + this.clientSecret
            + ", accessTokenRequestMethod="
            + this.accessTokenRequestMethod
            + ", httpClient="
            + this.httpClient
            + ", tokenEndpointAuthentication="
            + this.tokenEndpointAuthentication
            + ", tokenEndpointURL="
            + this.tokenEndpointURL
            + ", scopesToRequest="
            + this.scopesToRequest
            + ", timeToLive="
            + this.timeToLive
            + ", remoteAccessTokenLoader="
            + this.remoteAccessTokenLoader
            + ")";
      }
   }
}
