"""
Lazy result management and batch operations for Sprint 4 performance optimization
"""
from typing import List, Dict, Any, Optional, Union, Iterator, TYPE_CHECKING
import weakref
import threading
from collections.abc import Sequence
from .performance import get_memory_manager

if TYPE_CHECKING:
    from .core import Tag


class LazyResultSet(Sequence):
    """
    Lazy result set that minimizes Python object creation until needed.
    This eliminates the performance penalty of creating thousands of Tag objects.
    """
    
    def __init__(self, node_references: List[Any], tree_reference: Any, source_tag: 'Tag'):
        """
        Initialize lazy result set.
        
        Args:
            node_references: List of C++ node references
            tree_reference: Reference to the C++ tree
            source_tag: Source tag for creating new Tag objects
        """
        self._node_refs = node_references
        self._tree_ref = tree_reference
        self._source_tag = source_tag
        self._cached_objects: Dict[int, 'Tag'] = {}
        self._lock = threading.RLock()
        
        # Batch operation results cache
        self._text_cache: Optional[List[str]] = None
        self._attrs_cache: Dict[str, List[str]] = {}
        self._batch_cache_valid = True
    
    def __len__(self) -> int:
        """Return number of results."""
        return len(self._node_refs)
    
    def __getitem__(self, index: Union[int, slice]) -> Union['Tag', 'LazyResultSet']:
        """Get item(s) by index, creating Python objects lazily."""
        if isinstance(index, slice):
            # Return new LazyResultSet for slice
            sliced_refs = self._node_refs[index]
            return LazyResultSet(sliced_refs, self._tree_ref, self._source_tag)
        
        if not isinstance(index, int):
            raise TypeError("Index must be integer or slice")
        
        if index < 0:
            index += len(self._node_refs)
        
        if not (0 <= index < len(self._node_refs)):
            raise IndexError("Index out of range")
        
        # Check cache first
        with self._lock:
            if index in self._cached_objects:
                return self._cached_objects[index]
            
            # Create Tag object lazily
            node_ref = self._node_refs[index]
            from .core import Tag
            tag_obj = Tag._create_from_tree_node(node_ref, self._tree_ref)
            
            # Cache the object using weak reference tracking
            self._cached_objects[index] = tag_obj
            
            # Register with memory manager for tracking
            memory_mgr = get_memory_manager()
            memory_mgr.register_object(tag_obj)
            
            return tag_obj
    
    def __iter__(self) -> Iterator['Tag']:
        """Iterate over results, creating objects lazily."""
        for i in range(len(self._node_refs)):
            yield self[i]
    
    def __bool__(self) -> bool:
        """Return True if result set is not empty."""
        return len(self._node_refs) > 0
    
    def __repr__(self) -> str:
        """String representation."""
        return f"LazyResultSet({len(self._node_refs)} items)"
    
    # Batch Operations - Key Performance Feature
    
    def text(self, separator: str = "", strip: bool = True) -> List[str]:
        """
        Get text content from all results in a single optimized operation.
        This avoids creating individual Tag objects and accessing text separately.
        """
        if self._text_cache is not None and self._batch_cache_valid:
            return self._text_cache
        
        text_results = []
        
        # Optimized batch text extraction
        for node_ref in self._node_refs:
            try:
                if hasattr(node_ref, 'get_text'):
                    text = node_ref.get_text()
                else:
                    # Fallback to creating Tag object
                    tag = self[len(text_results)]
                    text = tag.text
                
                if strip:
                    text = text.strip()
                
                text_results.append(text)
                
            except Exception:
                text_results.append("")
        
        # Cache results
        self._text_cache = text_results
        return text_results
    
    def attrs(self, attr_name: str, default: str = "") -> List[str]:
        """
        Get attribute values from all results in batch.
        Much faster than accessing each Tag.attrs individually.
        """
        if attr_name in self._attrs_cache and self._batch_cache_valid:
            return self._attrs_cache[attr_name]
        
        attr_results = []
        
        # Optimized batch attribute extraction
        for node_ref in self._node_refs:
            try:
                if hasattr(node_ref, 'get_attribute'):
                    attr_value = node_ref.get_attribute(attr_name)
                    if attr_value is None:
                        attr_value = default
                else:
                    # Fallback to creating Tag object
                    tag = self[len(attr_results)]
                    attr_value = tag.get(attr_name, default)
                
                attr_results.append(attr_value)
                
            except Exception:
                attr_results.append(default)
        
        # Cache results
        self._attrs_cache[attr_name] = attr_results
        return attr_results
    
    def tag_names(self) -> List[str]:
        """Get tag names from all results in batch."""
        names = []
        
        for node_ref in self._node_refs:
            try:
                if hasattr(node_ref, 'get_name'):
                    names.append(node_ref.get_name())
                else:
                    tag = self[len(names)]
                    names.append(tag.name)
            except Exception:
                names.append("")
        
        return names
    
    def contents(self) -> List[List[Union['Tag', str]]]:
        """Get contents from all results in batch."""
        all_contents = []
        
        for i, node_ref in enumerate(self._node_refs):
            try:
                tag = self[i]
                all_contents.append(tag.contents)
            except Exception:
                all_contents.append([])
        
        return all_contents
    
    def lengths(self) -> List[int]:
        """Get text lengths from all results."""
        return [len(text) for text in self.text()]
    
    def count_children(self) -> List[int]:
        """Count children for each result."""
        counts = []
        
        for node_ref in self._node_refs:
            try:
                if hasattr(node_ref, 'get_children'):
                    children = node_ref.get_children()
                    counts.append(len(children))
                else:
                    tag = self[len(counts)]
                    counts.append(len(tag.children))
            except Exception:
                counts.append(0)
        
        return counts
    
    def has_attr(self, attr_name: str) -> List[bool]:
        """Check if attribute exists for each result."""
        results = []
        
        for node_ref in self._node_refs:
            try:
                if hasattr(node_ref, 'get_attributes'):
                    attrs = node_ref.get_attributes()
                    results.append(attr_name in attrs)
                else:
                    tag = self[len(results)]
                    results.append(tag.has_attr(attr_name))
            except Exception:
                results.append(False)
        
        return results
    
    def find_all_batch(self, name: str) -> 'BatchFindResult':
        """Find all elements within each result in batch."""
        batch_results = []
        
        for i in range(len(self._node_refs)):
            tag = self[i]
            results = tag.find_all(name)
            batch_results.append(results)
        
        return BatchFindResult(batch_results)
    
    def select_batch(self, selector: str) -> 'BatchFindResult':
        """CSS select within each result in batch."""
        batch_results = []
        
        for i in range(len(self._node_refs)):
            tag = self[i]
            results = tag.select(selector)
            batch_results.append(results)
        
        return BatchFindResult(batch_results)
    
    # Performance utilities
    
    def invalidate_cache(self):
        """Invalidate batch operation caches."""
        self._text_cache = None
        self._attrs_cache.clear()
        self._batch_cache_valid = False
    
    def prefetch(self, indices: Optional[List[int]] = None):
        """Prefetch Tag objects for specified indices."""
        if indices is None:
            indices = list(range(len(self._node_refs)))
        
        for index in indices:
            if 0 <= index < len(self._node_refs):
                # Access the item to trigger creation and caching
                _ = self[index]
    
    def get_memory_usage(self) -> Dict[str, int]:
        """Get memory usage statistics."""
        return {
            "node_references": len(self._node_refs),
            "cached_objects": len(self._cached_objects),
            "cache_hit_ratio": len(self._cached_objects) / max(len(self._node_refs), 1),
            "text_cache_size": len(self._text_cache) if self._text_cache else 0,
            "attrs_cache_keys": len(self._attrs_cache)
        }
    
    def optimize_memory(self):
        """Optimize memory usage by clearing unnecessary caches."""
        # Clear least recently used cached objects
        if len(self._cached_objects) > 100:  # Keep at most 100 cached objects
            # Simple LRU: remove first half
            indices_to_remove = list(self._cached_objects.keys())[:len(self._cached_objects)//2]
            for idx in indices_to_remove:
                del self._cached_objects[idx]


class BatchFindResult:
    """Result container for batch find/select operations."""
    
    def __init__(self, batch_results: List[List['Tag']]):
        self.batch_results = batch_results
    
    def __len__(self) -> int:
        return len(self.batch_results)
    
    def __getitem__(self, index: int) -> List['Tag']:
        return self.batch_results[index]
    
    def __iter__(self) -> Iterator[List['Tag']]:
        return iter(self.batch_results)
    
    def flatten(self) -> List['Tag']:
        """Flatten all results into a single list."""
        flattened = []
        for result_list in self.batch_results:
            flattened.extend(result_list)
        return flattened
    
    def count_all(self) -> int:
        """Count total number of results across all batches."""
        return sum(len(results) for results in self.batch_results)
    
    def text_all(self) -> List[List[str]]:
        """Get text from all results in all batches."""
        return [[tag.text for tag in results] for results in self.batch_results]


class SmartObjectCache:
    """Intelligent object caching system for Tag objects."""
    
    def __init__(self, max_size: int = 1000):
        self.max_size = max_size
        self._cache: Dict[Any, 'Tag'] = {}
        self._access_count: Dict[Any, int] = {}
        self._lock = threading.RLock()
        
        # Use weak references to avoid memory leaks
        self._weak_refs: Dict[Any, weakref.ref] = {}
    
    def get(self, node_ref: Any, tree_ref: Any) -> Optional['Tag']:
        """Get cached Tag object."""
        cache_key = id(node_ref)
        
        with self._lock:
            if cache_key in self._weak_refs:
                weak_ref = self._weak_refs[cache_key]
                tag_obj = weak_ref()
                if tag_obj is not None:
                    self._access_count[cache_key] = self._access_count.get(cache_key, 0) + 1
                    return tag_obj
                else:
                    # Weak reference is dead, clean up
                    del self._weak_refs[cache_key]
                    self._access_count.pop(cache_key, None)
        
        return None
    
    def put(self, node_ref: Any, tag_obj: 'Tag'):
        """Cache Tag object with weak reference."""
        cache_key = id(node_ref)
        
        with self._lock:
            # Clean up if cache is full
            if len(self._weak_refs) >= self.max_size:
                self._cleanup_least_used()
            
            # Create weak reference with cleanup callback
            def cleanup_callback(ref):
                with self._lock:
                    self._weak_refs.pop(cache_key, None)
                    self._access_count.pop(cache_key, None)
            
            weak_ref = weakref.ref(tag_obj, cleanup_callback)
            self._weak_refs[cache_key] = weak_ref
            self._access_count[cache_key] = 1
    
    def _cleanup_least_used(self):
        """Remove least frequently used items."""
        if not self._access_count:
            return
        
        # Remove items with lowest access count
        items_to_remove = sorted(self._access_count.items(), key=lambda x: x[1])[:self.max_size // 4]
        
        for cache_key, _ in items_to_remove:
            self._weak_refs.pop(cache_key, None)
            self._access_count.pop(cache_key, None)
    
    def clear(self):
        """Clear all cached objects."""
        with self._lock:
            self._weak_refs.clear()
            self._access_count.clear()
    
    def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        with self._lock:
            # Count live references
            live_refs = sum(1 for ref in self._weak_refs.values() if ref() is not None)
            
            return {
                "total_slots": len(self._weak_refs),
                "live_references": live_refs,
                "max_size": self.max_size,
                "utilization": live_refs / max(self.max_size, 1),
                "total_accesses": sum(self._access_count.values())
            }


# Global smart cache instance
_global_object_cache: Optional[SmartObjectCache] = None


def get_object_cache() -> SmartObjectCache:
    """Get global smart object cache."""
    global _global_object_cache
    if _global_object_cache is None:
        _global_object_cache = SmartObjectCache()
    return _global_object_cache


def create_lazy_result_set(node_references: List[Any], tree_reference: Any, source_tag: 'Tag') -> LazyResultSet:
    """Create optimized lazy result set."""
    return LazyResultSet(node_references, tree_reference, source_tag)


# Performance monitoring for lazy operations
class LazyOperationMonitor:
    """Monitor performance of lazy operations."""
    
    def __init__(self):
        self._stats = {
            "lazy_sets_created": 0,
            "objects_created_lazily": 0,
            "batch_operations": 0,
            "cache_hits": 0,
            "cache_misses": 0
        }
        self._lock = threading.Lock()
    
    def record_lazy_set_creation(self, size: int):
        """Record creation of lazy result set."""
        with self._lock:
            self._stats["lazy_sets_created"] += 1
    
    def record_lazy_object_creation(self):
        """Record lazy object creation."""
        with self._lock:
            self._stats["objects_created_lazily"] += 1
    
    def record_batch_operation(self, operation_type: str, count: int):
        """Record batch operation."""
        with self._lock:
            self._stats["batch_operations"] += 1
            self._stats[f"batch_{operation_type}"] = self._stats.get(f"batch_{operation_type}", 0) + count
    
    def record_cache_hit(self):
        """Record cache hit."""
        with self._lock:
            self._stats["cache_hits"] += 1
    
    def record_cache_miss(self):
        """Record cache miss."""
        with self._lock:
            self._stats["cache_misses"] += 1
    
    def get_stats(self) -> Dict[str, Any]:
        """Get monitoring statistics."""
        with self._lock:
            stats = self._stats.copy()
            total_cache_ops = stats["cache_hits"] + stats["cache_misses"]
            if total_cache_ops > 0:
                stats["cache_hit_rate"] = stats["cache_hits"] / total_cache_ops
            return stats


# Global monitor instance
_global_lazy_monitor = LazyOperationMonitor()


def get_lazy_monitor() -> LazyOperationMonitor:
    """Get global lazy operation monitor."""
    return _global_lazy_monitor