"""
Core CutySoup classes providing BeautifulSoup4-compatible API
"""

from typing import Dict, List, Optional, Union, Iterator, Callable, Pattern, Any
import threading
import weakref
from collections import OrderedDict
from .exceptions import ParseError, NavigationError
from .utils import is_tag_name_match, validate_html_content
from .parser_config import ParserConfig, EncodingDetector, get_default_config
from .css_selectors import select_tags, select_one_tag
from .text_processing import TextProcessor
from .tree_manipulation import TreeManipulator

try:
    from . import _cutysoup as _cutysoup_ext
except ImportError:
    # Fallback for development or when C++ extension isn't built
    try:
        import _cutysoup as _cutysoup_ext
    except ImportError:
        raise ImportError(
            "CutySoup C++ extension '_cutysoup' not found. "
            "Please build the extension using 'python dev_build.py' or 'pip install -e .'"
        )


class TagPool:
    """Memory pool for Tag objects to eliminate allocation overhead."""
    
    def __init__(self, initial_size: int = 100):
        self._pool = []
        self._active_tags = weakref.WeakSet()
        self._lock = threading.RLock()
        self._max_pool_size = 1000  # Prevent unbounded growth
        self._initial_size = initial_size
        self._initialized = False
        self._hits = 0
        self._misses = 0
    
    def get_tag(self, node, tree) -> 'Tag':
        """Get a Tag instance from the pool or create new one."""
        with self._lock:
            # Lazy initialization after Tag class is available
            if not self._initialized:
                self._initialize_pool()
            
            if self._pool:
                self._hits += 1
                tag = self._pool.pop()
                # Reset the tag for reuse - ensure it's fully initialized
                tag._node = node
                tag._tree = tree
                tag._text_cache = None
                tag._text_cache_valid = True
                if not hasattr(tag, '_cached_attrs'):
                    tag._cached_attrs = {}
                else:
                    tag._cached_attrs.clear()
                if not hasattr(tag, '_attr_deletions'):
                    tag._attr_deletions = set()
                else:
                    tag._attr_deletions.clear()
                tag._tag_name = None
                tag._detached = False
                tag._python_parent = None
                tag._python_contents = []
            else:
                self._misses += 1
                # Create a properly initialized Tag
                tag = Tag(node)
                tag._tree = tree
            
            self._active_tags.add(tag)
            
            return tag
    
    def _initialize_pool(self):
        """Initialize the pool with pre-allocated Tag objects."""
        if self._initialized:
            return
            
        # Start with empty pool - we'll create Tags as needed but reuse them
        # This avoids overhead of dummy nodes but still provides pooling benefits
        
        self._initialized = True
    
    def return_tag(self, tag: 'Tag'):
        """Return a Tag instance to the pool."""
        with self._lock:
            if len(self._pool) < self._max_pool_size:
                tag._reset()
                self._pool.append(tag)
    
    def cleanup_unused_tags(self):
        """Clean up unused Tag objects and return them to pool."""
        with self._lock:
            # Get a snapshot of active tags to avoid modification during iteration
            active_refs = list(self._active_tags)
            returned_count = 0
            
            for tag_ref in active_refs:
                # Check if the tag object still exists
                if tag_ref is not None:
                    # Check if it's no longer referenced elsewhere by checking ref count
                    import sys
                    if sys.getrefcount(tag_ref) <= 3:  # WeakSet + local var + getrefcount itself
                        if len(self._pool) < self._max_pool_size:
                            try:
                                tag_ref._reset()
                                self._pool.append(tag_ref)
                                returned_count += 1
                            except (AttributeError, TypeError):
                                pass  # Tag might be partially destroyed
            
            return returned_count
    
    def get_pool_stats(self) -> dict:
        """Get pool statistics for debugging."""
        with self._lock:
            return {
                'pool_size': len(self._pool),
                'active_tags': len(self._active_tags),
                'max_pool_size': self._max_pool_size,
                'hits': self._hits,
                'misses': self._misses,
                'hit_rate': self._hits / (self._hits + self._misses) if (self._hits + self._misses) > 0 else 0
            }


# Global tag pool instance (will be initialized after Tag class is defined)
_tag_pool = None
_tree_overlay_registry = {}
_TREE_SEARCH_CACHE_LIMIT = 256
_TREE_WRAPPER_PREWARM_LIMIT = 4096


def _get_tree_overlay_state(tree, create: bool = False) -> Optional[dict]:
    """Get tree-local Python overlay state for created or mutated nodes."""
    if tree is None:
        return None
    key = id(tree)
    state = _tree_overlay_registry.get(key)
    if state is None and create:
        state = {
            "node_overlays": {},
            "python_search_mutations": set(),
            "wrapper_cache": weakref.WeakValueDictionary(),
            "search_cache": OrderedDict(),
        }
        _tree_overlay_registry[key] = state
    return state


def _get_tree_wrapper_cache(tree, create: bool = False):
    """Get the per-tree cache of Python Tag wrappers."""
    state = _get_tree_overlay_state(tree, create=create)
    if state is None:
        return None
    return state["wrapper_cache"]


def _get_tree_search_cache(tree, create: bool = False):
    """Get the per-tree cache of simple search results."""
    state = _get_tree_overlay_state(tree, create=create)
    if state is None:
        return None
    return state["search_cache"]


def _invalidate_tree_search_cache(tree) -> None:
    """Clear cached simple-search results for a tree."""
    cache = _get_tree_search_cache(tree)
    if cache is not None:
        cache.clear()


def _cache_tree_search_results(tree, key, results) -> None:
    """Store simple-search results with basic LRU eviction."""
    cache = _get_tree_search_cache(tree, create=True)
    if cache is None:
        return
    if key in cache:
        cache.pop(key)
    elif len(cache) >= _TREE_SEARCH_CACHE_LIMIT:
        cache.popitem(last=False)
    cache[key] = tuple(results)


def _get_cached_tree_search_results(tree, key):
    """Look up simple-search results and refresh their LRU position."""
    cache = _get_tree_search_cache(tree)
    if cache is None:
        return None
    cached = cache.get(key)
    if cached is None:
        return None
    cache.move_to_end(key)
    return list(cached)


def _clear_tree_overlay_state(tree_key: int) -> None:
    """Release overlay state when the owning soup is garbage-collected."""
    _tree_overlay_registry.pop(tree_key, None)


class PageElement:
    """Base class for navigable elements in the parse tree."""
    pass


class NavigableString(str, PageElement):
    """A string that knows about its place in the parse tree."""
    
    def __new__(cls, value=''):
        obj = str.__new__(cls, value)
        obj._parent = None
        obj._is_comment = False
        obj._is_cdata = False
        obj._is_doctype = False
        return obj
    
    @property
    def name(self):
        """NavigableString's name is always None like BeautifulSoup4."""
        return None
    
    @property
    def parent(self):
        """Get the parent tag of this string."""
        return self._parent
    
    @parent.setter
    def parent(self, value):
        """Set the parent tag of this string."""
        self._parent = value
    
    def extract(self):
        """Remove this string from the tree."""
        try:
            # If this NavigableString has no node reference (standalone), just return self
            if not hasattr(self, '_node') or not self._node:
                return self
            return TreeManipulator.extract_tag(self)
        except Exception as e:
            raise NavigationError(f"Failed to extract string: {e}")
    
    def replace_with(self, new_content):
        """Replace this string with new content."""
        try:
            # If this NavigableString has no node reference (standalone), just return self
            if not hasattr(self, '_node') or not self._node:
                return self
            TreeManipulator.replace_tag_with(self, new_content)
            return self
        except Exception as e:
            raise NavigationError(f"Failed to replace string: {e}")
    
    def insert_before(self, new_content):
        """Insert content before this string."""
        try:
            # If this NavigableString has no node reference (standalone), do nothing
            if not hasattr(self, '_node') or not self._node:
                return
            TreeManipulator.insert_before_tag(self, new_content)
        except Exception as e:
            raise NavigationError(f"Failed to insert before string: {e}")
    
    def insert_after(self, new_content):
        """Insert content after this string."""
        try:
            # If this NavigableString has no node reference (standalone), do nothing
            if not hasattr(self, '_node') or not self._node:
                return
            TreeManipulator.insert_after_tag(self, new_content)
        except Exception as e:
            raise NavigationError(f"Failed to insert after string: {e}")
    
    @property
    def next_sibling(self):
        """Get next sibling of this string."""
        # For now, return None as NavigableString navigation is complex to implement
        # This requires tracking the string's position in the parent's children
        return None
    
    @property
    def previous_sibling(self):
        """Get previous sibling of this string."""
        # For now, return None as NavigableString navigation is complex to implement
        # This requires tracking the string's position in the parent's children
        return None
    
    def __str__(self):
        """String representation based on content type."""
        content = str.__str__(self)
        
        if getattr(self, '_is_comment', False):
            return f"<!--{content}-->"
        elif getattr(self, '_is_cdata', False):
            return f"<![CDATA[{content}]]>"
        elif getattr(self, '_is_doctype', False):
            return f"<!DOCTYPE {content}>"
        else:
            return content


