import time
import hashlib
import pickle
from typing import Any, Optional, Callable
from functools import wraps
import os


class CacheBackend:
    """Base cache backend"""
    
    def get(self, key: str) -> Optional[Any]:
        raise NotImplementedError
    
    def set(self, key: str, value: Any, ttl: int = 300):
        raise NotImplementedError
    
    def delete(self, key: str):
        raise NotImplementedError
    
    def clear(self):
        raise NotImplementedError


class MemoryCache(CacheBackend):
    """In-memory cache backend"""
    
    def __init__(self):
        self._cache = {}
    
    def get(self, key: str) -> Optional[Any]:
        if key not in self._cache:
            return None
        
        expiry, value = self._cache[key]
        if time.time() > expiry:
            del self._cache[key]
            return None
        
        return value
    
    def set(self, key: str, value: Any, ttl: int = 300):
        expiry = time.time() + ttl
        self._cache[key] = (expiry, value)
    
    def delete(self, key: str):
        if key in self._cache:
            del self._cache[key]
    
    def clear(self):
        self._cache.clear()


class RedisCache(CacheBackend):
    """Redis cache backend"""
    
    def __init__(self, host: str = 'localhost', port: int = 6379, db: int = 0):
        try:
            import redis
            self.redis = redis.Redis(host=host, port=port, db=db)
        except ImportError:
            raise ImportError("redis package required for RedisCache")
    
    def get(self, key: str) -> Optional[Any]:
        value = self.redis.get(key)
        if value is None:
            return None
        return pickle.loads(value)
    
    def set(self, key: str, value: Any, ttl: int = 300):
        self.redis.setex(key, ttl, pickle.dumps(value))
    
    def delete(self, key: str):
        self.redis.delete(key)
    
    def clear(self):
        self.redis.flushdb()


class CacheManager:
    """Cache manager with decorator support"""
    
    def __init__(self, backend: CacheBackend = None):
        self.backend = backend or MemoryCache()
    
    def get(self, key: str) -> Optional[Any]:
        """Get value from cache"""
        return self.backend.get(key)
    
    def set(self, key: str, value: Any, ttl: int = 300):
        """Set value in cache"""
        self.backend.set(key, value, ttl)
    
    def delete(self, key: str):
        """Delete value from cache"""
        self.backend.delete(key)
    
    def clear(self):
        """Clear all cache"""
        self.backend.clear()
    
    def cached(self, ttl: int = 300, key_prefix: str = ''):
        """Decorator to cache function results"""
        def decorator(func: Callable):
            @wraps(func)
            def wrapper(*args, **kwargs):
                cache_key = self._generate_key(func, key_prefix, args, kwargs)
                
                cached_value = self.backend.get(cache_key)
                if cached_value is not None:
                    return cached_value
                
                result = func(*args, **kwargs)
                self.backend.set(cache_key, result, ttl)
                return result
            
            wrapper.cache_clear = lambda: self.backend.clear()
            return wrapper
        
        return decorator
    
    def memoize(self, func: Callable):
        """Memoize function results (cache forever)"""
        cache = {}
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            key = self._generate_key(func, '', args, kwargs)
            
            if key not in cache:
                cache[key] = func(*args, **kwargs)
            
            return cache[key]
        
        wrapper.cache = cache
        wrapper.cache_clear = cache.clear
        return wrapper
    
    def _generate_key(self, func: Callable, prefix: str, args: tuple, kwargs: dict) -> str:
        """Generate cache key"""
        key_parts = [
            prefix or func.__module__,
            func.__name__,
            str(args),
            str(sorted(kwargs.items()))
        ]
        
        key_string = '|'.join(key_parts)
        return hashlib.md5(key_string.encode()).hexdigest()


cache = CacheManager()


def use_redis_cache():
    """Switch to Redis cache backend"""
    redis_host = os.environ.get('REDIS_HOST', 'localhost')
    redis_port = int(os.environ.get('REDIS_PORT', 6379))
    cache.backend = RedisCache(host=redis_host, port=redis_port)
