"""CRDT document parser factory and main API functions.

This module provides the main API for parsing CRDT documents containing
ProseMirror schema structures for various social media platforms.
"""

from __future__ import annotations

from typing import Any, cast

from pycrdt import Doc, XmlElement, XmlFragment

from marqetive.utils.crdt.exceptions import CRDTUnsupportedPlatformError
from marqetive.utils.crdt.models import (
    ParsedBlueSkyDocument,
    ParsedDocument,
    ParsedInstagramDocument,
    ParsedLinkedInDocument,
    ParsedThreadsDocument,
    ParsedTikTokDocument,
    ParsedTwitterDocument,
)
from marqetive.utils.crdt.platforms.base import BaseCRDTParser
from marqetive.utils.crdt.platforms.bluesky import BlueskyCRDTParser
from marqetive.utils.crdt.platforms.instagram import InstagramCRDTParser
from marqetive.utils.crdt.platforms.linkedin import LinkedInCRDTParser
from marqetive.utils.crdt.platforms.threads import ThreadsCRDTParser
from marqetive.utils.crdt.platforms.tiktok import TikTokCRDTParser
from marqetive.utils.crdt.platforms.twitter import TwitterCRDTParser

# Supported platforms
SUPPORTED_PLATFORMS: frozenset[str] = frozenset(
    ["twitter", "linkedin", "instagram", "tiktok", "bluesky", "threads"]
)

# Platform to root node type mapping (for detection)
_PLATFORM_NODE_TYPES: dict[str, str] = {
    "twitter": "twittercard",
    "linkedin": "linkedinpost",
    "instagram": "instagrampost",
    "tiktok": "tiktokpost",
    "bluesky": "blueskycard",
    "threads": "threadcard",
}


class CRDTParserFactory:
    """Factory for creating platform-specific CRDT parsers.

    Provides a centralized way to create parser instances based on
    platform name, with caching for efficiency.

    Example:
        >>> factory = CRDTParserFactory()
        >>> parser = factory.get_parser("twitter")
        >>> result = parser.parse_document(doc)
    """

    def __init__(self) -> None:
        """Initialize the factory with parser cache."""
        self._parsers: dict[str, BaseCRDTParser[ParsedDocument]] = {}

    def get_parser(self, platform: str) -> BaseCRDTParser[ParsedDocument]:
        """Get a parser for the specified platform.

        Args:
            platform: Platform name (twitter, linkedin, instagram, tiktok)

        Returns:
            Platform-specific parser instance

        Raises:
            CRDTUnsupportedPlatformError: If platform is not supported
        """
        platform = platform.lower().strip()

        if platform not in SUPPORTED_PLATFORMS:
            raise CRDTUnsupportedPlatformError(
                platform,
                supported_platforms=list(SUPPORTED_PLATFORMS),
            )

        if platform not in self._parsers:
            self._parsers[platform] = self._create_parser(platform)

        return self._parsers[platform]

    def _create_parser(self, platform: str) -> BaseCRDTParser[ParsedDocument]:
        """Create a new parser instance for the platform.

        Args:
            platform: Platform name

        Returns:
            New parser instance
        """
        match platform:
            case "twitter":
                return TwitterCRDTParser()
            case "linkedin":
                return LinkedInCRDTParser()
            case "instagram":
                return InstagramCRDTParser()
            case "tiktok":
                return TikTokCRDTParser()
            case "bluesky":
                return BlueskyCRDTParser()
            case "threads":
                return ThreadsCRDTParser()
            case _:
                raise CRDTUnsupportedPlatformError(
                    platform,
                    supported_platforms=list(SUPPORTED_PLATFORMS),
                )

    def get_supported_platforms(self) -> frozenset[str]:
        """Get the set of supported platform names.

        Returns:
            Frozenset of platform names
        """
        return SUPPORTED_PLATFORMS


# Global factory instance
_factory = CRDTParserFactory()


def parse_crdt_document(
    platform: str,
    doc: Doc[Any],
    fragment_name: str = "default",
    *,
    include_auto_number: bool = False,
) -> ParsedDocument:
    """Parse a CRDT document for a specific platform.

    This is the main entry point for parsing CRDT documents.

    Args:
        platform: Platform name (twitter, linkedin, instagram, tiktok)
        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:
        Platform-specific parsed document result

    Raises:
        CRDTUnsupportedPlatformError: If platform is not supported
        CRDTParseError: If parsing fails
        CRDTEmptyDocumentError: If document is empty

    Example:
        >>> from pycrdt import Doc
        >>> doc = Doc()
        >>> doc.apply_update(binary_data)
        >>> result = parse_crdt_document("twitter", doc)
        >>> for card in result.cards:
        ...     print(card.text)
    """
    parser = _factory.get_parser(platform)
    return parser.parse_document(
        doc, fragment_name, include_auto_number=include_auto_number
    )