class Tag(PageElement):
    """
    A Tag represents an HTML element and provides BeautifulSoup4-compatible interface.
    """
    
    def __init__(self, node: _cutysoup_ext.Node):
        self._node = node
        self._tree = None
        self._cached_attrs = {}  # Track attribute modifications
        self._attr_deletions = set()  # Track deleted attributes
        self._tag_name = None  # Override tag name if needed
        self._text_cache = None  # Cache for text extraction
        self._text_cache_valid = True  # Track if cache is valid
        self._python_parent = None
        self._python_contents = []
        self._detached = False
    
    def _reset(self):
        """Reset Tag instance for memory pool reuse."""
        self._node = None
        self._tree = None
        self._cached_attrs.clear()
        self._attr_deletions.clear()
        self._tag_name = None
        self._text_cache = None
        self._text_cache_valid = True
        self._python_parent = None
        self._python_contents = []
        self._detached = False
    
    def _invalidate_text_cache(self):
        """Invalidate text cache when content changes."""
        self._text_cache_valid = False
        # Also invalidate parent's cache since text changes affect ancestors
        if hasattr(self, 'parent') and self.parent:
            if hasattr(self.parent, '_invalidate_text_cache'):
                self.parent._invalidate_text_cache()

    def _node_key(self) -> Optional[int]:
        """Return a stable identity for the underlying parsed node."""
        if not self._node or not hasattr(self._node, 'get_gumbo_node'):
            return None
        try:
            return self._node.get_gumbo_node()
        except Exception:
            return None

    def _get_overlay_state(self, create: bool = False) -> Optional[dict]:
        """Get overlay state shared across wrappers for the same parsed node."""
        node_key = self._node_key()
        if node_key is None or self._tree is None:
            return None
        tree_state = _get_tree_overlay_state(self._tree, create=create)
        if tree_state is None:
            return None
        overlays = tree_state["node_overlays"]
        state = overlays.get(node_key)
        if state is None and create:
            state = {"appended": [], "inserted": [], "removed": set(), "cleared": False}
            overlays[node_key] = state
        elif state is not None and "removed" not in state:
            state["removed"] = set()
        return state

    def _tree_has_python_overlays(self) -> bool:
        """Check whether any Python-side overlays exist for this tree."""
        if self._tree is None:
            return False
        tree_state = _get_tree_overlay_state(self._tree)
        if not tree_state:
            return False
        return bool(tree_state["node_overlays"])

    def _tree_has_python_search_mutations(self) -> bool:
        """Check whether searches must fall back to Python for correctness."""
        if self._tree is None:
            return False
        tree_state = _get_tree_overlay_state(self._tree)
        if not tree_state:
            return False
        return bool(tree_state["node_overlays"] or tree_state["python_search_mutations"])

    def _mark_tree_search_mutation(self) -> None:
        """Invalidate cached searches after Python-side structural or attr changes."""
        if self._tree is None:
            return
        tree_state = _get_tree_overlay_state(self._tree, create=True)
        if tree_state is None:
            return
        node_key = self._node_key()
        if node_key is not None:
            tree_state["python_search_mutations"].add(node_key)
        else:
            tree_state["python_search_mutations"].add(id(self))
        _invalidate_tree_search_cache(self._tree)

    def _has_python_overlay(self) -> bool:
        """Check whether this tag has Python-managed children or mutations."""
        if not self._node:
            return bool(self._python_contents)
        overlay = self._get_overlay_state()
        return bool(
            overlay and (
                overlay["cleared"] or
                overlay["appended"] or
                overlay["inserted"] or
                overlay.get("removed")
            )
        )

    def _same_identity(self, other) -> bool:
        """Check whether two Tag wrappers reference the same underlying node."""
        if self is other:
            return True
        if not hasattr(other, '_node'):
            return False
        self_key = self._node_key()
        other_key = other._node_key() if hasattr(other, '_node_key') else None
        if self_key is not None and other_key is not None:
            return self_key == other_key
        return False

    def _child_matches(self, existing, candidate) -> bool:
        """Compare child objects, accounting for pooled wrappers around parsed nodes."""
        if existing is candidate:
            return True
        if isinstance(existing, Tag) and isinstance(candidate, Tag):
            return existing._same_identity(candidate)
        return False

    def _find_child_index(self, target) -> int:
        """Find a child index using wrapper-aware identity checks."""
        for index, child in enumerate(self.children):
            if self._child_matches(child, target):
                return index
        return -1

    def _attach_child_parent(self, child):
        """Maintain parent links for Python-managed children."""
        if isinstance(child, NavigableString):
            existing_parent = getattr(child, 'parent', None)
            if existing_parent is not None and existing_parent is not self and hasattr(existing_parent, '_remove_child_reference'):
                existing_parent._remove_child_reference(child)
            child.parent = self
        elif hasattr(child, '_python_parent'):
            existing_parent = child.parent if hasattr(child, 'parent') else None
            if existing_parent is not None and existing_parent is not self and hasattr(existing_parent, '_remove_child_reference'):
                existing_parent._remove_child_reference(child)
            child._python_parent = self
            child._detached = False

    def _append_overlay_child(self, child) -> None:
        """Append a Python-managed child to this tag."""
        self._attach_child_parent(child)
        if not self._node:
            self._python_contents.append(child)
        else:
            overlay = self._get_overlay_state(create=True)
            overlay["appended"].append(child)
        _invalidate_tree_search_cache(self._tree)
        self._invalidate_text_cache()

    def _insert_overlay_child(self, position: int, child) -> None:
        """Insert a Python-managed child at the given position."""
        self._attach_child_parent(child)
        if not self._node:
            index = max(0, min(position, len(self._python_contents)))
            self._python_contents.insert(index, child)
        else:
            overlay = self._get_overlay_state(create=True)
            overlay["inserted"].append((position, child))
        _invalidate_tree_search_cache(self._tree)
        self._invalidate_text_cache()

    def _clear_overlay_children(self) -> None:
        """Clear children visible from the Python layer."""
        if not self._node:
            self._python_contents = []
        else:
            overlay = self._get_overlay_state(create=True)
            overlay["appended"].clear()
            overlay["inserted"].clear()
            overlay["removed"].clear()
            overlay["cleared"] = True
        _invalidate_tree_search_cache(self._tree)
        self._invalidate_text_cache()

    def _remove_child_reference(self, child) -> None:
        """Hide a child from this tag's Python-visible children list."""
        if not self._node:
            self._python_contents = [
                existing for existing in self._python_contents
                if not self._child_matches(existing, child)
            ]
            self._invalidate_text_cache()
            return

        overlay = self._get_overlay_state(create=True)

        inserted = []
        removed_from_overlay = False
        for position, existing in overlay["inserted"]:
            if not removed_from_overlay and self._child_matches(existing, child):
                removed_from_overlay = True
                continue
            inserted.append((position, existing))
        overlay["inserted"] = inserted

        appended = []
        for existing in overlay["appended"]:
            if not removed_from_overlay and self._child_matches(existing, child):
                removed_from_overlay = True
                continue
            appended.append(existing)
        overlay["appended"] = appended

        if not removed_from_overlay and isinstance(child, Tag):
            child_key = child._node_key()
            if child_key is not None:
                overlay["removed"].add(child_key)

        _invalidate_tree_search_cache(self._tree)
        self._invalidate_text_cache()

    def _get_simple_search_cache_key(self, name, search_attrs: Dict[str, Any], string,
                                     limit: Optional[int], recursive: bool):
        """Build a cache key for simple, immutable C++-backed searches."""
        if self._tree is None or self._node is None:
            return None
        if string is not None or not recursive or self._tree_has_python_search_mutations():
            return None
        if not isinstance(name, str) and name not in ("", None):
            return None

        node_key = self._node_key()
        if node_key is None:
            return None

        normalized_attrs = tuple(sorted(search_attrs.items()))

        if isinstance(name, str) and name and not search_attrs:
            return ("tag", node_key, name, limit)

        if (name in ("", None) and len(search_attrs) == 1 and
            isinstance(search_attrs.get("class"), str)):
            return ("class", node_key, search_attrs["class"], limit)

        if (name in ("", None) and len(search_attrs) == 1 and
            isinstance(search_attrs.get("id"), str)):
            return ("id", node_key, search_attrs["id"], limit)

        if normalized_attrs and isinstance(name, str):
            if all(isinstance(value, str) for _, value in normalized_attrs):
                return ("attrs", node_key, name, normalized_attrs, limit)

        return None

    def _detach_from_parent(self) -> Optional['Tag']:
        """Detach this tag from the Python-visible tree."""
        parent = self.parent
        if parent is None:
            return None
        if hasattr(parent, '_remove_child_reference'):
            parent._remove_child_reference(self)
        self._python_parent = None
        self._detached = True
        self._invalidate_text_cache()
        return parent

    def _base_children(self) -> List[Union['Tag', NavigableString]]:
        """Get children directly from the underlying parsed tree."""
        if not self._node:
            return list(self._python_contents)

        children = []
        child_nodes = self._node.get_children()

        for child_node in child_nodes:
            if child_node.is_text():
                nav_string = NavigableString(child_node.get_text())
                nav_string.parent = self
                children.append(nav_string)
            elif child_node.is_element():
                children.append(Tag._create_from_tree_node(child_node, self._tree))

        return children

    def _merged_children(self) -> List[Union['Tag', NavigableString]]:
        """Return children after applying Python-side insert/append overlays."""
        if not self._node:
            children = list(self._python_contents)
        else:
            overlay = self._get_overlay_state()
            if overlay and overlay["cleared"]:
                children = []
            else:
                children = self._base_children()
                removed = overlay.get("removed") if overlay else None
                if removed:
                    children = [
                        child for child in children
                        if not (isinstance(child, Tag) and child._node_key() in removed)
                    ]

            if overlay:
                for position, child in sorted(overlay["inserted"], key=lambda item: item[0]):
                    index = max(0, min(position, len(children)))
                    children.insert(index, child)
                children.extend(overlay["appended"])

        return [
            child for child in children
            if not getattr(child, '_decomposed', False) and not getattr(child, '_extracted', False)
        ]

    def _matches_search_filters(self, tag: 'Tag', search_name, search_attrs: Dict[str, Any], string) -> bool:
        """Apply BeautifulSoup-style filters to a candidate tag."""
        if not is_tag_name_match(tag, search_name):
            return False

        for attr_name, attr_value in search_attrs.items():
            if attr_name == 'class_':
                attr_name = 'class'

            tag_value = tag.get(attr_name)
            if attr_name == 'class' and isinstance(tag_value, list):
                if isinstance(attr_value, list):
                    if not any(value in tag_value for value in attr_value):
                        return False
                elif isinstance(attr_value, str):
                    if attr_value not in tag_value:
                        return False
                elif hasattr(attr_value, 'match'):
                    if not any(attr_value.match(str(value)) for value in tag_value):
                        return False
                elif callable(attr_value):
                    if not attr_value(tag_value):
                        return False
                elif tag_value != attr_value:
                    return False
                continue

            if isinstance(attr_value, str):
                if tag_value != attr_value:
                    return False
            elif hasattr(attr_value, 'match'):
                if tag_value is None or not attr_value.match(str(tag_value)):
                    return False
            elif callable(attr_value):
                if not attr_value(tag_value):
                    return False
            elif attr_value is True:
                if tag_value is None:
                    return False
            elif attr_value in (False, None):
                if tag_value is not None:
                    return False
            elif tag_value != attr_value:
                return False

        if string is not None:
            tag_text = tag.get_text()
            if isinstance(string, str):
                if string not in tag_text:
                    return False
            elif hasattr(string, 'search'):
                if not string.search(tag_text):
                    return False

        return True

    def _find_string_only_python(self, string: Union[str, Pattern], limit: Optional[int] = None) -> List[NavigableString]:
        """Search string nodes using the Python-side tree view."""
        results = []

        def _visit(children):
            for child in children:
                if limit and len(results) >= limit:
                    return
                if isinstance(child, str):
                    match = False
                    if isinstance(string, str):
                        match = string in child
                    elif hasattr(string, 'search'):
                        match = bool(string.search(child))
                    if match:
                        nav_string = child if isinstance(child, NavigableString) else NavigableString(str(child))
                        if not getattr(nav_string, 'parent', None):
                            nav_string.parent = self
                        results.append(nav_string)
                elif hasattr(child, 'contents'):
                    _visit(child.contents)

        _visit(self.contents)
        return results

    def _find_all_python(self, name, attrs: Dict[str, Any], string, limit: Optional[int], recursive: bool, **kwargs):
        """Fallback search that respects Python-managed children."""
        search_attrs = attrs or {}
        search_attrs.update(kwargs)

        if string is not None and not name and not attrs and not kwargs:
            return self._find_string_only_python(string, limit)

        results = []

        def _visit(children):
            for child in children:
                if limit and len(results) >= limit:
                    return
                if hasattr(child, 'contents'):
                    if self._matches_search_filters(child, name, search_attrs, string):
                        results.append(child)
                        if limit and len(results) >= limit:
                            return
                    if recursive:
                        _visit(child.contents)

        _visit(self.contents)
        return results
    
    @classmethod
    def _create_from_tree_node(cls, node: _cutysoup_ext.Node, tree) -> 'Tag':
        """Create Tag instance from tree node using memory pool optimization."""
        if node is None:
            return None

        node_key = None
        if hasattr(node, 'get_gumbo_node'):
            try:
                node_key = node.get_gumbo_node()
            except Exception:
                node_key = None

        if tree is not None and node_key is not None:
            wrapper_cache = _get_tree_wrapper_cache(tree, create=True)
            if wrapper_cache is not None:
                cached_tag = wrapper_cache.get(node_key)
                if cached_tag is not None:
                    return cached_tag

                tag = cls(node)
                tag._tree = tree
                wrapper_cache[node_key] = tag
                return tag

        # Fallback to regular allocation when no stable node identity is available.
        tag = cls(node)
        tag._tree = tree
        return tag
    
    @property
    def name(self) -> str:
        """Get the tag name."""
        if self._tag_name is not None:
            return self._tag_name
        return self._node.get_name() if self._node else ''
    
    @property
    def attrs(self) -> Dict[str, str]:
        """Get all attributes as a dictionary."""
        if not self._node:
            result = self._cached_attrs.copy()
            # Handle class attribute as list like BeautifulSoup4
            if 'class' in result and isinstance(result['class'], str):
                result['class'] = result['class'].split()
            return result
        
        try:
            original_attrs = self._node.get_attributes()
            # Apply modifications
            result = original_attrs.copy()
            result.update(self._cached_attrs)
            # Remove deleted attributes
            for deleted_attr in self._attr_deletions:
                result.pop(deleted_attr, None)
            
            # Handle class attribute as list like BeautifulSoup4
            if 'class' in result and isinstance(result['class'], str):
                result['class'] = result['class'].split()
            
            return result
        except Exception:
            result = self._cached_attrs.copy()
            # Handle class attribute as list like BeautifulSoup4
            if 'class' in result and isinstance(result['class'], str):
                result['class'] = result['class'].split()
            return result
    
    def __getitem__(self, key: str):
        """Get attribute value using dictionary-style access."""
        if key in self._attr_deletions:
            raise KeyError(key)
        
        value = None
        if key in self._cached_attrs:
            value = self._cached_attrs[key]
        elif self._node:
            # Check if attribute exists before getting it
            if self._node.has_attribute(key):
                value = self._node.get_attribute(key)
            else:
                raise KeyError(key)
        else:
            raise KeyError(key)
        
        # Handle class attribute as list like BeautifulSoup4
        if key == 'class' and isinstance(value, str):
            return value.split()
        
        return value
    
    def __setitem__(self, key: str, value: str):
        """Set attribute value using dictionary-style access."""
        TreeManipulator.set_tag_attribute(self, key, value)
    
    def __delitem__(self, key: str):
        """Delete attribute using dictionary-style access."""
        TreeManipulator.delete_tag_attribute(self, key)
    
    def __contains__(self, key: str) -> bool:
        """Check if attribute exists."""
        return key in self.attrs
    
    def get(self, key: str, default=None):
        """Get attribute value with default."""
        try:
            return self[key]
        except KeyError:
            return default
    
    def has_attr(self, key: str) -> bool:
        """Check if attribute exists."""
        return key in self.attrs
    
    @property
    def text(self) -> str:
        """Get text content of tag and descendants."""
        if not self._node or self._has_python_overlay():
            return self.get_text()

        # ULTRA-OPTIMIZED: Use fast C++ get_text method directly (8000x faster)
        return self._node.get_text()
    
    def get_text(self, separator: str = "", strip: bool = False, 
                 types: tuple = (str,)) -> str:
        """Get text with options for separator and stripping."""
        if not self._node or self._has_python_overlay():
            return TextProcessor.extract_text_from_tag(self, separator, strip, types)

        # MaxCpp Phase 32: Use pure C++ implementations for maximum performance
        if types != (str,):
            # Fall back to Python implementation for complex type handling
            return TextProcessor.extract_text_from_tag(self, separator, strip, types)
        
        try:
            if separator == "" and not strip:
                # Simple case - use blazingly fast C++ method
                return self._node.get_text()
            elif separator == "" and strip:
                # Stripped text - pure C++
                return self._node.get_text_stripped()
            elif not strip:
                # With separator - pure C++
                return self._node.get_text_with_separator(separator)
            else:
                # With separator and strip - pure C++
                return self._node.get_text_with_separator_stripped(separator)
        except AttributeError:
            # Fallback if C++ methods not available
            return TextProcessor.extract_text_from_tag(self, separator, strip, types)
    
    def stripped_strings_filtered(self, min_length: int = 1) -> Iterator[str]:
        """Get filtered strings with minimum length."""
        return TextProcessor.get_filtered_strings(self, min_length)
    
    def find_all_text(self, string: str, case_sensitive: bool = True) -> List[str]:
        """Find all text occurrences."""
        return TextProcessor.find_all_text_occurrences(self, string, case_sensitive)
    
    def replace_text(self, old_text: str, new_text: str, count: int = -1) -> int:
        """Replace text occurrences."""
        # Queue the replacement operation
        if not hasattr(self, '_text_replacements'):
            self._text_replacements = []
        
        # Store the replacement operation
        self._text_replacements.append((old_text, new_text, count))
        
        # Execute the replacement and return count
        return TextProcessor.replace_text_in_tag(self, old_text, new_text, count)
    
    @property
    def strings(self) -> Iterator[str]:
        """Iterate over all strings in tag and descendants."""
        if not self._node or self._has_python_overlay():
            return TextProcessor.get_strings_from_tag(self)

        # MaxCpp Phase 32: Use C++ bulk operation instead of Python generators
        try:
            # Get all strings in one C++ call, then convert to iterator
            string_list = self._node.get_strings()
            return iter(string_list)
        except AttributeError:
            # Fallback to Python implementation
            return TextProcessor.get_strings_from_tag(self)
    
    @property
    def stripped_strings(self) -> Iterator[str]:
        """Iterate over non-empty strings in tag and descendants."""
        if not self._node or self._has_python_overlay():
            return TextProcessor.get_stripped_strings_from_tag(self)

        # MaxCpp Phase 32: Use C++ bulk operation instead of Python generators
        try:
            # Get all stripped strings in one C++ call, then convert to iterator
            string_list = self._node.get_stripped_strings()
            return iter(string_list)
        except AttributeError:
            # Fallback to Python implementation
            return TextProcessor.get_stripped_strings_from_tag(self)
    
    @property
    def string(self) -> Optional[str]:
        """Get the single string content if tag has only one string child."""
        try:
            contents = self.contents
            if len(contents) == 1 and isinstance(contents[0], str):
                return contents[0]
            return None
        except (AttributeError, IndexError, TypeError):
            return None
    
    @property
    def parent(self) -> Optional['Tag']:
        """Get parent tag."""
        if self._detached:
            return None
        if self._python_parent is not None:
            return self._python_parent
        if not self._node:
            return None
        try:
            parent_node = self._node.get_parent()
            if parent_node:
                return Tag._create_from_tree_node(parent_node, self._tree)
            return None
        except Exception as e:
            raise NavigationError(f"Failed to get parent: {e}")
    
    @property
    def children(self) -> List[Union['Tag', NavigableString]]:
        """Get direct children."""
        try:
            return self._merged_children()
        except Exception as e:
            raise NavigationError(f"Failed to get children: {e}")
    
    @property
    def contents(self) -> List[Union['Tag', str]]:
        """Get contents (alias for children)."""
        return self.children
    
    @property
    def next_sibling(self) -> Optional[Union['Tag', str]]:
        """Get next sibling."""
        try:
            if self._python_parent is not None or self._tree_has_python_overlays():
                parent = self.parent
                if parent is None:
                    return None
                siblings = parent.children
                index = parent._find_child_index(self)
                if index < 0:
                    return None
                return siblings[index + 1] if index + 1 < len(siblings) else None

            if not self._node:
                return None
            
            next_node = self._node.get_next_sibling()
            if not next_node:
                return None
            
            if next_node.is_text():
                nav_string = NavigableString(next_node.get_text())
                nav_string.parent = self.parent
                return nav_string
            elif next_node.is_element():
                return Tag._create_from_tree_node(next_node, self._tree)
            else:
                # Skip nodes that are neither text nor element and try next sibling
                next_node = next_node.get_next_sibling()
                if next_node:
                    if next_node.is_text():
                        nav_string = NavigableString(next_node.get_text())
                        nav_string.parent = self.parent
                        return nav_string
                    elif next_node.is_element():
                        return Tag._create_from_tree_node(next_node, self._tree)
                return None
        except Exception as e:
            raise NavigationError(f"Failed to get next sibling: {e}")
    
    @property
    def previous_sibling(self) -> Optional[Union['Tag', str]]:
        """Get previous sibling."""
        try:
            if self._python_parent is not None or self._tree_has_python_overlays():
                parent = self.parent
                if parent is None:
                    return None
                siblings = parent.children
                index = parent._find_child_index(self)
                if index < 0:
                    return None
                return siblings[index - 1] if index > 0 else None

            if not self._node:
                return None
            
            prev_node = self._node.get_previous_sibling()
            if not prev_node:
                return None
            
            if prev_node.is_text():
                nav_string = NavigableString(prev_node.get_text())
                nav_string.parent = self.parent
                return nav_string
            elif prev_node.is_element():
                return Tag._create_from_tree_node(prev_node, self._tree)
            else:
                # Skip nodes that are neither text nor element and try previous sibling
                prev_node = prev_node.get_previous_sibling()
                if prev_node:
                    if prev_node.is_text():
                        nav_string = NavigableString(prev_node.get_text())
                        nav_string.parent = self.parent
                        return nav_string
                    elif prev_node.is_element():
                        return Tag._create_from_tree_node(prev_node, self._tree)
                return None
        except Exception as e:
            raise NavigationError(f"Failed to get previous sibling: {e}")
    
    @property
    def next_element(self) -> Optional[Union['Tag', 'NavigableString']]:
        """Get next element in tree traversal."""
        try:
            if not self._node:
                return None
            
            # Try to use get_next_element if available (from C++)
            if hasattr(self._node, 'get_next_element'):
                next_node = self._node.get_next_element()
                if next_node:
                    if next_node.is_text():
                        nav_string = NavigableString(next_node.get_text())
                        nav_string.parent = self.parent
                        return nav_string
                    else:
                        return Tag._create_from_tree_node(next_node, self._tree)
            else:
                # Fallback: find next element, skipping non-element nodes (CutySoup behavior)
                next_node = self._node.get_next_sibling()
                while next_node and not next_node.is_element():
                    next_node = next_node.get_next_sibling()
                
                if next_node and next_node.is_element():
                    return Tag._create_from_tree_node(next_node, self._tree)
                    
                # If no next sibling element, try going up to parent and continue search
                current = self._node
                while current and current.get_parent():
                    parent = current.get_parent()
                    if not parent:
                        break
                        
                    # Find next sibling of parent
                    parent_next = parent.get_next_sibling()
                    while parent_next and not parent_next.is_element():
                        parent_next = parent_next.get_next_sibling()
                    
                    if parent_next and parent_next.is_element():
                        return Tag._create_from_tree_node(parent_next, self._tree)
                        
                    current = parent
            return None
        except Exception as e:
            raise NavigationError(f"Failed to get next element: {e}")
    
    @property
    def previous_element(self) -> Optional[Union['Tag', 'NavigableString']]:
        """Get previous element in tree traversal."""
        try:
            if not self._node:
                return None
            
            # Try to use get_previous_element if available (from C++)
            if hasattr(self._node, 'get_previous_element'):
                prev_node = self._node.get_previous_element()
                if prev_node:
                    if prev_node.is_text():
                        nav_string = NavigableString(prev_node.get_text())
                        nav_string.parent = self.parent
                        return nav_string
                    else:
                        return Tag._create_from_tree_node(prev_node, self._tree)
            else:
                # Fallback: find previous element, skipping non-element nodes (CutySoup behavior)
                prev_node = self._node.get_previous_sibling()
                while prev_node and not prev_node.is_element():
                    prev_node = prev_node.get_previous_sibling()
                
                if prev_node and prev_node.is_element():
                    return Tag._create_from_tree_node(prev_node, self._tree)
                    
                # If no previous sibling element, try going up to parent and continue search
                current = self._node
                while current and current.get_parent():
                    parent = current.get_parent()
                    if not parent:
                        break
                        
                    # Find previous sibling of parent
                    parent_prev = parent.get_previous_sibling()
                    while parent_prev and not parent_prev.is_element():
                        parent_prev = parent_prev.get_previous_sibling()
                    
                    if parent_prev and parent_prev.is_element():
                        return Tag._create_from_tree_node(parent_prev, self._tree)
                        
                    current = parent
            return None
        except Exception as e:
            raise NavigationError(f"Failed to get previous element: {e}")
    
    @property
    def descendants(self) -> Iterator[Union['Tag', 'NavigableString']]:
        """Iterate over all descendant elements and text nodes, matching BeautifulSoup4 behavior."""
        if not self._node or self._tree_has_python_overlays():
            def _walk(tag: 'Tag'):
                for child in tag.children:
                    yield child
                    if hasattr(child, 'contents'):
                        yield from _walk(child)

            yield from _walk(self)
            return

        # MAXCPP OPTIMIZATION: Use C++ bulk operation for maximum performance
        if not self._node:
            return
        
        try:
            # Use new C++ method that efficiently collects all descendants including text nodes
            if hasattr(self._node, 'get_all_descendants_with_text'):
                cpp_descendants = self._node.get_all_descendants_with_text()
                # Convert C++ nodes to appropriate Python objects
                for cpp_node in cpp_descendants:
                    if cpp_node.is_element():
                        yield Tag._create_from_tree_node(cpp_node, self._tree)
                    elif cpp_node.is_text():
                        # Text nodes are returned as NavigableString objects to match BeautifulSoup4 behavior
                        text_content = cpp_node.get_text()
                        nav_string = NavigableString(text_content)
                        nav_string.parent = self  # Set parent reference
                        yield nav_string
            else:
                # Fallback to Python implementation if C++ method not available
                def _hybrid_descendants_fallback(node):
                    children = node.get_children()
                    for child in children:
                        if child.is_text():
                            text_content = child.get_text() if hasattr(child, 'get_text') else str(child)
                            nav_string = NavigableString(text_content)
                            nav_string.parent = Tag._create_from_tree_node(node, self._tree) if node else self
                            yield nav_string
                        elif child.is_element():
                            yield Tag._create_from_tree_node(child, self._tree)
                            yield from _hybrid_descendants_fallback(child)
                
                yield from _hybrid_descendants_fallback(self._node)
        except Exception as e:
            raise NavigationError(f"Failed to get descendants: {e}")
    
    @property
    def next_siblings(self) -> Iterator[Union['Tag', str]]:
        """Iterate over next siblings."""
        if self._python_parent is not None or self._tree_has_python_overlays():
            parent = self.parent
            if parent is None:
                return
            siblings = parent.children
            index = parent._find_child_index(self)
            if index < 0:
                return
            for sibling in siblings[index + 1:]:
                yield sibling
            return

        # MaxCpp Phase 33: Use C++ bulk operation instead of step-by-step navigation
        try:
            if not self._node:
                return
            
            # Get all next siblings (including text) in one C++ call
            sibling_nodes = self._node.get_all_next_siblings_including_text()
            # Convert C++ nodes to Python objects
            for sibling_node in sibling_nodes:
                if sibling_node.is_text():
                    text_content = sibling_node.get_text()
                    if text_content.strip():  # Only yield non-empty text
                        nav_string = NavigableString(text_content)
                        nav_string.parent = self.parent
                        yield nav_string
                elif sibling_node.is_element():
                    yield Tag._create_from_tree_node(sibling_node, self._tree)
        except AttributeError:
            # Fallback to Python implementation if C++ method not available
            if not self._node:
                return
                
            current_node = self._node.get_next_sibling()
            while current_node is not None:
                if current_node.is_text():
                    text_content = current_node.get_text()
                    if text_content.strip():  # Only yield non-empty text
                        nav_string = NavigableString(text_content)
                        nav_string.parent = self.parent
                        yield nav_string
                elif current_node.is_element():
                    yield Tag._create_from_tree_node(current_node, self._tree)
                
                current_node = current_node.get_next_sibling()
    
    @property
    def previous_siblings(self) -> Iterator[Union['Tag', str]]:
        """Iterate over previous siblings."""
        if self._python_parent is not None or self._tree_has_python_overlays():
            parent = self.parent
            if parent is None:
                return
            siblings = parent.children
            index = parent._find_child_index(self)
            if index < 0:
                return
            for sibling in reversed(siblings[:index]):
                yield sibling
            return

        # MaxCpp Phase 33: Use C++ bulk operation instead of step-by-step navigation
        try:
            if not self._node:
                return
            
            # Get all previous siblings (including text) in one C++ call
            sibling_nodes = self._node.get_all_previous_siblings_including_text()
            # Convert C++ nodes to Python objects (reverse order for previous siblings)
            for sibling_node in reversed(sibling_nodes):
                if sibling_node.is_text():
                    text_content = sibling_node.get_text()
                    if text_content.strip():  # Only yield non-empty text
                        nav_string = NavigableString(text_content)
                        nav_string.parent = self.parent
                        yield nav_string
                elif sibling_node.is_element():
                    yield Tag._create_from_tree_node(sibling_node, self._tree)
        except AttributeError:
            # Fallback to Python implementation if C++ method not available
            if not self._node:
                return
                
            current_node = self._node.get_previous_sibling()
            while current_node is not None:
                if current_node.is_text():
                    text_content = current_node.get_text()
                    if text_content.strip():  # Only yield non-empty text
                        nav_string = NavigableString(text_content)
                        nav_string.parent = self.parent
                        yield nav_string
                elif current_node.is_element():
                    yield Tag._create_from_tree_node(current_node, self._tree)
                
                current_node = current_node.get_previous_sibling()
    
    @property
    def parents(self) -> Iterator['Tag']:
        """Iterate over parent tags."""
        # MaxCpp Phase 33: Use C++ bulk operation instead of step-by-step navigation
        try:
            if not self._node:
                return
            
            # Get all parents in one C++ call
            parent_nodes = self._node.get_all_parents()
            # Convert C++ nodes to Python Tags
            for parent_node in parent_nodes:
                yield Tag._create_from_tree_node(parent_node, self._tree)
        except AttributeError:
            # Fallback to Python implementation if C++ method not available
            current = self.parent
            while current is not None:
                yield current
                current = current.parent
    
    def find(self, name: Union[str, List[str], Pattern, Callable, bool] = "", 
             attrs: Dict[str, str] = None, string: Union[str, Pattern] = None, 
             recursive: bool = True, **kwargs) -> Optional['Tag']:
        """Find first matching tag."""
        try:
            for tag in self.find_all(name, attrs, string, limit=1, recursive=recursive, **kwargs):
                return tag
            return None
        except Exception as e:
            raise NavigationError(f"Failed to find tag: {e}")
    
    def find_all(self, name: Union[str, List[str], Pattern, Callable, bool] = "", 
                 attrs: Dict[str, str] = None, string: Union[str, Pattern] = None, 
                 limit: Optional[int] = None, recursive: bool = True, **kwargs) -> List[Union['Tag', NavigableString]]:
        """Find all matching tags."""
        try:
            if not self._node or self._tree_has_python_search_mutations():
                return self._find_all_python(name, attrs, string, limit, recursive, **kwargs)
            
            results = []
            count = 0
            
            # Handle string-only search (BeautifulSoup4 behavior)
            if string is not None and not name and not attrs and not kwargs:
                # This is a pure string search - return NavigableString objects
                return self._find_string_only(string, limit)
            
            # Merge attrs and kwargs
            search_attrs = attrs or {}
            search_attrs.update(kwargs)
            if 'class_' in search_attrs:
                search_attrs['class'] = search_attrs.pop('class_')

            cache_key = self._get_simple_search_cache_key(name, search_attrs, string, limit, recursive)
            if cache_key is not None:
                cached_results = _get_cached_tree_search_results(self._tree, cache_key)
                if cached_results is not None:
                    return cached_results
            
            # Use bulk operations for root-level searches (eliminates Python/C++ boundary overhead)
            # Only use bulk ops when searching from root node to avoid descendant filtering complexity
            root_node = self._tree.get_root() if self._tree else None
            is_root_search = (root_node and hasattr(self._node, 'get_gumbo_node') and 
                             hasattr(root_node, 'get_gumbo_node') and
                             self._node.get_gumbo_node() == root_node.get_gumbo_node())
            
            if (is_root_search and isinstance(name, str) and name and 
                not search_attrs and recursive and hasattr(root_node, 'find_all_bulk')):
                # Simple tag-only search from root - use bulk operations for maximum speed
                try:
                    bulk_results = root_node.find_all_bulk(name, {})
                    results = []
                    
                    for bulk_node in bulk_results:
                        tag = Tag._create_from_tree_node(bulk_node, self._tree)
                        
                        # Apply auto-generated head element filtering for compatibility
                        if (tag.name == 'head' and name is True and not attrs and not search_attrs and 
                            hasattr(tag, 'contents') and len(tag.contents) == 0):
                            continue
                        
                        results.append(tag)
                        count += 1
                        if limit and count >= limit:
                            break
                    if cache_key is not None:
                        _cache_tree_search_results(self._tree, cache_key, results)
                    return results
                except Exception:
                    # Fall back to regular traversal if bulk operations fail
                    pass
            
            # Use bulk operations for class searches from root too
            if (is_root_search and not name and search_attrs.get('class') and len(search_attrs) == 1 and 
                recursive and hasattr(root_node, 'find_all_bulk')):
                try:
                    bulk_results = root_node.find_all_bulk("", search_attrs)
                    results = []
                    
                    for bulk_node in bulk_results:
                        tag = Tag._create_from_tree_node(bulk_node, self._tree)
                        results.append(tag)
                        count += 1
                        if limit and count >= limit:
                            break
                    if cache_key is not None:
                        _cache_tree_search_results(self._tree, cache_key, results)
                    return results
                except Exception:
                    # Fall back to regular traversal if bulk operations fail
                    pass
            
            # ULTRA OPTIMIZED: Use C++ find_descendants_by_tag when possible
            if (isinstance(name, str) and name and recursive and 
                hasattr(self._node, 'find_descendants_by_tag')):
                
                # Case 1: Pure tag search (fastest path)
                if not search_attrs:
                    try:
                        matching_nodes = self._node.find_descendants_by_tag(name)
                        for desc_node in matching_nodes:
                            tag = Tag._create_from_tree_node(desc_node, self._tree)
                            
                            # Apply basic filtering
                            if (tag.name == 'head' and name is True and not attrs and not search_attrs and 
                                hasattr(tag, 'contents') and len(tag.contents) == 0):
                                continue
                            
                            results.append(tag)
                            count += 1
                            if limit and count >= limit:
                                break
                        if cache_key is not None:
                            _cache_tree_search_results(self._tree, cache_key, results)
                        return results
                    except Exception:
                        pass
                
                # Case 2: Tag + single attribute search (optimized path)
                elif len(search_attrs) == 1:
                    try:
                        # Get all elements of the target tag first (fast C++ call)
                        matching_nodes = self._node.find_descendants_by_tag(name)
                        
                        # Filter by attributes using C++ calls (faster than Python)
                        attr_name, attr_value = next(iter(search_attrs.items()))
                        if attr_name == 'class_':
                            attr_name = 'class'
                        
                        for desc_node in matching_nodes:
                            # Fast C++ attribute check before creating Tag object
                            node_attr_value = desc_node.get_attribute(attr_name)
                            
                            if attr_name == 'class':
                                # Handle class attribute (space-separated)
                                if node_attr_value:
                                    node_class_list = node_attr_value.split()
                                    # Handle both single class string and list of classes
                                    if isinstance(attr_value, list):
                                        # For list: element matches if it has ANY of the specified classes (BeautifulSoup4 behavior)
                                        if any(cls in node_class_list for cls in attr_value):
                                            tag = Tag._create_from_tree_node(desc_node, self._tree)
                                            results.append(tag)
                                            count += 1
                                            if limit and count >= limit:
                                                break
                                    else:
                                        # For string: element matches if it has the exact class
                                        if attr_value in node_class_list:
                                            tag = Tag._create_from_tree_node(desc_node, self._tree)
                                            results.append(tag)
                                            count += 1
                                            if limit and count >= limit:
                                                break
                            else:
                                # Handle other attributes (exact match or regex)
                                match = False
                                if hasattr(attr_value, 'match'):  # regex pattern
                                    if node_attr_value and attr_value.match(str(node_attr_value)):
                                        match = True
                                elif node_attr_value == attr_value:  # exact match
                                    match = True

                                if match:
                                    tag = Tag._create_from_tree_node(desc_node, self._tree)
                                    results.append(tag)
                                    count += 1
                                    if limit and count >= limit:
                                        break
                        if cache_key is not None:
                            _cache_tree_search_results(self._tree, cache_key, results)
                        return results
                    except Exception:
                        pass
            
            # OPTIMIZED: Filter at C++ level before creating Tag objects (reduces boundary crossings)
            def _get_matching_descendants_optimized(node):
                """Get only matching descendants, filtering at C++ level to reduce boundary crossings."""
                children = node.get_children()
                for child in children:
                    if child.is_element():
                        # Pre-filter at C++ level using existing fast methods
                        child_matches = True
                        
                        # Quick C++ level name check (if we have a simple string name)
                        if isinstance(name, str) and name:
                            child_name = child.get_name()  # Single C++ call
                            if child_name != name:
                                child_matches = False
                        
                        # Quick C++ level attribute check for simple cases
                        if child_matches and search_attrs and len(search_attrs) == 1:
                            attr_name, attr_value = next(iter(search_attrs.items()))
                            # Handle class_ parameter (Python convention)
                            if attr_name == 'class_':
                                attr_name = 'class'
                            
                            child_attr_value = child.get_attribute(attr_name)  # Single C++ call
                            
                            if attr_name == 'class':
                                # Handle class attribute specially (space-separated values)
                                if not child_attr_value:
                                    child_matches = False
                                else:
                                    class_list = child_attr_value.split()
                                    # Handle both single class string and list of classes
                                    if isinstance(attr_value, list):
                                        # For list: element matches if it has ANY of the specified classes (BeautifulSoup4 behavior)
                                        if not any(cls in class_list for cls in attr_value):
                                            child_matches = False
                                    else:
                                        # For string: element matches if it has the exact class
                                        if attr_value not in class_list:
                                            child_matches = False
                            else:
                                # Handle other attributes (exact match or regex)
                                if hasattr(attr_value, 'match'):  # regex pattern
                                    if not child_attr_value or not attr_value.match(str(child_attr_value)):
                                        child_matches = False
                                elif child_attr_value != attr_value:  # exact match
                                    child_matches = False
                        
                        if child_matches:
                            yield child
                        
                        # Recurse into children regardless of match (but only if recursive)
                        if recursive:
                            yield from _get_matching_descendants_optimized(child)
            
            # Use optimized traversal that pre-filters at C++ level
            for desc_node in _get_matching_descendants_optimized(self._node):
                # Only create Tag objects for nodes that passed C++ level filtering
                tag = Tag._create_from_tree_node(desc_node, self._tree)
                
                # Final Python-level filtering for complex cases
                # Filter out auto-generated empty head elements for BeautifulSoup4 compatibility
                if (tag.name == 'head' and name is True and not attrs and not search_attrs and 
                    hasattr(tag, 'contents') and len(tag.contents) == 0):
                    continue
                
                # Double-check name match for complex patterns (regex, callable, etc.)
                if not is_tag_name_match(tag, name):
                    continue
                
                # Double-check attributes for complex cases
                if search_attrs and len(search_attrs) > 1:
                    match = True
                    for attr_name, attr_value in search_attrs.items():
                        if attr_name == 'class_':
                            attr_name = 'class'
                        
                        tag_value = tag.get(attr_name)
                        if isinstance(attr_value, str):
                            # Handle class attribute special case where tag_value is a list
                            if attr_name == 'class' and isinstance(tag_value, list):
                                if attr_value not in tag_value:
                                    match = False
                                    break
                            elif tag_value != attr_value:
                                match = False
                                break
                        elif hasattr(attr_value, 'match'):  # regex
                            if tag_value is None or not attr_value.match(str(tag_value)):
                                match = False
                                break
                        elif callable(attr_value):
                            if not attr_value(tag_value):
                                match = False
                                break
                    
                    if not match:
                        continue
                
                # Check string content
                if string is not None:
                    tag_text = tag.get_text()
                    if isinstance(string, str):
                        if string not in tag_text:
                            continue
                    elif hasattr(string, 'search'):  # regex
                        if not string.search(tag_text):
                            continue
                
                results.append(tag)
                count += 1
                
                if limit and count >= limit:
                    break
            
            if cache_key is not None:
                _cache_tree_search_results(self._tree, cache_key, results)
            return results
        except Exception as e:
            raise NavigationError(f"Failed to find tags: {e}")
    
    
    def _find_string_only(self, string: Union[str, Pattern], limit: Optional[int] = None) -> List[NavigableString]:
        """Find NavigableString objects that match the string criteria."""
        if not self._node or self._tree_has_python_search_mutations():
            return self._find_string_only_python(string, limit)

        results = []
        count = 0
        
        def _search_strings_recursive(node):
            nonlocal count
            if limit and count >= limit:
                return
                
            children = node.get_children()
            for child in children:
                if child.is_text():
                    text_content = child.get_text()
                    match = False
                    
                    if isinstance(string, str):
                        match = string in text_content
                    elif hasattr(string, 'search'):  # regex
                        match = bool(string.search(text_content))
                    
                    if match:
                        nav_string = NavigableString(text_content)
                        nav_string.parent = self
                        results.append(nav_string)
                        count += 1
                        
                        if limit and count >= limit:
                            return
                else:
                    _search_strings_recursive(child)
        
        _search_strings_recursive(self._node)
        return results

    def find_all_lazy(self, name: Union[str, List[str], Pattern, Callable, bool] = "",
                      attrs: Dict[str, str] = None, recursive: bool = True, **kwargs):
        """Return a lazy result set for simple parsed-node searches."""
        from .lazy_results import create_lazy_result_set

        if not self._node or self._tree_has_python_search_mutations():
            materialized = self.find_all(name, attrs, recursive=recursive, **kwargs)
            node_refs = [tag._node for tag in materialized if isinstance(tag, Tag) and tag._node]
            return create_lazy_result_set(node_refs, self._tree, self)

        search_attrs = attrs.copy() if attrs else {}
        search_attrs.update(kwargs)
        if 'class_' in search_attrs:
            search_attrs['class'] = search_attrs.pop('class_')

        node_refs = None

        if isinstance(name, str) and name and recursive and not search_attrs:
            if hasattr(self._node, 'find_descendants_by_tag'):
                node_refs = self._node.find_descendants_by_tag(name)
            else:
                node_refs = self._node.find_all(name, {})
        elif not name and search_attrs.get('class') and len(search_attrs) == 1:
            node_refs = self._node.find_all("", {"class": search_attrs['class']})
        elif not name and search_attrs.get('id') and len(search_attrs) == 1:
            node = self._node.find("", {"id": search_attrs['id']})
            node_refs = [node] if node else []

        if node_refs is None:
            materialized = self.find_all(name, attrs, recursive=recursive, **kwargs)
            node_refs = [tag._node for tag in materialized if isinstance(tag, Tag) and tag._node]

        return create_lazy_result_set(node_refs, self._tree, self)
    
    def select(self, selector: str) -> List['Tag']:
        """Select tags using CSS selector."""
        return select_tags(self, selector)
    
    def select_one(self, selector: str) -> Optional['Tag']:
        """Select first tag using CSS selector."""
        return select_one_tag(self, selector)
    
    def prettify(self, indent: str = "  ") -> str:
        """Return prettified HTML representation."""
        try:
            if not self._node:
                return f"<{self.name}></{self.name}>"
            
            return self._prettify_recursive(0, indent)
        except Exception as e:
            raise NavigationError(f"Failed to prettify: {e}")
    
    def _prettify_recursive(self, depth: int, indent: str) -> str:
        """Recursive prettify helper."""
        result = []
        indent_str = indent * depth
        
        # Opening tag
        tag_str = f"<{self.name}"
        for attr_name, attr_value in self.attrs.items():
            tag_str += f' {attr_name}="{attr_value}"'
        tag_str += ">"
        
        result.append(indent_str + tag_str)
        
        # Contents
        for child in self.children:
            if isinstance(child, str):
                if child.strip():
                    result.append(indent_str + indent + child.strip())
            else:
                result.append(child._prettify_recursive(depth + 1, indent))
        
        # Closing tag
        result.append(indent_str + f"</{self.name}>")
        
        return "\n".join(result)
    
    # Tree manipulation methods
    def decompose(self):
        """Remove tag and contents from tree."""
        TreeManipulator.decompose_tag(self)
    
    def extract(self) -> 'Tag':
        """Remove tag from tree but return it."""
        return TreeManipulator.extract_tag(self)
    
    def replace_with(self, new_content):
        """Replace tag with new content."""
        TreeManipulator.replace_tag_with(self, new_content)
        return self
    
    def wrap(self, wrapper_tag):
        """Wrap tag with another tag."""
        return TreeManipulator.wrap_tag(self, wrapper_tag)
    
    def unwrap(self):
        """Remove tag but keep contents."""
        return TreeManipulator.unwrap_tag(self)
    
    def clear(self):
        """Remove all contents."""
        TreeManipulator.clear_tag(self)
    
    def append(self, new_child):
        """Append new child."""
        TreeManipulator.append_to_tag(self, new_child)
    
    def insert(self, position: int, new_child):
        """Insert child at position."""
        TreeManipulator.insert_into_tag(self, position, new_child)
    
    def insert_before(self, new_sibling):
        """Insert sibling before this tag."""
        TreeManipulator.insert_before_tag(self, new_sibling)
    
    def insert_after(self, new_sibling):
        """Insert sibling after this tag."""
        TreeManipulator.insert_after_tag(self, new_sibling)
    
    def copy(self) -> 'Tag':
        """Create a copy of the tag."""
        return TreeManipulator.copy_tag(self)
    
    def clone(self) -> 'Tag':
        """Create a deep copy of the tag."""
        return TreeManipulator.clone_tag(self)
    
    def __str__(self) -> str:
        """String representation."""
        try:
            # Build HTML string manually since C++ to_string doesn't exist
            result = f"<{self.name}"
            
            # Add attributes
            for attr_name, attr_value in self.attrs.items():
                # Handle class attribute which is a list
                if attr_name == 'class' and isinstance(attr_value, list):
                    attr_value = ' '.join(attr_value)
                result += f' {attr_name}="{attr_value}"'
            
            result += ">"
            
            # Add children content (both text and element children)
            try:
                for child in self.children:
                    result += str(child)
            except (AttributeError, TypeError):
                # Fallback to text content only
                try:
                    text_content = self.get_text()
                    if text_content:
                        result += text_content
                except (AttributeError, TypeError):
                    pass
            
            result += f"</{self.name}>"
            return result
        except (AttributeError, TypeError):
            return f"<{self.name}></{self.name}>"
    
    def __repr__(self) -> str:
        """Representation for debugging."""
        return f"<Tag: {self.name}>"


