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

from typing import Dict, List, Optional, Union, Iterator, Callable, Pattern
import re
import chardet
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 CSSSelector:
    """Simple CSS selector parser and matcher."""
    
    # Simple cache for parsed selectors
    _selector_cache = {}
    
    def __init__(self, selector: str):
        self.selector = selector.strip()
        # Use cache for parsing to improve performance
        if selector in CSSSelector._selector_cache:
            self.parts = CSSSelector._selector_cache[selector]
        else:
            self.parts = self._parse_selector()
            # Limit cache size to prevent memory leaks
            if len(CSSSelector._selector_cache) < 100:
                CSSSelector._selector_cache[selector] = self.parts
    
    def _parse_selector(self) -> List[Dict]:
        """Parse CSS selector into components with combinator support."""
        # Handle multiple selectors separated by commas
        if ',' in self.selector:
            # For multiple selectors, we'll treat each as a separate selector
            # and combine results later
            self.multiple_selectors = [s.strip() for s in self.selector.split(',')]
            # Return the first one for now, handle multiple in matching
            return self._parse_single_selector(self.multiple_selectors[0])
        else:
            return self._parse_single_selector(self.selector)
    
    def _parse_single_selector(self, selector: str) -> List[Dict]:
        """Parse a single selector with combinators."""
        parts = []
        
        # Split by different combinators while preserving them
        # First, protect attribute selectors by temporarily replacing them
        protected_attrs = []
        attr_placeholder = "ATTR_PLACEHOLDER_{}"
        temp_selector = selector.strip()
        
        # Find and protect attribute selectors
        attr_pattern = r'\[[^\]]*\]'
        attrs = re.findall(attr_pattern, temp_selector)
        for i, attr in enumerate(attrs):
            placeholder = attr_placeholder.format(i)
            protected_attrs.append(attr)
            temp_selector = temp_selector.replace(attr, placeholder, 1)
        
        # Now split on combinators with protected attributes
        combinator_pattern = r'(\s*[>+~]\s*|\s+)'
        tokens = re.split(combinator_pattern, temp_selector)
        
        # Restore attribute selectors in tokens
        for i, token in enumerate(tokens):
            for j, attr in enumerate(protected_attrs):
                placeholder = attr_placeholder.format(j)
                if placeholder in token:
                    tokens[i] = token.replace(placeholder, attr)
        
        current_combinator = 'descendant'  # Default combinator
        
        for i, token in enumerate(tokens):
            token = token.strip()
            if not token:
                continue
                
            if token == '>':
                current_combinator = 'child'
            elif token == '+':
                current_combinator = 'adjacent_sibling'
            elif token == '~':
                current_combinator = 'general_sibling'
            else:
                # This is a selector part (not a standalone combinator)
                if token:
                    part = self._parse_simple_selector(token)
                    part['combinator'] = current_combinator
                    parts.append(part)
                    current_combinator = 'descendant'  # Reset to default
        
        return parts
    
    def _parse_simple_selector(self, selector: str) -> Dict:
        """Parse a simple selector like 'div.class#id:pseudo'."""
        result = {'tag': '', 'id': '', 'classes': [], 'attrs': {}, 'pseudo': ''}
        
        # Extract pseudo-selectors first
        if ':' in selector:
            colon_pos = selector.find(':')
            selector_part = selector[:colon_pos]
            pseudo_part = selector[colon_pos + 1:]
            
            # Handle parenthesized pseudo-selectors like :not() and :nth-child()
            if '(' in pseudo_part:
                # Find the matching closing parenthesis
                paren_count = 0
                end_pos = 0
                for i, char in enumerate(pseudo_part):
                    if char == '(':
                        paren_count += 1
                    elif char == ')':
                        paren_count -= 1
                        if paren_count == 0:
                            end_pos = i + 1
                            break
                if end_pos > 0:
                    result['pseudo'] = pseudo_part[:end_pos]
                    # Remove pseudo from selector part for further parsing
                    selector = selector_part
                else:
                    # Malformed parentheses, fallback to old method
                    result['pseudo'] = pseudo_part.split('.')[0].split('#')[0].split('[')[0]
                    selector = selector_part
            else:
                # Simple pseudo-selector without parentheses
                result['pseudo'] = pseudo_part.split('.')[0].split('#')[0].split('[')[0]
                selector = selector_part
        
        # Extract attributes FIRST with advanced operators [attr^=value], [attr$=value], etc.
        # Enhanced pattern to capture different attribute operators
        attr_pattern = r'\[([^=\]~|^$*]+)([~|^$*]?=?)([^\]]*)\]'
        attrs = re.findall(attr_pattern, selector)
        for attr_name, operator, attr_value in attrs:
            attr_name = attr_name.strip()
            attr_value = attr_value.strip('"\'') if attr_value else ''
            
            # Store the operator and value for advanced matching
            if operator == '=':
                result['attrs'][attr_name] = ('exact', attr_value)
            elif operator == '^=':
                result['attrs'][attr_name] = ('starts_with', attr_value)
            elif operator == '$=':
                result['attrs'][attr_name] = ('ends_with', attr_value)
            elif operator == '*=':
                result['attrs'][attr_name] = ('contains', attr_value)
            elif operator == '~=':
                result['attrs'][attr_name] = ('word', attr_value)
            elif operator == '|=':
                result['attrs'][attr_name] = ('lang', attr_value)
            elif not operator:
                result['attrs'][attr_name] = ('exists', '')
        
        # Remove attribute selectors from tag name
        selector = re.sub(r'\[[^\]]*\]', '', selector)
        
        # Extract ID
        if '#' in selector:
            parts = selector.split('#')
            selector = parts[0]
            if len(parts) > 1:
                id_part = parts[1].split('.')[0]
                result['id'] = id_part
        
        # Extract classes
        if '.' in selector:
            parts = selector.split('.')
            selector = parts[0]
            for class_part in parts[1:]:
                class_name = class_part.split('#')[0]
                if class_name:
                    result['classes'].append(class_name)
        
        # What's left is the tag name
        if selector and selector != '*':
            result['tag'] = selector
        
        return result
    
    def matches(self, tag) -> bool:
        """Check if a tag matches this selector."""
        # Handle multiple selectors (comma-separated)
        if hasattr(self, 'multiple_selectors') and len(self.multiple_selectors) > 1:
            for selector_str in self.multiple_selectors:
                temp_selector = CSSSelector(selector_str)
                if temp_selector.matches(tag):
                    return True
            return False
        
        if len(self.parts) == 1:
            return self._matches_simple(tag, self.parts[0])
        else:
            # For complex selectors with combinators
            return self._matches_complex(tag, self.parts)
    
    def _matches_complex(self, tag, parts: List[Dict]) -> bool:
        """Check if tag matches complex selector with combinators."""
        # Must match the last part
        if not self._matches_simple(tag, parts[-1]):
            return False
        
        if len(parts) == 1:
            return True
        
        # Work backwards through the parts
        current_tag = tag
        for i in range(len(parts) - 2, -1, -1):
            part = parts[i]
            combinator = parts[i + 1]['combinator']
            
            if combinator == 'descendant':
                # Find any ancestor that matches
                found = False
                for ancestor in current_tag.parents:
                    if self._matches_simple(ancestor, part):
                        current_tag = ancestor
                        found = True
                        break
                if not found:
                    return False
                    
            elif combinator == 'child':
                # Must be direct parent
                parent = current_tag.parent
                if not parent or not self._matches_simple(parent, part):
                    return False
                current_tag = parent
                
            elif combinator == 'adjacent_sibling':
                # Must be immediately preceding sibling
                prev_sibling = current_tag.previous_element
                if not prev_sibling or not self._matches_simple(prev_sibling, part):
                    return False
                current_tag = prev_sibling
                
            elif combinator == 'general_sibling':
                # Must be any preceding sibling
                found = False
                for sibling in current_tag.previous_siblings:
                    if isinstance(sibling, Tag) and self._matches_simple(sibling, part):
                        current_tag = sibling
                        found = True
                        break
                if not found:
                    return False
        
        return True
    
    def _matches_simple(self, tag, selector_part: Dict) -> bool:
        """Check if tag matches a simple selector part."""
        # Check tag name
        if selector_part['tag'] and tag.name != selector_part['tag']:
            return False
        
        # Check ID
        if selector_part['id'] and tag.get('id') != selector_part['id']:
            return False
        
        # Check classes (handle both string and list class attributes)
        class_attr = tag.get('class', '')
        if isinstance(class_attr, list):
            tag_classes = class_attr
        else:
            tag_classes = class_attr.split() if class_attr else []
        for required_class in selector_part['classes']:
            if required_class not in tag_classes:
                return False
        
        # Check attributes with advanced operators
        for attr_name, attr_spec in selector_part['attrs'].items():
            if isinstance(attr_spec, tuple):
                operator, attr_value = attr_spec
                if not self._matches_attribute(tag, attr_name, operator, attr_value):
                    return False
            else:
                # Legacy format for backward compatibility
                if attr_spec:
                    if tag.get(attr_name) != attr_spec:
                        return False
                else:
                    if attr_name not in tag.attrs:
                        return False
        
        # Check pseudo-selectors
        if selector_part['pseudo']:
            if not self._matches_pseudo(tag, selector_part['pseudo']):
                return False
        
        return True
    
    def _matches_attribute(self, tag, attr_name: str, operator: str, attr_value: str) -> bool:
        """Check if tag's attribute matches the given operator and value."""
        if operator == 'exists':
            return tag.has_attr(attr_name)
        
        tag_attr_value = tag.get(attr_name)
        if tag_attr_value is None:
            return False
        
        # Handle list attributes (like class) specially for certain operators
        if isinstance(tag_attr_value, list):
            if attr_name == 'class':
                # For class attributes, join with spaces for most operators
                if operator in ['starts_with', 'ends_with', 'contains']:
                    # For class attributes, check each class value individually
                    for class_val in tag_attr_value:
                        class_str = str(class_val)
                        if operator == 'starts_with' and class_str.startswith(attr_value):
                            return True
                        elif operator == 'ends_with' and class_str.endswith(attr_value):
                            return True
                        elif operator == 'contains' and attr_value in class_str:
                            return True
                    return False
                elif operator == 'word':
                    # ~= matches if attr_value is one of the class values
                    return attr_value in tag_attr_value
                elif operator == 'exact':
                    # For exact match, join with spaces (standard HTML behavior)
                    tag_attr_value = ' '.join(tag_attr_value)
            else:
                # For non-class list attributes, convert to string
                tag_attr_value = ' '.join(str(val) for val in tag_attr_value)
        
        tag_attr_value = str(tag_attr_value)
        
        if operator == 'exact':
            return tag_attr_value == attr_value
        elif operator == 'starts_with':
            return tag_attr_value.startswith(attr_value)
        elif operator == 'ends_with':
            return tag_attr_value.endswith(attr_value)
        elif operator == 'contains':
            return attr_value in tag_attr_value
        elif operator == 'word':
            # ~= matches if attr_value is one of the whitespace-separated words
            words = tag_attr_value.split()
            return attr_value in words
        elif operator == 'lang':
            # |= matches if tag_attr_value = attr_value or starts with attr_value + '-'
            return tag_attr_value == attr_value or tag_attr_value.startswith(attr_value + '-')
        
        return False
    
    def _matches_pseudo(self, tag, pseudo: str) -> bool:
        """Check if tag matches a pseudo-selector."""
        if pseudo == 'first-child':
            return self._is_first_child(tag)
        elif pseudo == 'last-child':
            return self._is_last_child(tag)
        elif pseudo == 'first-of-type':
            return self._is_first_of_type(tag)
        elif pseudo == 'last-of-type':
            return self._is_last_of_type(tag)
        elif pseudo == 'only-child':
            return self._is_only_child(tag)
        elif pseudo == 'only-of-type':
            return self._is_only_of_type(tag)
        elif pseudo == 'empty':
            return self._is_empty(tag)
        elif pseudo.startswith('nth-child('):
            return self._is_nth_child(tag, pseudo)
        elif pseudo.startswith('nth-of-type('):
            return self._is_nth_of_type(tag, pseudo)
        elif pseudo.startswith('nth-last-child('):
            return self._is_nth_last_child(tag, pseudo)
        elif pseudo.startswith('nth-last-of-type('):
            return self._is_nth_last_of_type(tag, pseudo)
        elif pseudo.startswith('contains('):
            return self._contains_text(tag, pseudo)
        elif pseudo.startswith('not('):
            return self._matches_not(tag, pseudo)
        
        return False
    
    def _is_first_child(self, tag) -> bool:
        """Check if tag is the first child of its parent."""
        if not tag.parent:
            return False
        siblings = [child for child in tag.parent.children if isinstance(child, Tag)]
        return siblings and siblings[0] == tag
    
    def _is_last_child(self, tag) -> bool:
        """Check if tag is the last child of its parent."""
        if not tag.parent:
            return False
        siblings = [child for child in tag.parent.children if isinstance(child, Tag)]
        return siblings and siblings[-1] == tag
    
    def _is_first_of_type(self, tag) -> bool:
        """Check if tag is the first of its type among siblings."""
        if not tag.parent:
            return False
        siblings = [child for child in tag.parent.children 
                   if isinstance(child, Tag) and child.name == tag.name]
        return siblings and siblings[0] == tag
    
    def _is_last_of_type(self, tag) -> bool:
        """Check if tag is the last of its type among siblings."""
        if not tag.parent:
            return False
        siblings = [child for child in tag.parent.children 
                   if isinstance(child, Tag) and child.name == tag.name]
        return siblings and siblings[-1] == tag
    
    def _is_only_child(self, tag) -> bool:
        """Check if tag is the only child of its parent."""
        if not tag.parent:
            return False
        siblings = [child for child in tag.parent.children if isinstance(child, Tag)]
        return len(siblings) == 1 and siblings[0] == tag
    
    def _is_only_of_type(self, tag) -> bool:
        """Check if tag is the only one of its type among siblings."""
        if not tag.parent:
            return False
        siblings = [child for child in tag.parent.children 
                   if isinstance(child, Tag) and child.name == tag.name]
        return len(siblings) == 1 and siblings[0] == tag
    
    def _is_empty(self, tag) -> bool:
        """Check if tag has no children or only whitespace."""
        children = tag.children
        if not children:
            return True
        
        # Check if all children are whitespace-only text
        for child in children:
            if isinstance(child, Tag):
                return False
            elif isinstance(child, str) and child.strip():
                return False
        
        return True
    
    def _is_nth_child(self, tag, pseudo: str) -> bool:
        """Check nth-child pseudo-selector."""
        if not tag.parent:
            return False
        
        # Extract the nth expression
        match = re.match(r'nth-child\(([^)]+)\)', pseudo)
        if not match:
            return False
        
        nth_expr = match.group(1).strip()
        siblings = [child for child in tag.parent.children if isinstance(child, Tag)]
        
        try:
            tag_index = siblings.index(tag) + 1  # 1-based indexing
        except ValueError:
            return False
        
        return self._matches_nth_expression(tag_index, nth_expr)
    
    def _is_nth_of_type(self, tag, pseudo: str) -> bool:
        """Check nth-of-type pseudo-selector."""
        if not tag.parent:
            return False
        
        match = re.match(r'nth-of-type\(([^)]+)\)', pseudo)
        if not match:
            return False
        
        nth_expr = match.group(1).strip()
        siblings = [child for child in tag.parent.children 
                   if isinstance(child, Tag) and child.name == tag.name]
        
        try:
            tag_index = siblings.index(tag) + 1  # 1-based indexing
        except ValueError:
            return False
        
        return self._matches_nth_expression(tag_index, nth_expr)
    
    def _is_nth_last_child(self, tag, pseudo: str) -> bool:
        """Check nth-last-child pseudo-selector."""
        if not tag.parent:
            return False
        
        match = re.match(r'nth-last-child\(([^)]+)\)', pseudo)
        if not match:
            return False
        
        nth_expr = match.group(1).strip()
        siblings = [child for child in tag.parent.children if isinstance(child, Tag)]
        
        try:
            tag_index = len(siblings) - siblings.index(tag)  # From the end
        except ValueError:
            return False
        
        return self._matches_nth_expression(tag_index, nth_expr)
    
    def _is_nth_last_of_type(self, tag, pseudo: str) -> bool:
        """Check nth-last-of-type pseudo-selector."""
        if not tag.parent:
            return False
        
        match = re.match(r'nth-last-of-type\(([^)]+)\)', pseudo)
        if not match:
            return False
        
        nth_expr = match.group(1).strip()
        siblings = [child for child in tag.parent.children 
                   if isinstance(child, Tag) and child.name == tag.name]
        
        try:
            tag_index = len(siblings) - siblings.index(tag)  # From the end
        except ValueError:
            return False
        
        return self._matches_nth_expression(tag_index, nth_expr)
    
    def _matches_nth_expression(self, index: int, nth_expr: str) -> bool:
        """Parse and evaluate nth expressions like 'odd', 'even', '2n+1', '3'."""
        nth_expr = nth_expr.strip()
        
        if nth_expr == 'odd':
            return index % 2 == 1
        elif nth_expr == 'even':
            return index % 2 == 0
        elif nth_expr.isdigit():
            return index == int(nth_expr)
        else:
            # Parse expressions like '2n+1', '3n', '-n+6'
            import re
            match = re.match(r'([+-]?\d*)n([+-]\d+)?', nth_expr)
            if match:
                a_str, b_str = match.groups()
                a = int(a_str) if a_str and a_str not in ['+', '-'] else (1 if a_str != '-' else -1)
                b = int(b_str) if b_str else 0
                
                if a == 0:
                    return index == b
                elif a > 0:
                    return index >= b and (index - b) % a == 0
                else:  # a < 0
                    return index <= b and (b - index) % (-a) == 0
        
        return False
    
    def _is_not(self, tag, pseudo: str) -> bool:
        """Check :not() pseudo-selector."""
        match = re.match(r'not\(([^)]+)\)', pseudo)
        if not match:
            return False
        
        inner_selector = match.group(1).strip()
        # Create a temporary selector for the inner part
        try:
            inner_css = CSSSelector(inner_selector)
            return not inner_css._matches_simple(tag, inner_css.parts[0])
        except (ValueError, AttributeError):
            return True  # If we can't parse the inner selector, assume it doesn't match
    
    def _contains_text(self, tag, pseudo: str) -> bool:
        """Check :contains() pseudo-selector (BeautifulSoup extension)."""
        match = re.match(r'contains\(([^)]+)\)', pseudo)
        if not match:
            return False
        
        search_text = match.group(1).strip('\'"')
        return search_text.lower() in tag.get_text().lower()
    
    def _matches_not(self, tag, pseudo: str) -> bool:
        """Check :not() pseudo-selector."""
        match = re.match(r'not\(([^)]+)\)', pseudo)
        if not match:
            return False
        
        not_selector = match.group(1).strip()
        
        # Parse the selector inside :not()
        # For now, handle simple cases like .class, #id, tag
        if not_selector.startswith('.'):
            # Class selector
            class_name = not_selector[1:]
            class_attr = tag.get('class', '')
            if isinstance(class_attr, list):
                tag_classes = class_attr
            else:
                tag_classes = class_attr.split() if class_attr else []
            return class_name not in tag_classes
        elif not_selector.startswith('#'):
            # ID selector  
            id_name = not_selector[1:]
            return tag.get('id') != id_name
        else:
            # Tag selector
            return tag.name != not_selector
    
    def _matches_descendant(self, tag, parts: List[Dict]) -> bool:
        """Check if tag matches descendant selector (optimized)."""
        # Must match the last part
        if not self._matches_simple(tag, parts[-1]):
            return False
        
        # For single part selectors, we're done
        if len(parts) == 1:
            return True
        
        # Check if ancestors match the preceding parts (optimized)
        current = tag.parent
        part_index = len(parts) - 2
        
        while part_index >= 0 and current:
            if self._matches_simple(current, parts[part_index]):
                part_index -= 1
                if part_index < 0:  # All parts matched
                    return True
            current = current.parent
        
        return False


