/**
 * Copyright © 2017 Jeremy Custenborder ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.jcustenborder.kafka.connect.redis;

import io.lettuce.core.AbstractRedisClient;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisConnectionException;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.SslOptions;
import io.lettuce.core.api.StatefulConnection;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
import io.lettuce.core.codec.ByteArrayCodec;
import io.lettuce.core.codec.RedisCodec;
import org.apache.kafka.common.utils.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;

class RedisSessionFactoryImpl implements RedisSessionFactory {
  Time time = Time.SYSTEM;

  private static final Logger log = LoggerFactory.getLogger(RedisSessionFactoryImpl.class);

  @Override
  public RedisSession create(RedisConnectorConfig config) {
    int attempts = 0;
    RedisSession result;

    while (true) {
      attempts++;
      try {
        log.info("Creating Redis session. Attempt {} of {}", attempts, config.maxAttempts);
        result = RedisSessionImpl.create(config);
        break;
      } catch (RedisConnectionException ex) {
        if (attempts == config.maxAttempts) {
          throw ex;
        } else {
          log.warn("Exception thrown connecting to redis. Waiting {} ms to try again.", config.retryDelay);
          this.time.sleep(config.retryDelay);
        }
      }
    }

    return result;
  }

  private static class RedisSessionImpl implements RedisSession {
    private static final Logger log = LoggerFactory.getLogger(RedisSessionImpl.class);

    private final AbstractRedisClient client;
    private final StatefulConnection connection;
    private final RedisClusterAsyncCommands<byte[], byte[]> asyncCommands;
    private final RedisConnectorConfig config;

    RedisSessionImpl(AbstractRedisClient client, StatefulConnection connection, RedisClusterAsyncCommands<byte[], byte[]> asyncCommands, RedisConnectorConfig config) {
      this.client = client;
      this.connection = connection;
      this.asyncCommands = asyncCommands;
      this.config = config;
    }

    public AbstractRedisClient client() {
      return this.client;
    }

    public StatefulConnection connection() {
      return this.connection;
    }

    public RedisClusterAsyncCommands<byte[], byte[]> asyncCommands() {
      return this.asyncCommands;
    }

    public static RedisSessionImpl create(RedisConnectorConfig config) {
      RedisSessionImpl result;
      final RedisCodec<byte[], byte[]> codec = new ByteArrayCodec();

      final SslOptions sslOptions;

      if (config.sslEnabled) {
        SslOptions.Builder builder = SslOptions.builder();
        switch (config.sslProvider) {
          case JDK:
            builder.jdkSslProvider();
            break;
          case OPENSSL:
            builder.openSslProvider();
            break;
          default:
            throw new UnsupportedOperationException(
                String.format(
                    "%s is not a supported value for %s.",
                    config.sslProvider,
                    RedisConnectorConfig.SSL_PROVIDER_CONFIG
                )
            );
        }
        if (null != config.keystorePath) {
          if (null != config.keystorePassword) {
            builder.keystore(config.keystorePath, config.keystorePassword.toCharArray());
          } else {
            builder.keystore(config.keystorePath);
          }
        }
        if (null != config.truststorePath) {
          if (null != config.truststorePassword) {
            builder.truststore(config.truststorePath, config.keystorePassword);
          } else {
            builder.truststore(config.truststorePath);
          }
        }
        sslOptions = builder.build();
      } else {
        sslOptions = null;
      }

      final SocketOptions socketOptions = SocketOptions.builder()
          .tcpNoDelay(config.tcpNoDelay)
          .connectTimeout(Duration.ofMillis(config.connectTimeout))
          .keepAlive(config.keepAliveEnabled)
          .build();


      if (RedisConnectorConfig.ClientMode.Cluster == config.clientMode) {
        ClusterClientOptions.Builder clientOptions = ClusterClientOptions.builder()
            .requestQueueSize(config.requestQueueSize)
            .autoReconnect(config.autoReconnectEnabled);
        if (config.sslEnabled) {
          clientOptions.sslOptions(sslOptions);
        }
        final RedisClusterClient client = RedisClusterClient.create(config.redisURIs());
        client.setOptions(clientOptions.build());

        final StatefulRedisClusterConnection<byte[], byte[]> connection = client.connect(codec);
        result = new RedisSessionImpl(client, connection, connection.async(), config);
      } else if (RedisConnectorConfig.ClientMode.Standalone == config.clientMode) {
        final ClientOptions.Builder clientOptions = ClientOptions.builder()
            .socketOptions(socketOptions)
            .requestQueueSize(config.requestQueueSize)
            .autoReconnect(config.autoReconnectEnabled);
        if (config.sslEnabled) {
          clientOptions.sslOptions(sslOptions);
        }
        final RedisClient client = RedisClient.create(config.redisURIs().get(0));
        client.setOptions(clientOptions.build());
        final StatefulRedisConnection<byte[], byte[]> connection = client.connect(codec);
        result = new RedisSessionImpl(client, connection, connection.async(), config);
      } else {
        throw new UnsupportedOperationException(
            String.format("%s is not supported", config.clientMode)
        );
      }

      return result;
    }


    @Override
    public void close() throws Exception {
      this.connection.close();
    }
  }
}