# Initialize global tag pool for memory optimization
_tag_pool = TagPool(initial_size=500)


class CutySoup:
    """
    Main parser class providing BeautifulSoup4-compatible interface.
    High-performance HTML parsing using C++ Gumbo backend.
    """
    
    def __init__(self, markup: Union[str, bytes] = "", 
                 features: str = "html.parser",
                 config: Optional[ParserConfig] = None,
                 from_encoding: Optional[str] = None,
                 exclude_encodings: Optional[List[str]] = None,
                 parse_only: Optional[Any] = None,
                 **kwargs):
        """Initialize CutySoup parser."""
        self._config = config or get_default_config()
        self._encoding_detector = EncodingDetector(self._config)
        
        # Handle BeautifulSoup4-compatible parameters
        self._from_encoding = from_encoding
        self._exclude_encodings = exclude_encodings or []
        self._parse_only = parse_only
        
        try:
            # Validate and normalize input
            if markup:
                markup = validate_html_content(markup)
                if isinstance(markup, bytes):
                    if from_encoding:
                        # Use specified encoding
                        try:
                            markup = markup.decode(from_encoding)
                        except UnicodeDecodeError:
                            markup = self._encoding_detector.convert_to_unicode(markup)
                    else:
                        markup = self._encoding_detector.convert_to_unicode(markup)
            
            # Apply parse_only filter if specified
            if parse_only:
                markup = self._apply_parse_only_filter(markup, parse_only)
            
            # Create tree using C++ extension
            self._tree = _cutysoup_ext.Tree(markup or "")
            self._tree_state_key = id(self._tree)
            self._tree_state_finalizer = weakref.finalize(self, _clear_tree_overlay_state, self._tree_state_key)
            
            if markup and not self._tree:
                if self._config.get('error_handling') == 'strict':
                    raise ParseError("Failed to parse HTML markup", {"markup_length": len(markup)})
            
            # Store original markup and encoding info
            self._original_markup = markup
            self._declared_encoding = from_encoding or self._encoding_detector.get_declared_encoding(markup) if markup else None
            if self._config.get("prewarm_python_cache", False):
                self._prewarm_wrapper_cache()
            
        except Exception as e:
            if self._config.get('error_handling') == 'strict':
                raise ParseError(f"Parser initialization failed: {e}")
            # In lenient mode, create empty tree
            self._tree = _cutysoup_ext.Tree("")
            self._tree_state_key = id(self._tree)
            self._tree_state_finalizer = weakref.finalize(self, _clear_tree_overlay_state, self._tree_state_key)
            self._original_markup = ""
            self._declared_encoding = None
            if self._config.get("prewarm_python_cache", False):
                self._prewarm_wrapper_cache()
    
    @property
    def name(self) -> str:
        """Get document name (always '[document]')."""
        return '[document]'
    
    def find(self, name: Union[str, List[str], Pattern, Callable, bool] = "", 
             attrs: Dict[str, str] = None, string: Union[str, Pattern] = None, 
             recursive: bool = True, **kwargs) -> Optional[Tag]:
        """Find first matching tag in document."""
        try:
            root = self._get_root_tag()
            if root:
                return root.find(name, attrs, string, recursive=recursive, **kwargs)
            return None
        except Exception as e:
            raise NavigationError(f"Failed to find tag: {e}")
    
    def find_all(self, name: Union[str, List[str], Pattern, Callable, bool] = "", 
                 attrs: Dict[str, str] = None, string: Union[str, Pattern] = None, 
                 limit: Optional[int] = None, recursive: bool = True, **kwargs) -> List[Union[Tag, NavigableString]]:
        """Find all matching tags in document."""
        try:
            from .utils import is_tag_name_match
            
            root = self._get_root_tag()
            if not root:
                return []
            
            results = []
            
            # Handle string-only search separately (don't include root for these)
            if string is not None and not name and not attrs and not kwargs:
                # This is a pure string search - delegate to root's implementation
                return root.find_all(name, attrs, string, limit, recursive, **kwargs)

            search_attrs = attrs.copy() if attrs else {}
            search_attrs.update(kwargs)
            if 'class_' in search_attrs:
                search_attrs['class'] = search_attrs.pop('class_')

            # Delegate common bs4-style searches to the cached root implementation.
            if recursive and string is None:
                if isinstance(name, str) and name and not search_attrs:
                    return root.find_all(name, attrs, string, limit, recursive, **kwargs)
                if not name and len(search_attrs) == 1 and isinstance(search_attrs.get('class'), str):
                    return root.find_all(name, attrs, string, limit, recursive, **kwargs)
                if not name and len(search_attrs) == 1 and isinstance(search_attrs.get('id'), str):
                    return root.find_all(name, attrs, string, limit, recursive, **kwargs)
            
            # Use bulk operations for soup-level searches (maximum performance)
            if (isinstance(name, str) and name and 
                not attrs and not kwargs and recursive and 
                hasattr(root._node, 'find_all_bulk')):
                try:
                    # Use bulk operations for simple tag searches
                    bulk_results = root._node.find_all_bulk(name, {})
                    for bulk_node in bulk_results:
                        tag = Tag._create_from_tree_node(bulk_node, self._tree)
                        results.append(tag)
                        if limit and len(results) >= limit:
                            break
                    return results
                except Exception:
                    # Fall back to regular search
                    pass
            
            # Use bulk operations for class searches at soup level
            if (not name and search_attrs.get('class') and len(search_attrs) == 1 and 
                recursive and hasattr(root._node, 'find_all_bulk')):
                try:
                    bulk_results = root._node.find_all_bulk("", search_attrs)
                    for bulk_node in bulk_results:
                        tag = Tag._create_from_tree_node(bulk_node, self._tree)
                        results.append(tag)
                        if limit and len(results) >= limit:
                            break
                    return results
                except Exception:
                    # Fall back to regular search
                    pass
            
            # For non-string searches, check if root matches (for soup.find_all behavior)
            # Check if root matches the criteria
            if is_tag_name_match(root, name):
                if not search_attrs:
                    # No attribute criteria, root matches
                    results.append(root)
                else:
                    # Check attributes for root
                    root_matches = True
                    for attr_name, attr_value in search_attrs.items():
                        if attr_name == 'class_':
                            attr_name = 'class'
                        
                        root_value = root.get(attr_name)
                        if not self._attribute_matches(root_value, attr_value, attr_name):
                            root_matches = False
                            break
                    
                    if root_matches:
                        results.append(root)
            
            # Then get descendants from root
            descendants = root.find_all(name, attrs, string, limit, recursive, **kwargs)
            results.extend(descendants)
            
            # Filter out auto-generated empty head elements for BeautifulSoup4 compatibility
            # Only when searching for all elements (name is True or empty)
            if (name is True or name == "") and not attrs and not search_attrs:
                # Check if original markup contained a head element
                original_has_head = '<head' in self._original_markup.lower()
                if not original_has_head:
                    # Filter out empty head elements
                    results = [r for r in results if not (hasattr(r, 'name') and r.name == 'head' and 
                                                         hasattr(r, 'contents') and len(r.contents) == 0)]
            
            # Apply limit if specified
            if limit and limit > 0:
                results = results[:limit]
                
            return results
        except Exception as e:
            raise NavigationError(f"Failed to find tags: {e}")
    
    def _attribute_matches(self, tag_value, search_value, attr_name=None):
        """Check if a tag's attribute value matches the search criteria."""
        if isinstance(search_value, str):
            # Handle class attribute special case where tag_value is a list
            if attr_name == 'class' and isinstance(tag_value, list):
                return search_value in tag_value
            else:
                return tag_value == search_value
        elif hasattr(search_value, 'match'):  # regex
            if tag_value is None:
                return False
            return search_value.match(str(tag_value))
        elif callable(search_value):
            return search_value(tag_value)
        elif search_value is True:
            return tag_value is not None
        elif search_value is False or search_value is None:
            return tag_value is None
        else:
            return tag_value == search_value
    
    @property
    def text(self) -> str:
        """Get all text content from document."""
        root = self._get_root_tag()
        if root:
            return root.text
        return ""
    
    @property
    def strings(self) -> Iterator[str]:
        """Iterate over all strings in document."""
        root = self._get_root_tag()
        if root:
            return root.strings
        return iter([])
    
    @property
    def stripped_strings(self) -> Iterator[str]:
        """Iterate over non-empty strings in document."""
        root = self._get_root_tag()
        if root:
            return root.stripped_strings
        return iter([])
    
    @property
    def original_encoding(self) -> str:
        """Get original encoding of document."""
        return self._declared_encoding or self._config.encoding
    
    def get_text(self, separator: str = "", strip: bool = False) -> str:
        """Get text with separator and strip options."""
        # ULTRA-OPTIMIZED: Use fast C++ get_text method when possible  
        if separator == "" and not strip:
            # Simple case - use blazingly fast C++ method directly
            root_node = self._tree.get_root() if self._tree else None
            if root_node:
                return root_node.get_text()
            return ""
        else:
            # Complex case - fall back to Python implementation
            root = self._get_root_tag()
            if root:
                return root.get_text(separator, strip)
            return ""
    
    def select(self, selector: str) -> List[Tag]:
        """Select tags using CSS selector."""
        root = self._get_root_tag()
        if root:
            return root.select(selector)
        return []
    
    def select_one(self, selector: str) -> Optional[Tag]:
        """Select first tag using CSS selector."""
        root = self._get_root_tag()
        if root:
            return root.select_one(selector)
        return None
    
    def prettify(self, indent: str = "  ") -> str:
        """Return prettified HTML."""
        try:
            # Get the root tag and prettify it
            root_tag = self._get_root_tag()
            if root_tag:
                return root_tag.prettify(indent)
            
            # Fallback: try to prettify from html tag
            html_tag = self.find('html')
            if html_tag:
                return html_tag.prettify(indent)
            
            # Last resort: prettify the first child
            if hasattr(self, 'contents') and self.contents:
                first_child = self.contents[0]
                if hasattr(first_child, 'prettify'):
                    return first_child.prettify(indent)
            
            return ""
        except Exception as e:
            raise NavigationError(f"Failed to prettify document: {e}")
    
    def _get_root_tag(self) -> Optional[Tag]:
        """Get root tag of document."""
        try:
            if not self._tree:
                return None
            
            root_node = self._tree.get_root()
            if root_node:
                return Tag._create_from_tree_node(root_node, self._tree)
            return None
        except Exception as e:
            if self._config.debug_mode:
                raise NavigationError(f"Failed to get root tag: {e}")
            return None

    def _prewarm_wrapper_cache(self) -> None:
        """Materialize wrappers for bounded-size trees to speed first queries."""
        if not self._tree or not hasattr(self._tree, "get_indexed_element_count"):
            return

        try:
            indexed_count = self._tree.get_indexed_element_count()
        except Exception:
            return

        if indexed_count <= 0 or indexed_count > _TREE_WRAPPER_PREWARM_LIMIT:
            return

        try:
            root_node = self._tree.get_root()
            if not root_node:
                return

            root_tag = Tag._create_from_tree_node(root_node, self._tree)
            root_key = root_tag._node_key() if root_tag else None
            tags_by_name = {}
            tags_by_class = {}
            tags_by_id = {}

            stack = [root_node]
            while stack:
                node = stack.pop()
                if node is None:
                    continue

                is_element = node.is_element()
                is_document = node.is_document()
                if is_element or is_document:
                    tag = Tag._create_from_tree_node(node, self._tree)
                    if is_element and tag is not None:
                        tag_name = node.get_name()
                        if tag_name:
                            tags_by_name.setdefault(tag_name, []).append(tag)

                        class_attr = node.get_attribute("class")
                        if class_attr:
                            for class_name in class_attr.split():
                                if class_name:
                                    tags_by_class.setdefault(class_name, []).append(tag)

                        id_attr = node.get_attribute("id")
                        if id_attr:
                            tags_by_id[id_attr] = tag

                try:
                    children = node.get_children()
                except Exception:
                    continue

                for child in reversed(children):
                    if child is None:
                        continue
                    if child.is_element() or child.is_document():
                        stack.append(child)

            if root_key is not None:
                for tag_name, tags in tags_by_name.items():
                    _cache_tree_search_results(self._tree, ("tag", root_key, tag_name, None), tags)
                    _cache_tree_search_results(self._tree, ("tag", root_key, tag_name, 1), tags[:1])
                for class_name, tags in tags_by_class.items():
                    _cache_tree_search_results(self._tree, ("class", root_key, class_name, None), tags)
                    _cache_tree_search_results(self._tree, ("class", root_key, class_name, 1), tags[:1])
                if len(tags_by_id) <= 64:
                    for tag_id, tag in tags_by_id.items():
                        _cache_tree_search_results(self._tree, ("id", root_key, tag_id, None), [tag])
                        _cache_tree_search_results(self._tree, ("id", root_key, tag_id, 1), [tag])
        except Exception:
            # Parsing must stay resilient even if prewarming fails.
            return
    
    def _apply_parse_only_filter(self, markup: str, parse_only) -> str:
        """Apply parse_only filter to markup."""
        # Simple implementation - in a full implementation this would be more sophisticated
        if hasattr(parse_only, 'name'):
            # Filter to only include specific tag types
            import re
            tag_pattern = f'<{re.escape(parse_only.name)}[^>]*>.*?</{re.escape(parse_only.name)}>'
            matches = re.findall(tag_pattern, markup, re.DOTALL | re.IGNORECASE)
            if matches:
                return f'<html><body>{"".join(matches)}</body></html>'
        
        return markup
    
    # Tag creation methods
    def new_tag(self, tag_name: str, attrs: Dict[str, str] = None, **kwargs) -> Tag:
        """Create a new tag."""
        try:
            from .utils import validate_tag_name
            
            tag_name = validate_tag_name(tag_name)
            all_attrs = {}
            if attrs:
                all_attrs.update(attrs)
            
            # Handle BeautifulSoup4 class_ -> class conversion
            if 'class_' in kwargs:
                kwargs['class'] = kwargs.pop('class_')
            
            all_attrs.update(kwargs)
            
            # Try to create a new tag using C++ extension
            try:
                new_node = self._tree.create_element(tag_name, all_attrs)
                if new_node and hasattr(new_node, 'get_name') and new_node.get_name() == tag_name:
                    # C++ created the correct tag
                    return Tag._create_from_tree_node(new_node, self._tree)
            except (AttributeError, TypeError):
                pass
            
            # Fallback: Create a Python-only tag for compatibility
            new_tag = Tag(None)
            new_tag._tree = self._tree
            new_tag._tag_name = tag_name
            new_tag._cached_attrs = all_attrs.copy()
            new_tag._is_new_tag = True
            new_tag._python_contents = []
            new_tag._python_parent = None
            return new_tag
            
        except Exception as e:
            raise ParseError(f"Failed to create new tag: {e}", 
                           {"tag_name": tag_name, "attrs": attrs})
    
    def new_string(self, text: str) -> NavigableString:
        """Create a new navigable string."""
        return NavigableString(text)
    
    def new_comment(self, text: str) -> NavigableString:
        """Create a new comment string."""
        comment = NavigableString(text)
        comment._is_comment = True
        return comment
    
    def new_cdata(self, text: str) -> NavigableString:
        """Create a new CDATA string."""
        cdata = NavigableString(text)
        cdata._is_cdata = True
        return cdata
    
    def new_doctype(self, doctype: str) -> NavigableString:
        """Create a new doctype string."""
        doctype_str = NavigableString(doctype)
        doctype_str._is_doctype = True
        return doctype_str
    
    # Diagnostic methods
    def diagnose(self, markup: Union[str, bytes] = None) -> str:
        """Provide diagnostic information about parsing."""
        try:
            if markup is None:
                markup = self._original_markup
            
            if isinstance(markup, bytes):
                markup = self._encoding_detector.convert_to_unicode(markup)
            
            diagnostics = []
            diagnostics.append("Diagnostic Report")
            diagnostics.append("=" * 50)
            diagnostics.append(f"Original encoding: {self.original_encoding}")
            diagnostics.append(f"Markup length: {len(markup)} characters")
            diagnostics.append(f"Parser config: {self._config.to_dict()}")
            
            # Parse statistics
            root = self._get_root_tag()
            if root:
                tag_count = len(list(root.descendants)) + 1
                text_nodes = len(list(root.strings))
                diagnostics.append(f"Total tags: {tag_count}")
                diagnostics.append(f"Text nodes: {text_nodes}")
            
            return "\n".join(diagnostics)
            
        except Exception as e:
            return f"Diagnostic failed: {e}"
    
    def contains_replacement_characters(self) -> bool:
        """Check if document contains replacement characters."""
        return '\ufffd' in self.get_text()
    
    def declared_html_encoding(self) -> str:
        """Get encoding declared in HTML."""
        return self._declared_encoding or ""
    
    def __getattr__(self, name: str):
        """Allow direct access to HTML elements like soup.html, soup.body, etc."""
        # Look for a child element with the given tag name
        root = self._get_root_tag()
        if root:
            # First check if the root tag itself matches
            if root.name == name:
                return root
            # Then look for a child with that name
            result = root.find(name)
            if result:
                return result
        raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
    
    # Zero-context methods for ultra-high performance
    def get_all_hrefs(self) -> List[str]:
        """Get all href attributes from anchor tags (zero-context)."""
        root = self._get_root_tag()
        if root and hasattr(root._node, 'get_all_hrefs'):
            return root._node.get_all_hrefs()
        return []
    
    def get_all_src_attributes(self) -> List[str]:
        """Get all src attributes from img tags (zero-context)."""
        root = self._get_root_tag()
        if root and hasattr(root._node, 'get_all_src_attributes'):
            return root._node.get_all_src_attributes()
        return []
    
    def get_all_text_by_tag(self, tag_name: str) -> List[str]:
        """Get all text content for elements with specified tag (zero-context)."""
        root = self._get_root_tag()
        if root and hasattr(root._node, 'get_all_text_by_tag'):
            return root._node.get_all_text_by_tag(tag_name)
        return []
    
    def get_all_text_by_class(self, class_name: str) -> List[str]:
        """Get all text content for elements with specified class (zero-context)."""
        root = self._get_root_tag()
        if root and hasattr(root._node, 'get_all_text_by_class'):
            return root._node.get_all_text_by_class(class_name)
        return []
    
    def count_elements_by_tag(self, tag_name: str) -> int:
        """Count elements with specified tag (zero-context)."""
        root = self._get_root_tag()
        if root and hasattr(root._node, 'count_elements_by_tag'):
            return root._node.count_elements_by_tag(tag_name)
        return 0
    
    def count_elements_by_class(self, class_name: str) -> int:
        """Count elements with specified class (zero-context)."""
        root = self._get_root_tag()
        if root and hasattr(root._node, 'count_elements_by_class'):
            return root._node.count_elements_by_class(class_name)
        return 0

    def find_all_lazy(self, name: Union[str, List[str], Pattern, Callable, bool] = "",
                      attrs: Dict[str, str] = None, recursive: bool = True, **kwargs):
        """Return a lazy result set for parsed-node scraping workflows."""
        root = self._get_root_tag()
        if not root:
            from .lazy_results import create_lazy_result_set
            return create_lazy_result_set([], self._tree, None)
        return root.find_all_lazy(name, attrs, recursive=recursive, **kwargs)
    
    def __str__(self) -> str:
        """String representation of document."""
        try:
            root = self._get_root_tag()
            if root:
                return str(root)
            return ""
        except (AttributeError, TypeError):
            return ""
    
    def query(self, query_plan: Union[str, Dict]) -> Dict[str, Any]:
        """
        Execute a CSQL (CutySoup Query Language) query for structured data extraction.
        
        Args:
            query_plan: JSON string or dict containing the CSQL query plan
            
        Returns:
            Dict containing the extracted data according to the query plan
            
        Example:
            plan = {
                "titles": {
                    "selector": "h1, h2",
                    "type": "list",
                    "extract": "text"
                },
                "links": {
                    "selector": "a",
                    "type": "list",
                    "extract": "attribute",
                    "attribute": "href"
                }
            }
            result = soup.query(plan)
        """
        from .csql import CSQLEngine
        engine = CSQLEngine(self)
        return engine.execute_query(query_plan)
    
    def __repr__(self) -> str:
        """Representation for debugging."""
        return f"<CutySoup: {len(self._original_markup)} chars>"
