package cache

import (
	"context"
	"errors"
	"path/filepath"
	"sync"
	"time"

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

type Cache struct {
	client   *redis.Client
	localTTL time.Duration
	localMu  sync.RWMutex
	local    map[string]localEntry
}

type localEntry struct {
	value   string
	expires time.Time
}

func New(client *redis.Client, localTTL time.Duration) *Cache {
	if localTTL <= 0 {
		localTTL = 30 * time.Second
	}
	return &Cache{
		client:   client,
		localTTL: localTTL,
		local:    make(map[string]localEntry),
	}
}

func (c *Cache) Get(ctx context.Context, key string) (string, error) {
	if c == nil {
		return "", nil
	}
	if val, ok := c.getLocal(key); ok {
		return val, nil
	}
	if c.client == nil {
		return "", nil
	}
	result, err := c.client.Get(ctx, key).Result()
	if err != nil {
		if errors.Is(err, redis.Nil) {
			return "", nil
		}
		return "", err
	}
	c.setLocal(key, result)
	return result, nil
}

func (c *Cache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
	if c == nil {
		return nil
	}
	switch val := value.(type) {
	case string:
		c.setLocalWithTTL(key, val, ttl)
	case []byte:
		c.setLocalWithTTL(key, string(val), ttl)
	}
	if c.client == nil {
		return nil
	}
	return c.client.Set(ctx, key, value, ttl).Err()
}

func (c *Cache) Delete(ctx context.Context, key string) error {
	if c == nil {
		return nil
	}
	c.deleteLocal(key)
	if c.client == nil {
		return nil
	}
	return c.client.Del(ctx, key).Err()
}

func (c *Cache) InvalidateByPattern(ctx context.Context, pattern string) error {
	if c == nil {
		return nil
	}
	c.invalidateLocal(pattern)
	if c.client == nil {
		return nil
	}
	iter := c.client.Scan(ctx, 0, pattern, 0).Iterator()
	for iter.Next(ctx) {
		if err := c.client.Del(ctx, iter.Val()).Err(); err != nil {
			return err
		}
	}
	return iter.Err()
}

func (c *Cache) getLocal(key string) (string, bool) {
	c.localMu.RLock()
	entry, ok := c.local[key]
	c.localMu.RUnlock()
	if !ok {
		return "", false
	}
	if time.Now().After(entry.expires) {
		c.deleteLocal(key)
		return "", false
	}
	return entry.value, true
}

func (c *Cache) setLocal(key, value string) {
	c.setLocalWithTTL(key, value, c.localTTL)
}

func (c *Cache) setLocalWithTTL(key, value string, ttl time.Duration) {
	if ttl <= 0 {
		ttl = c.localTTL
	}
	c.localMu.Lock()
	c.local[key] = localEntry{value: value, expires: time.Now().Add(ttl)}
	c.localMu.Unlock()
}

func (c *Cache) deleteLocal(key string) {
	c.localMu.Lock()
	delete(c.local, key)
	c.localMu.Unlock()
}

func (c *Cache) invalidateLocal(pattern string) {
	c.localMu.Lock()
	defer c.localMu.Unlock()
	for key := range c.local {
		matched, err := pathMatch(pattern, key)
		if err == nil && matched {
			delete(c.local, key)
		}
	}
}

// pathMatch provides simple glob-style pattern matching for cache keys.
func pathMatch(pattern, key string) (bool, error) {
	if pattern == "" || pattern == "*" {
		return true, nil
	}
	return filepath.Match(pattern, key)
}
