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.GroupProperty;
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.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletResult;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletResult.TestResult;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.plugin.impl.PropertyExternalFileUtils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.api.redisnode.RedisNodes;
import org.redisson.client.RedisConnectionException;
import org.redisson.config.Config;

@Plugin(
   description = "<p>State repository that stores all values in a Redis service.</p><p><b>Caution:</b> This plugin supports a very wide range of configuration options and not all of them have been tested to work with Airlock IAM.<br>It is recommended to use as few Redis config options as possible: this minimizes the possible need for manual changes for future versions of Redis. Please refer to the property documentations for example configurations.</p>"
)
@Groups(groupSequence = {"Basic Settings", "Advanced Settings"}, openGroups = "Basic Settings")
public class ExpertModeRedisStateRepositoryConfig extends AbstractRedisStateRepositoryConfig {
   private static final ELogger LOG = ELoggerFactory.getLogger(ExpertModeRedisStateRepositoryConfig.class);
   private static final String TEST_REDIS_CONNECTION = "Test Redis Connection";
   private static final String YAML_CONFIG_DESCRIPTION = "<p>The configuration of the built-in Redisson Redis client in YAML format. Please consult the <a href=\"https://github.com/redisson/redisson/blob/redisson-3.44.0/docs/configuration.md\" target=\"_blank\">Redisson Configuration Documentation</a> for details on\nthe available configuration options.</p>";
   private static final String YAML_CONFIG_LIMITATIONS = "<p><b>Note:</b>\n<ul><li>Cluster mode is currently not supported</li>\n<li>Configuring read-only Redis nodes does do not improve performance, because Airlock IAM does not use any read-only operations.</li></ul></p>";
   @StringProperty(
      description = "<p>The configuration of the built-in Redisson Redis client in YAML format. Please consult the <a href=\"https://github.com/redisson/redisson/blob/redisson-3.44.0/docs/configuration.md\" target=\"_blank\">Redisson Configuration Documentation</a> for details on\nthe available configuration options.</p><p>A configuration file which contains the YAML configuration can be used as an alternative to this configuration (see below).</p><p><b>Note:</b>\n<ul><li>Cluster mode is currently not supported</li>\n<li>Configuring read-only Redis nodes does do not improve performance, because Airlock IAM does not use any read-only operations.</li></ul></p>",
      displayName = "YAML Configuration",
      isMultiLineText = true,
      optional = false,
      exampleValues = {
            "singleServerConfig:</br>\n&nbsp;&nbsp;address: \"redis://127.0.0.1:6379\"\n",
            "sentinelServersConfig:</br>\n&nbsp;&nbsp;masterName: \"mymaster\"</br>\n&nbsp;&nbsp;connectTimeout: 10000</br>\n&nbsp;&nbsp;sentinelAddresses:</br>\n&nbsp;&nbsp;- \"redis://127.0.0.1:26379\"</br>\n&nbsp;&nbsp;- \"redis://127.0.0.1:26389\"\n",
            "replicatedServersConfig:</br>\n&nbsp;&nbsp;connectTimeout: 10000</br>\n&nbsp;&nbsp;nodeAddresses:</br>\n&nbsp;&nbsp;- \"redis://redishost1:2812\"</br>\n&nbsp;&nbsp;- \"redis://redishost2:2815\"</br>\n&nbsp;&nbsp;- \"redis://redishost3:2813\"</br>\n"
      }
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   @TestletProperty(buttonToolTip = "Test Redis Connection", testletClass = ExpertModeRedisStateRepositoryConfig.YamlConfigTestlet.class)
   private String yamlConfig;
   static final String YAML_CONFIG_PROPERTY_FIELD_NAME = "yamlConfig";
   private static final String YAML_CONFIG_PROPERTY_DISPLAY_NAME = "YAML Configuration";

   public ExpertModeRedisStateRepositoryConfig(StateEncryptionConfig encryption, boolean legacyKeyFormat, String namespace, String yamlConfig) {
      super(encryption, legacyKeyFormat, namespace);
      this.yamlConfig = yamlConfig;
   }

   public RedissonClient createResource() throws FailureException {
      try {
         Config redissonConfig = Config.fromYAML(this.yamlConfig);
         return Redisson.create(redissonConfig);
      } catch (RedisConnectionException e) {
         LOG.fatal(LogTypes.REDIS_ACCESS, e.getMessage());
         throw e;
      } catch (IOException e) {
         throw new FailureException(e);
      } catch (IllegalArgumentException e) {
         LOG.warn(LogTypes.REDIS_ACCESS, "Illegal Redis configuration:" + e.getMessage());
         throw e;
      }
   }

   @ConfigurationValidator
   public static void validateYamlConfig(
      PluginConfiguration pluginConfiguration, ConfigurationEnvironment env, ConfigurationContext context, Environment valenv
   ) throws ConfigurationException {
      if (valenv != Environment.GENERAL
         || !PropertyExternalFileUtils.isStringInExternalFile(pluginConfiguration.getSimplePropertyConfigurationValue("yamlConfig", env, context))) {
         String yamlConfig = StringPropertyValueReader.effectiveStringConfigValue(pluginConfiguration, "yamlConfig", env, context);
         if (StringUtils.isNotEmpty(yamlConfig)) {
            validateYamlConfig(pluginConfiguration, env, context, yamlConfig, "yamlConfig");
         }
      }
   }

   private static void validateYamlConfig(
      PluginConfiguration pluginConfiguration, ConfigurationEnvironment env, ConfigurationContext context, String yamlConfig, String propertyName
   ) throws PropertyConstraintViolationException {
      try {
         Config redissonConfig = Config.fromYAML(yamlConfig);
         if (redissonConfig.isClusterConfig()) {
            throw new PropertyConstraintViolationException(propertyName, pluginConfiguration, env, context, "Cluster Mode not currently supported by IAM.");
         }
      } catch (IOException e) {
         throw new PropertyConstraintViolationException(propertyName, pluginConfiguration, env, context, "Invalid configuration: " + e.getMessage());
      }
   }

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

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

   private abstract static class AbstractRedisConfigTestlet extends ConnectionTestlet {
      private static final String TESTLET_TITLE = "Redis Connection";
      private final String propertyName;

      public AbstractRedisConfigTestlet(String propertyName) {
         this.propertyName = propertyName;
      }

      protected abstract String getYaml(PluginConfiguration var1, ConfigurationEnvironment var2, ConfigurationContext var3) throws PropertyTypeMismatchException;

      public TestletResult performTest(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConfigurationException {
         String yamlConfig = this.getYaml(config, env, context);

         try {
            if (StringUtils.isNotEmpty(yamlConfig)) {
               Config redissonConfig = Config.fromYAML(yamlConfig);
               RedissonClient redissonClient = Redisson.create(redissonConfig);
               if (redissonConfig.isSingleConfig()) {
                  return testResult(ping(redissonClient, RedisNodes.SINGLE));
               }

               if (redissonConfig.isSentinelConfig()) {
                  return testResult(ping(redissonClient, RedisNodes.SENTINEL_MASTER_SLAVE));
               }

               return new TestletResult(TestResult.FAILED, "Redis Connection", "Only Single and Sentinel modes supported.");
            }
         } catch (IOException e) {
            return new TestletResult(TestResult.FAILED, "Redis Connection", "Invalid Configuration: " + e.getMessage());
         }

         return testResult(false);
      }

      private static boolean ping(RedissonClient redissonClient, RedisNodes<?> redisNodes) {
         return redissonClient.getRedisNodes(redisNodes).pingAll();
      }

      private static TestletResult testResult(boolean success) {
         return success
            ? new TestletResult(TestResult.SUCCESSFUL, "Redis Connection", "Success!")
            : new TestletResult(TestResult.FAILED, "Redis Connection", "Failed!");
      }

      public String getRequiredPropertiesMissingMessage() {
         return "Please configure the \"" + this.propertyName + "\" property.";
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class ExpertModeRedisStateRepositoryConfigBuilder {
      @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 yamlConfig;

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

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public ExpertModeRedisStateRepositoryConfig.ExpertModeRedisStateRepositoryConfigBuilder yamlConfig(String yamlConfig) {
         this.yamlConfig = yamlConfig;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ExpertModeRedisStateRepositoryConfig build() {
         return new ExpertModeRedisStateRepositoryConfig(this.encryption, this.legacyKeyFormat, this.namespace, this.yamlConfig);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "ExpertModeRedisStateRepositoryConfig.ExpertModeRedisStateRepositoryConfigBuilder(encryption="
            + this.encryption
            + ", legacyKeyFormat="
            + this.legacyKeyFormat
            + ", namespace="
            + this.namespace
            + ", yamlConfig="
            + this.yamlConfig
            + ")";
      }
   }

   public static class YamlConfigTestlet extends ExpertModeRedisStateRepositoryConfig.AbstractRedisConfigTestlet {
      public YamlConfigTestlet() {
         super("YAML Configuration");
      }

      @Override
      protected String getYaml(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
         return StringPropertyValueReader.effectiveStringConfigValue(config, "yamlConfig", env, context);
      }

      public boolean areRequiredPropertiesConfigured(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) {
         try {
            return StringUtils.isNotEmpty(StringPropertyValueReader.effectiveStringConfigValue(config, "yamlConfig", env, context));
         } catch (PropertyTypeMismatchException e) {
            return false;
         }
      }
   }
}
