package com.airlock.iam.common.infrastructure.state.redis;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.json.JsonCoding;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.domain.model.state.FindResult;
import com.airlock.iam.common.domain.model.state.InsertAndUnlockAdditionalKeysResult;
import com.airlock.iam.common.domain.model.state.InsertResult;
import com.airlock.iam.common.domain.model.state.LockAndFindResult;
import com.airlock.iam.common.domain.model.state.LockResult;
import com.airlock.iam.common.domain.model.state.PutResult;
import com.airlock.iam.common.domain.model.state.RemoveAndUnlockResult;
import com.airlock.iam.common.domain.model.state.RemoveResult;
import com.airlock.iam.common.domain.model.state.StateTimeout;
import com.airlock.iam.common.domain.model.state.UpdateAndUnlockResult;
import com.airlock.iam.common.domain.service.state.StateRepositoryKey;
import com.airlock.iam.common.domain.service.state.StateRepositoryLockKey;
import com.airlock.iam.common.domain.service.state.StateRepositoryOwnedLock;
import com.airlock.iam.common.infrastructure.state.shared.StateRepositoryRetryUtil;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.redisson.api.RScript;
import org.redisson.api.RedissonClient;
import org.redisson.api.RScript.Mode;
import org.redisson.api.RScript.ReturnType;
import org.redisson.client.RedisConnectionException;
import org.redisson.client.codec.StringCodec;

public final class RedisCommandExecutor {
   private static final ELogger LOG = ELoggerFactory.getLogger(RedisCommandExecutor.class);
   private static final String COMMON_FRAGMENTS = LuaScriptReader.readLuaScript("/redis/fragments/common_fragments.lua");
   private static final String CHECK_LOCK_FRAGMENTS = LuaScriptReader.readLuaScript("/redis/fragments/check_lock_fragments.lua", COMMON_FRAGMENTS);
   private static final String INSERT_AND_UPDATE_FRAGMENTS = LuaScriptReader.readLuaScript(
      "/redis/fragments/insert_and_update_fragments.lua", CHECK_LOCK_FRAGMENTS
   );
   private static final String ACQUIRE_AND_RELEASE_LOCKS_FRAGMENTS = LuaScriptReader.readLuaScript(
      "/redis/fragments/acquire_and_release_locks_fragments.lua", CHECK_LOCK_FRAGMENTS
   );
   static final String SCRIPT_FIND = LuaScriptReader.readLuaScript("/redis/scripts/find.lua", CHECK_LOCK_FRAGMENTS);
   static final String SCRIPT_FIND_READ_ONLY = LuaScriptReader.readLuaScript("/redis/scripts/find_read_only.lua", COMMON_FRAGMENTS);
   static final String SCRIPT_INSERT = LuaScriptReader.readLuaScript("/redis/scripts/insert.lua", INSERT_AND_UPDATE_FRAGMENTS);
   static final String SCRIPT_PUT = LuaScriptReader.readLuaScript("/redis/scripts/put.lua", INSERT_AND_UPDATE_FRAGMENTS);
   static final String SCRIPT_REMOVE = LuaScriptReader.readLuaScript("/redis/scripts/remove.lua", CHECK_LOCK_FRAGMENTS);
   static final String SCRIPT_LOCK = LuaScriptReader.readLuaScript("/redis/scripts/lock.lua", ACQUIRE_AND_RELEASE_LOCKS_FRAGMENTS);
   static final String SCRIPT_UNLOCK_ALL_ASSOCIATED_KEYS = LuaScriptReader.readLuaScript(
      "/redis/scripts/unlock_all_associated_keys.lua", ACQUIRE_AND_RELEASE_LOCKS_FRAGMENTS
   );
   static final String SCRIPT_LOCK_AND_FIND = LuaScriptReader.readLuaScript("/redis/scripts/lock_and_find.lua", ACQUIRE_AND_RELEASE_LOCKS_FRAGMENTS);
   static final String SCRIPT_INSERT_AND_UNLOCK_ADDITIONAL_KEYS = LuaScriptReader.readLuaScript(
      "/redis/scripts/insert_and_unlock_additional_keys.lua", ACQUIRE_AND_RELEASE_LOCKS_FRAGMENTS
   );
   static final String SCRIPT_UPDATE_AND_UNLOCK = LuaScriptReader.readLuaScript("/redis/scripts/update_and_unlock.lua", ACQUIRE_AND_RELEASE_LOCKS_FRAGMENTS);
   static final String SCRIPT_REMOVE_AND_UNLOCK = LuaScriptReader.readLuaScript("/redis/scripts/remove_and_unlock.lua", ACQUIRE_AND_RELEASE_LOCKS_FRAGMENTS);
   static final String SCRIPT_REMOVE_AND_FORCE_UNLOCK = LuaScriptReader.readLuaScript(
      "/redis/scripts/remove_and_force_unlock.lua", ACQUIRE_AND_RELEASE_LOCKS_FRAGMENTS
   );
   static final String SCRIPT_FIND_ALL_LOCK_KEYS = LuaScriptReader.readLuaScript("/redis/scripts/find_all_lock_keys.lua", COMMON_FRAGMENTS);
   static final String SCRIPT_KEEP_ALIVE_LOCKS = LuaScriptReader.readLuaScript("/redis/scripts/keep_alive_locks.lua", COMMON_FRAGMENTS);
   static final String SCRIPT_FIND_ALL_LOCKS_FOR_CURRENT_LOCK_OWNER = LuaScriptReader.readLuaScript(
      "/redis/scripts/find_all_locks_for_current_lock_owner.lua", COMMON_FRAGMENTS
   );
   static final String SCRIPT_TEST_INSERT_LOCK_OWNER_ENTRY = LuaScriptReader.readLuaScript("/redis/scripts/test_insert_lock_owner_entry.lua", COMMON_FRAGMENTS);
   private final Provider<RedissonClient> client;

