"""Abstract base class for platform-specific CRDT parsers.

This module defines the BaseCRDTParser ABC that serves as a blueprint
for implementing platform-specific CRDT document parsers.
"""

from __future__ import annotations

import json
import logging
from abc import ABC, abstractmethod
from typing import Any

from pycrdt import Doc, XmlElement, XmlFragment, XmlText

from marqetive.utils.crdt.exceptions import CRDTSchemaError

logger = logging.getLogger(__name__)

# Type alias for XML node types
type XmlNode = XmlFragment | XmlElement | XmlText | str


class BaseCRDTParser[T](ABC):
    """Abstract base class for platform-specific CRDT parsers.

    This class defines the interface that all platform parsers must implement.
    It uses PEP 695 generics to provide type-safe parsing results.

    Type Parameters:
        T: The type of parsed document result (e.g., ParsedTwitterDocument)
    """

    @property
    @abstractmethod
    def platform_name(self) -> str:
        """Return the platform name for this parser."""
        ...

    @property
    @abstractmethod
    def root_node_type(self) -> str:
        """Return the root node type expected in CRDT documents."""
        ...

    @abstractmethod
    def parse_document(self, doc: Doc, fragment_name: str = "default") -> T:
        """Parse a CRDT document and return platform-specific content.

        Args:
            doc: The pycrdt Doc object to parse
            fragment_name: Name of the XML fragment to parse

        Returns:
            Parsed document result of type T

        Raises:
            CRDTParseError: If parsing fails
            CRDTEmptyDocumentError: If document is empty
            CRDTSchemaError: If document schema is invalid
        """
        ...

    @abstractmethod
    def validate_schema(self, doc: Doc, fragment_name: str = "default") -> list[str]:
        """Validate a CRDT document against platform-specific rules.

        Args:
            doc: The pycrdt Doc object to validate
            fragment_name: Name of the XML fragment to validate

        Returns:
            List of validation error messages (empty if valid)
        """
        ...

    # ==================== Common Utility Methods ====================

    def _get_fragment(self, doc: Doc, fragment_name: str) -> XmlFragment:
        """Get an XML fragment from the document.

        Args:
            doc: The pycrdt Doc object
            fragment_name: Name of the fragment to retrieve

        Returns:
            The XmlFragment object

        Raises:
            CRDTSchemaError: If fragment doesn't exist
        """
        try:
            return doc.get(fragment_name, type=XmlFragment)
        except Exception as e:
            raise CRDTSchemaError(
                f"Failed to get fragment '{fragment_name}': {e}",
                schema_element=fragment_name,
            ) from e

    def _extract_text(self, node: XmlNode) -> str:
        """Extract text content from a node with paragraph-aware handling.

        Finds paragraph children and extracts inline text from each,
        joining them with newlines. Handles hard_break nodes within
        paragraphs as newlines.

        Args:
            node: The XML node to extract text from

        Returns:
            Combined text content with paragraph boundaries preserved
        """
        if isinstance(node, str):
            return node

        if isinstance(node, XmlText):
            return str(node)

        if not isinstance(node, (XmlFragment, XmlElement)):
            return ""

        # Find paragraph nodes among children
        paragraphs: list[str] = []
        has_paragraphs = False

        try:
            for child in node.children:
                if (
                    isinstance(child, XmlElement)
                    and self._get_node_type(child) == "paragraph"
                ):
                    has_paragraphs = True
                    para_text = self._extract_paragraph_text(child)
                    paragraphs.append(para_text)
        except (AttributeError, TypeError):
            logger.debug(
                "Failed to iterate children of node %r during text extraction",
                node,
                exc_info=True,
            )

        if has_paragraphs:
            return "\n".join(paragraphs)

        # Fallback: collect text directly if no paragraph nodes found
        text_parts: list[str] = []
        self._collect_text_recursive(node, text_parts)
        return "".join(text_parts)

    def _extract_paragraph_text(self, node: XmlElement) -> str:
        """Extract inline text from a single paragraph node.

        Handles hard_break nodes as newlines within the paragraph.

        Args:
            node: The paragraph XML element

        Returns:
            Text content of the paragraph
        """
        parts: list[str] = []
        try:
            for child in node.children:
                if isinstance(child, str):
                    parts.append(child)
                elif isinstance(child, XmlText):
                    parts.append(str(child))
                elif isinstance(child, XmlElement):
                    if self._get_node_type(child) == "hard_break":
                        parts.append("\n")
                    else:
                        # Recurse into inline elements
                        parts.append(self._extract_text(child))
        except (AttributeError, TypeError):
            logger.debug(
                "Failed to iterate children of node %r in paragraph text extraction",
                node,
                exc_info=True,
            )
        return "".join(parts)

    def _collect_text_recursive(self, node: XmlNode, text_parts: list[str]) -> None:
        """Recursively collect text from node tree.

        Args:
            node: Current node or text string
            text_parts: List to accumulate text parts
        """
        if isinstance(node, str):
            text_parts.append(node)
            return

        if isinstance(node, XmlText):
            text_parts.append(str(node))
            return

        if isinstance(node, (XmlFragment, XmlElement)):
            try:
                for child in node.children:
                    self._collect_text_recursive(child, text_parts)
            except (AttributeError, TypeError):
                logger.debug(
                    "Failed to iterate children of node %r in recursive text collection",
                    node,
                    exc_info=True,
                )

    def _get_node_attributes(self, node: XmlNode) -> dict[str, Any]:
        """Get all attributes from a node.

        Args:
            node: The XML node to get attributes from

        Returns:
            Dictionary of attribute name-value pairs
        """
        if isinstance(node, XmlElement):
            try:
                # XmlAttributesView is iterable and produces (str, Any) tuples
                # pycrdt type stubs don't correctly type XmlAttributesView as iterable
                return {str(k): v for k, v in node.attributes}  # type: ignore[misc]
            except (AttributeError, TypeError, RuntimeError):
                return {}
        return {}

    def _parse_media_urls(self, node: XmlNode, key: str = "media") -> list[str]:
        """Parse media URLs from a node attribute.

        Handles both list format and space-separated URL strings,
        matching production parser behavior.

        Args:
            node: The XML node containing the attribute
            key: Attribute name to read (default: "media")

        Returns:
            List of media URL strings
        """
        value = self._safe_get_attribute(node, key)
        if not value:
            return []
        if isinstance(value, list):
            return [str(url) for url in value if url]
        if isinstance(value, str) and value.strip():
            return [url.strip() for url in value.split() if url.strip()]
        return []

    def _find_nodes_by_type(
        self, fragment: XmlFragment, node_type: str
    ) -> list[XmlElement]:
        """Find all nodes of a specific type in a fragment.

        Args:
            fragment: The XML fragment to search
            node_type: Type of node to find (tag name)

        Returns:
            List of matching XmlElement nodes
        """
        nodes: list[XmlElement] = []
        self._find_nodes_recursive(fragment, node_type, nodes)
        return nodes

    def _find_nodes_recursive(
        self,
        node: XmlNode,
        node_type: str,
        result: list[XmlElement],
    ) -> None:
        """Recursively find nodes of a specific type.

        Args:
            node: Current node to search
            node_type: Type of node to find (tag name)
            result: List to accumulate found nodes
        """
        if isinstance(node, str):
            return

        if isinstance(node, XmlElement):
            current_type = self._get_node_type(node)
            if current_type == node_type:
                result.append(node)

        if isinstance(node, (XmlFragment, XmlElement)):
            try:
                for child in node.children:
                    self._find_nodes_recursive(child, node_type, result)
            except (AttributeError, TypeError):
                logger.debug(
                    "Failed to iterate children of node %r in recursive node search",
                    node,
                    exc_info=True,
                )

    def _get_node_type(self, node: XmlNode) -> str | None:
        """Get the type name of a node.

        Args:
            node: The XML node

        Returns:
            Node type name (tag) or None
        """
        if isinstance(node, XmlElement):
            try:
                return node.tag
            except (AttributeError, TypeError):
                logger.debug(
                    "Failed to get tag from node %r",
                    node,
                    exc_info=True,
                )

            attrs = self._get_node_attributes(node)
            if "type" in attrs:
                return str(attrs["type"])

        return None

    def _safe_get_attribute(self, node: XmlNode, key: str, default: Any = None) -> Any:
        """Safely get an attribute value from a node.

        Args:
            node: The XML node
            key: Attribute key to get
            default: Default value if attribute doesn't exist

        Returns:
            Attribute value or default
        """
        try:
            attrs = self._get_node_attributes(node)
            return attrs.get(key, default)
        except (AttributeError, TypeError):
            return default

    def _validate_text_length(
        self, text: str, max_length: int, field_name: str
    ) -> str | None:
        """Validate text length against a maximum.

        Args:
            text: Text to validate
            max_length: Maximum allowed length
            field_name: Name of the field for error message

        Returns:
            Error message if validation fails, None otherwise
        """
        if len(text) > max_length:
            return (
                f"{field_name} exceeds maximum length: "
                f"{len(text)} > {max_length} characters"
            )
        return None

    def _validate_media_count(
        self, media: list[str], max_count: int, min_count: int = 0
    ) -> str | None:
        """Validate media count against min/max constraints.

        Args:
            media: List of media URL strings
            max_count: Maximum allowed count
            min_count: Minimum required count

        Returns:
            Error message if validation fails, None otherwise
        """
        count = len(media)
        if count < min_count:
            return f"Requires at least {min_count} media items, got {count}"
        if count > max_count:
            return f"Exceeds maximum media count: {count} > {max_count}"
        return None

    def _parse_json_attribute(
        self, node: XmlNode, key: str
    ) -> dict[str, Any] | list[Any] | None:
        """Parse a JSON-encoded attribute value.

        Args:
            node: The XML node
            key: Attribute key containing JSON

        Returns:
            Parsed JSON value or None (returns None on parse errors)
        """
        value = self._safe_get_attribute(node, key)
        if not value:
            return None

        if isinstance(value, dict | list):
            return value

        if isinstance(value, str):
            try:
                return json.loads(value)
            except json.JSONDecodeError:
                return None

        return None
