"""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 pydantic import BaseModel

from marqetive.utils.crdt.exceptions import CRDTSchemaError
from marqetive.utils.crdt.models import AutoNumberConfig

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[Any],
        fragment_name: str = "default",
        *,
        include_auto_number: bool = False,
    ) -> 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
            include_auto_number: If True, materialize auto-numbering into card text

        Returns:
            Parsed document result of type T

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

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

    def _get_fragment(self, doc: Doc[Any], 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 self._xml_text_plain(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(self._xml_text_plain(child))
                elif isinstance(child, XmlElement):
                    if self._get_node_type(child) == "hard_break":
                        parts.append("\n")
                    else:
                        # Strip rich text marks (twitterLink, overflow, etc.)
                        # and extract only raw text content
                        inline_parts: list[str] = []
                        self._collect_text_recursive(child, inline_parts)
                        parts.append("".join(inline_parts))
        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(self._xml_text_plain(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 _xml_text_plain(self, node: XmlText) -> str:
        """Extract plain text from XmlText, stripping all inline marks.

        Uses XmlText.diff() to walk the text segments and concatenate
        only the raw text, discarding mark attributes (overflow,
        twitterLink, etc.).
        """
        return "".join(segment for segment, _attrs in node.diff())

    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 _parse_auto_number_config(self, node: XmlElement) -> AutoNumberConfig | None:
        """Parse auto-number configuration from a thread container node.

        Args:
            node: The thread container XML element (e.g. twitterthread,
                  blueskythread, threadContainer)

        Returns:
            AutoNumberConfig or None
        """
        config = self._safe_get_attribute(node, "autoNumberConfig")
        if not config:
            return None
        if isinstance(config, str):
            try:
                config = json.loads(config)
            except json.JSONDecodeError:
                return None
        if isinstance(config, dict):
            return AutoNumberConfig(
                enabled=bool(config.get("enabled", False)),
                position=config.get("position", "start"),
                format_style=config.get("formatStyle", "#1"),
                skip_first=config.get("skipFirst", 1),
                skip_last=config.get("skipLast", 1),
            )
        return None

    def _apply_auto_numbering[C: BaseModel](
        self,
        cards: list[C],
        config: AutoNumberConfig | None,
        text_field: str = "text",
    ) -> list[C]:
        """Apply auto-numbering to card text fields.

        Inserts sequential numbers into each card's text, skipping cards
        in the skip_first / skip_last ranges. Numbering is sequential
        among numbered cards only (1, 2, 3, ...).

        Args:
            cards: List of card models (frozen Pydantic BaseModel)
            config: Auto-number configuration, or None to skip
            text_field: Name of the text field on the card model

        Returns:
            New list of cards with numbering applied (originals unchanged)
        """
        if config is None or not config.enabled:
            return cards

        total = len(cards)
        # Count how many cards will actually be numbered
        numbered_total = max(0, total - config.skip_first - config.skip_last)
        counter = 1
        result: list[C] = []

        for i, card in enumerate(cards):
            if i < config.skip_first or i >= total - config.skip_last:
                result.append(card)
                continue

            number_str = config.format_style.replace("1", str(counter)).replace(
                "9", str(numbered_total)
            )
            original_text = getattr(card, text_field, "")

            if config.position == "end":
                new_text = f"{original_text} {number_str}"
            else:
                new_text = f"{number_str} {original_text}"

            result.append(card.model_copy(update={text_field: new_text}))
            counter += 1

        return result

    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