   @Inject
   public RedisCommandExecutor(Provider<RedissonClient> client) {
      this.client = client;
   }

   public FindResult find(
      StateRepositoryKey key, String lockId, StateEncryption encryption, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper
   ) {
      return StateRepositoryRetryUtil.executeWithRetries(() -> this.findInternal(key, lockId, encryption, settings, legacyKeyHelper));
   }

   private FindResult findInternal(
      StateRepositoryKey key, String lockId, StateEncryption encryption, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper
   ) {
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(SCRIPT_FIND, key, settings, legacyKeyHelper, lockId), key, "find"
      );

      return switch (result.getCode()) {
         case OK -> (FindResult)this.tryDecryptOrRemove(key, lockId, result.getFirstValue().orElseThrow(), encryption, settings, legacyKeyHelper)
            .map(FindResult::found)
            .orElseGet(FindResult::notFound);
         case NOT_FOUND -> FindResult.notFound();
         case ALREADY_EXISTS, LOCK_NOT_OWNED -> throw new FailureException("Unexpected return code: " + result.getCode());
         case LOCKED_BY_OTHER -> FindResult.locked();
      };
   }

   public Optional<String> findReadOnly(
      StateRepositoryKey key, String lockId, StateEncryption encryption, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper
   ) {
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(SCRIPT_FIND_READ_ONLY, key, settings, legacyKeyHelper), key, "findReadOnly"
      );

      return switch (result.getCode()) {
         case OK -> this.tryDecryptOrRemove(key, lockId, result.getFirstValue().orElseThrow(), encryption, settings, legacyKeyHelper);
         case NOT_FOUND -> Optional.empty();
         case ALREADY_EXISTS, LOCK_NOT_OWNED, LOCKED_BY_OTHER -> throw new FailureException("Unexpected return code: " + result.getCode());
      };
   }

   public InsertResult insert(
      StateRepositoryKey key,
      String value,
      StateTimeout timeout,
      String lockId,
      StateEncryption encryption,
      RedisStateRepositorySettings settings,
      RedisLegacyKeyHelper legacyKeyHelper,
      StateRepositoryKey... additionalLockKeys
   ) {
      return StateRepositoryRetryUtil.executeWithRetries(
         () -> this.insertInternal(key, value, timeout, lockId, encryption, settings, legacyKeyHelper, additionalLockKeys)
      );
   }

   private InsertResult insertInternal(
      StateRepositoryKey key,
      String value,
      StateTimeout timeout,
      String lockId,
      StateEncryption encryption,
      RedisStateRepositorySettings settings,
      RedisLegacyKeyHelper legacyKeyHelper,
      StateRepositoryKey... additionalLockKeys
   ) {
      Long lifetime = timeout.lifetime().map(Duration::toMillis).orElse(-1L);
      Long idleTimeout = timeout.idleTimeout().map(Duration::toMillis).orElse(-1L);
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(
            SCRIPT_INSERT,
            key,
            settings,
            legacyKeyHelper,
            encryption.encrypt(value),
            lockId,
            lifetime,
            idleTimeout,
            JsonCoding.toJson(this.toStringValueArray(settings, legacyKeyHelper, additionalLockKeys))
         ),
         key,
         "insert"
      );

      return switch (result.getCode()) {
         case OK -> (InsertResult)this.tryDecryptOrRemove(key, lockId, result.getFirstValue().orElseThrow(), encryption, settings, legacyKeyHelper)
            .map(InsertResult::inserted)
            .orElseThrow(() -> new FailureException("Unable to decrypt inserted value"));
         case NOT_FOUND, LOCK_NOT_OWNED -> throw new FailureException("Unexpected return code: " + result.getCode());
         case ALREADY_EXISTS -> InsertResult.alreadyExists();
         case LOCKED_BY_OTHER -> InsertResult.locked();
      };
   }

   public PutResult put(
      StateRepositoryKey key,
      String value,
      StateTimeout timeout,
      String lockId,
      StateEncryption encryption,
      RedisStateRepositorySettings settings,
      RedisLegacyKeyHelper legacyKeyHelper
   ) {
      return StateRepositoryRetryUtil.executeWithRetries(() -> this.putInternal(key, value, timeout, lockId, encryption, settings, legacyKeyHelper));
   }

   private PutResult putInternal(
      StateRepositoryKey key,
      String value,
      StateTimeout timeout,
      String lockId,
      StateEncryption encryption,
      RedisStateRepositorySettings settings,
      RedisLegacyKeyHelper legacyKeyHelper
   ) {
      Long lifetime = timeout.lifetime().map(Duration::toMillis).orElse(-1L);
      Long idleTimeout = timeout.idleTimeout().map(Duration::toMillis).orElse(-1L);
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(SCRIPT_PUT, key, settings, legacyKeyHelper, encryption.encrypt(value), lockId, lifetime, idleTimeout), key, "put"
      );

      return switch (result.getCode()) {
         case OK -> (PutResult)this.tryDecryptOrRemove(key, lockId, result.getFirstValue().orElseThrow(), encryption, settings, legacyKeyHelper)
            .map(PutResult::put)
            .orElseThrow(() -> new FailureException("Unable to decrypt put value"));
         case NOT_FOUND, ALREADY_EXISTS, LOCK_NOT_OWNED -> throw new FailureException("Unexpected return code: " + result.getCode());
         case LOCKED_BY_OTHER -> PutResult.locked();
      };
   }

   public RemoveResult remove(
      StateRepositoryKey key, String lockId, StateEncryption encryption, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper
   ) {
      return StateRepositoryRetryUtil.executeWithRetries(() -> this.removeInternal(key, lockId, encryption, settings, legacyKeyHelper));
   }

   private RemoveResult removeInternal(
      StateRepositoryKey key, String lockId, StateEncryption encryption, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper
   ) {
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(SCRIPT_REMOVE, key, settings, legacyKeyHelper, lockId), key, "remove"
      );

      return switch (result.getCode()) {
         case OK -> (RemoveResult)this.tryDecrypt(result.getFirstValue().orElseThrow(), encryption)
            .map(RemoveResult::removed)
            .orElseGet(RemoveResult::notFound);
         case NOT_FOUND -> RemoveResult.notFound();
         case ALREADY_EXISTS, LOCK_NOT_OWNED -> throw new FailureException("Unexpected return code: " + result.getCode());
         case LOCKED_BY_OTHER -> RemoveResult.locked();
      };
   }

   public LockAndFindResult lockAndFind(
      StateRepositoryKey key, String lockId, StateEncryption encryption, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper
   ) {
      return StateRepositoryRetryUtil.executeWithRetries(() -> this.lockAndFindInternal(key, lockId, encryption, settings, legacyKeyHelper));
   }

   private LockAndFindResult lockAndFindInternal(
      StateRepositoryKey key, String lockId, StateEncryption encryption, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper
   ) {
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(SCRIPT_LOCK_AND_FIND, key, settings, legacyKeyHelper, lockId, RedisStateRepository.DEFAULT_LOCK_LIFETIME.toMillis()),
         key,
         "lockAndFind"
      );

      return switch (result.getCode()) {
         case OK -> (LockAndFindResult)this.tryDecryptOrRemove(key, lockId, result.getFirstValue().orElseThrow(), encryption, settings, legacyKeyHelper)
            .map(LockAndFindResult::found)
            .orElseGet(LockAndFindResult::notFound);
         case NOT_FOUND -> LockAndFindResult.notFound();
         case ALREADY_EXISTS, LOCK_NOT_OWNED -> throw new FailureException("Unexpected return code: " + result.getCode());
         case LOCKED_BY_OTHER -> LockAndFindResult.lockedBySomeoneElse();
      };
   }

   public InsertAndUnlockAdditionalKeysResult insertAndUnlockAdditionalKeys(
      StateRepositoryKey key,
      String value,
      StateTimeout timeout,
      String lockId,
      StateEncryption encryption,
      RedisStateRepositorySettings settings,
      RedisLegacyKeyHelper legacyKeyHelper,
      StateRepositoryKey... additionalLockKeys
   ) {
      return StateRepositoryRetryUtil.executeWithRetries(
         () -> this.insertAndUnlockAdditionalKeysInternal(key, value, timeout, lockId, encryption, settings, legacyKeyHelper, additionalLockKeys)
      );
   }

   private InsertAndUnlockAdditionalKeysResult insertAndUnlockAdditionalKeysInternal(
      StateRepositoryKey key,
      String value,
      StateTimeout timeout,
      String lockId,
      StateEncryption encryption,
      RedisStateRepositorySettings settings,
      RedisLegacyKeyHelper legacyKeyHelper,
      StateRepositoryKey... additionalLockKeys
   ) {
      Long lifetime = timeout.lifetime().map(Duration::toMillis).orElse(-1L);
      Long idleTimeout = timeout.idleTimeout().map(Duration::toMillis).orElse(-1L);
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(
            SCRIPT_INSERT_AND_UNLOCK_ADDITIONAL_KEYS,
            key,
            settings,
            legacyKeyHelper,
            encryption.encrypt(value),
            lockId,
            lifetime,
            idleTimeout,
            JsonCoding.toJson(this.toStringValueArray(settings, legacyKeyHelper, additionalLockKeys))
         ),
         key,
         "insertAndUnlockAdditionalKeys"
      );

      return switch (result.getCode()) {
         case OK -> (InsertAndUnlockAdditionalKeysResult)this.tryDecryptOrRemove(
               key, lockId, result.getFirstValue().orElseThrow(), encryption, settings, legacyKeyHelper
            )
            .map(InsertAndUnlockAdditionalKeysResult::inserted)
            .orElseThrow(() -> new FailureException("Unable to decrypt inserted value"));
         case NOT_FOUND -> throw new FailureException("Unexpected return code: " + result.getCode());
         case ALREADY_EXISTS -> InsertAndUnlockAdditionalKeysResult.alreadyExists();
         case LOCK_NOT_OWNED, LOCKED_BY_OTHER -> InsertAndUnlockAdditionalKeysResult.mainKeyLockedBySomeoneElseOrDidNotOwnAllAdditionalLocks();
      };
   }

   public UpdateAndUnlockResult updateAndUnlock(
      StateRepositoryKey key,
      String value,
      StateTimeout timeout,
      String lockId,
      StateEncryption encryption,
      RedisStateRepositorySettings settings,
      RedisLegacyKeyHelper legacyKeyHelper,
      StateRepositoryKey... newAdditionalKeys
   ) {
      Long lifetime = timeout.lifetime().map(Duration::toMillis).orElse(-1L);
      Long idleTimeout = timeout.idleTimeout().map(Duration::toMillis).orElse(-1L);
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(
            SCRIPT_UPDATE_AND_UNLOCK,
            key,
            settings,
            legacyKeyHelper,
            encryption.encrypt(value),
            lockId,
            lifetime,
            idleTimeout,
            JsonCoding.toJson(this.toStringValueArray(settings, legacyKeyHelper, newAdditionalKeys))
         ),
         key,
         "updateAndUnlock"
      );

      return switch (result.getCode()) {
         case OK -> (UpdateAndUnlockResult)this.tryDecryptOrRemove(key, lockId, result.getFirstValue().orElseThrow(), encryption, settings, legacyKeyHelper)
            .map(UpdateAndUnlockResult::updated)
            .orElseThrow(() -> new FailureException("Unable to decrypt updated value"));
         case NOT_FOUND -> UpdateAndUnlockResult.notFound();
         case ALREADY_EXISTS -> throw new FailureException("Unexpected return code: " + result.getCode());
         case LOCK_NOT_OWNED, LOCKED_BY_OTHER -> UpdateAndUnlockResult.notAllLocksOwned();
      };
   }

   private String[] toStringValueArray(RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper, StateRepositoryKey[] newAdditionalKeys) {
      return Arrays.stream(newAdditionalKeys).map(key -> this.transformKeyToString(key, settings, legacyKeyHelper)).toArray(String[]::new);
   }

   public RemoveAndUnlockResult removeAndUnlock(
      StateRepositoryKey key, String lockId, StateEncryption encryption, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper
   ) {
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(SCRIPT_REMOVE_AND_UNLOCK, key, settings, legacyKeyHelper, lockId), key, "removeAndUnlock"
      );

      return switch (result.getCode()) {
         case OK -> (RemoveAndUnlockResult)this.tryDecrypt(result.getFirstValue().orElseThrow(), encryption)
            .map(RemoveAndUnlockResult::removed)
            .orElseGet(RemoveAndUnlockResult::notFound);
         case NOT_FOUND -> RemoveAndUnlockResult.notFound();
         case ALREADY_EXISTS -> throw new FailureException("Unexpected return code: " + result.getCode());
         case LOCK_NOT_OWNED, LOCKED_BY_OTHER -> RemoveAndUnlockResult.doNotOwnAllLocks();
      };
   }

   public void removeAndForceUnlock(StateRepositoryKey key, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper) {
      this.tryRedisCommand(() -> this.executeScript(SCRIPT_REMOVE_AND_FORCE_UNLOCK, key, settings, legacyKeyHelper), key, "removeAndForceUnlock");
   }

   public LockResult lock(StateRepositoryKey key, String lockId, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper) {
      return StateRepositoryRetryUtil.executeWithRetries(() -> this.lockInternal(key, lockId, settings, legacyKeyHelper));
   }

   private LockResult lockInternal(StateRepositoryKey key, String lockId, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper) {
      RedisCommandExecutor.RedisCommandResult result = this.tryRedisCommand(
         () -> this.executeScript(SCRIPT_LOCK, key, settings, legacyKeyHelper, lockId, RedisStateRepository.DEFAULT_LOCK_LIFETIME.toMillis()), key, "lock"
      );
      return result.getCode().equals(RedisCommandExecutor.RedisCommandResultCode.LOCKED_BY_OTHER) ? LockResult.alreadyLocked() : LockResult.acquiredLock();
   }

   public boolean unlock(StateRepositoryKey key, String lockId, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper) {
      return this.tryRedisCommand(
            () -> this.executeScript(SCRIPT_UNLOCK_ALL_ASSOCIATED_KEYS, key, settings, legacyKeyHelper, lockId), key, "unlockAllAssociatedKeys"
         )
         .getCode()
         .equals(RedisCommandExecutor.RedisCommandResultCode.OK);
   }

   public List<StateRepositoryLockKey> retrieveAllLockKeys(RedisStateRepositorySettings settings) {
      RedisCommandExecutor.RedisCommandResult result = tryRedisCommand(
         () -> this.executeScript(SCRIPT_FIND_ALL_LOCK_KEYS, settings.keySettings().legacyKeyFormat()),
         "Failed to retrieve all lock keys because the connection to Redis failed."
      );
      return result.getValues().stream().map(StateRepositoryLockKey::new).toList();
   }

   public void clear() {
      try {
         ((RedissonClient)this.client.get()).getKeys().flushall();
      } catch (RedisConnectionException e) {
         throw new FailureException("Failed to clear Redis because the connection to Redis failed.", e);
      }
   }

   public void keepAliveLocks() {
      tryRedisCommand(
         () -> this.executeScript(SCRIPT_KEEP_ALIVE_LOCKS, false),
         "Failed to extend lock lifetimes for lock owner %s".formatted(LockOwnerId.getInstance().value())
      );
   }

   public void insertTestLockOwnerEntry(StateRepositoryOwnedLock lockEntry) {
      tryRedisCommand(
         () -> this.executeScript(SCRIPT_TEST_INSERT_LOCK_OWNER_ENTRY, false, lockEntry.lockKey().value(), lockEntry.lockCreationTimestamp().toEpochMilli()),
         "Failed to add test lock owner entry."
      );
   }

   public List<StateRepositoryOwnedLock> retrieveRegisteredLocksForCurrentLockOwner() {
      RedisCommandExecutor.RedisCommandResult result = tryRedisCommand(
         () -> this.executeScript(SCRIPT_FIND_ALL_LOCKS_FOR_CURRENT_LOCK_OWNER, false), "Failed to retrieve all currently owned locks."
      );
      return result.getValues()
         .stream()
         .map(JsonCoding::fromJson)
         .map(
            lock -> new StateRepositoryOwnedLock(
               new StateRepositoryLockKey((String)lock.get("lock_key")), Instant.ofEpochMilli((Long)lock.get("creation_time_milliseconds"))
            )
         )
         .toList();
   }

   private Optional<String> tryDecryptOrRemove(
      StateRepositoryKey key,
      String lockId,
      String response,
      StateEncryption encryption,
      RedisStateRepositorySettings settings,
      RedisLegacyKeyHelper legacyKeyHelper
   ) {
      return this.tryDecrypt(response, encryption).or(() -> {
         LOG.debug("Could not decrypt value from Redis. Removing key...");
         this.remove(key, lockId, encryption, settings, legacyKeyHelper);
         return Optional.empty();
      });
   }

   private Optional<String> tryDecrypt(String response, StateEncryption encryption) {
      return encryption.decrypt(response);
   }

   private RedisCommandExecutor.RedisCommandResult tryRedisCommand(Supplier<List<?>> command, StateRepositoryKey key, String commandName) {
      return tryRedisCommand(command, String.format("Failed to %s key \"%s\" because the connection to Redis failed.", commandName, key));
   }

   private static RedisCommandExecutor.RedisCommandResult tryRedisCommand(Supplier<List<?>> command, String errorMessage) {
      try {
         RedisCommandExecutor.RedisCommandResult result = new RedisCommandExecutor.RedisCommandResult(command.get());
         if (result.getCode() != RedisCommandExecutor.RedisCommandResultCode.OK) {
            LOG.debug(String.format("Redis returned with result code %s", result.getCode().name()));
         }

         result.getLogBuffer().forEach(RedisCommandExecutor.RedisLogMessage::log);
         return result;
      } catch (RedisConnectionException e) {
         LOG.fatal(LogTypes.REDIS_ACCESS, e.getMessage());
         throw new FailureException(errorMessage, e);
      }
   }

   private List<?> executeScript(
      String luaScript, StateRepositoryKey key, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper, Object... parameters
   ) {
      return (List<?>)this.script()
         .eval(
            Mode.READ_WRITE,
            luaScript,
            ReturnType.VALUE,
            List.of(this.transformKeyToString(key, settings, legacyKeyHelper)),
            this.toParametersWithDefaultArguments(this.mustUseLegacyFormat(key, settings, legacyKeyHelper), parameters)
         );
   }

   private List<?> executeScript(String luaScript, boolean legacyKeyFormat, Object... parameters) {
      return (List<?>)this.script()
         .eval(Mode.READ_WRITE, luaScript, ReturnType.VALUE, Collections.emptyList(), this.toParametersWithDefaultArguments(legacyKeyFormat, parameters));
   }

   private Object[] toParametersWithDefaultArguments(boolean legacyKeyFormat, Object... parameters) {
      List<Object> params = new ArrayList<>();
      params.add(legacyKeyFormat);
      params.add(LockOwnerId.getInstance().value());
      params.addAll(Arrays.asList(parameters));
      return params.toArray();
   }

   private String transformKeyToString(StateRepositoryKey key, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper) {
      return this.mustUseLegacyFormat(key, settings, legacyKeyHelper) ? legacyKeyHelper.toLegacyKey(key) : settings.keySettings().getPrefix() + key.value();
   }

   private boolean mustUseLegacyFormat(StateRepositoryKey key, RedisStateRepositorySettings settings, RedisLegacyKeyHelper legacyKeyHelper) {
      return settings.keySettings().legacyKeyFormat() && legacyKeyHelper.mustTransformToLegacyKey(key);
   }

   private RScript script() {
      return ((RedissonClient)this.client.get()).getScript(StringCodec.INSTANCE);
   }

   static final class RedisCommandResult {
      private final List<RedisCommandExecutor.RedisLogMessage> logBuffer;
      private final RedisCommandExecutor.RedisCommandResultCode code;
      private final List<String> values;

      RedisCommandResult(List<?> response) {
         FailureException.checkTrue(response != null, "redis lua script must return a response");
         this.logBuffer = parseLogBuffer(response);
         this.code = parseCode(response);
         this.values = parseValues(response);
      }

      Optional<String> getFirstValue() {
         List<String> values = this.getValues();
         return values.isEmpty() ? Optional.empty() : Optional.ofNullable(values.getFirst());
      }

      private static List<RedisCommandExecutor.RedisLogMessage> parseLogBuffer(List<?> response) {
         FailureException.checkTrue(!response.isEmpty(), "redis lua script must return table with log buffer at index 1");
         List<?> logBuffer = (List<?>)response.getFirst();
         FailureException.checkTrue(logBuffer.size() == 4, "redis lua script must return log buffer table with size 4");
         Stream<RedisCommandExecutor.RedisLogMessage> debugStream = ((List)logBuffer.get(0))
            .stream()
            .map(log -> new RedisCommandExecutor.RedisLogMessage(RedisCommandExecutor.RedisLogMessage.RedisLogLevel.DEBUG, (String)log));
         Stream<RedisCommandExecutor.RedisLogMessage> infoStream = ((List)logBuffer.get(1))
            .stream()
            .map(log -> new RedisCommandExecutor.RedisLogMessage(RedisCommandExecutor.RedisLogMessage.RedisLogLevel.INFO, (String)log));
         Stream<RedisCommandExecutor.RedisLogMessage> warnStream = ((List)logBuffer.get(2))
            .stream()
            .map(log -> new RedisCommandExecutor.RedisLogMessage(RedisCommandExecutor.RedisLogMessage.RedisLogLevel.WARN, (String)log));
         Stream<RedisCommandExecutor.RedisLogMessage> errorStream = ((List)logBuffer.get(3))
            .stream()
            .map(log -> new RedisCommandExecutor.RedisLogMessage(RedisCommandExecutor.RedisLogMessage.RedisLogLevel.ERROR, (String)log));
         return Stream.of(debugStream, infoStream, warnStream, errorStream).flatMap(s -> (Stream<? extends RedisCommandExecutor.RedisLogMessage>)s).toList();
      }

      private static RedisCommandExecutor.RedisCommandResultCode parseCode(List<?> response) {
         FailureException.checkTrue(response.size() > 1, "redis lua script must return table with response code at index 2");
         return RedisCommandExecutor.RedisCommandResultCode.of((String)response.get(1));
      }

      private static List<String> parseValues(List<?> response) {
         if (response.size() >= 3 && response.get(2) != null) {
            Object values = response.get(2);
            return List.class.isAssignableFrom(values.getClass()) ? ((List)values).stream().map(String.class::cast).toList() : List.of((String)values);
         } else {
            return Collections.emptyList();
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      public List<RedisCommandExecutor.RedisLogMessage> getLogBuffer() {
         return this.logBuffer;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RedisCommandExecutor.RedisCommandResultCode getCode() {
         return this.code;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public boolean equals(Object o) {
         if (o == this) {
            return true;
         } else if (!(o instanceof RedisCommandExecutor.RedisCommandResult other)) {
            return false;
         } else {
            Object this$logBuffer = this.logBuffer;
            Object other$logBuffer = other.logBuffer;
            if (this$logBuffer == null ? other$logBuffer == null : this$logBuffer.equals(other$logBuffer)) {
               Object this$code = this.code;
               Object other$code = other.code;
               if (this$code == null ? other$code == null : this$code.equals(other$code)) {
                  Object this$values = this.values;
                  Object other$values = other.values;
                  return this$values == null ? other$values == null : this$values.equals(other$values);
               } else {
                  return false;
               }
            } else {
               return false;
            }
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public int hashCode() {
         int PRIME = 59;
         int result = 1;
         Object $logBuffer = this.logBuffer;
         result = result * 59 + ($logBuffer == null ? 43 : $logBuffer.hashCode());
         Object $code = this.code;
         result = result * 59 + ($code == null ? 43 : $code.hashCode());
         Object $values = this.values;
         return result * 59 + ($values == null ? 43 : $values.hashCode());
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "RedisCommandExecutor.RedisCommandResult(logBuffer=" + this.getLogBuffer() + ", code=" + this.getCode() + ", values=" + this.getValues() + ")";
      }
   }

   enum RedisCommandResultCode {
      OK,
      NOT_FOUND,
      ALREADY_EXISTS,
      LOCK_NOT_OWNED,
      LOCKED_BY_OTHER;

      public static RedisCommandExecutor.RedisCommandResultCode of(String code) {
         return switch (code) {
            case "ok" -> OK;
            case "not_found" -> NOT_FOUND;
            case "already_exists" -> ALREADY_EXISTS;
            case "lock_not_owned" -> LOCK_NOT_OWNED;
            case "locked_by_other" -> LOCKED_BY_OTHER;
            default -> throw new FailureException("Unknown Redis command result code: " + LogUtil.logSafeQuote(code));
         };
      }
   }

   record RedisLogMessage(RedisCommandExecutor.RedisLogMessage.RedisLogLevel logLevel, String message) {
      private void log() {
         switch (this.logLevel) {
            case DEBUG:
               RedisCommandExecutor.LOG.debug(this.message);
               break;
            case INFO:
               RedisCommandExecutor.LOG.info(EDefaultLogType.GENERIC_LOG_TYPE, this.message);
               break;
            case WARN:
               RedisCommandExecutor.LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, this.message);
               break;
            case ERROR:
               RedisCommandExecutor.LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, this.message);
               break;
            default:
               throw new FailureException("Unknown log level from redis script: " + LogUtil.logSafeQuote(this.logLevel));
         }
      }

      enum RedisLogLevel {
         DEBUG,
         INFO,
         WARN,
         ERROR;
      }
   }
}
