"""
CSS selector parsing and matching functionality
"""
from typing import Dict, List, Optional, TYPE_CHECKING, OrderedDict as TypingOrderedDict
import re
import sys
from collections import OrderedDict
from .exceptions import SelectorError

# String interning for performance
if sys.version_info >= (3, 8):
    # Use sys.intern for better performance
    _intern = sys.intern
else:
    # Fallback for older Python versions
    _interned_strings = {}
    def _intern(s):
        if s not in _interned_strings:
            _interned_strings[s] = s
        return _interned_strings[s]

# Pre-compile regex patterns for performance
_ATTR_PATTERN = re.compile(r'\[[^\]]*\]')
_COMBINATOR_PATTERN = re.compile(r'(\s*[>+~]\s*|\s+)')
_SELECTOR_PARTS_PATTERN = re.compile(r'([.#]?[^\s.#[]+|\[[^\]]*\])')
_PSEUDO_PATTERN = re.compile(r':([a-zA-Z-]+)(?:\(([^)]*)\))?')
_NTH_PATTERN = re.compile(r'(-?\d*)n?\s*([+-]\s*\d+)?')
_SIMPLE_ATTR_PATTERN = re.compile(r'(\w+)(?:([~|^$*]?=)"?([^"]*)"?)?')
_ATTR_SELECTOR_PATTERN = re.compile(r'\[([^\]]+)\]')
_PSEUDO_SELECTOR_PATTERN = re.compile(r':([a-zA-Z0-9\-_]+(?:\([^)]*\))?)')
_ID_PATTERN = re.compile(r'#([a-zA-Z0-9\-_]+)')
_CLASS_PATTERN = re.compile(r'\.([a-zA-Z0-9\-_]+)')

if TYPE_CHECKING:
    from .core import Tag

