"""
Tree manipulation and DOM modification functionality
"""
from typing import Union, List, TYPE_CHECKING
from .exceptions import TreeManipulationError, ValidationError
from .utils import validate_tag_name, validate_attribute_name

if TYPE_CHECKING:
    from .core import Tag, NavigableString


class TreeManipulator:
    """Handles tree modification operations for CutySoup."""

    @staticmethod
    def _normalize_child(new_child: Union['Tag', 'NavigableString', str]):
        """Normalize child values into Tag or NavigableString instances."""
        from .core import NavigableString

        if isinstance(new_child, str) and not isinstance(new_child, NavigableString):
            return TreeManipulator._create_string_node(new_child)
        if not hasattr(new_child, '_node') and not isinstance(new_child, NavigableString):
            raise ValidationError("Content must be Tag, NavigableString, or string")
        return new_child
    
    @staticmethod
    def decompose_tag(tag: 'Tag') -> None:
        """Completely remove tag and its contents from the tree."""
        try:
            parent = None
            if hasattr(tag, '_detach_from_parent'):
                parent = tag._detach_from_parent()
            if hasattr(tag, '_clear_overlay_children'):
                tag._clear_overlay_children()
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'decompose',
                'target': tag
            })
            
            # Mark as decomposed
            tag._decomposed = True
            if parent and hasattr(parent, '_invalidate_text_cache'):
                parent._invalidate_text_cache()
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to decompose tag: {e}", 
                                      {"tag": str(tag)})
    
    @staticmethod
    def extract_tag(tag: 'Tag') -> 'Tag':
        """Remove tag from tree but return it for potential reuse."""
        try:
            parent = None
            if hasattr(tag, '_detach_from_parent'):
                parent = tag._detach_from_parent()
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'extract',
                'target': tag
            })
            
            # Mark as extracted
            tag._extracted = True
            if parent and hasattr(parent, '_invalidate_text_cache'):
                parent._invalidate_text_cache()
            
            return tag
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to extract tag: {e}", 
                                      {"tag": str(tag)})
    
    @staticmethod
    def replace_tag_with(tag: 'Tag', new_content: Union['Tag', 'NavigableString', str]) -> None:
        """Replace tag with new content."""
        try:
            new_content = TreeManipulator._normalize_child(new_content)

            parent = tag.parent
            if parent is not None:
                index = parent._find_child_index(tag) if hasattr(parent, '_find_child_index') else -1
                if hasattr(parent, '_remove_child_reference'):
                    parent._remove_child_reference(tag)
                if index >= 0 and hasattr(parent, '_insert_overlay_child'):
                    parent._insert_overlay_child(index, new_content)
                tag._detached = True
            
            # Mark tag as replaced for testing
            tag._replaced_with = new_content
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'replace_with',
                'target': tag,
                'new_content': new_content
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to replace tag: {e}", 
                                      {"tag": str(tag), "new_content": str(new_content)})
    
    @staticmethod
    def wrap_tag(tag: 'Tag', wrapper_tag: 'Tag') -> 'Tag':
        """Wrap tag with another tag."""
        try:
            if not hasattr(wrapper_tag, '_node'):
                raise ValidationError("Wrapper must be a Tag object")

            parent = tag.parent
            if parent is not None and hasattr(parent, '_find_child_index'):
                index = parent._find_child_index(tag)
                if hasattr(parent, '_remove_child_reference'):
                    parent._remove_child_reference(tag)
                if index >= 0 and hasattr(parent, '_insert_overlay_child'):
                    parent._insert_overlay_child(index, wrapper_tag)
            if hasattr(wrapper_tag, '_append_overlay_child'):
                wrapper_tag._append_overlay_child(tag)
            tag._detached = False
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'wrap',
                'target': tag,
                'wrapper': wrapper_tag
            })
            
            # Mark tag as wrapped
            tag._wrapped_with = wrapper_tag
            
            return wrapper_tag
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to wrap tag: {e}", 
                                      {"tag": str(tag), "wrapper": str(wrapper_tag)})
    
    @staticmethod
    def unwrap_tag(tag: 'Tag') -> 'Tag':
        """Remove tag but keep its contents."""
        try:
            parent = tag.parent
            if parent is not None and hasattr(parent, '_find_child_index'):
                index = parent._find_child_index(tag)
                children = list(tag.children) if hasattr(tag, 'children') else []
                if hasattr(parent, '_remove_child_reference'):
                    parent._remove_child_reference(tag)
                for offset, child in enumerate(children):
                    if hasattr(parent, '_insert_overlay_child'):
                        parent._insert_overlay_child(index + offset, child)
                tag._detached = True
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'unwrap',
                'target': tag
            })
            
            # Mark tag as unwrapped
            tag._unwrapped = True
            
            return tag
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to unwrap tag: {e}", 
                                      {"tag": str(tag)})
    
    @staticmethod
    def clear_tag(tag: 'Tag') -> None:
        """Remove all contents from tag."""
        try:
            # Mark children as cleared for testing
            tag._children_cleared = True
            if hasattr(tag, '_clear_overlay_children'):
                tag._clear_overlay_children()
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'clear',
                'target': tag
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to clear tag: {e}", 
                                      {"tag": str(tag)})
    
    @staticmethod
    def append_to_tag(tag: 'Tag', new_child: Union['Tag', 'NavigableString', str]) -> None:
        """Append new child to tag."""
        try:
            new_child = TreeManipulator._normalize_child(new_child)
            
            # Track appended children for testing
            if not hasattr(tag, '_appended_children'):
                tag._appended_children = []
            tag._appended_children.append(new_child)

            if hasattr(tag, '_append_overlay_child'):
                tag._append_overlay_child(new_child)
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'append',
                'target': tag,
                'new_child': new_child
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to append to tag: {e}", 
                                      {"tag": str(tag), "new_child": str(new_child)})
    
    @staticmethod
    def insert_into_tag(tag: 'Tag', position: int, new_child: Union['Tag', 'NavigableString', str]) -> None:
        """Insert new child at specific position in tag."""
        try:
            # Validate position
            if not isinstance(position, int):
                raise ValidationError("Position must be an integer")
            
            new_child = TreeManipulator._normalize_child(new_child)
            
            # Track inserted children for testing
            if not hasattr(tag, '_inserted_children'):
                tag._inserted_children = []
            tag._inserted_children.append((position, new_child))

            if hasattr(tag, '_insert_overlay_child'):
                tag._insert_overlay_child(position, new_child)
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'insert',
                'target': tag,
                'position': position,
                'new_child': new_child
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to insert into tag: {e}", 
                                      {"tag": str(tag), "position": position, "new_child": str(new_child)})
    
    @staticmethod
    def insert_before_tag(tag: 'Tag', new_sibling: Union['Tag', 'NavigableString', str]) -> None:
        """Insert new sibling before tag."""
        try:
            if not hasattr(tag, '_node') or not tag._node:
                raise TreeManipulationError("Cannot insert before tag: invalid node reference")
            
            # Validate and prepare new sibling
            if isinstance(new_sibling, str):
                new_sibling = TreeManipulator._create_string_node(new_sibling)
            elif not hasattr(new_sibling, '_node'):
                raise ValidationError("Sibling must be Tag, NavigableString, or string")
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'insert_before',
                'target': tag,
                'new_sibling': new_sibling
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to insert before tag: {e}", 
                                      {"tag": str(tag), "new_sibling": str(new_sibling)})
    
    @staticmethod
    def insert_after_tag(tag: 'Tag', new_sibling: Union['Tag', 'NavigableString', str]) -> None:
        """Insert new sibling after tag."""
        try:
            if not hasattr(tag, '_node') or not tag._node:
                raise TreeManipulationError("Cannot insert after tag: invalid node reference")
            
            # Validate and prepare new sibling
            if isinstance(new_sibling, str):
                new_sibling = TreeManipulator._create_string_node(new_sibling)
            elif not hasattr(new_sibling, '_node'):
                raise ValidationError("Sibling must be Tag, NavigableString, or string")
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'insert_after',
                'target': tag,
                'new_sibling': new_sibling
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to insert after tag: {e}", 
                                      {"tag": str(tag), "new_sibling": str(new_sibling)})
    
    @staticmethod
    def copy_tag(tag: 'Tag', deep: bool = True) -> 'Tag':
        """Create a copy of the tag."""
        try:
            if not hasattr(tag, '_node') or not tag._node:
                raise TreeManipulationError("Cannot copy tag: invalid node reference")
            
            # Create a new CutySoup instance for making new tags
            from .core import CutySoup
            
            # Create a minimal soup instance to use new_tag functionality
            soup = CutySoup('')
            
            # Create new tag with same name and attributes
            attrs = {}
            if hasattr(tag, 'attrs'):
                attrs = dict(tag.attrs)
            
            new_tag = soup.new_tag(tag.name, **attrs)
            
            # For now, just return the tag with attributes copied
            # Full content copying would require more C++ support
            # Mark this as a copied tag
            new_tag._copied_from = tag
            
            return new_tag
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to copy tag: {e}", 
                                      {"tag": str(tag), "deep": deep})
    
    @staticmethod
    def clone_tag(tag: 'Tag') -> 'Tag':
        """Create a deep copy of the tag (alias for copy with deep=True)."""
        return TreeManipulator.copy_tag(tag, deep=True)
    
    @staticmethod
    def set_tag_attribute(tag: 'Tag', name: str, value: str) -> None:
        """Set an attribute on the tag."""
        try:
            name = validate_attribute_name(name)
            
            if not hasattr(tag, '_cached_attrs'):
                tag._cached_attrs = {}
            
            tag._cached_attrs[name] = str(value)
            if hasattr(tag, '_mark_tree_search_mutation'):
                tag._mark_tree_search_mutation()
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'set_attribute',
                'target': tag,
                'attr_name': name,
                'attr_value': str(value)
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to set attribute: {e}", 
                                      {"tag": str(tag), "attr_name": name, "attr_value": value})
    
    @staticmethod
    def delete_tag_attribute(tag: 'Tag', name: str) -> None:
        """Delete an attribute from the tag."""
        try:
            name = validate_attribute_name(name)
            
            # Mark attribute as deleted
            if not hasattr(tag, '_attr_deletions'):
                tag._attr_deletions = set()
            
            tag._attr_deletions.add(name)
            
            # Remove from cached attributes if present
            if hasattr(tag, '_cached_attrs') and name in tag._cached_attrs:
                del tag._cached_attrs[name]
            if hasattr(tag, '_mark_tree_search_mutation'):
                tag._mark_tree_search_mutation()
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'delete_attribute',
                'target': tag,
                'attr_name': name
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to delete attribute: {e}", 
                                      {"tag": str(tag), "attr_name": name})
    
    @staticmethod
    def remove_tag_attribute(tag: 'Tag', name: str) -> None:
        """Remove an attribute from the tag."""
        try:
            name = validate_attribute_name(name)
            
            if hasattr(tag, '_cached_attrs') and name in tag._cached_attrs:
                del tag._cached_attrs[name]
            if hasattr(tag, '_mark_tree_search_mutation'):
                tag._mark_tree_search_mutation()
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'remove_attribute',
                'target': tag,
                'attr_name': name
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to remove attribute: {e}", 
                                      {"tag": str(tag), "attr_name": name})
    
    @staticmethod
    def change_tag_name(tag: 'Tag', new_name: str) -> None:
        """Change the name of a tag."""
        try:
            new_name = validate_tag_name(new_name)
            
            tag._tag_name = new_name
            if hasattr(tag, '_mark_tree_search_mutation'):
                tag._mark_tree_search_mutation()
            
            # Store the operation for later execution
            if not hasattr(tag, '_pending_operations'):
                tag._pending_operations = []
            
            tag._pending_operations.append({
                'type': 'change_name',
                'target': tag,
                'new_name': new_name
            })
            
        except Exception as e:
            raise TreeManipulationError(f"Failed to change tag name: {e}", 
                                      {"tag": str(tag), "new_name": new_name})
    
    @staticmethod
    def _create_string_node(text: str) -> 'NavigableString':
        """Create a NavigableString node from text."""
        from .core import NavigableString
        return NavigableString(text)
    
    @staticmethod
    def execute_pending_operations(tag: 'Tag') -> None:
        """Execute all pending operations on a tag (for future C++ integration)."""
        if not hasattr(tag, '_pending_operations'):
            return
        
        # This would integrate with the C++ extension to actually modify the tree
        # For now, operations are stored as metadata
        
        # Clear operations after execution
        tag._pending_operations.clear()
    
    @staticmethod
    def get_pending_operations(tag: 'Tag') -> list:
        """Get list of pending operations for debugging."""
        return getattr(tag, '_pending_operations', [])
    
    @staticmethod
    def has_pending_operations(tag: 'Tag') -> bool:
        """Check if tag has any pending operations."""
        return bool(getattr(tag, '_pending_operations', []))
    
    @staticmethod
    def validate_tree_integrity(tag: 'Tag') -> List[str]:
        """Validate tree structure and return list of issues found."""
        issues = []
        
        try:
            # Check for circular references
            visited = set()
            
            def _check_circular(current_tag, path):
                if id(current_tag) in visited:
                    issues.append(f"Circular reference detected in path: {' -> '.join(path)}")
                    return
                
                visited.add(id(current_tag))
                
                if hasattr(current_tag, 'contents'):
                    for child in current_tag.contents:
                        if hasattr(child, 'name'):
                            _check_circular(child, path + [child.name])
            
            _check_circular(tag, [tag.name])
            
            # Check for orphaned nodes
            def _check_orphans(current_tag):
                if hasattr(current_tag, 'contents'):
                    for child in current_tag.contents:
                        if hasattr(child, 'parent') and child.parent != current_tag:
                            issues.append(f"Orphaned node found: {child} has incorrect parent reference")
                        
                        if hasattr(child, 'name'):
                            _check_orphans(child)
            
            _check_orphans(tag)
            
        except Exception as e:
            issues.append(f"Error during tree validation: {e}")
        
        return issues
