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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.core.misc.util.httpclient.ConnectionTestlet;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
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.property.BooleanProperty;
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.annotation.property.TestletProperty;
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.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletResult;
import com.airlock.iam.plugin.api.valuereader.FilePropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.IntegerPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.RedisConnectionException;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.redisson.config.SslVerificationMode;
import org.redisson.misc.RedisURI;

@Plugin(description = "<p>State repository that stores all values in a single Redis instance.</p>")
@Groups(groupSequence = {"Basic Settings", "TLS Settings", "Performance Tuning", "Advanced Settings"}, openGroups = "Basic Settings")
public final class SingleModeRedisStateRepositoryConfig extends AbstractRedisStateRepositoryConfig {
   private static final ELogger LOG = ELoggerFactory.getLogger(SingleModeRedisStateRepositoryConfig.class);
   static final String TRUST_AND_KEYSTORE_SETTINGS = "TLS Settings";
   static final String PERFORMANCE_TUNING = "Performance Tuning";
   @StringProperty(
      description = "The URI to connect to Redis.<ul><li>For TLS, use rediss://host:port (double 's' in 'rediss:', <b>recommended</b>)</li><li>For an unencrypted connection, use redis://host:port (not recommended for production setups)</li></ul>",
      exampleValues = {"rediss://redis:6379", "redis://localhost:6379"},
      optional = false,
      displayName = "Redis URI"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   @TestletProperty(buttonToolTip = "Test TCP connection", testletClass = SingleModeRedisStateRepositoryConfig.RedisTestlet.class)
   private String redisUri;
   static final String REDIS_URI_PROPERTY = "redisUri";
   static final String REDIS_URI_DISPLAY_NAME = "Redis URI";
   @StringProperty(description = "The username to login on Redis.", optional = true)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String username;
   @StringProperty(description = "The password to login on Redis.", optional = true, isSensitive = true)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String password;
   @IntegerProperty(
      description = "The minimum amount of idle Redis connections.",
      displayName = "Minimum Idle Connections",
      defaultValue = 24,
      minValue = 0,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Performance Tuning")
   private Integer minimumIdleConnections;
   private static final String MINIMUM_IDLE_CONNECTIONS_PROPERTY = "minimumIdleConnections";
   private static final String MINIMUM_IDLE_CONNECTIONS_DISPLAY_NAME = "Minimum Idle Connections";
   @IntegerProperty(
      description = "The maximum Redis connection pool size.",
      displayName = "Maximum Pool Size",
      defaultValue = 64,
      minValue = 1,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Performance Tuning")
   private Integer maximumPoolSize;
   private static final String MAXIMUM_POOL_SIZE_PROPERTY = "maximumPoolSize";
   private static final String MAXIMUM_POOL_SIZE_DISPLAY_NAME = "Maximum Pool Size";
   @IntegerProperty(
      description = "The maximum time in milliseconds a connection is allowed to be idle before it is closed and removed from the pool. The connection is closed and removed from the pool only if the current number of connections exceeds the minimum idle connections pool size.",
      defaultValue = 10000,
      minValue = 1,
      optional = true,
      displayName = "Idle Timeout [ms]"
   )
   @GroupProperty(groupIdentifier = "Performance Tuning")
   private Integer idleTimeoutInMs;
   @IntegerProperty(
      description = "The maximum time in milliseconds to wait to acquire a live connection to the Redis server.",
      defaultValue = 10000,
      minValue = 1,
      optional = true,
      displayName = "Connection Timeout [ms]"
   )
   @GroupProperty(groupIdentifier = "Performance Tuning")
   private Integer connectionTimeoutInMs;
   @IntegerProperty(
      description = "The Redis server response timeout. The timer starts as soon as the Redis command is sent successfully.",
      defaultValue = 10000,
      minValue = 1,
      optional = true,
      displayName = "Response Timeout [ms]"
   )
   @GroupProperty(groupIdentifier = "Performance Tuning")
   private Integer responseTimeoutInMs;
   @IntegerProperty(
      description = "The time interval with which \"PING\" commands (https://redis.io/commands/ping/) are sent to Redis for each connection. To disable the pinging set the value to 0.",
      defaultValue = 30000,
      optional = true,
      minValue = 0,
      displayName = "Ping Connection Interval [ms]"
   )
   @GroupProperty(groupIdentifier = "Performance Tuning")
   private Integer pingConnectionIntervalInMs;
   @FileProperty(
      description = "Keystore file name containing trusted certificate issuers and trusted certificates.",
      optional = true,
      exampleValues = {"truststore.jks", "/etc/airlock/truststore.jks"},
      assureExistence = true,
      assureReadable = true,
      isFile = true,
      displayName = "Trust Store Path"
   )
   @GroupProperty(groupIdentifier = "TLS Settings")
   private File trustStorePath;
   static final String TRUST_STORE_PATH_FIELD = "trustStorePath";
   static final String TRUST_STORE_PATH_DISPLAY_NAME = "Trust Store Path";
   @StringProperty(
      description = "The password used to check the integrity of the trust store, or to unlock the trust store.<p>The password must be provided if a trust store is configured.</p>",
      optional = true,
      isSensitive = true,
      displayName = "Trust Store Password"
   )
   @GroupProperty(groupIdentifier = "TLS Settings")
   private String trustStorePassword;
   private static final String TRUST_STORE_PASSWORD_FIELD = "trustStorePassword";
   private static final String TRUST_STORE_PASSWORD_DISPLAY_NAME = "Trust Store Password";
   @BooleanProperty(
      description = "<p>Enables hostname verification, i.e. the actual hostname must be the same as in the server certificate.</p>",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "TLS Settings")
   private boolean verifyServerHostname;
   @FileProperty(
      description = "The keystore containing a client certificate (including a private key) for Airlock IAM. The client certificate is used to establish a mutual SSL connection with Redis.",
      optional = true,
      exampleValues = {"keystore.jks", "/etc/airlock/keystore.jks"},
      assureExistence = true,
      assureReadable = true,
      isFile = true,
      displayName = "Key Store Path"
   )
   @GroupProperty(groupIdentifier = "TLS Settings")
   private File keyStorePath;
   private static final String KEY_STORE_PATH_FIELD = "keyStorePath";
   private static final String KEY_STORE_PATH_DISPLAY_NAME = "Key Store Path";
   @StringProperty(
      description = "The password used to check the integrity of the keystore, or to unlock the keystore. <p>The password must be provided if a keystore is configured.</p>",
      optional = true,
      isSensitive = true,
      displayName = "Key Store Password"
   )
   @GroupProperty(groupIdentifier = "TLS Settings")
   private String keyStorePassword;
   private static final String KEY_STORE_PASSWORD_FIELD = "keyStorePassword";
   private static final String KEY_STORE_PASSWORD_DISPLAY_NAME = "Key Store Password";

   public SingleModeRedisStateRepositoryConfig(
      StateEncryptionConfig encryption,
      boolean legacyKeyFormat,
      String namespace,
      String redisUri,
      String username,
      String password,
      Integer minimumIdleConnections,
      Integer maximumPoolSize,
      Integer idleTimeoutInMs,
      Integer connectionTimeoutInMs,
      Integer responseTimeoutInMs,
      Integer pingConnectionIntervalInMs,
      File trustStorePath,
      String trustStorePassword,
      boolean verifyServerHostname,
      File keyStorePath,
      String keyStorePassword
   ) {
      super(encryption, legacyKeyFormat, namespace);
      this.redisUri = redisUri;
      this.username = username;
      this.password = password;
      this.minimumIdleConnections = minimumIdleConnections;
      this.maximumPoolSize = maximumPoolSize;
      this.idleTimeoutInMs = idleTimeoutInMs;
      this.connectionTimeoutInMs = connectionTimeoutInMs;
      this.responseTimeoutInMs = responseTimeoutInMs;
      this.pingConnectionIntervalInMs = pingConnectionIntervalInMs;
      this.trustStorePath = trustStorePath;
      this.trustStorePassword = trustStorePassword;
      this.verifyServerHostname = verifyServerHostname;
      this.keyStorePath = keyStorePath;
      this.keyStorePassword = keyStorePassword;
   }

   @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "The cast is done by the library and it will not fail.")
   public RedissonClient createResource() throws FailureException {
      Config redissonConfig = new Config().setUseScriptCache(true);
      SingleServerConfig singleServerConfig = redissonConfig.useSingleServer();
      ((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)((SingleServerConfig)singleServerConfig.setSslVerificationMode(
                                          this.verifyServerHostname ? SslVerificationMode.STRICT : SslVerificationMode.NONE
                                       ))
                                       .setAddress(this.redisUri)
                                       .setConnectionMinimumIdleSize(this.minimumIdleConnections)
                                       .setConnectionPoolSize(this.maximumPoolSize)
                                       .setConnectTimeout(this.connectionTimeoutInMs))
                                    .setIdleConnectionTimeout(this.idleTimeoutInMs))
                                 .setTimeout(this.responseTimeoutInMs))
                              .setPingConnectionInterval(this.pingConnectionIntervalInMs))
                           .setUsername(this.username))
                        .setPassword(this.password))
                     .setSslTruststore(this.toUrl(this.trustStorePath)))
                  .setSslTruststorePassword(this.trustStorePassword))
               .setSslKeystore(this.toUrl(this.keyStorePath)))
            .setSslKeystorePassword(this.keyStorePassword))
         .setSslProtocols(new String[]{"TLSv1.3", "TLSv1.2"});

      try {
         return Redisson.create(redissonConfig);
      } catch (RedisConnectionException e) {
         LOG.fatal(LogTypes.REDIS_ACCESS, e.getMessage());
         throw e;
      }
   }

   private URL toUrl(File file) {
      if (file == null) {
         return null;
      }

      try {
         return file.toURI().toURL();
      } catch (MalformedURLException e) {
         throw new IllegalStateException("This should not happen as configuration validator should prevent this!", e);
      }
   }

   @ConfigurationValidator
   public static void checkTrustStoreSettingsBothOrNone(
      PluginConfiguration conf, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      checkStoreAndPassword(conf, env, context, "trustStorePath", "Trust Store Path", "trustStorePassword", "Trust Store Password");
   }

   @ConfigurationValidator
   public static void checkKeyStoreSettingsBothOrNone(
      PluginConfiguration conf, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      checkStoreAndPassword(conf, env, context, "keyStorePath", "Key Store Path", "keyStorePassword", "Key Store Password");
   }

   @ConfigurationValidator
   public static void checkRedisUri(PluginConfiguration conf, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      String redisUri = StringPropertyValueReader.effectiveStringConfigValue(conf, "redisUri", env, context);

      try {
         RedisURI uri = new RedisURI(redisUri);
         if (uri.getPort() == -1) {
            throw new PropertyConstraintViolationException(
               "redisUri", conf, env, context, "Invalid Redis URI: It must start with either redis:// or rediss:// and must contain a port"
            );
         }
      } catch (RuntimeException e) {
         throw new PropertyConstraintViolationException(
            "redisUri", conf, env, context, "Invalid Redis URI: It must start with either redis:// or rediss:// and must contain a port"
         );
      }

      String storeValue = FilePropertyValueReader.effectiveFilePropertyValue(conf, "trustStorePath", env, context);
      if (StringUtils.isNotEmpty(storeValue) && !redisUri.startsWith("rediss://")) {
         throw new PropertyConfigurationException(
            conf, env, context, String.format("If %s is configured, %s must start with rediss://.", "Trust Store Path", "Redis URI"), new String[]{"redisUri"}
         );
      }

      if (StringUtils.isEmpty(storeValue) && redisUri.startsWith("rediss://")) {
         throw new PropertyConfigurationException(
            conf, env, context, String.format("If %s starts with rediss://, %s must be configured.", "Redis URI", "Trust Store Path"), new String[]{"redisUri"}
         );
      }
   }

   @ConfigurationValidator
   public static void checkMinimumNotLargerThanMaximum(
      PluginConfiguration conf, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      Integer minimum = IntegerPropertyValueReader.effectiveIntegerConfigValue(conf, "minimumIdleConnections", env, context);
      Integer maximum = IntegerPropertyValueReader.effectiveIntegerConfigValue(conf, "maximumPoolSize", env, context);
      if (minimum > maximum) {
         throw new PropertyConfigurationException(
            conf,
            env,
            context,
            String.format("%s must not be larger than %s.", "Minimum Idle Connections", "Maximum Pool Size"),
            new String[]{"minimumIdleConnections"}
         );
      }
   }

   private static void checkStoreAndPassword(
      PluginConfiguration conf,
      ConfigurationEnvironment env,
      ConfigurationContext context,
      String storeField,
      String storeDisplayName,
      String passwordField,
      String passwordDisplayName
   ) throws PropertyConfigurationException {
      String storeValue = FilePropertyValueReader.effectiveFilePropertyValue(conf, storeField, env, context);
      String passwordValue = StringPropertyValueReader.effectiveStringConfigValue(conf, passwordField, env, context);
      if (StringUtils.isNotEmpty(storeValue) && StringUtils.isEmpty(passwordValue)) {
         throw ifConfiguredOtherMustBeConfiguredAsWell(conf, env, context, storeDisplayName, passwordDisplayName, passwordField);
      }

      if (StringUtils.isNotEmpty(passwordValue) && StringUtils.isEmpty(storeValue)) {
         throw ifConfiguredOtherMustBeConfiguredAsWell(conf, env, context, passwordDisplayName, storeDisplayName, storeField);
      }
   }

   private static PropertyConfigurationException ifConfiguredOtherMustBeConfiguredAsWell(
      PluginConfiguration conf,
      ConfigurationEnvironment env,
      ConfigurationContext context,
      String configuredPropertyDisplayName,
      String notConfiguredPropertyDisplayName,
      String notConfiguredProperty
   ) {
      return new PropertyConfigurationException(
         conf,
         env,
         context,
         String.format("If %s is configured, %s must be configured as well.", configuredPropertyDisplayName, notConfiguredPropertyDisplayName),
         new String[]{notConfiguredProperty}
      );
   }

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

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

   public static final class RedisTestlet extends ConnectionTestlet {
      public TestletResult performTest(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConfigurationException {
         String redisUrl = StringPropertyValueReader.effectiveStringConfigValue(config, "redisUri", env, context);

         try {
            URI redisUri = new URI(redisUrl);
            return checkTcp(redisUri.getHost(), redisUri.getPort(), Duration.ofSeconds(15L));
         } catch (URISyntaxException e) {
            throw new PropertyConstraintViolationException("redisUri", config, env, context, "Invalid URI");
         }
      }

      public boolean areRequiredPropertiesConfigured(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) {
         return isURLPropertyConfigured(config, env, context, "redisUri");
      }

      public String getRequiredPropertiesMissingMessage() {
         return "Please configure the \"Redis URI\" property.";
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class SingleModeRedisStateRepositoryConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private StateEncryptionConfig encryption;
      @SuppressFBWarnings(justification = "generated code")
      private boolean legacyKeyFormat;
      @SuppressFBWarnings(justification = "generated code")
      private String namespace;
      @SuppressFBWarnings(justification = "generated code")
      private String redisUri;
      @SuppressFBWarnings(justification = "generated code")
      private String username;
      @SuppressFBWarnings(justification = "generated code")
      private String password;
      @SuppressFBWarnings(justification = "generated code")
      private Integer minimumIdleConnections;
      @SuppressFBWarnings(justification = "generated code")
      private Integer maximumPoolSize;
      @SuppressFBWarnings(justification = "generated code")
      private Integer idleTimeoutInMs;
      @SuppressFBWarnings(justification = "generated code")
      private Integer connectionTimeoutInMs;
      @SuppressFBWarnings(justification = "generated code")
      private Integer responseTimeoutInMs;
      @SuppressFBWarnings(justification = "generated code")
      private Integer pingConnectionIntervalInMs;
      @SuppressFBWarnings(justification = "generated code")
      private File trustStorePath;
      @SuppressFBWarnings(justification = "generated code")
      private String trustStorePassword;
      @SuppressFBWarnings(justification = "generated code")
      private boolean verifyServerHostname;
      @SuppressFBWarnings(justification = "generated code")
      private File keyStorePath;
      @SuppressFBWarnings(justification = "generated code")
      private String keyStorePassword;

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

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder encryption(StateEncryptionConfig encryption) {
         this.encryption = encryption;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder legacyKeyFormat(boolean legacyKeyFormat) {
         this.legacyKeyFormat = legacyKeyFormat;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder namespace(String namespace) {
         this.namespace = namespace;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder redisUri(String redisUri) {
         this.redisUri = redisUri;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder username(String username) {
         this.username = username;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder password(String password) {
         this.password = password;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder minimumIdleConnections(Integer minimumIdleConnections) {
         this.minimumIdleConnections = minimumIdleConnections;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder maximumPoolSize(Integer maximumPoolSize) {
         this.maximumPoolSize = maximumPoolSize;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder idleTimeoutInMs(Integer idleTimeoutInMs) {
         this.idleTimeoutInMs = idleTimeoutInMs;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder connectionTimeoutInMs(Integer connectionTimeoutInMs) {
         this.connectionTimeoutInMs = connectionTimeoutInMs;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder responseTimeoutInMs(Integer responseTimeoutInMs) {
         this.responseTimeoutInMs = responseTimeoutInMs;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder pingConnectionIntervalInMs(Integer pingConnectionIntervalInMs) {
         this.pingConnectionIntervalInMs = pingConnectionIntervalInMs;
         return this;
      }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder verifyServerHostname(boolean verifyServerHostname) {
         this.verifyServerHostname = verifyServerHostname;
         return this;
      }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public SingleModeRedisStateRepositoryConfig build() {
         return new SingleModeRedisStateRepositoryConfig(
            this.encryption,
            this.legacyKeyFormat,
            this.namespace,
            this.redisUri,
            this.username,
            this.password,
            this.minimumIdleConnections,
            this.maximumPoolSize,
            this.idleTimeoutInMs,
            this.connectionTimeoutInMs,
            this.responseTimeoutInMs,
            this.pingConnectionIntervalInMs,
            this.trustStorePath,
            this.trustStorePassword,
            this.verifyServerHostname,
            this.keyStorePath,
            this.keyStorePassword
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "SingleModeRedisStateRepositoryConfig.SingleModeRedisStateRepositoryConfigBuilder(encryption="
            + this.encryption
            + ", legacyKeyFormat="
            + this.legacyKeyFormat
            + ", namespace="
            + this.namespace
            + ", redisUri="
            + this.redisUri
            + ", username="
            + this.username
            + ", password="
            + this.password
            + ", minimumIdleConnections="
            + this.minimumIdleConnections
            + ", maximumPoolSize="
            + this.maximumPoolSize
            + ", idleTimeoutInMs="
            + this.idleTimeoutInMs
            + ", connectionTimeoutInMs="
            + this.connectionTimeoutInMs
            + ", responseTimeoutInMs="
            + this.responseTimeoutInMs
            + ", pingConnectionIntervalInMs="
            + this.pingConnectionIntervalInMs
            + ", trustStorePath="
            + this.trustStorePath
            + ", trustStorePassword="
            + this.trustStorePassword
            + ", verifyServerHostname="
            + this.verifyServerHostname
            + ", keyStorePath="
            + this.keyStorePath
            + ", keyStorePassword="
            + this.keyStorePassword
            + ")";
      }
   }
}
