"""
Dual-engine XML parser with namespace-aware processing for OOXML and ODF formats.
Supports both lxml and ElementTree engines for comprehensive XML analysis.
"""

import re
from pathlib import Path
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
import xml.etree.ElementTree as ElementTree

ET = ElementTree

try:
    from lxml import etree as lxml_etree

    LXML_AVAILABLE = True
except ImportError:
    lxml_etree = None  # type: ignore[assignment]
    LXML_AVAILABLE = False


@dataclass
class ParsedXMLStructure:
    """Container for parsed XML structure information."""

    root: Any
    elements: List[str]
    attributes: Dict[str, List[str]]
    namespaces: Dict[str, str]
    token_placeholders: List[str]
    engine_used: str


class XMLParser:
    """Dual-engine XML parser supporting both lxml and ElementTree."""

    def __init__(self, engine: str = "auto"):
        """
        Initialize XML parser with specified engine.

        Args:
            engine: 'lxml', 'elementtree', or 'auto' (defaults to lxml if available)
        """
        if engine == "auto":
            self.engine = "lxml" if LXML_AVAILABLE else "elementtree"
        elif engine == "lxml" and not LXML_AVAILABLE:
            raise ImportError("lxml not available, install with: pip install lxml")
        else:
            self.engine = engine

        self.token_pattern = re.compile(r"\{\{([^}]+)\}\}")

    def parse_string(self, xml_string: str) -> Any:
        """Parse XML from string using selected engine."""
        if self.engine == "lxml":
            assert lxml_etree is not None
            return lxml_etree.fromstring(xml_string.encode("utf-8"))
        else:
            return ElementTree.fromstring(xml_string)

    def parse_file(self, file_path: Path) -> Any:
        """Parse XML from file using selected engine."""
        if self.engine == "lxml":
            assert lxml_etree is not None
            return lxml_etree.parse(str(file_path)).getroot()
        else:
            return ElementTree.parse(str(file_path)).getroot()

    def extract_namespaces(self, root: Any) -> Dict[str, str]:
        """Extract all namespace declarations from XML root."""
        if self.engine == "lxml":
            return dict(root.nsmap) if root.nsmap else {}
        else:
            # ElementTree stores namespaces differently
            namespaces = {}
            for elem in root.iter():
                if elem.tag.startswith("{"):
                    uri, local = elem.tag[1:].split("}", 1)
                    # Find prefix by looking at namespace declarations
                    prefix = self._find_namespace_prefix(root, uri)
                    if prefix:
                        namespaces[prefix] = uri
            return namespaces

    def _find_namespace_prefix(self, root: Any, uri: str) -> Optional[str]:
        """Find namespace prefix for given URI in ElementTree."""
        # This is a simplified approach - in practice, we'd need more robust prefix detection
        common_prefixes = {
            "http://schemas.openxmlformats.org/drawingml/2006/main": "a",
            "http://schemas.openxmlformats.org/presentationml/2006/main": "p",
            "http://schemas.openxmlformats.org/wordprocessingml/2006/main": "w",
            "http://schemas.openxmlformats.org/spreadsheetml/2006/main": "x",
            "urn:oasis:names:tc:opendocument:xmlns:office:1.0": "office",
            "urn:oasis:names:tc:opendocument:xmlns:style:1.0": "style",
            "urn:oasis:names:tc:opendocument:xmlns:text:1.0": "text",
        }
        return common_prefixes.get(uri)

    def extract_elements(self, root: Any) -> List[str]:
        """Extract all element names from XML structure."""
        elements = set()

        for elem in root.iter():
            elements.add(elem.tag)

        return sorted(list(elements))

    def extract_attributes(self, root: Any) -> Dict[str, List[str]]:
        """Extract all attributes grouped by element."""
        attributes = {}

        for elem in root.iter():
            if elem.attrib:
                tag = elem.tag
                if tag not in attributes:
                    attributes[tag] = set()
                attributes[tag].update(elem.attrib.keys())

        # Convert sets to sorted lists
        return {tag: sorted(list(attrs)) for tag, attrs in attributes.items()}

    def extract_token_placeholders(self, xml_content: str) -> List[str]:
        """Extract all token placeholders ({{token.name}}) from XML content."""
        matches = self.token_pattern.findall(xml_content)
        return [f"{{{{{match}}}}}" for match in matches]

    def get_element_xpath(self, element: Any, root: Any) -> str:
        """Get XPath expression for element (lxml only)."""
        if self.engine == "lxml":
            return root.getroottree().getpath(element)
        else:
            # ElementTree doesn't have built-in XPath generation
            # Return a simplified path
            return f"//{element.tag}"

    def analyze_structure(self, xml_content: str) -> ParsedXMLStructure:
        """Comprehensive analysis of XML structure."""
        root = self.parse_string(xml_content)

        return ParsedXMLStructure(
            root=root,
            elements=self.extract_elements(root),
            attributes=self.extract_attributes(root),
            namespaces=self.extract_namespaces(root),
            token_placeholders=self.extract_token_placeholders(xml_content),
            engine_used=self.engine,
        )

    def validate_xml_wellformed(self, xml_content: str) -> bool:
        """Validate that XML is well-formed."""
        try:
            self.parse_string(xml_content)
            return True
        except Exception:
            return False

    def compare_structures(
        self, structure1: ParsedXMLStructure, structure2: ParsedXMLStructure
    ) -> Dict[str, Any]:
        """Compare two XML structures for differences."""
        comparison = {
            "elements_added": set(structure2.elements) - set(structure1.elements),
            "elements_removed": set(structure1.elements) - set(structure2.elements),
            "attributes_changed": {},
            "tokens_added": set(structure2.token_placeholders)
            - set(structure1.token_placeholders),
            "tokens_removed": set(structure1.token_placeholders)
            - set(structure2.token_placeholders),
            "namespaces_changed": set(structure2.namespaces.items())
            - set(structure1.namespaces.items()),
        }

        # Compare attributes for common elements
        common_elements = set(structure1.elements) & set(structure2.elements)
        for element in common_elements:
            attrs1 = set(structure1.attributes.get(element, []))
            attrs2 = set(structure2.attributes.get(element, []))
            if attrs1 != attrs2:
                comparison["attributes_changed"][element] = {
                    "added": attrs2 - attrs1,
                    "removed": attrs1 - attrs2,
                }

        return comparison