def parse_crdt_binary(
    platform: str,
    binary_data: bytes,
    fragment_name: str = "default",
    *,
    include_auto_number: bool = False,
) -> ParsedDocument:
    """Parse a CRDT document from binary data.

    Convenience function that creates a Doc from binary data and parses it.

    Args:
        platform: Platform name (twitter, linkedin, instagram, tiktok)
        binary_data: Y.js CRDT binary update data
        fragment_name: Name of the XML fragment to parse

    Returns:
        Platform-specific parsed document result

    Raises:
        CRDTUnsupportedPlatformError: If platform is not supported
        CRDTParseError: If parsing fails
        CRDTEmptyDocumentError: If document is empty
        CRDTSchemaError: If binary data is invalid

    Example:
        >>> with open("document.yjs", "rb") as f:
        ...     binary_data = f.read()
        >>> result = parse_crdt_binary("twitter", binary_data)
        >>> print(result.cards[0].text)
    """
    from marqetive.utils.crdt.exceptions import CRDTSchemaError

    try:
        doc = Doc()
        doc.apply_update(binary_data)
    except Exception as e:
        raise CRDTSchemaError(
            f"Failed to apply binary update to document: {e}",
            schema_element="binary_data",
        ) from e

    return parse_crdt_document(
        platform, doc, fragment_name, include_auto_number=include_auto_number
    )


def _has_node_type(fragment: XmlFragment | XmlElement, node_type: str) -> bool:
    """Check if any node of the given type exists in a fragment.

    Performs a recursive search without needing a parser instance.

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

    Returns:
        True if at least one matching node is found
    """
    try:
        for child in fragment.children:
            if isinstance(child, XmlElement):
                try:
                    if child.tag == node_type:
                        return True
                except (AttributeError, TypeError):
                    pass
                # Recurse into child elements
                if _has_node_type(child, node_type):
                    return True
    except (AttributeError, TypeError):
        pass
    return False


def detect_platform(
    doc: Doc[Any],
    fragment_name: str = "default",
) -> str | None:
    """Detect the platform type from a CRDT document.

    Examines the document to determine which platform's schema it follows
    by looking for platform-specific root node types.

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

    Returns:
        Platform name if detected, None if unknown

    Example:
        >>> platform = detect_platform(doc)
        >>> if platform:
        ...     result = parse_crdt_document(platform, doc)
        ... else:
        ...     print("Unknown document format")
    """
    try:
        fragment = doc.get(fragment_name, type=XmlFragment)
    except Exception:
        return None

    # Try to find platform-specific nodes
    for platform, node_type in _PLATFORM_NODE_TYPES.items():
        if _has_node_type(fragment, node_type):
            return platform

    return None


# Type-specific parsing functions for better type inference


def parse_twitter_document(
    doc: Doc[Any],
    fragment_name: str = "default",
    *,
    include_auto_number: bool = False,
) -> ParsedTwitterDocument:
    """Parse a Twitter CRDT document with proper type inference.

    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:
        ParsedTwitterDocument

    Example:
        >>> result = parse_twitter_document(doc)
        >>> for card in result.cards:
        ...     print(card.text)
    """
    result = parse_crdt_document(
        "twitter", doc, fragment_name, include_auto_number=include_auto_number
    )
    return cast(ParsedTwitterDocument, result)


def parse_linkedin_document(
    doc: Doc[Any],
    fragment_name: str = "default",
    *,
    include_auto_number: bool = False,
) -> ParsedLinkedInDocument:
    """Parse a LinkedIn CRDT document with proper type inference.

    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:
        ParsedLinkedInDocument

    Example:
        >>> result = parse_linkedin_document(doc)
        >>> print(result.post.text)
    """
    result = parse_crdt_document(
        "linkedin", doc, fragment_name, include_auto_number=include_auto_number
    )
    return cast(ParsedLinkedInDocument, result)


def parse_instagram_document(
    doc: Doc[Any],
    fragment_name: str = "default",
    *,
    include_auto_number: bool = False,
) -> ParsedInstagramDocument:
    """Parse an Instagram CRDT document with proper type inference.

    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:
        ParsedInstagramDocument

    Example:
        >>> result = parse_instagram_document(doc)
        >>> print(result.post.caption)
    """
    result = parse_crdt_document(
        "instagram", doc, fragment_name, include_auto_number=include_auto_number
    )
    return cast(ParsedInstagramDocument, result)


def parse_tiktok_document(
    doc: Doc[Any],
    fragment_name: str = "default",
    *,
    include_auto_number: bool = False,
) -> ParsedTikTokDocument:
    """Parse a TikTok CRDT document with proper type inference.

    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:
        ParsedTikTokDocument

    Example:
        >>> result = parse_tiktok_document(doc)
        >>> print(result.post.title)
    """
    result = parse_crdt_document(
        "tiktok", doc, fragment_name, include_auto_number=include_auto_number
    )
    return cast(ParsedTikTokDocument, result)


def parse_bluesky_document(
    doc: Doc[Any],
    fragment_name: str = "default",
    *,
    include_auto_number: bool = False,
) -> ParsedBlueSkyDocument:
    """Parse a Bluesky CRDT document with proper type inference.

    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:
        ParsedBlueSkyDocument

    Example:
        >>> result = parse_bluesky_document(doc)
        >>> for card in result.cards:
        ...     print(card.text)
    """
    result = parse_crdt_document(
        "bluesky", doc, fragment_name, include_auto_number=include_auto_number
    )
    return cast(ParsedBlueSkyDocument, result)


def parse_threads_document(
    doc: Doc[Any],
    fragment_name: str = "default",
    *,
    include_auto_number: bool = False,
) -> ParsedThreadsDocument:
    """Parse a Threads CRDT document with proper type inference.

    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:
        ParsedThreadsDocument

    Example:
        >>> result = parse_threads_document(doc)
        >>> for card in result.cards:
        ...     print(card.text)
    """
    result = parse_crdt_document(
        "threads", doc, fragment_name, include_auto_number=include_auto_number
    )
    return cast(ParsedThreadsDocument, result)
