package cache

import (
	"context"
	"time"

	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"

	"github.com/kms/api-key-service/internal/config"
	"github.com/kms/api-key-service/internal/errors"
)

// RedisCache implements CacheProvider using Redis
type RedisCache struct {
	client *redis.Client
	config config.ConfigProvider
	logger *zap.Logger
}

// NewRedisCache creates a new Redis cache provider
func NewRedisCache(config config.ConfigProvider, logger *zap.Logger) (CacheProvider, error) {
	// Redis configuration
	redisAddr := config.GetString("REDIS_ADDR")
	if redisAddr == "" {
		redisAddr = "localhost:6379"
	}

	redisPassword := config.GetString("REDIS_PASSWORD")
	redisDB := config.GetInt("REDIS_DB")

	// Create Redis client
	client := redis.NewClient(&redis.Options{
		Addr:         redisAddr,
		Password:     redisPassword,
		DB:           redisDB,
		PoolSize:     config.GetInt("REDIS_POOL_SIZE"),
		MinIdleConns: config.GetInt("REDIS_MIN_IDLE_CONNS"),
		MaxRetries:   config.GetInt("REDIS_MAX_RETRIES"),
		DialTimeout:  config.GetDuration("REDIS_DIAL_TIMEOUT"),
		ReadTimeout:  config.GetDuration("REDIS_READ_TIMEOUT"),
		WriteTimeout: config.GetDuration("REDIS_WRITE_TIMEOUT"),
	})

	// Test connection
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := client.Ping(ctx).Err(); err != nil {
		logger.Error("Failed to connect to Redis", zap.Error(err))
		return nil, errors.NewInternalError("Failed to connect to Redis").WithInternal(err)
	}

	logger.Info("Connected to Redis successfully", zap.String("addr", redisAddr))

	return &RedisCache{
		client: client,
		config: config,
		logger: logger,
	}, nil
}

// Get retrieves a value from Redis cache
func (r *RedisCache) Get(ctx context.Context, key string) ([]byte, error) {
	r.logger.Debug("Getting value from Redis cache", zap.String("key", key))

	result, err := r.client.Get(ctx, key).Result()
	if err != nil {
		if err == redis.Nil {
			return nil, errors.NewNotFoundError("cache key")
		}
		r.logger.Error("Failed to get value from Redis", zap.Error(err))
		return nil, errors.NewInternalError("Failed to get cached value").WithInternal(err)
	}

	return []byte(result), nil
}

// Set stores a value in Redis cache with TTL
func (r *RedisCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
	r.logger.Debug("Setting value in Redis cache", 
		zap.String("key", key), 
		zap.Duration("ttl", ttl))

	err := r.client.Set(ctx, key, value, ttl).Err()
	if err != nil {
		r.logger.Error("Failed to set value in Redis", zap.Error(err))
		return errors.NewInternalError("Failed to cache value").WithInternal(err)
	}

	return nil
}

// Delete removes a value from Redis cache
func (r *RedisCache) Delete(ctx context.Context, key string) error {
	r.logger.Debug("Deleting value from Redis cache", zap.String("key", key))

	err := r.client.Del(ctx, key).Err()
	if err != nil {
		r.logger.Error("Failed to delete value from Redis", zap.Error(err))
		return errors.NewInternalError("Failed to delete cached value").WithInternal(err)
	}

	return nil
}

// Exists checks if a key exists in Redis cache
func (r *RedisCache) Exists(ctx context.Context, key string) (bool, error) {
	count, err := r.client.Exists(ctx, key).Result()
	if err != nil {
		r.logger.Error("Failed to check key existence in Redis", zap.Error(err))
		return false, errors.NewInternalError("Failed to check cache key existence").WithInternal(err)
	}

	return count > 0, nil
}

// Clear removes all values from Redis cache (use with caution)
func (r *RedisCache) Clear(ctx context.Context) error {
	r.logger.Warn("Clearing Redis cache - this will remove ALL cached data")

	err := r.client.FlushDB(ctx).Err()
	if err != nil {
		r.logger.Error("Failed to clear Redis cache", zap.Error(err))
		return errors.NewInternalError("Failed to clear cache").WithInternal(err)
	}

	return nil
}

// Close closes the Redis connection
func (r *RedisCache) Close() error {
	r.logger.Info("Closing Redis connection")
	return r.client.Close()
}

// SetNX sets a key only if it doesn't exist (Redis-specific operation)
func (r *RedisCache) SetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error) {
	r.logger.Debug("Setting value in Redis cache with NX", 
		zap.String("key", key), 
		zap.Duration("ttl", ttl))

	result, err := r.client.SetNX(ctx, key, value, ttl).Result()
	if err != nil {
		r.logger.Error("Failed to set NX value in Redis", zap.Error(err))
		return false, errors.NewInternalError("Failed to cache value with NX").WithInternal(err)
	}

	return result, nil
}

// Expire sets TTL for an existing key
func (r *RedisCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
	r.logger.Debug("Setting TTL for Redis key", 
		zap.String("key", key), 
		zap.Duration("ttl", ttl))

	result, err := r.client.Expire(ctx, key, ttl).Result()
	if err != nil {
		r.logger.Error("Failed to set TTL in Redis", zap.Error(err))
		return errors.NewInternalError("Failed to set key TTL").WithInternal(err)
	}

	if !result {
		return errors.NewNotFoundError("cache key")
	}

	return nil
}

// TTL returns the remaining time to live for a key
func (r *RedisCache) TTL(ctx context.Context, key string) (time.Duration, error) {
	ttl, err := r.client.TTL(ctx, key).Result()
	if err != nil {
		r.logger.Error("Failed to get TTL from Redis", zap.Error(err))
		return 0, errors.NewInternalError("Failed to get key TTL").WithInternal(err)
	}

	return ttl, nil
}

// Keys returns all keys matching a pattern
func (r *RedisCache) Keys(ctx context.Context, pattern string) ([]string, error) {
	keys, err := r.client.Keys(ctx, pattern).Result()
	if err != nil {
		r.logger.Error("Failed to get keys from Redis", zap.Error(err))
		return nil, errors.NewInternalError("Failed to get cache keys").WithInternal(err)
	}

	return keys, nil
}
