package com.airlock.iam.factor.application.configuration.airlock2fa;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.common.application.configuration.restclient.NoRetryPolicyConfig;
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.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.factor.domain.service.airlock2fa.FuturaeServerDummyPropertyWarner;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
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.license.PluginLicenseRestriction;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
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.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 com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.plugin.framework.application.service.builder.plugin.PluginPropertyBuilderInitializer;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.List;
import java.util.Optional;

@Plugin(description = "Settings to configure access to Futurae servers.", displayName = "Futurae Server")
@PluginLicenseRestriction(moduleAffiliation = "Airlock2FA")
@Groups(groupSequence = {"Basic Settings", "Advanced Settings", "Trust and Keystore Settings", "Proxy Settings"}, openGroups = "Basic Settings")
public final class FuturaeServerConfig {
   public static final String DISPLAY_NAME = "Futurae Server";
   static final String PROXY_SETTINGS = "Proxy Settings";
   static final String TRUST_AND_KEYSTORE_SETTINGS = "Trust and Keystore Settings";
   private static final ELogger LOG = ELoggerFactory.getLogger(FuturaeServerConfig.class);
   private static final NoRetryPolicyConfig NO_RETRY_POLICY = createNoRetryPolicy();
   private static final String CSP_DESCRIPTION = "<p><b>Note:</b> When using a custom URL and a CSP, add exceptions for img-src and connect-src directives with the configured URL to it.</p>";
   @StringProperty(
      description = "The service ID of the client.<p>The value is of the form 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.</p>",
      optional = false,
      displayName = "Service ID"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String serviceId;
   public static final String SERVICE_ID_DISPLAY_NAME = "Service ID";
   @StringProperty(description = "The API key for the Futurae Auth API.", optional = false, isSensitive = true, displayName = "Auth API Key")
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String authApiKey;
   public static final String AUTH_API_KEY_DISPLAY_NAME = "Auth API Key";
   @StringProperty(
      description = "The API key for the Futurae Admin API.<p>The Admin API key is <strong>different</strong> than the Auth API key, which is used to access the Auth API.</p>",
      optional = false,
      isSensitive = true,
      displayName = "Admin API Key"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String adminApiKey;
   public static final String ADMIN_API_KEY_DISPLAY_NAME = "Admin API Key";
   @StringProperty(
      displayName = "Back-End Server URL",
      description = "The back-end URL of the Futurae server (without path or trailing slash).<p>This URL is used by the back-end of IAM to connect to the Futurae API.</p><p><b>Note:</b> When using a custom URL and a CSP, add exceptions for img-src and connect-src directives with the configured URL to it.</p>",
      optional = true,
      validationRegex = "https?:\\/\\/[^\\/]+",
      defaultValue = "https://api.futurae.com"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String backEndUrl;
   @StringProperty(
      displayName = "Front-End Server URL",
      description = "The front-end URL of the Futurae server (without path or trailing slash).<p>This URL is used by the browser of the end user to access the Futurae API via the internet.</p><p><b>Note:</b> When using a custom URL and a CSP, add exceptions for img-src and connect-src directives with the configured URL to it.</p>",
      optional = true,
      validationRegex = "https?:\\/\\/[^\\/]+",
      defaultValue = "https://api.futurae.com"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String frontEndUrl;
   @IntegerProperty(
      description = "The timeout (in milliseconds) that is used for requests to the Futurae server. The setting affects both the connection establishment and response reception. A value of zero is interpreted as 30 seconds.",
      displayName = "Connect/Read Timeout [ms]",
      optional = true,
      defaultValue = 10000,
      exampleValues = {0, 1000, 5000, 10000},
      minValue = 0,
      maxValue = 30000
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int callTimeoutMs;
   @IntegerProperty(
      description = "The maximum size of the connection pool that is used for requests to the Futurae server.",
      displayName = "Connection Pool Max Size",
      optional = true,
      defaultValue = 50,
      exampleValues = {10, 50, 100, 200},
      minValue = 1,
      maxValue = 1000
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int connectionPoolMaxSize;
   @FileProperty(
      description = "Keystore file name containing trusted certificate issuers (and trusted certificates).\n<p>If this property is <em>not defined</em> the following certificate issuers are trusted:\n<ul>\n <li>The list of issuers known to the Java VM if the system property \"javax.net.ssl.trustStore\" is not defined.</li>\n <li>The list of issuers in a keystore referenced by system property \"javax.net.ssl.trustStore\" if defined in instance.properties using iam.java.opts</li>\n</ul></p>\n<p>If this property <em>is defined</em> then the following certificate issuers are trusted:\n<ul>\n <li>The list of issuers in the referenced truststore file and no others.</li>\n</ul></p><p>In any case, IAM will only establish an HTTPS connection to the Futurae server or its proxy if the server certificate issuer is trusted or the certificate itself is trusted according to the aforementioned definition of trust.</p>",
      optional = true,
      exampleValues = {"truststore.jks", "/etc/airlock/truststore.jks"},
      assureExistence = true,
      assureReadable = true,
      isFile = true
   )
   @GroupProperty(groupIdentifier = "Trust and Keystore 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 = "Trust and Keystore 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 = "Trust and Keystore 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 = "Trust and Keystore Settings")
   private String keyStorePassword;
   @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 = "https://proxy.company.com"
   )
   @GroupProperty(groupIdentifier = "Proxy Settings")
   private String proxyUri;
   private static final String FIELD_PROXY_URI = "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;
   private RestClient restClient;

   public FuturaeServerConfig(
      String serviceId,
      String authApiKey,
      String adminApiKey,
      String backEndUrl,
      String frontEndUrl,
      int callTimeoutMs,
      int connectionPoolMaxSize,
      File trustStorePath,
      String trustStorePassword,
      File keyStorePath,
      String keyStorePassword,
      String proxyUri,
      String proxyLoginUser,
      String proxyLoginPassword
   ) {
      this.serviceId = serviceId;
      this.authApiKey = authApiKey;
      this.adminApiKey = adminApiKey;
      this.backEndUrl = backEndUrl;
      this.frontEndUrl = frontEndUrl;
      this.callTimeoutMs = callTimeoutMs;
      this.connectionPoolMaxSize = connectionPoolMaxSize;
      this.trustStorePath = trustStorePath;
      this.trustStorePassword = trustStorePassword;
      this.keyStorePath = keyStorePath;
      this.keyStorePassword = keyStorePassword;
      this.proxyUri = proxyUri;
      this.proxyLoginUser = proxyLoginUser;
      this.proxyLoginPassword = proxyLoginPassword;
   }

   public URI getServerURI() {
      try {
         return new URI(this.backEndUrl);
      } catch (URISyntaxException e) {
         throw new FailureException("This should not happen as configuration validator should prevent this!", e);
      }
   }

   @AfterConfigActivation
   void afterConfigActivation() {
      FuturaeServerDummyPropertyWarner.warnAboutDummies(this, LOG);
      this.restClient = this.createJerseyRestClientBuilder()
         .timeout(Duration.ofMillis(this.callTimeoutMs))
         .connectionPoolMaxSize(this.connectionPoolMaxSize)
         .build();
   }

   @ConfigurationValidator
   static void checkProxyUri(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      Optional<URISyntaxException> exception = Optional.ofNullable(StringPropertyValueReader.effectiveStringConfigValue(config, "proxyUri", env, context))
         .flatMap(FuturaeServerConfig::uriSyntaxError);
      if (exception.isPresent()) {
         throw new PropertyConstraintViolationException("proxyUri", config, env, context, "Invalid URI: " + exception.get().getMessage());
      }
   }

   public String getServiceId() {
      return this.serviceId;
   }

   public String getAuthApiKey() {
      return this.authApiKey;
   }

   public String getAdminApiKey() {
      return this.adminApiKey;
   }

   public String getBackEndUrl() {
      return this.backEndUrl;
   }

   public String getFrontEndUrl() {
      return this.frontEndUrl;
   }

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

   private static Optional<URISyntaxException> uriSyntaxError(String uri) {
      try {
         new URI(uri);
         return Optional.empty();
      } catch (URISyntaxException e) {
         return Optional.of(e);
      }
   }

   private JerseyRestClient.JerseyRestClientBuilder createJerseyRestClientBuilder() {
      return JerseyRestClient.builder()
         .serverUrls(List.of(this.backEndUrl))
         .retryPolicy(NO_RETRY_POLICY)
         .keyStorePath(this.keyStorePath)
         .keyStorePassword(this.keyStorePassword)
         .trustStorePath(this.trustStorePath)
         .trustStorePassword(this.trustStorePassword)
         .proxySettings(this.proxyUri == null ? null : new RestClientProxySettings(URI.create(this.proxyUri), this.proxyLoginUser, this.proxyLoginPassword))
         .requestEntityProcessing(RequestEntityProcessing.CHUNKED);
   }

   private static NoRetryPolicyConfig createNoRetryPolicy() {
      return ((NoRetryPolicyConfig.NoRetryPolicyConfigBuilder)PluginPropertyBuilderInitializer.builder(NoRetryPolicyConfig.NoRetryPolicyConfigBuilder.class))
         .build();
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class FuturaeServerConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String serviceId;
      @SuppressFBWarnings(justification = "generated code")
      private String authApiKey;
      @SuppressFBWarnings(justification = "generated code")
      private String adminApiKey;
      @SuppressFBWarnings(justification = "generated code")
      private String backEndUrl;
      @SuppressFBWarnings(justification = "generated code")
      private String frontEndUrl;
      @SuppressFBWarnings(justification = "generated code")
      private int callTimeoutMs;
      @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 String proxyUri;
      @SuppressFBWarnings(justification = "generated code")
      private String proxyLoginUser;
      @SuppressFBWarnings(justification = "generated code")
      private String proxyLoginPassword;

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

      @SuppressFBWarnings(justification = "generated code")
      public FuturaeServerConfig.FuturaeServerConfigBuilder serviceId(String serviceId) {
         this.serviceId = serviceId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FuturaeServerConfig.FuturaeServerConfigBuilder authApiKey(String authApiKey) {
         this.authApiKey = authApiKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FuturaeServerConfig.FuturaeServerConfigBuilder adminApiKey(String adminApiKey) {
         this.adminApiKey = adminApiKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FuturaeServerConfig.FuturaeServerConfigBuilder backEndUrl(String backEndUrl) {
         this.backEndUrl = backEndUrl;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FuturaeServerConfig.FuturaeServerConfigBuilder frontEndUrl(String frontEndUrl) {
         this.frontEndUrl = frontEndUrl;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FuturaeServerConfig.FuturaeServerConfigBuilder callTimeoutMs(int callTimeoutMs) {
         this.callTimeoutMs = callTimeoutMs;
         return this;
      }

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

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

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

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

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

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public FuturaeServerConfig build() {
         return new FuturaeServerConfig(
            this.serviceId,
            this.authApiKey,
            this.adminApiKey,
            this.backEndUrl,
            this.frontEndUrl,
            this.callTimeoutMs,
            this.connectionPoolMaxSize,
            this.trustStorePath,
            this.trustStorePassword,
            this.keyStorePath,
            this.keyStorePassword,
            this.proxyUri,
            this.proxyLoginUser,
            this.proxyLoginPassword
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "FuturaeServerConfig.FuturaeServerConfigBuilder(serviceId="
            + this.serviceId
            + ", authApiKey="
            + this.authApiKey
            + ", adminApiKey="
            + this.adminApiKey
            + ", backEndUrl="
            + this.backEndUrl
            + ", frontEndUrl="
            + this.frontEndUrl
            + ", callTimeoutMs="
            + this.callTimeoutMs
            + ", connectionPoolMaxSize="
            + this.connectionPoolMaxSize
            + ", trustStorePath="
            + this.trustStorePath
            + ", trustStorePassword="
            + this.trustStorePassword
            + ", keyStorePath="
            + this.keyStorePath
            + ", keyStorePassword="
            + this.keyStorePassword
            + ", proxyUri="
            + this.proxyUri
            + ", proxyLoginUser="
            + this.proxyLoginUser
            + ", proxyLoginPassword="
            + this.proxyLoginPassword
            + ")";
      }
   }
}
