"""
Performance optimization features including memory management and caching
"""
from typing import Dict, Any, Optional, List, TypeVar, Generic, Callable, OrderedDict as TypingOrderedDict
import weakref
import threading
import time
from collections import deque, OrderedDict
from .parser_config import ParserConfig

T = TypeVar('T')


class ObjectPool(Generic[T]):
    """Generic object pool for memory management optimization."""
    
    def __init__(self, factory: Callable[[], T], max_size: int = 100, cleanup: Optional[Callable[[T], None]] = None):
        """
        Initialize object pool.
        
        Args:
            factory: Function to create new objects
            max_size: Maximum number of objects to pool
            cleanup: Optional cleanup function called when returning objects
        """
        self._factory = factory
        self._max_size = max_size
        self._cleanup = cleanup
        self._pool = deque()
        self._lock = threading.Lock()
        self._created_count = 0
        self._acquired_count = 0
        self._returned_count = 0
    
    def acquire(self) -> T:
        """Acquire an object from the pool."""
        with self._lock:
            if self._pool:
                obj = self._pool.popleft()
                self._acquired_count += 1
                return obj
            else:
                # Create new object
                obj = self._factory()
                self._created_count += 1
                self._acquired_count += 1
                return obj
    
    def release(self, obj: T) -> None:
        """Return an object to the pool."""
        if obj is None:
            return
        
        with self._lock:
            # Cleanup object if cleanup function provided
            if self._cleanup:
                try:
                    self._cleanup(obj)
                except Exception:
                    # If cleanup fails, don't return to pool
                    return
            
            # Only return to pool if we haven't exceeded max size
            if len(self._pool) < self._max_size:
                self._pool.append(obj)
                self._returned_count += 1
    
    def get_stats(self) -> Dict[str, int]:
        """Get pool statistics."""
        with self._lock:
            return {
                'pool_size': len(self._pool),
                'max_size': self._max_size,
                'created_count': self._created_count,
                'acquired_count': self._acquired_count,
                'returned_count': self._returned_count,
                'hit_rate': self._returned_count / max(self._acquired_count, 1)
            }
    
    def clear(self) -> None:
        """Clear all objects from pool."""
        with self._lock:
            self._pool.clear()


class LRUCache(Generic[T]):
    """Least Recently Used cache implementation."""
    
    def __init__(self, max_size: int = 128):
        self._max_size = max_size
        self._cache: TypingOrderedDict[Any, T] = OrderedDict()
        self._lock = threading.RLock()
        self._hits = 0
        self._misses = 0
    
    def get(self, key: Any) -> Optional[T]:
        """Get item from cache."""
        with self._lock:
            if key in self._cache:
                # Move to end (most recently used)
                value = self._cache.pop(key)
                self._cache[key] = value
                self._hits += 1
                return value
            else:
                self._misses += 1
                return None
    
    def put(self, key: Any, value: T) -> None:
        """Put item in cache."""
        with self._lock:
            if key in self._cache:
                # Update existing
                self._cache.pop(key)
            elif len(self._cache) >= self._max_size:
                # Remove least recently used
                self._cache.popitem(last=False)
            
            self._cache[key] = value
    
    def remove(self, key: Any) -> bool:
        """Remove item from cache."""
        with self._lock:
            if key in self._cache:
                del self._cache[key]
                return True
            return False
    
    def clear(self) -> None:
        """Clear all items from cache."""
        with self._lock:
            self._cache.clear()
            self._hits = 0
            self._misses = 0
    
    def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        with self._lock:
            total_requests = self._hits + self._misses
            return {
                'size': len(self._cache),
                'max_size': self._max_size,
                'hits': self._hits,
                'misses': self._misses,
                'hit_rate': self._hits / max(total_requests, 1),
                'utilization': len(self._cache) / self._max_size
            }