class NavigableString(str):
    """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 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."""
        if self._parent:
            # Mark for removal from parent
            if not hasattr(self._parent, '_extracted_strings'):
                self._parent._extracted_strings = []
            self._parent._extracted_strings.append(self)
        return self
    
    def replace_with(self, new_content):
        """Replace this string with new content."""
        if self._parent:
            if not hasattr(self._parent, '_replaced_strings'):
                self._parent._replaced_strings = []
            self._parent._replaced_strings.append((self, new_content))
        return self
    
    def insert_before(self, new_content):
        """Insert content before this string."""
        if self._parent:
            if not hasattr(self._parent, '_string_insertions'):
                self._parent._string_insertions = []
            self._parent._string_insertions.append(('before', self, new_content))
    
    def insert_after(self, new_content):
        """Insert content after this string."""
        if self._parent:
            if not hasattr(self._parent, '_string_insertions'):
                self._parent._string_insertions = []
            self._parent._string_insertions.append(('after', self, new_content))


class Tag:
    """
    A Tag represents an HTML element and provides BeautifulSoup4-compatible interface.
    """
    
    def __init__(self, node: _cutysoup_ext.Node):
        self._node = node
        self._modified_attrs = {}  # Track attribute modifications
        self._attr_deletions = set()  # Track deleted attributes
    
    @property
    def name(self) -> str:
        """Get the tag name."""
        return self._node.get_name()
    
    @property
    def attrs(self) -> Dict[str, str]:
        """Get all attributes as a dictionary."""
        original_attrs = self._node.get_attributes()
        # Apply modifications
        result = original_attrs.copy()
        result.update(self._modified_attrs)
        # Remove deleted attributes
        for deleted_attr in self._attr_deletions:
            result.pop(deleted_attr, None)
        return result
    
    def __getitem__(self, key: str) -> str:
        """Get attribute value using dictionary-style access."""
        if key in self._attr_deletions:
            raise KeyError(key)
        if key in self._modified_attrs:
            return self._modified_attrs[key]
        return self._node.get_attribute(key)
    
    def __setitem__(self, key: str, value: str):
        """Set attribute value using dictionary-style access."""
        self._attr_deletions.discard(key)  # Remove from deletions if present
        self._modified_attrs[key] = str(value)
    
    def __contains__(self, key: str) -> bool:
        """Check if attribute exists."""
        return key in self.attrs
    
    def get(self, key: str, default: str = "") -> str:
        """Get attribute value with default."""
        try:
            return self[key]
        except KeyError:
            return default
    
    def __delitem__(self, key: str):
        """Delete an attribute."""
        if key not in self.attrs:
            raise KeyError(key)
        self._attr_deletions.add(key)
        self._modified_attrs.pop(key, None)
    
    def has_attr(self, key: str) -> bool:
        """Check if attribute exists (alias for __contains__)."""
        return key in self
    
    @property
    def text(self) -> str:
        """Get all text content within this tag."""
        return self._node.get_text()
    
    def get_text(self, separator: str = "", strip: bool = False, 
                 types: tuple = None) -> str:
        """Get text with options for separator, stripping, and text types."""
        if types is None:
            # Default behavior - get all text
            if separator == "":
                text = self._node.get_text()
            else:
                text_parts = []
                for string in self.strings:
                    text_parts.append(string)
                text = separator.join(text_parts)
        else:
            # Filter by text types (for future NavigableString types)
            text_parts = []
            for string in self.strings:
                # For now, treat all strings as regular text
                # In full implementation, would check string types
                text_parts.append(string)
            text = separator.join(text_parts)
        
        if strip:
            text = text.strip()
        return text
    
    def stripped_strings_filtered(self, min_length: int = 1) -> Iterator[str]:
        """Generator that yields non-whitespace strings with minimum length."""
        for string in self.strings:
            stripped = string.strip()
            if stripped and len(stripped) >= min_length:
                yield stripped
    
    def find_all_text(self, string: str, case_sensitive: bool = True) -> List[str]:
        """Find all occurrences of text within this tag."""
        matches = []
        full_text = self.get_text()
        
        if not case_sensitive:
            full_text = full_text.lower()
            string = string.lower()
        
        start = 0
        while True:
            pos = full_text.find(string, start)
            if pos == -1:
                break
            matches.append(full_text[pos:pos + len(string)])
            start = pos + 1
        
        return matches
    
    def replace_text(self, old_text: str, new_text: str, count: int = -1) -> int:
        """Replace text within this tag (marks for replacement)."""
        # Since we can't modify the C++ tree directly, store replacement info
        if not hasattr(self, '_text_replacements') or self._text_replacements is None:
            self._text_replacements = []
        
        self._text_replacements.append((old_text, new_text, count))
        return len(self._text_replacements)  # Return number of replacements queued
    
    @property
    def strings(self) -> Iterator[str]:
        """Generator that yields all strings in this tag."""
        yield from self._get_strings_recursive(self._node)
    
    @property
    def stripped_strings(self) -> Iterator[str]:
        """Generator that yields all non-whitespace strings in this tag."""
        for string in self.strings:
            stripped = string.strip()
            if stripped:
                yield stripped
    
    def _get_strings_recursive(self, node) -> Iterator[str]:
        """Helper to recursively get all text strings."""
        for child_node in node.get_children():
            if child_node.is_text():
                text = child_node.get_text()
                if text:
                    yield text
            elif child_node.is_element():
                yield from self._get_strings_recursive(child_node)
    
    @property
    def string(self) -> Optional[str]:
        """Get text content if this tag has only one text child, None otherwise."""
        children = self.children
        text_children = [child for child in children if isinstance(child, str)]
        if len(text_children) == 1 and len(children) == 1:
            return text_children[0]
        return None
    
    @property
    def parent(self) -> Optional['Tag']:
        """Get the parent tag."""
        parent_node = self._node.get_parent()
        return Tag(parent_node) if parent_node else None
    
    @property
    def children(self) -> List[Union['Tag', str]]:
        """Get all child elements and text nodes."""
        child_nodes = self._node.get_children()
        children = []
        for child_node in child_nodes:
            if child_node.is_text():
                children.append(child_node.get_text())
            elif child_node.is_element():
                children.append(Tag(child_node))
        return children
    
    @property
    def contents(self) -> List[Union['Tag', str]]:
        """Get all child elements and text nodes (alias for children)."""
        return self.children
    
    @property
    def next_sibling(self) -> Optional[Union['Tag', str]]:
        """Get the next sibling."""
        sibling_node = self._node.get_next_sibling()
        if not sibling_node:
            return None
        if sibling_node.is_text():
            return sibling_node.get_text()
        elif sibling_node.is_element():
            return Tag(sibling_node)
        return None
    
    @property
    def previous_sibling(self) -> Optional[Union['Tag', str]]:
        """Get the previous sibling."""
        sibling_node = self._node.get_previous_sibling()
        if not sibling_node:
            return None
        if sibling_node.is_text():
            return sibling_node.get_text()
        elif sibling_node.is_element():
            return Tag(sibling_node)
        return None
    
    @property
    def next_element(self) -> Optional['Tag']:
        """Get the next sibling element (skipping text nodes)."""
        current = self._node.get_next_sibling()
        while current:
            if current.is_element():
                return Tag(current)
            current = current.get_next_sibling()
        return None
    
    @property
    def previous_element(self) -> Optional['Tag']:
        """Get the previous sibling element (skipping text nodes)."""
        current = self._node.get_previous_sibling()
        while current:
            if current.is_element():
                return Tag(current)
            current = current.get_previous_sibling()
        return None
    
    def descendants(self) -> Iterator['Tag']:
        """Generator that yields all descendant elements."""
        for child_node in self._node.get_children():
            if child_node.is_element():
                child_tag = Tag(child_node)
                yield child_tag
                yield from child_tag.descendants
    
    def next_siblings(self) -> Iterator[Union['Tag', str]]:
        """Generator that yields all following siblings."""
        current = self._node.get_next_sibling()
        while current:
            if current.is_text():
                text = current.get_text()
                if text.strip():  # Only yield non-empty text
                    yield text
            elif current.is_element():
                yield Tag(current)
            current = current.get_next_sibling()
    
    def previous_siblings(self) -> Iterator[Union['Tag', str]]:
        """Generator that yields all preceding siblings."""
        current = self._node.get_previous_sibling()
        while current:
            if current.is_text():
                text = current.get_text()
                if text.strip():  # Only yield non-empty text
                    yield text
            elif current.is_element():
                yield Tag(current)
            current = current.get_previous_sibling()
    
    def parents(self) -> Iterator['Tag']:
        """Generator that yields all parent elements."""
        current = self._node.get_parent()
        while current and current.is_element():
            yield Tag(current)
            current = current.get_parent()
    
    def find(self, name: Union[str, List[str], Pattern, Callable, bool] = "", 
             attrs: Dict[str, str] = None, **kwargs) -> Optional['Tag']:
        """Find the first matching tag with advanced filter support."""
        if attrs is None:
            attrs = {}
        
        # Handle BeautifulSoup4 class_ attribute convention FIRST
        if 'class_' in kwargs:
            attrs['class'] = kwargs.pop('class_')
        
        # Then update with remaining kwargs
        attrs.update(kwargs)
        
        # Handle advanced filters
        if callable(name) or isinstance(name, Pattern) or isinstance(name, list) or name is True:
            for tag in self._find_with_filter(name, attrs):
                return tag
            return None
        
        # Standard string search
        result_node = self._node.find(str(name), attrs)
        return Tag(result_node) if result_node else None
    
    def find_all(self, name: Union[str, List[str], Pattern, Callable, bool] = "", 
                 attrs: Dict[str, str] = None, **kwargs) -> List['Tag']:
        """Find all matching tags with advanced filter support."""
        if attrs is None:
            attrs = {}
        
        # Handle BeautifulSoup4 class_ attribute convention FIRST
        if 'class_' in kwargs:
            attrs['class'] = kwargs.pop('class_')
        
        # Then update with remaining kwargs
        attrs.update(kwargs)
        
        # Handle advanced filters
        if callable(name) or isinstance(name, Pattern) or isinstance(name, list) or name is True:
            return list(self._find_with_filter(name, attrs))
        
        # Standard string search
        result_nodes = self._node.find_all(str(name), attrs)
        return [Tag(node) for node in result_nodes]
    
    def _find_with_filter(self, name_filter: Union[List[str], Pattern, Callable, bool], 
                         attrs: Dict[str, str]) -> Iterator['Tag']:
        """Helper method for advanced filtering."""
        # Get all elements first
        all_nodes = self._node.find_all("", {})
        
        for node in all_nodes:
            tag = Tag(node)
            
            # Check attributes first
            if attrs:
                matches_attrs = True
                for attr_name, attr_value in attrs.items():
                    if attr_name not in tag.attrs or tag.attrs[attr_name] != attr_value:
                        matches_attrs = False
                        break
                if not matches_attrs:
                    continue
            
            # Check name filter
            if name_filter is True:
                yield tag
            elif isinstance(name_filter, list):
                if tag.name in name_filter:
                    yield tag
            elif isinstance(name_filter, Pattern):
                if name_filter.match(tag.name):
                    yield tag
            elif callable(name_filter):
                try:
                    if name_filter(tag):
                        yield tag
                except Exception:
                    continue
    
    def select(self, selector: str) -> List['Tag']:
        """Find all tags matching the CSS selector."""
        css_selector = CSSSelector(selector)
        results = []
        
        # Get all elements and filter by CSS selector
        all_elements = self.find_all(True)  # True gets all elements
        for element in all_elements:
            if css_selector.matches(element):
                results.append(element)
        
        return results
    
    def select_one(self, selector: str) -> Optional['Tag']:
        """Find the first tag matching the CSS selector."""
        results = self.select(selector)
        return results[0] if results else None
    
    def prettify(self, indent: str = "  ") -> str:
        """Return a prettified string representation of the tag."""
        return self._prettify_recursive(self._node, 0, indent)
    
    def _prettify_recursive(self, node, depth: int, indent: str) -> str:
        """Recursively prettify HTML."""
        result = []
        indent_str = indent * depth
        
        if node.is_element():
            # Opening tag
            tag_name = node.get_name()
            attrs = node.get_attributes()
            
            if attrs:
                attr_str = " " + " ".join(f'{k}="{v}"' for k, v in attrs.items())
            else:
                attr_str = ""
            
            children = node.get_children()
            
            if not children:
                # Self-closing or empty tag
                result.append(f"{indent_str}<{tag_name}{attr_str} />")
            else:
                result.append(f"{indent_str}<{tag_name}{attr_str}>")
                
                # Children
                for child in children:
                    if child.is_text():
                        text = child.get_text().strip()
                        if text:
                            result.append(f"{indent_str}{indent}{text}")
                    else:
                        result.append(self._prettify_recursive(child, depth + 1, indent))
                
                # Closing tag
                result.append(f"{indent_str}</{tag_name}>")
        
        return "\n".join(result)
    
    def decompose(self):
        """Remove this tag from the tree and destroy it."""
        # Mark as decomposed and remove from parent's children list
        self._decomposed = True
        
        # If this tag has a parent, remove it from parent's children
        if hasattr(self, '_parent_soup') and self._parent_soup:
            # Regenerate parent's HTML without this element
            self._remove_from_parent()
    
    def extract(self) -> 'Tag':
        """Remove this tag from the tree and return it."""
        # Mark as extracted but keep the tag intact
        self._extracted = True
        
        # Remove from parent if it has one
        if hasattr(self, '_parent_soup') and self._parent_soup:
            self._remove_from_parent()
        
        return self
    
    def _remove_from_parent(self):
        """Helper method to remove this tag from its parent's tree."""
        # Since we can't modify the C++ tree directly, we mark it as removed
        # Real DOM operations would require rebuilding the tree
        self._removed_from_tree = True
    
    def replace_with(self, new_content):
        """Replace this tag with another tag or string."""
        # For tags that are part of a parsed document, mark for replacement
        if hasattr(self, '_parent_soup') and self._parent_soup:
            self._replaced_with = new_content
            self._remove_from_parent()
            return self
        else:
            # For standalone tags, just mark the replacement without error
            self._replaced_with = new_content
            return self
    
    def wrap(self, wrapper_tag):
        """Wrap this tag with another tag."""
        if not isinstance(wrapper_tag, (Tag, str)):
            raise TypeError("Wrapper must be a Tag or string")
        
        # Store wrapper information for later reconstruction
        self._wrapped_with = wrapper_tag
        return wrapper_tag
    
    def unwrap(self):
        """Remove this tag but keep its contents."""
        # Mark for unwrapping - contents should remain in parent
        self._unwrapped = True
        if hasattr(self, '_parent_soup') and self._parent_soup:
            self._remove_from_parent()
        return self
    
    def clear(self):
        """Remove all children from this tag."""
        # Mark all children as cleared
        self._children_cleared = True
        # In a full implementation, this would rebuild the tree without children
    
    def append(self, new_child):
        """Add a child tag or string."""
        if not hasattr(self, '_appended_children') or self._appended_children is None:
            self._appended_children = []
        
        if isinstance(new_child, str):
            # Convert string to NavigableString
            new_child = NavigableString(new_child)
        elif not isinstance(new_child, (Tag, NavigableString)):
            raise TypeError("Can only append Tag or string objects")
        
        self._appended_children.append(new_child)
        # Set parent reference
        if hasattr(new_child, '_parent_soup'):
            new_child._parent_soup = self._parent_soup if hasattr(self, '_parent_soup') else None
    
    def insert(self, position: int, new_child):
        """Insert a child tag or string at a specific position."""
        if not hasattr(self, '_inserted_children') or self._inserted_children is None:
            self._inserted_children = []
        
        if isinstance(new_child, str):
            new_child = NavigableString(new_child)
        elif not isinstance(new_child, (Tag, NavigableString)):
            raise TypeError("Can only insert Tag or string objects")
        
        self._inserted_children.append((position, new_child))
        # Set parent reference
        if hasattr(new_child, '_parent_soup'):
            new_child._parent_soup = self._parent_soup if hasattr(self, '_parent_soup') else None
    
    def insert_before(self, new_sibling):
        """Insert a new sibling before this tag."""
        if not hasattr(self, '_siblings_before'):
            self._siblings_before = []
        
        if isinstance(new_sibling, str):
            new_sibling = NavigableString(new_sibling)
        
        self._siblings_before.append(new_sibling)
    
    def insert_after(self, new_sibling):
        """Insert a new sibling after this tag."""
        if not hasattr(self, '_siblings_after'):
            self._siblings_after = []
        
        if isinstance(new_sibling, str):
            new_sibling = NavigableString(new_sibling)
        
        self._siblings_after.append(new_sibling)
    
    def copy(self) -> 'Tag':
        """Create a deep copy of this tag and all its children."""
        # Create a new tag with the same name and attributes
        if hasattr(self, '_parent_soup') and self._parent_soup:
            soup = self._parent_soup
        else:
            soup = CutySoup('<html></html>')  # Temporary soup for the copy
        
        # Copy all attributes
        attrs = self.attrs.copy()
        new_tag = soup.new_tag(self.name, attrs)
        
        # Copy text content directly if this tag has text but no element children
        text_content = self.get_text()
        if text_content and not any(isinstance(child, Tag) for child in self.children):
            new_tag.append(text_content)
        else:
            # Copy children recursively
            for child in self.children:
                if isinstance(child, str):
                    new_tag.append(NavigableString(child))
                elif isinstance(child, Tag):
                    new_tag.append(child.copy())
        
        return new_tag
    
    def clone(self) -> 'Tag':
        """Alias for copy() to match some BeautifulSoup usage patterns."""
        return self.copy()
    
    def __getattr__(self, name: str) -> Optional['Tag']:
        """Get the first child tag with the given name."""
        return self.find(name)
    
    def __str__(self) -> str:
        """Return HTML string representation."""
        return self._to_html()
    
    def _to_html(self) -> str:
        """Convert tag to HTML string."""
        if not self.name:
            return ""
        
        attrs_str = ""
        if self.attrs:
            attrs_items = []
            for k, v in self.attrs.items():
                # Escape quotes in attribute values
                v_escaped = v.replace('"', '&quot;')
                attrs_items.append(f'{k}="{v_escaped}"')
            attrs_str = " " + " ".join(attrs_items)
        
        # Self-closing tags
        if self.name.lower() in ['br', 'hr', 'img', 'input', 'meta', 'link']:
            return f"<{self.name}{attrs_str} />"
        
        # Get text content
        content = self.text
        
        return f"<{self.name}{attrs_str}>{content}</{self.name}>"
    
    def __repr__(self) -> str:
        """Return detailed string representation."""
        attrs_str = ""
        if self.attrs:
            attrs_items = [f'{k}="{v}"' for k, v in self.attrs.items()]
            attrs_str = " " + " ".join(attrs_items)
        return f"<{self.name}{attrs_str}>"
    
    def __eq__(self, other) -> bool:
        """Check equality based on underlying node."""
        if not isinstance(other, Tag):
            return False
        # Compare based on tag properties since nodes are recreated
        return (
            self.name == other.name and
            self.get_text() == other.get_text() and
            self.attrs == other.attrs
        )
    
    def __hash__(self) -> int:
        """Make Tag hashable based on content."""
        return hash((self.name, self.get_text(), tuple(sorted(self.attrs.items()))))