# Use local CSSSelector implementation
class CSSSelector:
    """Advanced CSS selector parser and matcher with comprehensive CSS3 support."""
    
    # LRU Cache for parsed selectors to improve performance
    _selector_cache: TypingOrderedDict[str, List[Dict]] = OrderedDict()
    _cache_size_limit = 100
    
    def __init__(self, selector: str):
        if not isinstance(selector, str):
            raise SelectorError("Selector must be a string", {"selector_type": type(selector)})
        
        self.selector = selector.strip()
        if not self.selector:
            raise SelectorError("Selector cannot be empty")
        
        # Use LRU cache for parsing to improve performance
        if selector in CSSSelector._selector_cache:
            # Move to end (most recently used)
            self.parts = CSSSelector._selector_cache.pop(selector)
            CSSSelector._selector_cache[selector] = self.parts
        else:
            self.parts = self._parse_selector()
            # Add to cache with LRU eviction
            if len(CSSSelector._selector_cache) >= self._cache_size_limit:
                # Remove least recently used item
                CSSSelector._selector_cache.popitem(last=False)
            CSSSelector._selector_cache[selector] = self.parts
    
    def _parse_selector(self) -> List[Dict]:
        """Parse CSS selector into components with combinator support."""
        try:
            # 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)
        except Exception as e:
            raise SelectorError(f"Failed to parse selector: {e}", {"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
        attrs = _ATTR_PATTERN.findall(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
        tokens = _COMBINATOR_PATTERN.split(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[attr=value]'."""
        result = {'tag': '', 'id': '', 'classes': [], 'attrs': {}, 'pseudo': ''}
        
        # Extract and handle attribute selectors first
        attr_matches = _ATTR_SELECTOR_PATTERN.findall(selector)
        selector = _ATTR_SELECTOR_PATTERN.sub('', selector)
        
        for attr_match in attr_matches:
            attr_info = self._parse_attribute_selector(attr_match)
            result['attrs'].update(attr_info)
        
        # Extract pseudo-selectors
        pseudo_matches = _PSEUDO_SELECTOR_PATTERN.findall(selector)
        selector = _PSEUDO_SELECTOR_PATTERN.sub('', selector)
        
        if pseudo_matches:
            result['pseudo'] = pseudo_matches[0]  # Take the first pseudo-selector
        
        # Extract ID
        id_match = _ID_PATTERN.search(selector)
        if id_match:
            result['id'] = id_match.group(1)
            selector = selector.replace('#' + result['id'], '')
        
        # Extract classes
        class_matches = _CLASS_PATTERN.findall(selector)
        result['classes'] = class_matches
        selector = _CLASS_PATTERN.sub('', selector)
        
        # What's left should be the tag name
        tag = selector.strip()
        if tag and tag != '*':
            result['tag'] = tag
        
        return result
    
    def _parse_attribute_selector(self, attr_str: str) -> Dict[str, tuple]:
        """Parse attribute selector like 'name=value' or 'name~=value'."""
        attr_str = attr_str.strip()
        
        # Handle different attribute selector types
        operators = ['~=', '|=', '^=', '$=', '*=', '=']
        
        for op in operators:
            if op in attr_str:
                parts = attr_str.split(op, 1)
                if len(parts) == 2:
                    attr_name = parts[0].strip()
                    attr_value = parts[1].strip().strip('"\'')
                    return {attr_name: (op, attr_value)}
        
        # Just attribute name without value (checks for existence)
        return {attr_str: ('exists', '')}
    
    def matches(self, tag: 'Tag') -> bool:
        """Check if a tag matches this selector."""
        try:
            # Handle multiple selectors (OR logic)
            if hasattr(self, 'multiple_selectors'):
                for selector_str in self.multiple_selectors:
                    single_selector = CSSSelector(selector_str)
                    if single_selector._matches_complex(tag, single_selector.parts):
                        return True
                return False
            
            return self._matches_complex(tag, self.parts)
        except Exception as e:
            raise SelectorError(f"Error matching selector: {e}", 
                              {"selector": self.selector, "tag": str(tag)})
    
    def _matches_complex(self, tag: 'Tag', parts: List[Dict]) -> bool:
        """Match a complex selector with combinators."""
        if not parts:
            return True
        
        current_part = parts[-1]  # Start from the rightmost part
        remaining_parts = parts[:-1]
        
        # Check if current tag matches the rightmost selector part
        if not self._matches_simple(tag, current_part):
            return False
        
        # If this is the only part, we're done
        if not remaining_parts:
            return True
        
        # Handle combinator for remaining parts
        combinator = current_part.get('combinator', 'descendant')
        
        if combinator == 'descendant':
            # Look for any ancestor that matches remaining parts
            current = tag.parent
            while current:
                if self._matches_complex(current, remaining_parts):
                    return True
                current = current.parent
        
        elif combinator == 'child':
            # Direct parent must match remaining parts
            if tag.parent:
                return self._matches_complex(tag.parent, remaining_parts)
        
        elif combinator == 'adjacent_sibling':
            # Find the immediately previous element sibling (skip whitespace-only text)
            if not tag.parent:
                return False
            
            # Get all children and find element siblings
            all_children = tag.parent.children
            element_siblings = []
            target_position = -1
            
            for i, child in enumerate(all_children):
                if hasattr(child, 'name') and child.name is not None:
                    element_siblings.append(child)
                    # Check if this child matches our target tag
                    if (child.name == tag.name and 
                        getattr(child, 'attrs', {}) == getattr(tag, 'attrs', {})):
                        target_position = len(element_siblings) - 1
            
            # Check if we found the target and it has a previous sibling
            if target_position > 0:
                prev_element = element_siblings[target_position - 1]
                return self._matches_complex(prev_element, remaining_parts)
        
        elif combinator == 'general_sibling':
            # Any previous sibling must match remaining parts
            for sibling in tag.previous_siblings:
                if hasattr(sibling, 'name'):
                    if self._matches_complex(sibling, remaining_parts):
                        return True
        
        return False
    
    def _matches_simple(self, tag: 'Tag', selector_part: Dict) -> bool:
        """Check if a tag matches a simple selector part."""
        # NavigableString objects cannot match CSS selectors
        if hasattr(tag, '__class__') and 'NavigableString' in str(tag.__class__):
            return False

        # Short-circuit evaluation: check most selective criteria first
        
        # Check ID first (most specific) - fail fast
        if selector_part['id'] and tag.get('id') != selector_part['id']:
            return False
        
        # Check tag name (use interned strings for faster comparison)
        if selector_part['tag'] and _intern(tag.name) != _intern(selector_part['tag']):
            return False
        
        # Check classes
        if selector_part['classes']:
            class_attr = tag.get('class', '')
            if isinstance(class_attr, list):
                tag_classes = set(class_attr)  # Use set for O(1) lookup
            else:
                tag_classes = set(class_attr.split()) if class_attr else set()
            
            # Fast check: if we need more classes than available, fail immediately
            if len(selector_part['classes']) > len(tag_classes):
                return False
                
            for required_class in selector_part['classes']:
                if required_class not in tag_classes:
                    return False
        
        # Check attributes
        for attr_name, (operator, attr_value) in selector_part['attrs'].items():
            if not self._matches_attribute(tag, attr_name, operator, attr_value):
                return False
        
        # Check pseudo-selectors (most expensive, check last)
        if selector_part['pseudo']:
            if not self._matches_pseudo(tag, selector_part['pseudo']):
                return False
        
        return True
    
    def _matches_attribute(self, tag: 'Tag', attr_name: str, operator: str, attr_value: str) -> bool:
        """Check if tag's attribute matches the selector criteria."""
        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 specially for certain operators
        if isinstance(tag_attr_value, list):
            if operator == '~=':
                # Word match - check if attr_value is in the list
                return attr_value in tag_attr_value
            elif operator == '=':
                # Exact match - for lists, check if it's a single-item list with exact match
                return len(tag_attr_value) == 1 and tag_attr_value[0] == attr_value
            else:
                # For other operators, convert to string representation
                tag_attr_value = ' '.join(tag_attr_value)
        else:
            tag_attr_value = str(tag_attr_value)
        
        if operator == '=':
            return tag_attr_value == attr_value
        elif operator == '~=':
            # Word match
            return attr_value in tag_attr_value.split()
        elif operator == '|=':
            # Exact match or starts with value followed by hyphen
            return tag_attr_value == attr_value or tag_attr_value.startswith(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
        
        return False
    
    def _matches_pseudo(self, tag: 'Tag', pseudo: str) -> bool:
        """Check if tag matches pseudo-selector."""
        try:
            # Handle pseudo-selectors with parameters
            if '(' in pseudo:
                pseudo_name = pseudo.split('(')[0]
                pseudo_param = pseudo.split('(')[1].rstrip(')')
                # Strip quotes from parameters
                if pseudo_param.startswith('"') and pseudo_param.endswith('"'):
                    pseudo_param = pseudo_param[1:-1]
                elif pseudo_param.startswith("'") and pseudo_param.endswith("'"):
                    pseudo_param = pseudo_param[1:-1]
            else:
                pseudo_name = pseudo
                pseudo_param = None
            
            # Structural pseudo-selectors
            if pseudo_name == 'first-child':
                return self._is_first_child(tag)
            elif pseudo_name == 'last-child':
                return self._is_last_child(tag)
            elif pseudo_name == 'first-of-type':
                return self._is_first_of_type(tag)
            elif pseudo_name == 'last-of-type':
                return self._is_last_of_type(tag)
            elif pseudo_name == 'only-child':
                return self._is_only_child(tag)
            elif pseudo_name == 'only-of-type':
                return self._is_only_of_type(tag)
            elif pseudo_name == 'empty':
                return self._is_empty(tag)
            elif pseudo_name == 'nth-child':
                return self._is_nth_child(tag, pseudo)
            elif pseudo_name == 'nth-of-type':
                return self._is_nth_of_type(tag, pseudo)
            elif pseudo_name == 'nth-last-child':
                return self._is_nth_last_child(tag, pseudo)
            elif pseudo_name == 'nth-last-of-type':
                return self._is_nth_last_of_type(tag, pseudo)
            elif pseudo_name == 'not':
                return self._matches_not_pseudo(tag, pseudo_param)
            elif pseudo_name == 'contains':
                return self._matches_contains_pseudo(tag, pseudo_param)
            
            return False
        except Exception as e:
            raise SelectorError(f"Error in pseudo-selector matching: {e}",
                              {"pseudo": pseudo, "tag": str(tag)})
    
    def _is_first_child(self, tag: 'Tag') -> bool:
        """Check if tag is the first child of its parent."""
        if not tag.parent:
            return False
        children = [child for child in tag.parent.children if hasattr(child, 'name') and child.name is not None]
        if not children:
            return False
        
        # Find the position of the tag in the element children by comparing content
        target_content = tag.get_text() if hasattr(tag, 'get_text') else str(tag)
        target_attrs = getattr(tag, 'attrs', {})
        
        for i, child in enumerate(children):
            child_content = child.get_text() if hasattr(child, 'get_text') else str(child)
            child_attrs = getattr(child, 'attrs', {})
            
            if (child.name == tag.name and child_content == target_content and child_attrs == target_attrs):
                return i == 0
        
        return False
    
    def _is_last_child(self, tag: 'Tag') -> bool:
        """Check if tag is the last child of its parent."""
        if not tag.parent:
            return False
        children = [child for child in tag.parent.children if hasattr(child, 'name') and child.name is not None]
        if not children:
            return False
        
        # Find the position of the tag in the element children by comparing content
        target_content = tag.get_text() if hasattr(tag, 'get_text') else str(tag)
        target_attrs = getattr(tag, 'attrs', {})
        
        for i, child in enumerate(children):
            child_content = child.get_text() if hasattr(child, 'get_text') else str(child)
            child_attrs = getattr(child, 'attrs', {})
            
            if (child.name == tag.name and child_content == target_content and child_attrs == target_attrs):
                return i == len(children) - 1
        
        return False
    
    def _is_first_of_type(self, tag: '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 hasattr(child, 'name') and child.name == tag.name]
        if not siblings:
            return False
        
        # Find the position of the tag among siblings of the same type
        target_content = tag.get_text() if hasattr(tag, 'get_text') else str(tag)
        target_attrs = getattr(tag, 'attrs', {})
        
        for i, sibling in enumerate(siblings):
            sibling_content = sibling.get_text() if hasattr(sibling, 'get_text') else str(sibling)
            sibling_attrs = getattr(sibling, 'attrs', {})
            
            if sibling_content == target_content and sibling_attrs == target_attrs:
                return i == 0
        
        return False
    
    def _is_last_of_type(self, tag: '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 hasattr(child, 'name') and child.name == tag.name]
        if not siblings:
            return False
        
        # Find the position of the tag among siblings of the same type
        target_content = tag.get_text() if hasattr(tag, 'get_text') else str(tag)
        target_attrs = getattr(tag, 'attrs', {})
        
        for i, sibling in enumerate(siblings):
            sibling_content = sibling.get_text() if hasattr(sibling, 'get_text') else str(sibling)
            sibling_attrs = getattr(sibling, 'attrs', {})
            
            if sibling_content == target_content and sibling_attrs == target_attrs:
                return i == len(siblings) - 1
        
        return False
    
    def _is_only_child(self, tag: 'Tag') -> bool:
        """Check if tag is the only child of its parent."""
        if not tag.parent:
            return False
        children = [child for child in tag.parent.children if hasattr(child, 'name')]
        return len(children) == 1 and children[0] == tag
    
    def _is_only_of_type(self, tag: '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 hasattr(child, 'name') and child.name == tag.name]
        return len(siblings) == 1 and siblings[0] == tag
    
    def _is_empty(self, tag: 'Tag') -> bool:
        """Check if tag has no children or only whitespace."""
        if not hasattr(tag, 'contents'):
            return True
        
        contents = tag.contents
        if not contents:
            return True
        
        # Check if all content is whitespace
        for content in contents:
            if hasattr(content, 'name'):  # It's a tag
                return False
            elif isinstance(content, str) and content.strip():  # Non-empty text
                return False
        
        return True
    
    def _is_nth_child(self, tag: 'Tag', pseudo: str) -> bool:
        """Check if tag matches nth-child selector."""
        if not tag.parent:
            return False
        
        children = [child for child in tag.parent.children if hasattr(child, 'name')]
        if not children:
            return False
        
        try:
            position = children.index(tag) + 1  # 1-based indexing
        except ValueError:
            return False
        
        return self._matches_nth_formula(position, pseudo)
    
    def _is_nth_of_type(self, tag: 'Tag', pseudo: str) -> bool:
        """Check if tag matches nth-of-type selector."""
        if not tag.parent:
            return False
        
        siblings = [child for child in tag.parent.children 
                   if hasattr(child, 'name') and child.name == tag.name]
        if not siblings:
            return False
        
        # Find the position of the tag among siblings of the same type
        target_content = tag.get_text() if hasattr(tag, 'get_text') else str(tag)
        target_attrs = getattr(tag, 'attrs', {})
        
        for i, sibling in enumerate(siblings):
            sibling_content = sibling.get_text() if hasattr(sibling, 'get_text') else str(sibling)
            sibling_attrs = getattr(sibling, 'attrs', {})
            
            if sibling_content == target_content and sibling_attrs == target_attrs:
                position = i + 1  # 1-based indexing
                return self._matches_nth_formula(position, pseudo)
        
        return False
    
    def _is_nth_last_child(self, tag: 'Tag', pseudo: str) -> bool:
        """Check if tag matches nth-last-child selector."""
        if not tag.parent:
            return False
        
        children = [child for child in tag.parent.children if hasattr(child, 'name')]
        if not children:
            return False
        
        try:
            position = len(children) - children.index(tag)  # Position from end
        except ValueError:
            return False
        
        return self._matches_nth_formula(position, pseudo)
    
    def _is_nth_last_of_type(self, tag: 'Tag', pseudo: str) -> bool:
        """Check if tag matches nth-last-of-type selector."""
        if not tag.parent:
            return False
        
        siblings = [child for child in tag.parent.children 
                   if hasattr(child, 'name') and child.name == tag.name]
        if not siblings:
            return False
        
        try:
            position = len(siblings) - siblings.index(tag)  # Position from end
        except ValueError:
            return False
        
        return self._matches_nth_formula(position, pseudo)
    
    def _matches_nth_formula(self, position: int, pseudo: str) -> bool:
        """Match nth-child/nth-of-type formula like 'nth-child(2n+1)'."""
        # Extract the formula from pseudo-selector
        if '(' not in pseudo:
            return False
        
        formula = pseudo.split('(')[1].rstrip(')')
        
        # Handle special cases
        if formula == 'odd':
            return position % 2 == 1
        elif formula == 'even':
            return position % 2 == 0
        
        # Handle number only
        if formula.isdigit():
            return position == int(formula)
        
        # Handle an+b formula
        # Parse patterns like "2n+1", "3n", "-n+6", etc.
        formula = formula.replace(' ', '')
        
        # Default values
        a, b = 0, 0
        
        if 'n' in formula:
            if formula == 'n':
                a = 1
            elif formula == '-n':
                a = -1
            else:
                # Split on 'n'
                parts = formula.split('n')
                
                # Get coefficient of n
                if parts[0] == '' or parts[0] == '+':
                    a = 1
                elif parts[0] == '-':
                    a = -1
                else:
                    try:
                        a = int(parts[0])
                    except ValueError:
                        return False
                
                # Get constant term
                if len(parts) > 1 and parts[1]:
                    try:
                        b = int(parts[1])
                    except ValueError:
                        return False
        else:
            # No 'n', just a constant
            try:
                b = int(formula)
                a = 0
            except ValueError:
                return False
        
        # Check if position matches an+b formula
        if a == 0:
            return position == b
        elif a > 0:
            return position >= b and (position - b) % a == 0
        else:  # a < 0
            return position <= b and (b - position) % abs(a) == 0
    
    def _matches_not_pseudo(self, tag: 'Tag', not_selector: str) -> bool:
        """Check if tag does NOT match the given selector."""
        if not not_selector:
            return True
        
        try:
            # Create a new selector for the :not() content
            not_css_selector = CSSSelector(not_selector)
            return not not_css_selector.matches(tag)
        except (ValueError, AttributeError):
            # Fallback for simple cases
            if not_selector.startswith('.'):
                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_name = not_selector[1:]
                return tag.get('id') != id_name
            else:
                return tag.name != not_selector
    
    def _matches_contains_pseudo(self, tag: 'Tag', text: str) -> bool:
        """Check if tag contains the specified text."""
        if not text:
            return True
        
        tag_text = tag.get_text() if hasattr(tag, 'get_text') else ''
        return text in tag_text
    
    def _get_simple_tag_name(self) -> Optional[str]:
        """Get the tag name if this is a simple tag selector (e.g., 'div', 'p')."""
        if len(self.parts) == 1:
            selector = self.parts[0]
            if (selector.get('tag') and 
                not selector.get('classes') and 
                not selector.get('id') and 
                not selector.get('attrs') and
                not selector.get('pseudo')):
                return selector['tag']
        return None
    
    def has_complex_selectors(self) -> bool:
        """Check if this selector has complex features that can't be optimized with simple find_all."""
        for selector in self.parts:
            if (selector.get('combinator') or 
                selector.get('pseudo') or 
                selector.get('attrs') or
                len(self.parts) > 1):
                return True
        return False


def _get_simple_part_candidates(root_tag: 'Tag', selector_part: Dict) -> Optional[List['Tag']]:
    """Use bs4-compatible find_all as a seed set for simple selector parts."""
    attrs = selector_part.get('attrs') or {}
    classes = selector_part.get('classes') or []
    kwargs = {}

    for attr_name, (operator, attr_value) in attrs.items():
        if operator != '=':
            return None
        kwargs[attr_name] = attr_value

    if selector_part.get('id'):
        kwargs['id'] = selector_part['id']

    if classes:
        kwargs['class_'] = classes[0]

    tag_name = selector_part.get('tag') or ""
    if not tag_name and not kwargs:
        return None

    return root_tag.find_all(tag_name, **kwargs)


def _select_two_part_descendant(root_tag: 'Tag', css_selector: CSSSelector) -> Optional[List['Tag']]:
    """Optimize selectors like `article .link` using fast find_all seeds."""
    if len(css_selector.parts) != 2:
        return None

    ancestor_part, target_part = css_selector.parts
    if target_part.get('combinator') != 'descendant':
        return None
    if ancestor_part.get('pseudo') or target_part.get('pseudo'):
        return None

    candidates = _get_simple_part_candidates(root_tag, target_part)
    if candidates is None:
        return None

    results = []
    for candidate in candidates:
        current = candidate.parent
        while current:
            if css_selector._matches_simple(current, ancestor_part):
                results.append(candidate)
                break
            current = current.parent

    return results


def select_tags(root_tag: 'Tag', selector: str) -> List['Tag']:
    """Select all tags matching the CSS selector from root_tag."""
    try:
        # MaxCpp Phase 34: Use C++ CSS selector engine for maximum performance
        # C++ engine needs performance optimization - disabled for now to avoid regression
        if False and hasattr(root_tag, '_tree') and hasattr(root_tag._tree, 'select_by_complex_selector'):
            try:
                # Use advanced C++ CSS selector engine
                cpp_results = root_tag._tree.select_by_complex_selector(selector)
                
                # Convert C++ FastNode results to Python Tag objects
                results = []
                for cpp_node in cpp_results:
                    # Create Tag wrapper for each FastNode result
                    tag = Tag(cpp_node)
                    tag._tree = root_tag._tree  # Maintain tree reference
                    results.append(tag)
                
                return results
            except Exception:
                # Fallback to Python implementation if C++ fails
                pass
        
        # FALLBACK: Python CSS selector implementation for compatibility
        css_selector = CSSSelector(selector)
        results = []
        
        # OPTIMIZATION: Use fast find_all for simple cases
        simple_tag = css_selector._get_simple_tag_name()  # Check if it's a simple tag selector
        if simple_tag and not css_selector.has_complex_selectors():
            # Use our optimized find_all for simple tag selectors like 'div', 'p', etc.
            candidates = root_tag.find_all(simple_tag)
            for candidate in candidates:
                if css_selector.matches(candidate):
                    results.append(candidate)
            return results

        descendant_results = _select_two_part_descendant(root_tag, css_selector)
        if descendant_results is not None:
            return descendant_results
        
        # FALLBACK: Full descendant traversal for complex selectors
        # Only check descendants, not the root tag itself (matches BeautifulSoup4 behavior)
        for descendant in root_tag.descendants:
            # COMPATIBILITY FIX: descendants now includes text nodes, but CSS selectors only apply to Tag objects
            if hasattr(descendant, 'name') and descendant.name is not None:
                if css_selector.matches(descendant):
                    results.append(descendant)
        
        return results
    except Exception as e:
        raise SelectorError(f"Error selecting tags: {e}", {"selector": selector})


def select_one_tag(root_tag: 'Tag', selector: str) -> Optional['Tag']:
    """Select the first tag matching the CSS selector from root_tag."""
    try:
        # MaxCpp Phase 34: Use C++ CSS selector engine for maximum performance
        # C++ engine needs performance optimization - disabled for now to avoid regression
        if False and hasattr(root_tag, '_tree') and hasattr(root_tag._tree, 'select_by_complex_selector'):
            try:
                # Use advanced C++ CSS selector engine (get first result)
                cpp_results = root_tag._tree.select_by_complex_selector(selector)
                
                if cpp_results:
                    # Convert first C++ FastNode result to Python Tag object
                    cpp_node = cpp_results[0]
                    tag = Tag(cpp_node)
                    tag._tree = root_tag._tree  # Maintain tree reference
                    return tag
                else:
                    return None
            except Exception:
                # Fallback to Python implementation if C++ fails
                pass
        
        # FALLBACK: Python CSS selector implementation for compatibility
        css_selector = CSSSelector(selector)
        
        # OPTIMIZATION: Use fast find for simple cases
        simple_tag = css_selector._get_simple_tag_name()
        if simple_tag and not css_selector.has_complex_selectors():
            # Use our optimized find for simple tag selectors
            candidate = root_tag.find(simple_tag)
            if candidate and css_selector.matches(candidate):
                return candidate
            return None

        descendant_results = _select_two_part_descendant(root_tag, css_selector)
        if descendant_results is not None:
            return descendant_results[0] if descendant_results else None
        
        # FALLBACK: Full descendant traversal for complex selectors
        # Only check descendants, not the root tag itself (matches BeautifulSoup4 behavior)
        for descendant in root_tag.descendants:
            if css_selector.matches(descendant):
                return descendant
        
        return None
    except Exception as e:
        raise SelectorError(f"Error selecting tag: {e}", {"selector": selector})