class MemoryManager:
    """Memory management system for CutySoup."""
    
    def __init__(self, config: ParserConfig):
        self.config = config
        self._pools: Dict[str, ObjectPool] = {}
        self._caches: Dict[str, LRUCache] = {}
        self._weak_refs: weakref.WeakSet = weakref.WeakSet()
        self._memory_threshold = config.get('memory_limit_mb', 500) * 1024 * 1024  # Convert to bytes
        self._cleanup_interval = 60  # seconds
        self._last_cleanup = time.time()
    
    def create_pool(self, name: str, factory: Callable[[], T], 
                   max_size: int = None, cleanup: Optional[Callable[[T], None]] = None) -> ObjectPool[T]:
        """Create a new object pool."""
        if max_size is None:
            max_size = self.config.get('cache_size_limit', 100)
        
        pool = ObjectPool(factory, max_size, cleanup)
        self._pools[name] = pool
        return pool
    
    def get_pool(self, name: str) -> Optional[ObjectPool]:
        """Get existing object pool."""
        return self._pools.get(name)
    
    def create_cache(self, name: str, max_size: int = None) -> LRUCache:
        """Create a new LRU cache."""
        if max_size is None:
            max_size = self.config.get('cache_size_limit', 128)
        
        cache = LRUCache(max_size)
        self._caches[name] = cache
        return cache
    
    def get_cache(self, name: str) -> Optional[LRUCache]:
        """Get existing cache."""
        return self._caches.get(name)
    
    def register_object(self, obj: Any) -> None:
        """Register object for memory tracking."""
        try:
            self._weak_refs.add(obj)
        except TypeError:
            # Object doesn't support weak references
            pass
    
    def cleanup(self, force: bool = False) -> Dict[str, Any]:
        """Perform memory cleanup."""
        current_time = time.time()
        
        if not force and (current_time - self._last_cleanup) < self._cleanup_interval:
            return {"skipped": True, "reason": "cleanup_interval_not_reached"}
        
        stats = {
            "pools_cleared": 0,
            "caches_cleared": 0,
            "objects_tracked": len(self._weak_refs),
            "memory_freed": 0
        }
        
        # Check memory usage and perform cleanup if needed
        if self._should_cleanup():
            # Clear least recently used items from caches
            for cache in self._caches.values():
                old_size = cache.get_stats()['size']
                # Remove 25% of cache items
                items_to_remove = max(1, old_size // 4)
                for _ in range(items_to_remove):
                    if cache._cache:
                        cache._cache.popitem(last=False)
                
                new_size = cache.get_stats()['size']
                stats["caches_cleared"] += old_size - new_size
            
            # Optionally clear some pools
            for pool in self._pools.values():
                old_size = len(pool._pool)
                # Keep only half the objects in pools
                target_size = max(0, old_size // 2)
                while len(pool._pool) > target_size:
                    pool._pool.pop()
                
                new_size = len(pool._pool)
                stats["pools_cleared"] += old_size - new_size
        
        self._last_cleanup = current_time
        return stats
    
    def _should_cleanup(self) -> bool:
        """Check if cleanup should be performed."""
        # Simple heuristic based on number of tracked objects
        # In a real implementation, this would check actual memory usage
        return len(self._weak_refs) > 1000
    
    def get_memory_stats(self) -> Dict[str, Any]:
        """Get comprehensive memory statistics."""
        stats = {
            "pools": {},
            "caches": {},
            "tracked_objects": len(self._weak_refs),
            "memory_threshold_mb": self._memory_threshold / (1024 * 1024),
            "last_cleanup": self._last_cleanup
        }
        
        for name, pool in self._pools.items():
            stats["pools"][name] = pool.get_stats()
        
        for name, cache in self._caches.items():
            stats["caches"][name] = cache.get_stats()
        
        return stats
    
    def reset(self) -> None:
        """Reset all pools and caches."""
        for pool in self._pools.values():
            pool.clear()
        
        for cache in self._caches.values():
            cache.clear()
        
        self._weak_refs.clear()


class PerformanceMonitor:
    """Performance monitoring and profiling."""
    
    def __init__(self):
        self._timers: Dict[str, List[float]] = {}
        self._counters: Dict[str, int] = {}
        self._lock = threading.RLock()
    
    def start_timer(self, name: str) -> float:
        """Start a timer."""
        return time.perf_counter()
    
    def end_timer(self, name: str, start_time: float) -> float:
        """End a timer and record duration."""
        duration = time.perf_counter() - start_time
        
        with self._lock:
            if name not in self._timers:
                self._timers[name] = []
            self._timers[name].append(duration)
            
            # Keep only last 1000 measurements
            if len(self._timers[name]) > 1000:
                self._timers[name] = self._timers[name][-1000:]
        
        return duration
    
    def increment_counter(self, name: str, value: int = 1) -> None:
        """Increment a counter."""
        with self._lock:
            self._counters[name] = self._counters.get(name, 0) + value
    
    def get_timer_stats(self, name: str) -> Optional[Dict[str, float]]:
        """Get statistics for a timer."""
        with self._lock:
            if name not in self._timers or not self._timers[name]:
                return None
            
            times = self._timers[name]
            return {
                "count": len(times),
                "total": sum(times),
                "average": sum(times) / len(times),
                "min": min(times),
                "max": max(times),
                "last": times[-1]
            }
    
    def get_counter_value(self, name: str) -> int:
        """Get counter value."""
        with self._lock:
            return self._counters.get(name, 0)
    
    def get_all_stats(self) -> Dict[str, Any]:
        """Get all performance statistics."""
        with self._lock:
            stats = {
                "timers": {},
                "counters": self._counters.copy()
            }
            
            for name in self._timers:
                timer_stats = self.get_timer_stats(name)
                if timer_stats:
                    stats["timers"][name] = timer_stats
            
            return stats
    
    def reset(self) -> None:
        """Reset all statistics."""
        with self._lock:
            self._timers.clear()
            self._counters.clear()


class LazyEvaluator:
    """Lazy evaluation system to defer expensive operations."""
    
    def __init__(self):
        self._evaluators: Dict[str, Callable] = {}
        self._results: Dict[str, Any] = {}
        self._evaluated: set = set()
    
    def register(self, key: str, evaluator: Callable[[], Any]) -> None:
        """Register a lazy evaluator."""
        self._evaluators[key] = evaluator
        if key in self._evaluated:
            self._evaluated.remove(key)
        if key in self._results:
            del self._results[key]
    
    def get(self, key: str) -> Any:
        """Get result, evaluating if necessary."""
        if key not in self._evaluated:
            if key not in self._evaluators:
                raise KeyError(f"No evaluator registered for key: {key}")
            
            self._results[key] = self._evaluators[key]()
            self._evaluated.add(key)
        
        return self._results[key]
    
    def invalidate(self, key: str) -> None:
        """Invalidate cached result."""
        if key in self._evaluated:
            self._evaluated.remove(key)
        if key in self._results:
            del self._results[key]
    
    def clear(self) -> None:
        """Clear all evaluators and results."""
        self._evaluators.clear()
        self._results.clear()
        self._evaluated.clear()


# Global performance instances
_global_memory_manager: Optional[MemoryManager] = None
_global_performance_monitor = PerformanceMonitor()


def get_memory_manager(config: Optional[ParserConfig] = None) -> MemoryManager:
    """Get global memory manager instance."""
    global _global_memory_manager
    if _global_memory_manager is None:
        from .parser_config import get_default_config
        _global_memory_manager = MemoryManager(config or get_default_config())
    return _global_memory_manager


def get_performance_monitor() -> PerformanceMonitor:
    """Get global performance monitor instance."""
    return _global_performance_monitor


def reset_performance_systems() -> None:
    """Reset all performance systems."""
    global _global_memory_manager
    if _global_memory_manager:
        _global_memory_manager.reset()
    _global_performance_monitor.reset()


# Decorators for performance monitoring
def timed(name: str = None):
    """Decorator to time function execution."""
    def decorator(func):
        timer_name = name or f"{func.__module__}.{func.__name__}"
        
        def wrapper(*args, **kwargs):
            monitor = get_performance_monitor()
            start_time = monitor.start_timer(timer_name)
            try:
                result = func(*args, **kwargs)
                return result
            finally:
                monitor.end_timer(timer_name, start_time)
        
        return wrapper
    return decorator


def counted(name: str = None):
    """Decorator to count function calls."""
    def decorator(func):
        counter_name = name or f"{func.__module__}.{func.__name__}_calls"
        
        def wrapper(*args, **kwargs):
            monitor = get_performance_monitor()
            monitor.increment_counter(counter_name)
            return func(*args, **kwargs)
        
        return wrapper
    return decorator


def cached(cache_name: str, key_func: Optional[Callable] = None):
    """Decorator to cache function results."""
    def decorator(func):
        def wrapper(*args, **kwargs):
            memory_manager = get_memory_manager()
            cache = memory_manager.get_cache(cache_name)
            
            if cache is None:
                # Create cache if it doesn't exist
                cache = memory_manager.create_cache(cache_name)
            
            # Generate cache key
            if key_func:
                cache_key = key_func(*args, **kwargs)
            else:
                cache_key = (args, tuple(sorted(kwargs.items())))
            
            # Try to get from cache
            result = cache.get(cache_key)
            if result is not None:
                return result
            
            # Compute and cache result
            result = func(*args, **kwargs)
            cache.put(cache_key, result)
            return result
        
        return wrapper
    return decorator