class CutySoup:
    """
    Main class for parsing HTML with BeautifulSoup4-compatible API.
    """
    
    def __init__(self, markup: Union[str, bytes], features: str = None, 
                 from_encoding: str = None, exclude_encodings: List[str] = None,
                 builder: str = None, parse_only: object = None, **kwargs):
        """
        Initialize CutySoup with HTML markup.
        
        Args:
            markup: HTML string or bytes to parse
            features: Parser to use (ignored, always uses Gumbo for compatibility)
            from_encoding: Encoding to use for byte input
            exclude_encodings: List of encodings to exclude from detection
            builder: Tree builder to use (ignored, for BeautifulSoup compatibility)
            parse_only: SoupStrainer to limit parsing (basic support)
            **kwargs: Additional options for BeautifulSoup compatibility
        """
        self._original_markup = markup
        self._exclude_encodings = exclude_encodings or []
        self._parse_only = parse_only
        
        # Handle encoding with improved detection
        if isinstance(markup, bytes):
            markup = self._decode_markup(markup, from_encoding)
        
        # Look for meta charset in the HTML
        if not from_encoding and isinstance(markup, str):
            self._detected_encoding = self._detect_encoding_from_meta(markup)
        else:
            self._detected_encoding = from_encoding or 'utf-8'
        
        # Apply parse_only filter if specified
        if self._parse_only:
            markup = self._apply_parse_only_filter(markup)
        
        self._tree = _cutysoup_ext.Tree(markup)
        self._root_node = self._tree.get_root()
    
    def _decode_markup(self, markup: bytes, from_encoding: str = None) -> str:
        """Enhanced markup decoding with better encoding detection."""
        if from_encoding:
            try:
                return markup.decode(from_encoding)
            except UnicodeDecodeError:
                # Fall through to detection
                pass
        
        # Try to detect encoding with chardet
        detected = chardet.detect(markup)
        encoding = detected.get('encoding', 'utf-8')
        confidence = detected.get('confidence', 0)
        
        # If confidence is low or encoding is excluded, try alternatives
        if confidence < 0.7 or encoding in self._exclude_encodings:
            # Try common encodings in order
            encodings_to_try = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
            for enc in encodings_to_try:
                if enc not in self._exclude_encodings:
                    try:
                        return markup.decode(enc)
                    except UnicodeDecodeError:
                        continue
        
        # Use detected encoding as fallback
        try:
            return markup.decode(encoding)
        except UnicodeDecodeError:
            return markup.decode('utf-8', errors='ignore')
    
    def _apply_parse_only_filter(self, markup: str) -> str:
        """Apply SoupStrainer-like filtering (basic implementation)."""
        # This is a simplified implementation
        # In full BeautifulSoup, SoupStrainer is much more sophisticated
        if hasattr(self._parse_only, 'name') and self._parse_only.name:
            # Extract only specific tags
            import re
            tag_pattern = f'<{self._parse_only.name}[^>]*>.*?</{self._parse_only.name}>'
            matches = re.findall(tag_pattern, markup, re.DOTALL | re.IGNORECASE)
            if matches:
                markup = '<html><body>' + ''.join(matches) + '</body></html>'
        
        return markup
    
    def _detect_encoding_from_meta(self, html: str) -> str:
        """Try to detect encoding from meta tags."""
        
        # Look for charset in meta tags
        charset_pattern = r'<meta[^>]*?charset\s*=\s*["\']?([^"\'>\s]+)'
        match = re.search(charset_pattern, html, re.IGNORECASE)
        if match:
            return match.group(1)
        
        # Look for http-equiv content-type
        content_type_pattern = r'<meta[^>]*?http-equiv\s*=\s*["\']?content-type["\']?[^>]*?content\s*=\s*["\']?[^"\']*?charset=([^"\'>\s]+)'
        match = re.search(content_type_pattern, html, re.IGNORECASE)
        if match:
            return match.group(1)
        
        return 'utf-8'
    
    @property
    def name(self) -> str:
        """Get the document name (always None for document)."""
        return ""
    
    def find(self, name: Union[str, List[str], Pattern, Callable, bool] = "", 
             attrs: Dict[str, str] = None, **kwargs) -> Optional[Tag]:
        """Find the first matching tag with advanced filter support."""
        if attrs is None:
            attrs = {}
        
        # Handle BeautifulSoup4 class_ attribute convention FIRST
        if 'class_' in kwargs:
            attrs['class'] = kwargs.pop('class_')
        
        # Then update with remaining kwargs
        attrs.update(kwargs)
        
        # Create a temporary tag wrapper for the root to use the advanced find
        root_tag = Tag(self._root_node)
        return root_tag.find(name, attrs)
    
    def find_all(self, name: Union[str, List[str], Pattern, Callable, bool] = "", 
                 attrs: Dict[str, str] = None, **kwargs) -> List[Tag]:
        """Find all matching tags with advanced filter support."""
        if attrs is None:
            attrs = {}
        
        # Handle BeautifulSoup4 class_ attribute convention FIRST
        if 'class_' in kwargs:
            attrs['class'] = kwargs.pop('class_')
        
        # Then update with remaining kwargs
        attrs.update(kwargs)
        
        # Create a temporary tag wrapper for the root to use the advanced find
        root_tag = Tag(self._root_node)
        return root_tag.find_all(name, attrs)
    
    def __getattr__(self, name: str) -> Optional[Tag]:
        """Get the first tag with the given name."""
        return self.find(name)
    
    @property
    def text(self) -> str:
        """Get all text content."""
        return self._root_node.get_text()
    
    @property
    def strings(self) -> Iterator[str]:
        """Generator that yields all strings in the document."""
        root_tag = Tag(self._root_node)
        yield from root_tag.strings
    
    @property
    def stripped_strings(self) -> Iterator[str]:
        """Generator that yields all non-whitespace strings in the document."""
        root_tag = Tag(self._root_node)
        yield from root_tag.stripped_strings
    
    @property
    def original_encoding(self) -> str:
        """Get the detected or specified encoding."""
        return self._detected_encoding
    
    def get_text(self, separator: str = "", strip: bool = False) -> str:
        """Get text with options for separator and stripping."""
        root_tag = Tag(self._root_node)
        return root_tag.get_text(separator, strip)
    
    def select(self, selector: str) -> List[Tag]:
        """Find all tags matching the CSS selector."""
        root_tag = Tag(self._root_node)
        return root_tag.select(selector)
    
    def select_one(self, selector: str) -> Optional[Tag]:
        """Find the first tag matching the CSS selector."""
        root_tag = Tag(self._root_node)
        return root_tag.select_one(selector)
    
    def prettify(self, indent: str = "  ") -> str:
        """Return a prettified string representation of the HTML."""
        root_tag = Tag(self._root_node)
        return root_tag.prettify(indent)
    
    
    def new_tag(self, tag_name: str, attrs: Dict[str, str] = None, **kwargs) -> Tag:
        """Create a new tag (not attached to any document)."""
        # Combine attrs and kwargs, but handle reserved 'name' parameter
        if attrs is None:
            attrs = {}
        # Handle BeautifulSoup4 class_ convention and other parameters
        for key, value in kwargs.items():
            if key == 'class_':
                attrs['class'] = value
            else:
                attrs[key] = value
        
        # Create a simple tag without attributes first to avoid parsing issues
        minimal_html = f"<{tag_name}></{tag_name}>"
        temp_soup = CutySoup(minimal_html)
        tag = temp_soup.find(tag_name)
        
        # Manually set attributes to work around C++ parsing issues
        if attrs:
            for key, value in attrs.items():
                tag[key] = str(value)
        
        # Set parent soup reference for tree operations
        tag._parent_soup = self
        
        return tag
    
    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 HTML comment."""
        # For now, treat comments as special NavigableString
        comment = NavigableString(f"<!--{text}-->")
        comment._is_comment = True
        return comment
    
    def new_cdata(self, text: str) -> NavigableString:
        """Create a new CDATA section."""
        cdata = NavigableString(f"<![CDATA[{text}]]>")
        cdata._is_cdata = True
        return cdata
    
    def new_doctype(self, doctype: str) -> NavigableString:
        """Create a new DOCTYPE declaration."""
        doctype_str = NavigableString(f"<!DOCTYPE {doctype}>")
        doctype_str._is_doctype = True
        return doctype_str
    
    def diagnose(self, markup: Union[str, bytes] = None) -> str:
        """Diagnose parsing issues and suggest solutions."""
        if markup is None:
            markup = self._original_markup
        
        issues = []
        suggestions = []
        
        # Check encoding issues
        if isinstance(markup, bytes):
            try:
                markup.decode('utf-8')
            except UnicodeDecodeError:
                issues.append("Potential encoding issue detected")
                suggestions.append("Try specifying from_encoding parameter")
        
        # Check for common HTML issues
        if isinstance(markup, str):
            if '<html' not in markup.lower():
                issues.append("No <html> tag found")
                suggestions.append("Document may be a fragment")
            
            if markup.count('<') != markup.count('>'):
                issues.append("Unbalanced angle brackets")
                suggestions.append("Check for malformed tags")
            
            # Check for common problematic patterns
            if '&amp;' in markup and '&' in markup.replace('&amp;', ''):
                issues.append("Mixed escaped and unescaped entities")
                suggestions.append("Normalize entity encoding")
        
        diagnosis = "CutySoup Diagnostic Report:\n"
        diagnosis += f"Markup type: {type(markup).__name__}\n"
        diagnosis += f"Markup length: {len(markup)}\n"
        diagnosis += f"Detected encoding: {self._detected_encoding}\n\n"
        
        if issues:
            diagnosis += "Issues found:\n"
            for issue in issues:
                diagnosis += f"- {issue}\n"
            diagnosis += "\nSuggestions:\n"
            for suggestion in suggestions:
                diagnosis += f"- {suggestion}\n"
        else:
            diagnosis += "No obvious issues detected.\n"
        
        return diagnosis
    
    def contains_replacement_characters(self) -> bool:
        """Check if the parsed document contains Unicode replacement characters."""
        return '\ufffd' in self.get_text()
    
    def declared_html_encoding(self) -> str:
        """Get the encoding declared in HTML meta tags."""
        return self._detected_encoding
    
    def __str__(self) -> str:
        """Return HTML string representation."""
        # Return prettified HTML
        return self.prettify()
    
    def __repr__(self) -> str:
        """Return detailed string representation."""
        return "<CutySoup object>"
