package cache

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"strings"
	"time"

	redis "github.com/redis/go-redis/v9"
)

// ProbeRedis 探测 Redis 是否可达。
//
// 使用 2 秒超时发起 PING，成功返回 true，任何错误（连接拒绝、超时、DNS 解析失败）
// 均返回 false 并打印 warn 日志，调用方可据此决定是否启用 Redis。
//
// 此函数是幂等的，可在启动阶段安全调用多次。
func ProbeRedis(addr, password string, db int) bool {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	client := redis.NewClient(&redis.Options{
		Addr:        addr,
		Password:    password,
		DB:          db,
		DialTimeout: 2 * time.Second,
	})
	defer client.Close()

	if err := client.Ping(ctx).Err(); err != nil {
		log.Printf("redis probe: unreachable at %s — falling back to in-memory only (%v)", addr, err)
		return false
	}
	log.Printf("redis probe: reachable at %s — Redis L2 cache will be enabled", addr)
	return true
}

// L2Cache defines the distributed cache contract.
type L2Cache interface {
	Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error
	Get(ctx context.Context, key string) (interface{}, error)
	Delete(ctx context.Context, key string) error
	Exists(ctx context.Context, key string) (bool, error)
	Clear(ctx context.Context) error
	Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error)
	Close() error
}

// RedisCacheConfig configures the Redis-backed L2 cache.
type RedisCacheConfig struct {
	Enabled  bool
	Addr     string
	Password string
	DB       int
	PoolSize int
}

// RedisCache implements L2Cache using Redis.
type RedisCache struct {
	enabled bool
	client  *redis.Client
}

// NewRedisCache keeps the old test-friendly constructor.
func NewRedisCache(enabled bool) *RedisCache {
	return NewRedisCacheWithConfig(RedisCacheConfig{Enabled: enabled})
}

// NewRedisCacheWithConfig creates a Redis-backed L2 cache.
func NewRedisCacheWithConfig(cfg RedisCacheConfig) *RedisCache {
	cache := &RedisCache{enabled: cfg.Enabled}
	if !cfg.Enabled {
		return cache
	}

	addr := cfg.Addr
	if addr == "" {
		addr = "localhost:6379"
	}

	options := &redis.Options{
		Addr:     addr,
		Password: cfg.Password,
		DB:       cfg.DB,
	}
	if cfg.PoolSize > 0 {
		options.PoolSize = cfg.PoolSize
	}

	cache.client = redis.NewClient(options)
	return cache
}

func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
	if !c.enabled || c.client == nil {
		return nil
	}

	payload, err := json.Marshal(value)
	if err != nil {
		return err
	}

	return c.client.Set(ctx, key, payload, ttl).Err()
}

func (c *RedisCache) Get(ctx context.Context, key string) (interface{}, error) {
	if !c.enabled || c.client == nil {
		return nil, nil
	}

	raw, err := c.client.Get(ctx, key).Result()
	if errors.Is(err, redis.Nil) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	return decodeRedisValue(raw)
}

func (c *RedisCache) Delete(ctx context.Context, key string) error {
	if !c.enabled || c.client == nil {
		return nil
	}
	return c.client.Del(ctx, key).Err()
}

func (c *RedisCache) Exists(ctx context.Context, key string) (bool, error) {
	if !c.enabled || c.client == nil {
		return false, nil
	}

	count, err := c.client.Exists(ctx, key).Result()
	if err != nil {
		return false, err
	}
	return count > 0, nil
}

func (c *RedisCache) Clear(ctx context.Context) error {
	if !c.enabled || c.client == nil {
		return nil
	}
	return c.client.FlushDB(ctx).Err()
}

func (c *RedisCache) Close() error {
	if !c.enabled || c.client == nil {
		return nil
	}
	return c.client.Close()
}

func (c *RedisCache) Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error) {
	if !c.enabled || c.client == nil {
		return 0, errors.New("redis is not enabled")
	}
	result, err := c.client.IncrBy(ctx, key, delta).Result()
	if err != nil {
		return 0, err
	}
	if ttl > 0 {
		c.client.Expire(ctx, key, ttl)
	}
	return result, nil
}

func decodeRedisValue(raw string) (interface{}, error) {
	decoder := json.NewDecoder(strings.NewReader(raw))
	decoder.UseNumber()

	var value interface{}
	if err := decoder.Decode(&value); err != nil {
		return raw, nil
	}

	return normalizeRedisValue(value), nil
}

func normalizeRedisValue(value interface{}) interface{} {
	switch v := value.(type) {
	case json.Number:
		if n, err := v.Int64(); err == nil {
			return n
		}
		if n, err := v.Float64(); err == nil {
			return n
		}
		return v.String()
	case []interface{}:
		for i := range v {
			v[i] = normalizeRedisValue(v[i])
		}
		return v
	case map[string]interface{}:
		for key, item := range v {
			v[key] = normalizeRedisValue(item)
		}
		return v
	default:
		return v
	}
}