class NamespaceManager:
    """Manages XML namespaces for OOXML and ODF formats."""

    OOXML_NAMESPACES = {
        "a": "http://schemas.openxmlformats.org/drawingml/2006/main",
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
        "x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
        "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
        "cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
        "dc": "http://purl.org/dc/elements/1.1/",
        "dcterms": "http://purl.org/dc/terms/",
    }

    ODF_NAMESPACES = {
        "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
        "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
        "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
        "table": "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
        "draw": "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
        "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
        "xlink": "http://www.w3.org/1999/xlink",
        "dc": "http://purl.org/dc/elements/1.1/",
        "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
    }

    @classmethod
    def get_namespaces(cls, format_type: str) -> Dict[str, str]:
        """Get namespace mappings for format type."""
        if format_type.lower() in ["ooxml", "office", "microsoft"]:
            return cls.OOXML_NAMESPACES.copy()
        elif format_type.lower() in ["odf", "opendocument", "libreoffice"]:
            return cls.ODF_NAMESPACES.copy()
        else:
            # Return combined namespaces for unknown format
            combined = cls.OOXML_NAMESPACES.copy()
            combined.update(cls.ODF_NAMESPACES)
            return combined

    @classmethod
    def register_elementtree_namespaces(cls, format_type: str = "both"):
        """Register namespaces with ElementTree for find operations."""
        namespaces = cls.get_namespaces(format_type)
        for prefix, uri in namespaces.items():
            ET.register_namespace(prefix, uri)
