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

import com.airlock.iam.common.infrastructure.restclient.JerseyRestClient;
import com.airlock.iam.common.infrastructure.restclient.RequestEntityProcessing;
import com.airlock.iam.common.infrastructure.restclient.RestClient;
import com.airlock.iam.common.infrastructure.restclient.RestClientProxySettings;
import com.airlock.iam.common.infrastructure.restclient.RestClientRetryPolicy;
import com.airlock.iam.common.infrastructure.restclient.StaticRestRequestHeaderService;
import com.airlock.iam.core.application.configuration.basicauth.BasicAuthCredentialsConfig;
import com.airlock.iam.plugin.api.annotation.config.Groups;
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.EnumProperty;
import com.airlock.iam.plugin.api.annotation.property.FileProperty;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
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 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.File;
import java.net.URI;
import java.time.Duration;
import java.util.List;

@Plugin(
   description = "Configures an HTTPS REST client. Can be used to issue REST calls from Airlock IAM to REST servers.<p>Note that this REST client does <em>not</em> support cookies.</p>",
   displayName = "REST Client Config"
)
@Groups(groupSequence = {"Basic Settings", "Proxy Settings", "Static Request Header Settings", "Advanced Settings"}, openGroups = "Basic Settings")
public class RestClientConfig {
   static final String PROXY_SETTINGS = "Proxy Settings";
   static final String STATIC_REQUEST_HEADER_SETTINGS = "Static Request Header Settings";
   @StringListProperty(
      displayName = "Server URLs",
      description = "The REST server addresses. These have to be specified without a trailing slash.",
      optional = false,
      exampleElements = "https://my.company.com:8443"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private List<String> serverUrls;
   @PluginProperty(
      description = "The retry policy used to decide if a REST request should be repeated (using the next server URL in the list) depending on its outcome (response or exception).",
      optional = false
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private RestClientRetryPolicy retryPolicy;
   @IntegerProperty(
      displayName = "Connect/Read Timeout [ms]",
      description = "The timeout (in milliseconds) for connection establishment and response receiving when issuing a request to a server. A value of zero is interpreted as 30 seconds.",
      optional = true,
      defaultValue = 10000,
      exampleValues = {0, 1000, 10000},
      minValue = 0
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private int timeout;
   @IntegerProperty(
      description = "The maximum size of the connection pool.",
      optional = true,
      defaultValue = 50,
      exampleValues = {1, 10, 50, 100, 200},
      minValue = 1,
      maxValue = 1000
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private int connectionPoolMaxSize;
   @FileProperty(
      description = "The keystore containing the web server certificate. These certificates are trusted when establishing mutual SSL connections for HTTPS REST calls.",
      optional = true,
      exampleValues = {"truststore.jks", "/etc/airlock/truststore.jks"},
      assureExistence = true,
      assureReadable = true,
      isFile = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private File trustStorePath;
   @StringProperty(
      description = "The password used to check the integrity of the truststore, or to unlock the truststore.",
      optional = true,
      isSensitive = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String trustStorePassword;
   @FileProperty(
      description = "The keystore containing a client certificate (including a private key) for Airlock IAM. The client certificate is used to establish mutual SSL connections for HTTPS REST calls.",
      optional = true,
      exampleValues = {"keystore.jks", "/etc/airlock/keystore.jks"},
      assureExistence = true,
      assureReadable = true,
      isFile = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private File keyStorePath;
   @StringProperty(
      description = "The password used to check the integrity of the keystore, or to unlock the keystore. Must be provided in case the keystore is configured.",
      optional = true,
      isSensitive = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String keyStorePassword;
   @PluginProperty(
      description = "The credentials for HTTP Basic Authentication. If configured, the Basic Authentication header is sent along with each REST call. If not configured, no Basic Authentication header is sent.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private BasicAuthCredentialsConfig basicAuthCredentials;
   @EnumProperty(
      description = "This property specifies how the entity is serialized to the output stream; if buffering is used or the entity is streamed in chunked encoding. <p>The options are:<br><ul><li><b>BUFFERED</b>: the entity will be buffered and the content length will be sent in the Content-Length header.</li><li><b>CHUNKED</b>: chunked encoding will be used and the entity will be streamed.</li></ul></p>",
      optional = true,
      defaultValue = "CHUNKED"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private RequestEntityProcessing requestEntityProcessing;
   @IntegerProperty(
      displayName = "Connection Manager Timeout [ms]",
      description = "The timeout (in milliseconds) to wait for connection from the connection manager/pool (http.connection-manager.timeout). Setting this value to 0 disables the timeout and the thread would be blocked by waiting indefinitely for a new connection until one gets released back to the pool.",
      optional = true,
      defaultValue = 10000,
      exampleValues = {0, 1000, 10000},
      minValue = 0
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int connectionManagerTimeout;
   @StringProperty(
      description = "URI of a HTTP proxy the connector should use. If the port component of the URI is absent then a default port of 8080 is assumed. If this property is left empty then no proxy will be utilized.",
      displayName = "Proxy URI",
      optional = true,
      exampleValues = "proxy.company.com"
   )
   @GroupProperty(groupIdentifier = "Proxy Settings")
   private String proxyUri;
   @StringProperty(description = "Username for the proxy if proxy authentication is used.", optional = true)
   @GroupProperty(groupIdentifier = "Proxy Settings")
   private String proxyLoginUser;
   @StringProperty(description = "Password for the proxy if proxy authentication is used.", optional = true, isSensitive = true)
   @GroupProperty(groupIdentifier = "Proxy Settings")
   private String proxyLoginPassword;
   @PluginListProperty(
      displayName = "Static REST Request Headers",
      description = "A list of static request headers added to each request made with this REST Client.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Static Request Header Settings")
   private List<StaticRestRequestHeaderConfig> staticRestRequestHeaders;
   @EnumProperty(
      displayName = "Static REST Request Header Strategy",
      description = "This property specifies the behaviour when the request already contains a header for a configured header name. If no static request headers are configured, this property has no impact. <p>The options are:<br><ul><li><b>OVERWRITE_EXISTING</b>: if there is already a value for the configured header name it is overwritten with the configured value.</li><li><b>KEEP_ORIGINAL</b>: the already existing header value is kept and the configured value is ignored.</li></ul></p>",
      optional = true,
      defaultValue = "OVERWRITE_EXISTING"
   )
   @GroupProperty(groupIdentifier = "Static Request Header Settings")
   private RestClientConfig.StaticHeaderStrategy staticRestRequestHeaderStrategy;
   private RestClient restClient;

   public RestClientConfig(
      List<String> serverUrls,
      RestClientRetryPolicy retryPolicy,
      int timeout,
      int connectionPoolMaxSize,
      File trustStorePath,
      String trustStorePassword,
      File keyStorePath,
      String keyStorePassword,
      BasicAuthCredentialsConfig basicAuthCredentials,
      RequestEntityProcessing requestEntityProcessing,
      int connectionManagerTimeout,
      String proxyUri,
      String proxyLoginUser,
      String proxyLoginPassword,
      List<StaticRestRequestHeaderConfig> staticRestRequestHeaders,
      RestClientConfig.StaticHeaderStrategy staticRestRequestHeaderStrategy
   ) {
      this.serverUrls = serverUrls;
      this.retryPolicy = retryPolicy;
      this.timeout = timeout;
      this.connectionPoolMaxSize = connectionPoolMaxSize;
      this.trustStorePath = trustStorePath;
      this.trustStorePassword = trustStorePassword;
      this.keyStorePath = keyStorePath;
      this.keyStorePassword = keyStorePassword;
      this.basicAuthCredentials = basicAuthCredentials;
      this.requestEntityProcessing = requestEntityProcessing;
      this.connectionManagerTimeout = connectionManagerTimeout;
      this.proxyUri = proxyUri;
      this.proxyLoginUser = proxyLoginUser;
      this.proxyLoginPassword = proxyLoginPassword;
      this.staticRestRequestHeaders = staticRestRequestHeaders;
      this.staticRestRequestHeaderStrategy = staticRestRequestHeaderStrategy;
   }

   @AfterConfigActivation
   void afterConfigActivation() {
      this.restClient = JerseyRestClient.builder()
         .serverUrls(this.serverUrls)
         .retryPolicy(this.retryPolicy)
         .timeout(Duration.ofMillis(this.timeout))
         .connectionPoolMaxSize(this.connectionPoolMaxSize)
         .keyStorePath(this.keyStorePath)
         .keyStorePassword(this.keyStorePassword)
         .trustStorePath(this.trustStorePath)
         .trustStorePassword(this.trustStorePassword)
         .basicAuthCredentials(this.basicAuthCredentials)
         .staticRestRequestHeaderService(
            this.staticRestRequestHeaders.isEmpty()
               ? null
               : new StaticRestRequestHeaderService(this.staticRestRequestHeaders, this.staticRestRequestHeaderStrategy)
         )
         .proxySettings(this.proxyUri == null ? null : new RestClientProxySettings(URI.create(this.proxyUri), this.proxyLoginUser, this.proxyLoginPassword))
         .requestEntityProcessing(this.requestEntityProcessing)
         .connectionManagerTimeout(Duration.ofMillis(this.connectionManagerTimeout))
         .build();
   }

   public RestClient getRestClient() {
      return this.restClient;
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class RestClientConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<String> serverUrls;
      @SuppressFBWarnings(justification = "generated code")
      private RestClientRetryPolicy retryPolicy;
      @SuppressFBWarnings(justification = "generated code")
      private int timeout;
      @SuppressFBWarnings(justification = "generated code")
      private int connectionPoolMaxSize;
      @SuppressFBWarnings(justification = "generated code")
      private File trustStorePath;
      @SuppressFBWarnings(justification = "generated code")
      private String trustStorePassword;
      @SuppressFBWarnings(justification = "generated code")
      private File keyStorePath;
      @SuppressFBWarnings(justification = "generated code")
      private String keyStorePassword;
      @SuppressFBWarnings(justification = "generated code")
      private BasicAuthCredentialsConfig basicAuthCredentials;
      @SuppressFBWarnings(justification = "generated code")
      private RequestEntityProcessing requestEntityProcessing;
      @SuppressFBWarnings(justification = "generated code")
      private int connectionManagerTimeout;
      @SuppressFBWarnings(justification = "generated code")
      private String proxyUri;
      @SuppressFBWarnings(justification = "generated code")
      private String proxyLoginUser;
      @SuppressFBWarnings(justification = "generated code")
      private String proxyLoginPassword;
      @SuppressFBWarnings(justification = "generated code")
      private List<StaticRestRequestHeaderConfig> staticRestRequestHeaders;
      @SuppressFBWarnings(justification = "generated code")
      private RestClientConfig.StaticHeaderStrategy staticRestRequestHeaderStrategy;

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

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder serverUrls(List<String> serverUrls) {
         this.serverUrls = serverUrls;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder retryPolicy(RestClientRetryPolicy retryPolicy) {
         this.retryPolicy = retryPolicy;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder timeout(int timeout) {
         this.timeout = timeout;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder connectionPoolMaxSize(int connectionPoolMaxSize) {
         this.connectionPoolMaxSize = connectionPoolMaxSize;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder trustStorePath(File trustStorePath) {
         this.trustStorePath = trustStorePath;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder trustStorePassword(String trustStorePassword) {
         this.trustStorePassword = trustStorePassword;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder keyStorePath(File keyStorePath) {
         this.keyStorePath = keyStorePath;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder keyStorePassword(String keyStorePassword) {
         this.keyStorePassword = keyStorePassword;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder basicAuthCredentials(BasicAuthCredentialsConfig basicAuthCredentials) {
         this.basicAuthCredentials = basicAuthCredentials;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder requestEntityProcessing(RequestEntityProcessing requestEntityProcessing) {
         this.requestEntityProcessing = requestEntityProcessing;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder connectionManagerTimeout(int connectionManagerTimeout) {
         this.connectionManagerTimeout = connectionManagerTimeout;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder proxyUri(String proxyUri) {
         this.proxyUri = proxyUri;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder proxyLoginUser(String proxyLoginUser) {
         this.proxyLoginUser = proxyLoginUser;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder proxyLoginPassword(String proxyLoginPassword) {
         this.proxyLoginPassword = proxyLoginPassword;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder staticRestRequestHeaders(List<StaticRestRequestHeaderConfig> staticRestRequestHeaders) {
         this.staticRestRequestHeaders = staticRestRequestHeaders;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig.RestClientConfigBuilder staticRestRequestHeaderStrategy(RestClientConfig.StaticHeaderStrategy staticRestRequestHeaderStrategy) {
         this.staticRestRequestHeaderStrategy = staticRestRequestHeaderStrategy;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RestClientConfig build() {
         return new RestClientConfig(
            this.serverUrls,
            this.retryPolicy,
            this.timeout,
            this.connectionPoolMaxSize,
            this.trustStorePath,
            this.trustStorePassword,
            this.keyStorePath,
            this.keyStorePassword,
            this.basicAuthCredentials,
            this.requestEntityProcessing,
            this.connectionManagerTimeout,
            this.proxyUri,
            this.proxyLoginUser,
            this.proxyLoginPassword,
            this.staticRestRequestHeaders,
            this.staticRestRequestHeaderStrategy
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "RestClientConfig.RestClientConfigBuilder(serverUrls="
            + this.serverUrls
            + ", retryPolicy="
            + this.retryPolicy
            + ", timeout="
            + this.timeout
            + ", connectionPoolMaxSize="
            + this.connectionPoolMaxSize
            + ", trustStorePath="
            + this.trustStorePath
            + ", trustStorePassword="
            + this.trustStorePassword
            + ", keyStorePath="
            + this.keyStorePath
            + ", keyStorePassword="
            + this.keyStorePassword
            + ", basicAuthCredentials="
            + this.basicAuthCredentials
            + ", requestEntityProcessing="
            + this.requestEntityProcessing
            + ", connectionManagerTimeout="
            + this.connectionManagerTimeout
            + ", proxyUri="
            + this.proxyUri
            + ", proxyLoginUser="
            + this.proxyLoginUser
            + ", proxyLoginPassword="
            + this.proxyLoginPassword
            + ", staticRestRequestHeaders="
            + this.staticRestRequestHeaders
            + ", staticRestRequestHeaderStrategy="
            + this.staticRestRequestHeaderStrategy
            + ")";
      }
   }

   public enum StaticHeaderStrategy {
      KEEP_ORIGINAL,
      OVERWRITE_EXISTING;
   }
}
