"""
Structure traversal engine for systematic XML cataloging.
Analyzes OOXML and ODF file structures to enumerate all elements, attributes, and relationships.
"""

import zipfile
from pathlib import Path
from typing import Dict, List, Set, Any, Optional
from dataclasses import dataclass, field
from .xml_parser import XMLParser, NamespaceManager


@dataclass
class ElementInfo:
    """Information about an XML element."""

    tag: str
    namespace: Optional[str]
    local_name: str
    attributes: Set[str] = field(default_factory=set)
    text_content: bool = False
    token_placeholders: Set[str] = field(default_factory=set)
    xpath: Optional[str] = None
    parent_elements: Set[str] = field(default_factory=set)
    child_elements: Set[str] = field(default_factory=set)


@dataclass
class StructureAnalysis:
    """Complete analysis of XML structure."""

    file_path: str
    format_type: str  # 'ooxml' or 'odf'
    xml_files: Dict[str, str] = field(default_factory=dict)  # path -> content
    elements: Dict[str, ElementInfo] = field(default_factory=dict)
    attributes: Dict[str, Set[str]] = field(default_factory=dict)
    namespaces: Dict[str, str] = field(default_factory=dict)
    token_placeholders: Set[str] = field(default_factory=set)
    relationships: Dict[str, List[str]] = field(default_factory=dict)
    content_types: Dict[str, str] = field(default_factory=dict)
    total_elements: int = 0
    total_attributes: int = 0
    analysis_errors: List[str] = field(default_factory=list)


