package cache

import (
	"container/list"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"sync"
	"time"

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

// CacheInterface defines the cache operations
type CacheInterface interface {
	Get(key string) ([]byte, bool)
	Set(key string, value []byte, ttl time.Duration) error
	Delete(key string) error
	Clear() error
	Stats() CacheStats
}

// CacheStats provides cache statistics
type CacheStats struct {
	Hits        int64   `json:"hits"`
	Misses      int64   `json:"misses"`
	Size        int     `json:"size"`
	MaxSize     int     `json:"max_size"`
	HitRate     float64 `json:"hit_rate"`
	MemoryUsage int64   `json:"memory_usage_bytes"`
}

// LRUCache implements an in-memory LRU cache
type LRUCache struct {
	maxSize    int
	items      map[string]*list.Element
	evictList  *list.List
	hits       int64
	misses     int64
	memoryUsed int64
	mutex      sync.RWMutex
}

// cacheItem represents an item in the cache
type cacheItem struct {
	key      string
	value    []byte
	expiry   time.Time
	size     int64
	accessed time.Time
}

// NewLRUCache creates a new LRU cache
func NewLRUCache(maxSize int) *LRUCache {
	return &LRUCache{
		maxSize:   maxSize,
		items:     make(map[string]*list.Element),
		evictList: list.New(),
	}
}

// Get retrieves an item from the cache
func (c *LRUCache) Get(key string) ([]byte, bool) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	if element, exists := c.items[key]; exists {
		item := element.Value.(*cacheItem)
		
		// Check expiry
		if !item.expiry.IsZero() && time.Now().After(item.expiry) {
			c.removeElement(element)
			c.misses++
			return nil, false
		}

		// Move to front (most recently used)
		c.evictList.MoveToFront(element)
		item.accessed = time.Now()
		c.hits++
		return item.value, true
	}

	c.misses++
	return nil, false
}

// Set adds an item to the cache
func (c *LRUCache) Set(key string, value []byte, ttl time.Duration) error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	// Check if item already exists
	if element, exists := c.items[key]; exists {
		// Update existing item
		item := element.Value.(*cacheItem)
		c.memoryUsed -= item.size
		
		item.value = value
		item.size = int64(len(value))
		item.accessed = time.Now()
		
		if ttl > 0 {
			item.expiry = time.Now().Add(ttl)
		} else {
			item.expiry = time.Time{}
		}
		
		c.memoryUsed += item.size
		c.evictList.MoveToFront(element)
		return nil
	}

	// Add new item
	now := time.Now()
	item := &cacheItem{
		key:      key,
		value:    value,
		size:     int64(len(value)),
		accessed: now,
	}
	
	if ttl > 0 {
		item.expiry = now.Add(ttl)
	}

	element := c.evictList.PushFront(item)
	c.items[key] = element
	c.memoryUsed += item.size

	// Evict if necessary
	c.evictIfNeeded()

	return nil
}

// Delete removes an item from the cache
func (c *LRUCache) Delete(key string) error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	if element, exists := c.items[key]; exists {
		c.removeElement(element)
	}
	return nil
}

// Clear removes all items from the cache
func (c *LRUCache) Clear() error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	c.items = make(map[string]*list.Element)
	c.evictList.Init()
	c.memoryUsed = 0
	return nil
}

// Stats returns cache statistics
func (c *LRUCache) Stats() CacheStats {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	total := c.hits + c.misses
	hitRate := 0.0
	if total > 0 {
		hitRate = float64(c.hits) / float64(total)
	}

	return CacheStats{
		Hits:        c.hits,
		Misses:      c.misses,
		Size:        len(c.items),
		MaxSize:     c.maxSize,
		HitRate:     hitRate,
		MemoryUsage: c.memoryUsed,
	}
}

// evictIfNeeded removes old items if cache is full
func (c *LRUCache) evictIfNeeded() {
	for len(c.items) > c.maxSize {
		c.evictOldest()
	}
}

// evictOldest removes the least recently used item
func (c *LRUCache) evictOldest() {
	element := c.evictList.Back()
	if element != nil {
		c.removeElement(element)
	}
}

// removeElement removes an element from the cache
func (c *LRUCache) removeElement(element *list.Element) {
	c.evictList.Remove(element)
	item := element.Value.(*cacheItem)
	delete(c.items, item.key)
	c.memoryUsed -= item.size
}

// RedisCache implements cache using Redis
type RedisCache struct {
	client *redis.Client
	prefix string
	hits   int64
	misses int64
	mutex  sync.RWMutex
}

// NewRedisCache creates a new Redis-backed cache
func NewRedisCache(addr, password string, db int, prefix string) (*RedisCache, error) {
	client := redis.NewClient(&redis.Options{
		Addr:     addr,
		Password: password,
		DB:       db,
	})

	// Test connection
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	
	if err := client.Ping(ctx).Err(); err != nil {
		return nil, fmt.Errorf("failed to connect to Redis: %w", err)
	}

	return &RedisCache{
		client: client,
		prefix: prefix,
	}, nil
}

