package cache

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"tercul/internal/platform/config"
	"tercul/internal/platform/log"
	"time"

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

// RedisCache implements the Cache interface using Redis
type RedisCache struct {
	client        *redis.Client
	keyGenerator  KeyGenerator
	defaultExpiry time.Duration
}

// NewRedisCache creates a new RedisCache
func NewRedisCache(client *redis.Client, keyGenerator KeyGenerator, defaultExpiry time.Duration) *RedisCache {
	if keyGenerator == nil {
		keyGenerator = NewDefaultKeyGenerator("")
	}

	if defaultExpiry == 0 {
		defaultExpiry = 1 * time.Hour // Default expiry of 1 hour
	}

	return &RedisCache{
		client:        client,
		keyGenerator:  keyGenerator,
		defaultExpiry: defaultExpiry,
	}
}

// NewDefaultRedisCache creates a new RedisCache with default settings
func NewDefaultRedisCache(cfg *config.Config) (*RedisCache, error) {
	// Create Redis client from config
	client := redis.NewClient(&redis.Options{
		Addr:     cfg.RedisAddr,
		Password: cfg.RedisPassword,
		DB:       cfg.RedisDB,
	})

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

	if err := client.Ping(ctx).Err(); err != nil {
		return nil, err
	}

	return NewRedisCache(client, nil, 0), nil
}

// Get retrieves a value from the cache
func (c *RedisCache) Get(ctx context.Context, key string, value interface{}) error {
	data, err := c.client.Get(ctx, key).Bytes()
	if err != nil {
		if err == redis.Nil {
			return errors.New("cache miss")
		}
		return err
	}

	return json.Unmarshal(data, value)
}

// Set stores a value in the cache with an optional expiration
func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
	data, err := json.Marshal(value)
	if err != nil {
		return err
	}

	if expiration == 0 {
		expiration = c.defaultExpiry
	}

	return c.client.Set(ctx, key, data, expiration).Err()
}

// Delete removes a value from the cache
func (c *RedisCache) Delete(ctx context.Context, key string) error {
	return c.client.Del(ctx, key).Err()
}

// Clear removes all values from the cache
func (c *RedisCache) Clear(ctx context.Context) error {
	return c.client.FlushAll(ctx).Err()
}

// GetMulti retrieves multiple values from the cache
func (c *RedisCache) GetMulti(ctx context.Context, keys []string) (map[string][]byte, error) {
	if len(keys) == 0 {
		return make(map[string][]byte), nil
	}

	values, err := c.client.MGet(ctx, keys...).Result()
	if err != nil {
		return nil, err
	}

	result := make(map[string][]byte, len(keys))
	for i, key := range keys {
		if values[i] == nil {
			continue
		}

		str, ok := values[i].(string)
		if !ok {
			log.FromContext(ctx).With("key", key).With("type", fmt.Sprintf("%T", values[i])).Warn("Invalid type in Redis cache")
			continue
		}

		result[key] = []byte(str)
	}

	return result, nil
}

// SetMulti stores multiple values in the cache with an optional expiration
func (c *RedisCache) SetMulti(ctx context.Context, items map[string]interface{}, expiration time.Duration) error {
	if len(items) == 0 {
		return nil
	}

	if expiration == 0 {
		expiration = c.defaultExpiry
	}

	pipe := c.client.Pipeline()
	for key, value := range items {
		data, err := json.Marshal(value)
		if err != nil {
			return err
		}

		pipe.Set(ctx, key, data, expiration)
	}

	_, err := pipe.Exec(ctx)
	return err
}

// GetEntity retrieves an entity by ID from the cache
func (c *RedisCache) GetEntity(ctx context.Context, entityType string, id uint, value interface{}) error {
	key := c.keyGenerator.EntityKey(entityType, id)
	return c.Get(ctx, key, value)
}

// SetEntity stores an entity in the cache
func (c *RedisCache) SetEntity(ctx context.Context, entityType string, id uint, value interface{}, expiration time.Duration) error {
	key := c.keyGenerator.EntityKey(entityType, id)
	return c.Set(ctx, key, value, expiration)
}

// DeleteEntity removes an entity from the cache
func (c *RedisCache) DeleteEntity(ctx context.Context, entityType string, id uint) error {
	key := c.keyGenerator.EntityKey(entityType, id)
	return c.Delete(ctx, key)
}

// GetList retrieves a list of entities from the cache
func (c *RedisCache) GetList(ctx context.Context, entityType string, page, pageSize int, value interface{}) error {
	key := c.keyGenerator.ListKey(entityType, page, pageSize)
	return c.Get(ctx, key, value)
}

// SetList stores a list of entities in the cache
func (c *RedisCache) SetList(ctx context.Context, entityType string, page, pageSize int, value interface{}, expiration time.Duration) error {
	key := c.keyGenerator.ListKey(entityType, page, pageSize)
	return c.Set(ctx, key, value, expiration)
}

// DeleteList removes a list of entities from the cache
func (c *RedisCache) DeleteList(ctx context.Context, entityType string, page, pageSize int) error {
	key := c.keyGenerator.ListKey(entityType, page, pageSize)
	return c.Delete(ctx, key)
}

// InvalidateEntityType removes all cached data for a specific entity type
func (c *RedisCache) InvalidateEntityType(ctx context.Context, entityType string) error {
	pattern := c.keyGenerator.(*DefaultKeyGenerator).Prefix + entityType + ":*"

	// Use SCAN to find all keys matching the pattern
	iter := c.client.Scan(ctx, 0, pattern, 100).Iterator()
	var keys []string

	for iter.Next(ctx) {
		keys = append(keys, iter.Val())

		// Delete in batches of 100 to avoid large operations
		if len(keys) >= 100 {
			if err := c.client.Del(ctx, keys...).Err(); err != nil {
				return err
			}
			keys = keys[:0]
		}
	}

	// Delete any remaining keys
	if len(keys) > 0 {
		return c.client.Del(ctx, keys...).Err()
	}

	return iter.Err()
}