class StructureAnalyzer:
    """Systematic XML structure traversal and cataloging engine."""

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

        # Common Office file internal paths
        self.ooxml_xml_paths = {
            # PowerPoint
            "ppt/presentation.xml",
            "ppt/theme/theme1.xml",
            "ppt/slideMasters/slideMaster1.xml",
            "ppt/slideLayouts/slideLayout1.xml",
            "ppt/slides/slide1.xml",
            # Word
            "word/document.xml",
            "word/styles.xml",
            "word/theme/theme1.xml",
            "word/numbering.xml",
            "word/footnotes.xml",
            "word/endnotes.xml",
            # Excel
            "xl/workbook.xml",
            "xl/theme/theme1.xml",
            "xl/styles.xml",
            "xl/worksheets/sheet1.xml",
            "xl/sharedStrings.xml",
            # Common
            "[Content_Types].xml",
            "_rels/.rels",
            "docProps/core.xml",
            "docProps/app.xml",
        }

        self.odf_xml_paths = {
            "content.xml",
            "styles.xml",
            "meta.xml",
            "settings.xml",
            "META-INF/manifest.xml",
            "Thumbnails/thumbnail.png",
        }

    def analyze_file(self, file_path: Path) -> StructureAnalysis:
        """Analyze a single XML file or Office document."""
        file_path = Path(file_path)

        if file_path.suffix.lower() in [".xml"]:
            return self._analyze_xml_file(file_path)
        elif file_path.suffix.lower() in [
            ".potx",
            ".dotx",
            ".xltx",
            ".pptx",
            ".docx",
            ".xlsx",
        ]:
            return self._analyze_ooxml_file(file_path)
        elif file_path.suffix.lower() in [
            ".otp",
            ".ott",
            ".ots",
            ".odp",
            ".odt",
            ".ods",
        ]:
            return self._analyze_odf_file(file_path)
        else:
            raise ValueError(f"Unsupported file type: {file_path.suffix}")

    def _analyze_xml_file(self, xml_path: Path) -> StructureAnalysis:
        """Analyze a single XML file."""
        analysis = StructureAnalysis(file_path=str(xml_path), format_type="xml")

        try:
            with open(xml_path, "r", encoding="utf-8") as f:
                content = f.read()

            analysis.xml_files[str(xml_path)] = content
            self._analyze_xml_content(content, str(xml_path), analysis)

        except Exception as e:
            analysis.analysis_errors.append(f"Error reading {xml_path}: {str(e)}")

        return analysis

    def _analyze_ooxml_file(self, ooxml_path: Path) -> StructureAnalysis:
        """Analyze OOXML file (PowerPoint, Word, Excel)."""
        analysis = StructureAnalysis(file_path=str(ooxml_path), format_type="ooxml")

        try:
            with zipfile.ZipFile(ooxml_path, "r") as zf:
                # Get all XML files in the archive
                xml_files = [
                    name
                    for name in zf.namelist()
                    if name.endswith(".xml") or name.endswith(".rels")
                ]

                for xml_file in xml_files:
                    try:
                        content = zf.read(xml_file).decode("utf-8")
                        analysis.xml_files[xml_file] = content
                        self._analyze_xml_content(content, xml_file, analysis)

                    except Exception as e:
                        analysis.analysis_errors.append(
                            f"Error processing {xml_file}: {str(e)}"
                        )

                # Special handling for Content_Types.xml
                if "[Content_Types].xml" in analysis.xml_files:
                    self._analyze_content_types(
                        analysis.xml_files["[Content_Types].xml"], analysis
                    )

        except Exception as e:
            analysis.analysis_errors.append(f"Error reading OOXML file: {str(e)}")

        return analysis

    def _analyze_odf_file(self, odf_path: Path) -> StructureAnalysis:
        """Analyze ODF file (LibreOffice formats)."""
        analysis = StructureAnalysis(file_path=str(odf_path), format_type="odf")

        try:
            with zipfile.ZipFile(odf_path, "r") as zf:
                xml_files = [name for name in zf.namelist() if name.endswith(".xml")]

                for xml_file in xml_files:
                    try:
                        content = zf.read(xml_file).decode("utf-8")
                        analysis.xml_files[xml_file] = content
                        self._analyze_xml_content(content, xml_file, analysis)

                    except Exception as e:
                        analysis.analysis_errors.append(
                            f"Error processing {xml_file}: {str(e)}"
                        )

        except Exception as e:
            analysis.analysis_errors.append(f"Error reading ODF file: {str(e)}")

        return analysis

    def _analyze_xml_content(
        self, xml_content: str, file_path: str, analysis: StructureAnalysis
    ):
        """Analyze XML content and update structure analysis."""
        try:
            # Parse XML structure
            structure = self.parser.analyze_structure(xml_content)

            # Update namespaces
            analysis.namespaces.update(structure.namespaces)

            # Extract token placeholders
            analysis.token_placeholders.update(structure.token_placeholders)

            # Analyze elements in detail
            self._analyze_elements_detailed(structure.root, file_path, analysis)

            # Update totals
            analysis.total_elements += len(structure.elements)
            analysis.total_attributes += sum(
                len(attrs) for attrs in structure.attributes.values()
            )

        except Exception as e:
            analysis.analysis_errors.append(
                f"XML parsing error in {file_path}: {str(e)}"
            )

    def _analyze_elements_detailed(
        self, root: Any, file_path: str, analysis: StructureAnalysis
    ):
        """Perform detailed element analysis including relationships."""
        elements_in_file = {}

        # First pass: catalog all elements
        for elem in root.iter():
            tag = elem.tag
            namespace, local_name = self._split_namespace_tag(tag)

            if tag not in elements_in_file:
                elements_in_file[tag] = ElementInfo(
                    tag=tag,
                    namespace=namespace,
                    local_name=local_name,
                    xpath=self.parser.get_element_xpath(elem, root)
                    if hasattr(self.parser, "get_element_xpath")
                    else None,
                )

            element_info = elements_in_file[tag]

            # Collect attributes
            if elem.attrib:
                element_info.attributes.update(elem.attrib.keys())

            # Check for text content and tokens
            if elem.text and elem.text.strip():
                element_info.text_content = True
                tokens = self.parser.extract_token_placeholders(elem.text)
                element_info.token_placeholders.update(tokens)

            # Analyze attribute values for tokens
            for attr_value in elem.attrib.values():
                if attr_value:
                    tokens = self.parser.extract_token_placeholders(attr_value)
                    element_info.token_placeholders.update(tokens)
                    analysis.token_placeholders.update(tokens)

        # Second pass: establish parent-child relationships
        for elem in root.iter():
            tag = elem.tag
            element_info = elements_in_file[tag]

            # Parent relationships
            parent = elem.getparent() if hasattr(elem, "getparent") else None
            if parent is not None:
                element_info.parent_elements.add(parent.tag)

            # Child relationships
            for child in elem:
                element_info.child_elements.add(child.tag)

        # Merge into analysis
        for tag, element_info in elements_in_file.items():
            if tag in analysis.elements:
                # Merge with existing element info
                existing = analysis.elements[tag]
                existing.attributes.update(element_info.attributes)
                existing.token_placeholders.update(element_info.token_placeholders)
                existing.parent_elements.update(element_info.parent_elements)
                existing.child_elements.update(element_info.child_elements)
                existing.text_content = (
                    existing.text_content or element_info.text_content
                )
            else:
                analysis.elements[tag] = element_info

    def _split_namespace_tag(self, tag: str) -> tuple[Optional[str], str]:
        """Split namespaced tag into namespace URI and local name."""
        if tag.startswith("{"):
            # Clark notation: {namespace}localname
            namespace, local_name = tag[1:].split("}", 1)
            return namespace, local_name
        else:
            return None, tag

    def _analyze_content_types(
        self, content_types_xml: str, analysis: StructureAnalysis
    ):
        """Analyze [Content_Types].xml for format information."""
        try:
            structure = self.parser.analyze_structure(content_types_xml)

            # Extract content type mappings
            for elem in structure.root.iter():
                if elem.tag.endswith("}Override") or elem.tag == "Override":
                    part_name = elem.get("PartName", "")
                    content_type = elem.get("ContentType", "")
                    if part_name and content_type:
                        analysis.content_types[part_name] = content_type

        except Exception as e:
            analysis.analysis_errors.append(
                f"Error analyzing Content_Types.xml: {str(e)}"
            )

    def analyze_zip_file(self, zip_path: Path) -> StructureAnalysis:
        """Generic ZIP file analysis for Office documents."""
        if zip_path.suffix.lower() in [
            ".potx",
            ".dotx",
            ".xltx",
            ".pptx",
            ".docx",
            ".xlsx",
        ]:
            return self._analyze_ooxml_file(zip_path)
        elif zip_path.suffix.lower() in [
            ".otp",
            ".ott",
            ".ots",
            ".odp",
            ".odt",
            ".ods",
        ]:
            return self._analyze_odf_file(zip_path)
        else:
            raise ValueError(f"Unknown ZIP format: {zip_path.suffix}")

    def compare_structures(
        self, analysis1: StructureAnalysis, analysis2: StructureAnalysis
    ) -> Dict[str, Any]:
        """Compare two structure analyses for differences."""
        comparison = {
            "format_types": (analysis1.format_type, analysis2.format_type),
            "elements_added": set(analysis2.elements.keys())
            - set(analysis1.elements.keys()),
            "elements_removed": set(analysis1.elements.keys())
            - set(analysis2.elements.keys()),
            "tokens_added": analysis2.token_placeholders - analysis1.token_placeholders,
            "tokens_removed": analysis1.token_placeholders
            - analysis2.token_placeholders,
            "namespaces_added": set(analysis2.namespaces.items())
            - set(analysis1.namespaces.items()),
            "namespaces_removed": set(analysis1.namespaces.items())
            - set(analysis2.namespaces.items()),
            "xml_files_added": set(analysis2.xml_files.keys())
            - set(analysis1.xml_files.keys()),
            "xml_files_removed": set(analysis1.xml_files.keys())
            - set(analysis2.xml_files.keys()),
            "attribute_changes": {},
        }

        # Compare attributes for common elements
        common_elements = set(analysis1.elements.keys()) & set(
            analysis2.elements.keys()
        )
        for element in common_elements:
            attrs1 = analysis1.elements[element].attributes
            attrs2 = analysis2.elements[element].attributes
            if attrs1 != attrs2:
                comparison["attribute_changes"][element] = {
                    "added": attrs2 - attrs1,
                    "removed": attrs1 - attrs2,
                }

        return comparison

    def generate_structure_report(self, analysis: StructureAnalysis) -> str:
        """Generate a comprehensive structure analysis report."""
        report = f"""
# XML Structure Analysis Report

**File:** {analysis.file_path}
**Format:** {analysis.format_type.upper()}
**Total Elements:** {analysis.total_elements}
**Total Attributes:** {analysis.total_attributes}
**XML Files:** {len(analysis.xml_files)}
**Token Placeholders:** {len(analysis.token_placeholders)}

## Namespaces ({len(analysis.namespaces)})
"""
        for prefix, uri in sorted(analysis.namespaces.items()):
            report += f"- `{prefix}`: {uri}\n"

        report += f"\n## Elements ({len(analysis.elements)})\n"
        for tag, element_info in sorted(analysis.elements.items()):
            report += f"- `{tag}`"
            if element_info.attributes:
                report += f" ({len(element_info.attributes)} attributes)"
            if element_info.token_placeholders:
                report += f" [tokens: {len(element_info.token_placeholders)}]"
            report += "\n"

        if analysis.token_placeholders:
            report += f"\n## Token Placeholders ({len(analysis.token_placeholders)})\n"
            for token in sorted(analysis.token_placeholders):
                report += f"- `{token}`\n"

        if analysis.analysis_errors:
            report += f"\n## Analysis Errors ({len(analysis.analysis_errors)})\n"
            for error in analysis.analysis_errors:
                report += f"- {error}\n"

        return report