// Get retrieves an item from Redis cache
func (r *RedisCache) Get(key string) ([]byte, bool) {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	value, err := r.client.Get(ctx, r.prefix+key).Bytes()
	if err == redis.Nil {
		r.mutex.Lock()
		r.misses++
		r.mutex.Unlock()
		return nil, false
	} else if err != nil {
		log.Printf("Redis cache error: %v", err)
		r.mutex.Lock()
		r.misses++
		r.mutex.Unlock()
		return nil, false
	}

	r.mutex.Lock()
	r.hits++
	r.mutex.Unlock()
	return value, true
}

// Set adds an item to Redis cache
func (r *RedisCache) Set(key string, value []byte, ttl time.Duration) error {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	return r.client.Set(ctx, r.prefix+key, value, ttl).Err()
}

// Delete removes an item from Redis cache
func (r *RedisCache) Delete(key string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	return r.client.Del(ctx, r.prefix+key).Err()
}

// Clear removes all items with the cache prefix
func (r *RedisCache) Clear() error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// Use SCAN to find all keys with prefix
	iter := r.client.Scan(ctx, 0, r.prefix+"*", 0).Iterator()
	var keys []string
	
	for iter.Next(ctx) {
		keys = append(keys, iter.Val())
	}
	
	if err := iter.Err(); err != nil {
		return err
	}

	if len(keys) > 0 {
		return r.client.Del(ctx, keys...).Err()
	}
	
	return nil
}

// Stats returns Redis cache statistics
func (r *RedisCache) Stats() CacheStats {
	r.mutex.RLock()
	defer r.mutex.RUnlock()

	total := r.hits + r.misses
	hitRate := 0.0
	if total > 0 {
		hitRate = float64(r.hits) / float64(total)
	}

	return CacheStats{
		Hits:    r.hits,
		Misses:  r.misses,
		HitRate: hitRate,
	}
}

// TieredCache combines LRU and Redis for hot/warm caching
type TieredCache struct {
	l1 *LRUCache    // Hot cache (in-memory)
	l2 *RedisCache  // Warm cache (Redis)
	l1Size int      // L1 cache size limit
}

// NewTieredCache creates a tiered cache system
func NewTieredCache(l1Size int, redisAddr, redisPassword string, redisDB int) (*TieredCache, error) {
	l1 := NewLRUCache(l1Size)
	
	var l2 *RedisCache
	var err error
	if redisAddr != "" {
		l2, err = NewRedisCache(redisAddr, redisPassword, redisDB, "gateway:")
		if err != nil {
			log.Printf("Warning: Redis cache unavailable, using L1 only: %v", err)
		}
	}

	return &TieredCache{
		l1:     l1,
		l2:     l2,
		l1Size: l1Size,
	}, nil
}

// Get retrieves from L1 first, then L2
func (t *TieredCache) Get(key string) ([]byte, bool) {
	// Try L1 first
	if value, found := t.l1.Get(key); found {
		return value, true
	}

	// Try L2 if available
	if t.l2 != nil {
		if value, found := t.l2.Get(key); found {
			// Promote to L1
			t.l1.Set(key, value, 15*time.Minute)
			return value, true
		}
	}

	return nil, false
}

// Set stores in both L1 and L2
func (t *TieredCache) Set(key string, value []byte, ttl time.Duration) error {
	// Store in L1
	if err := t.l1.Set(key, value, ttl); err != nil {
		return err
	}

	// Store in L2 if available
	if t.l2 != nil {
		// Use longer TTL for L2
		l2TTL := ttl * 2
		if l2TTL > 24*time.Hour {
			l2TTL = 24 * time.Hour
		}
		return t.l2.Set(key, value, l2TTL)
	}

	return nil
}

// Delete removes from both caches
func (t *TieredCache) Delete(key string) error {
	t.l1.Delete(key)
	if t.l2 != nil {
		t.l2.Delete(key)
	}
	return nil
}

// Clear removes all items from both caches
func (t *TieredCache) Clear() error {
	t.l1.Clear()
	if t.l2 != nil {
		t.l2.Clear()
	}
	return nil
}

// Stats returns combined cache statistics
func (t *TieredCache) Stats() CacheStats {
	l1Stats := t.l1.Stats()
	
	if t.l2 != nil {
		l2Stats := t.l2.Stats()
		return CacheStats{
			Hits:        l1Stats.Hits + l2Stats.Hits,
			Misses:      l1Stats.Misses + l2Stats.Misses,
			Size:        l1Stats.Size,
			MaxSize:     l1Stats.MaxSize,
			HitRate:     float64(l1Stats.Hits+l2Stats.Hits) / float64(l1Stats.Hits+l1Stats.Misses+l2Stats.Hits+l2Stats.Misses),
			MemoryUsage: l1Stats.MemoryUsage,
		}
	}
	
	return l1Stats
}