"""CRDT document converter for cross-platform transformation.

This module provides the DocumentConverter class and top-level convenience
functions for converting parsed CRDT documents between platform formats.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

from pycrdt import Doc

from marqetive.utils.crdt.builders.bluesky import BlueskyCRDTBuilder
from marqetive.utils.crdt.builders.instagram import InstagramCRDTBuilder
from marqetive.utils.crdt.builders.linkedin import LinkedInCRDTBuilder
from marqetive.utils.crdt.builders.tiktok import TikTokCRDTBuilder
from marqetive.utils.crdt.builders.twitter import TwitterCRDTBuilder
from marqetive.utils.crdt.exceptions import CRDTConversionError
from marqetive.utils.crdt.models import (
    PLATFORM_LIMITS,
    BlueSkyCardContent,
    ContentType,
    ConversionExtras,
    ConversionResult,
    ConversionWarning,
    InstagramPostContent,
    LinkedInPostContent,
    ParsedBlueSkyDocument,
    ParsedDocument,
    ParsedInstagramDocument,
    ParsedLinkedInDocument,
    ParsedTikTokDocument,
    ParsedTwitterDocument,
    Platform,
    PlatformLimits,
    PostVisibility,
    TikTokPostContent,
    TwitterCardContent,
)
from marqetive.utils.crdt.parser import parse_crdt_binary


def _deduplicate_ordered(items: list[str]) -> list[str]:
    """Deduplicate a list while preserving order."""
    seen: set[str] = set()
    result: list[str] = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result


class DocumentConverter:
    """Converts parsed CRDT documents between platform formats."""

    def convert(
        self,
        source: ParsedDocument,
        target_platform: Platform | str,
    ) -> ConversionResult:
        """Convert a parsed document to another platform format.

        Args:
            source: The parsed source document.
            target_platform: Target platform name or enum.

        Returns:
            ConversionResult containing the converted document, warnings, and extras.

        Raises:
            CRDTConversionError: If conversion fails or same-platform conversion.
        """
        if isinstance(target_platform, str):
            target_platform = Platform(target_platform.lower())

        source_platform = self._detect_source_platform(source)

        if source_platform == target_platform:
            raise CRDTConversionError(
                f"Cannot convert {source_platform.value} to itself",
                source_platform=source_platform.value,
                target_platform=target_platform.value,
            )

        method_name = f"_{source_platform.value}_to_{target_platform.value}"
        method = getattr(self, method_name, None)
        if method is None:
            raise CRDTConversionError(
                f"No conversion path from {source_platform.value} to {target_platform.value}",
                source_platform=source_platform.value,
                target_platform=target_platform.value,
            )

        return method(source)

    def _detect_source_platform(self, source: ParsedDocument) -> Platform:
        """Detect the platform of a parsed document."""
        match source:
            case ParsedTwitterDocument():
                return Platform.TWITTER
            case ParsedLinkedInDocument():
                return Platform.LINKEDIN
            case ParsedInstagramDocument():
                return Platform.INSTAGRAM
            case ParsedTikTokDocument():
                return Platform.TIKTOK
            case ParsedBlueSkyDocument():
                return Platform.BLUESKY

    def _extract_text_body(self, source: ParsedDocument) -> str:
        """Extract the main text content from any parsed document."""
        match source:
            case ParsedTwitterDocument(cards=cards):
                return "\n\n".join(card.text for card in cards if card.text)
            case ParsedLinkedInDocument(post=post):
                return post.text
            case ParsedInstagramDocument(post=post):
                return post.caption
            case ParsedTikTokDocument(post=post):
                return post.title
            case ParsedBlueSkyDocument(cards=cards):
                return "\n\n".join(card.text for card in cards if card.text)

    def _extract_all_media(self, source: ParsedDocument) -> list[str]:
        """Extract all media URLs from any parsed document, deduplicated."""
        media: list[str] = []
        match source:
            case ParsedTwitterDocument(cards=cards):
                for card in cards:
                    media.extend(card.media)
            case ParsedLinkedInDocument(post=post):
                media = list(post.media)
            case ParsedInstagramDocument(post=post):
                media = list(post.media)
            case ParsedTikTokDocument(post=post):
                media = list(post.media)
            case ParsedBlueSkyDocument(cards=cards):
                for card in cards:
                    media.extend(card.media)
        return _deduplicate_ordered(media)

    def _validate_against_limits(
        self,
        text: str,
        media: list[str],
        target: Platform,
    ) -> list[ConversionWarning]:
        """Validate text and media against target platform limits."""
        warnings: list[ConversionWarning] = []
        limits: PlatformLimits = PLATFORM_LIMITS[target]

        if len(text) > limits.max_text_length:
            warnings.append(
                ConversionWarning(
                    code="text_exceeds_limit",
                    field="text",
                    message=(
                        f"Text length {len(text)} exceeds "
                        f"{target.value} limit of {limits.max_text_length}"
                    ),
                )
            )

        if len(media) > limits.max_media_count:
            warnings.append(
                ConversionWarning(
                    code="media_exceeds_limit",
                    field="media",
                    message=(
                        f"Media count {len(media)} exceeds "
                        f"{target.value} limit of {limits.max_media_count}"
                    ),
                )
            )

        if len(media) < limits.min_media_count:
            warnings.append(
                ConversionWarning(
                    code="media_below_minimum",
                    field="media",
                    message=(
                        f"{target.value} requires at least {limits.min_media_count} "
                        f"media item(s), got {len(media)}"
                    ),
                )
            )

        return warnings

    def _validate_thread_against_limits(
        self,
        document: ParsedDocument,
        target: Platform,
    ) -> list[ConversionWarning]:
        """Validate each card individually against target platform limits."""
        warnings: list[ConversionWarning] = []
        limits: PlatformLimits = PLATFORM_LIMITS[target]

        cards: list[TwitterCardContent] | list[BlueSkyCardContent]
        match document:
            case ParsedTwitterDocument(cards=tw_cards):
                cards = tw_cards
            case ParsedBlueSkyDocument(cards=bs_cards):
                cards = bs_cards
            case _:
                return warnings

        if len(cards) <= 1:
            return warnings

        for i, card in enumerate(cards):
            if len(card.text) > limits.max_text_length:
                warnings.append(
                    ConversionWarning(
                        code="text_exceeds_limit",
                        field=f"cards[{i}].text",
                        message=(
                            f"Card {i} text length {len(card.text)} exceeds "
                            f"{target.value} limit of {limits.max_text_length}"
                        ),
                    )
                )
            if len(card.media) > limits.max_media_count:
                warnings.append(
                    ConversionWarning(
                        code="media_exceeds_limit",
                        field=f"cards[{i}].media",
                        message=(
                            f"Card {i} media count {len(card.media)} exceeds "
                            f"{target.value} limit of {limits.max_media_count}"
                        ),
                    )
                )

        return warnings

    def _collect_extras(
        self,
        source: ParsedDocument,
        target: Platform,
    ) -> tuple[dict[str, Any], list[ConversionWarning]]:
        """Collect unmappable fields and generate field_dropped warnings."""
        extras: dict[str, Any] = {}
        warnings: list[ConversionWarning] = []
        source_platform = self._detect_source_platform(source)

        def _drop(field: str, value: Any) -> None:
            extras[field] = value
            warnings.append(
                ConversionWarning(
                    code="field_dropped",
                    field=field,
                    message=(
                        f"'{field}' from {source_platform.value} has no "
                        f"equivalent in {target.value}"
                    ),
                )
            )

        match source:
            case ParsedTwitterDocument(cards=cards, auto_number_config=anc):
                if target not in (Platform.TWITTER, Platform.BLUESKY):
                    if anc is not None:
                        _drop("auto_number_config", anc.model_dump())
                    if len(cards) > 1:
                        _drop("thread_card_count", len(cards))
                if target != Platform.TWITTER:
                    # Carry quoted tweet data (platform-specific)
                    for i, card in enumerate(cards):
                        if card.is_quoted:
                            _drop(
                                f"card_{i}_quoted",
                                {
                                    "is_quoted": card.is_quoted,
                                    "quoted_card_id": card.quoted_card_id,
                                    "quoted_tweet_id": card.quoted_tweet_id,
                                },
                            )

            case ParsedLinkedInDocument(post=post, comment=comment):
                if target not in (Platform.LINKEDIN,):
                    if post.visibility != PostVisibility.PUBLIC:
                        _drop("visibility", post.visibility.value)
                    if post.pdf is not None:
                        _drop("pdf", post.pdf)
                    if comment is not None:
                        _drop("comment", comment.model_dump())

            case ParsedInstagramDocument(post=post):
                if target not in (Platform.INSTAGRAM,):
                    if post.location is not None and target not in (Platform.TIKTOK,):
                        _drop("location", post.location)
                    if post.music is not None and target not in (Platform.TIKTOK,):
                        _drop("music", post.music)
                    if post.content_type != ContentType.POST:
                        _drop("content_type", post.content_type.value)

            case ParsedTikTokDocument(post=post):
                if target not in (Platform.TIKTOK,):
                    if post.location is not None and target not in (
                        Platform.INSTAGRAM,
                    ):
                        _drop("location", post.location)
                    if post.music is not None and target not in (Platform.INSTAGRAM,):
                        _drop("music", post.music)

            case ParsedBlueSkyDocument(cards=cards, auto_number_config=anc):
                if target not in (Platform.TWITTER, Platform.BLUESKY):
                    if anc is not None:
                        _drop("auto_number_config", anc.model_dump())
                    if len(cards) > 1:
                        _drop("thread_card_count", len(cards))
                if target != Platform.BLUESKY:
                    # Carry og link card data (platform-specific)
                    for i, card in enumerate(cards):
                        if card.og is not None:
                            _drop(f"card_{i}_og", card.og)

        return extras, warnings

    def _make_result(
        self,
        document: ParsedDocument,
        source: ParsedDocument,
        target: Platform,
        extra_warnings: list[ConversionWarning] | None = None,
    ) -> ConversionResult:
        """Build a ConversionResult with extras and validation warnings."""
        source_platform = self._detect_source_platform(source)
        text = self._extract_text_body(
            document  # type: ignore[arg-type]
        )
        media = self._extract_all_media(
            document  # type: ignore[arg-type]
        )

        extras_dict, extras_warnings = self._collect_extras(source, target)

        is_thread = False
        match document:
            case (
                ParsedTwitterDocument(cards=cards) | ParsedBlueSkyDocument(cards=cards)
            ):
                is_thread = len(cards) > 1
            case _:
                pass

        if is_thread:
            limit_warnings = []
            thread_warnings = self._validate_thread_against_limits(document, target)
        else:
            limit_warnings = self._validate_against_limits(text, media, target)
            thread_warnings = []

        all_warnings = (
            (extra_warnings or []) + extras_warnings + limit_warnings + thread_warnings
        )

        return ConversionResult(
            document=document,
            warnings=all_warnings,
            extras=ConversionExtras(
                source_platform=source_platform,
                source_fields=extras_dict,
            ),
            source_platform=source_platform,
            target_platform=target,
            converted_at=datetime.now(UTC),
        )

    # ==================== Twitter → Others ====================

    def _twitter_to_linkedin(self, source: ParsedTwitterDocument) -> ConversionResult:
        text = self._extract_text_body(source)
        media = self._extract_all_media(source)
        extra_warnings: list[ConversionWarning] = []

        if len(source.cards) > 1:
            extra_warnings.append(
                ConversionWarning(
                    code="thread_merged",
                    field="cards",
                    message=f"Merged {len(source.cards)} Twitter cards into single LinkedIn post",
                )
            )

        doc = ParsedLinkedInDocument(
            post=LinkedInPostContent(text=text, media=media),
        )
        return self._make_result(doc, source, Platform.LINKEDIN, extra_warnings)

    def _twitter_to_instagram(self, source: ParsedTwitterDocument) -> ConversionResult:
        text = self._extract_text_body(source)
        media = self._extract_all_media(source)
        extra_warnings: list[ConversionWarning] = []

        if len(source.cards) > 1:
            extra_warnings.append(
                ConversionWarning(
                    code="thread_merged",
                    field="cards",
                    message=f"Merged {len(source.cards)} Twitter cards into single Instagram post",
                )
            )

        content_type = self._infer_instagram_content_type(media, extra_warnings)

        doc = ParsedInstagramDocument(
            post=InstagramPostContent(
                caption=text,
                media=media,
                content_type=content_type,
            ),
        )
        return self._make_result(doc, source, Platform.INSTAGRAM, extra_warnings)

    def _twitter_to_tiktok(self, source: ParsedTwitterDocument) -> ConversionResult:
        text = self._extract_text_body(source)
        media = self._extract_all_media(source)
        extra_warnings: list[ConversionWarning] = []

        if len(source.cards) > 1:
            extra_warnings.append(
                ConversionWarning(
                    code="thread_merged",
                    field="cards",
                    message=f"Merged {len(source.cards)} Twitter cards into single TikTok post",
                )
            )

        doc = ParsedTikTokDocument(
            post=TikTokPostContent(title=text, media=media),
        )
        return self._make_result(doc, source, Platform.TIKTOK, extra_warnings)

    # ==================== LinkedIn → Others ====================

    def _linkedin_to_twitter(self, source: ParsedLinkedInDocument) -> ConversionResult:
        text = source.post.text
        media = list(source.post.media)

        doc = ParsedTwitterDocument(
            cards=[TwitterCardContent(text=text, media=media)],
        )
        return self._make_result(doc, source, Platform.TWITTER)

    def _linkedin_to_instagram(
        self, source: ParsedLinkedInDocument
    ) -> ConversionResult:
        text = source.post.text
        media = list(source.post.media)
        extra_warnings: list[ConversionWarning] = []

        content_type = self._infer_instagram_content_type(media, extra_warnings)

        doc = ParsedInstagramDocument(
            post=InstagramPostContent(
                caption=text,
                media=media,
                content_type=content_type,
            ),
        )
        return self._make_result(doc, source, Platform.INSTAGRAM, extra_warnings)

    def _linkedin_to_tiktok(self, source: ParsedLinkedInDocument) -> ConversionResult:
        text = source.post.text
        media = list(source.post.media)

        doc = ParsedTikTokDocument(
            post=TikTokPostContent(title=text, media=media),
        )
        return self._make_result(doc, source, Platform.TIKTOK)

    # ==================== Instagram → Others ====================

    def _instagram_to_twitter(
        self, source: ParsedInstagramDocument
    ) -> ConversionResult:
        text = source.post.caption
        media = list(source.post.media)

        doc = ParsedTwitterDocument(
            cards=[TwitterCardContent(text=text, media=media)],
        )
        return self._make_result(doc, source, Platform.TWITTER)

    def _instagram_to_linkedin(
        self, source: ParsedInstagramDocument
    ) -> ConversionResult:
        text = source.post.caption
        media = list(source.post.media)

        doc = ParsedLinkedInDocument(
            post=LinkedInPostContent(text=text, media=media),
        )
        return self._make_result(doc, source, Platform.LINKEDIN)

    def _instagram_to_tiktok(self, source: ParsedInstagramDocument) -> ConversionResult:
        text = source.post.caption
        media = list(source.post.media)

        doc = ParsedTikTokDocument(
            post=TikTokPostContent(
                title=text,
                media=media,
                location=source.post.location,
                music=source.post.music,
            ),
        )
        return self._make_result(doc, source, Platform.TIKTOK)

    # ==================== TikTok → Others ====================

    def _tiktok_to_twitter(self, source: ParsedTikTokDocument) -> ConversionResult:
        text = source.post.title
        media = list(source.post.media)

        doc = ParsedTwitterDocument(
            cards=[TwitterCardContent(text=text, media=media)],
        )
        return self._make_result(doc, source, Platform.TWITTER)

    def _tiktok_to_linkedin(self, source: ParsedTikTokDocument) -> ConversionResult:
        text = source.post.title
        media = list(source.post.media)

        doc = ParsedLinkedInDocument(
            post=LinkedInPostContent(text=text, media=media),
        )
        return self._make_result(doc, source, Platform.LINKEDIN)

    def _tiktok_to_instagram(self, source: ParsedTikTokDocument) -> ConversionResult:
        text = source.post.title
        media = list(source.post.media)
        extra_warnings: list[ConversionWarning] = []

        content_type = self._infer_instagram_content_type(media, extra_warnings)

        doc = ParsedInstagramDocument(
            post=InstagramPostContent(
                caption=text,
                media=media,
                content_type=content_type,
                location=source.post.location,
                music=source.post.music,
            ),
        )
        return self._make_result(doc, source, Platform.INSTAGRAM, extra_warnings)

    # ==================== Bluesky → Others ====================

    def _bluesky_to_twitter(self, source: ParsedBlueSkyDocument) -> ConversionResult:
        cards = [
            TwitterCardContent(
                id=card.id,
                text=card.text,
                media=list(card.media),
                auto_number=card.auto_number,
            )
            for card in source.cards
        ]
        doc = ParsedTwitterDocument(
            cards=cards,
            auto_number_config=source.auto_number_config,
        )
        return self._make_result(doc, source, Platform.TWITTER)

    def _bluesky_to_linkedin(self, source: ParsedBlueSkyDocument) -> ConversionResult:
        text = self._extract_text_body(source)
        media = self._extract_all_media(source)
        extra_warnings: list[ConversionWarning] = []

        if len(source.cards) > 1:
            extra_warnings.append(
                ConversionWarning(
                    code="thread_merged",
                    field="cards",
                    message=f"Merged {len(source.cards)} Bluesky cards into single LinkedIn post",
                )
            )

        doc = ParsedLinkedInDocument(
            post=LinkedInPostContent(text=text, media=media),
        )
        return self._make_result(doc, source, Platform.LINKEDIN, extra_warnings)

    def _bluesky_to_instagram(self, source: ParsedBlueSkyDocument) -> ConversionResult:
        text = self._extract_text_body(source)
        media = self._extract_all_media(source)
        extra_warnings: list[ConversionWarning] = []

        if len(source.cards) > 1:
            extra_warnings.append(
                ConversionWarning(
                    code="thread_merged",
                    field="cards",
                    message=f"Merged {len(source.cards)} Bluesky cards into single Instagram post",
                )
            )

        content_type = self._infer_instagram_content_type(media, extra_warnings)

        doc = ParsedInstagramDocument(
            post=InstagramPostContent(
                caption=text,
                media=media,
                content_type=content_type,
            ),
        )
        return self._make_result(doc, source, Platform.INSTAGRAM, extra_warnings)

    def _bluesky_to_tiktok(self, source: ParsedBlueSkyDocument) -> ConversionResult:
        text = self._extract_text_body(source)
        media = self._extract_all_media(source)
        extra_warnings: list[ConversionWarning] = []

        if len(source.cards) > 1:
            extra_warnings.append(
                ConversionWarning(
                    code="thread_merged",
                    field="cards",
                    message=f"Merged {len(source.cards)} Bluesky cards into single TikTok post",
                )
            )

        doc = ParsedTikTokDocument(
            post=TikTokPostContent(title=text, media=media),
        )
        return self._make_result(doc, source, Platform.TIKTOK, extra_warnings)

    # ==================== Others → Bluesky ====================

    def _twitter_to_bluesky(self, source: ParsedTwitterDocument) -> ConversionResult:
        cards = [
            BlueSkyCardContent(
                id=card.id,
                text=card.text,
                media=list(card.media),
                auto_number=card.auto_number,
            )
            for card in source.cards
        ]
        doc = ParsedBlueSkyDocument(
            cards=cards,
            auto_number_config=source.auto_number_config,
        )
        return self._make_result(doc, source, Platform.BLUESKY)

    def _linkedin_to_bluesky(self, source: ParsedLinkedInDocument) -> ConversionResult:
        text = source.post.text
        media = list(source.post.media)

        doc = ParsedBlueSkyDocument(
            cards=[BlueSkyCardContent(text=text, media=media)],
        )
        return self._make_result(doc, source, Platform.BLUESKY)

    def _instagram_to_bluesky(
        self, source: ParsedInstagramDocument
    ) -> ConversionResult:
        text = source.post.caption
        media = list(source.post.media)

        doc = ParsedBlueSkyDocument(
            cards=[BlueSkyCardContent(text=text, media=media)],
        )
        return self._make_result(doc, source, Platform.BLUESKY)

    def _tiktok_to_bluesky(self, source: ParsedTikTokDocument) -> ConversionResult:
        text = source.post.title
        media = list(source.post.media)

        doc = ParsedBlueSkyDocument(
            cards=[BlueSkyCardContent(text=text, media=media)],
        )
        return self._make_result(doc, source, Platform.BLUESKY)

    # ==================== Helpers ====================

    def _infer_instagram_content_type(
        self,
        media: list[str],
        warnings: list[ConversionWarning],
    ) -> ContentType:
        """Infer Instagram content type from media count."""
        if len(media) > 1:
            content_type = ContentType.CAROUSEL
        elif len(media) == 1:
            content_type = ContentType.POST
        else:
            content_type = ContentType.POST

        warnings.append(
            ConversionWarning(
                code="content_type_inferred",
                field="content_type",
                message=f"Instagram content_type set to {content_type.value} based on media count",
            )
        )
        return content_type


# ==================== Builder Registry ====================

_BUILDERS: dict[Platform, Any] = {
    Platform.TWITTER: TwitterCRDTBuilder(),
    Platform.LINKEDIN: LinkedInCRDTBuilder(),
    Platform.INSTAGRAM: InstagramCRDTBuilder(),
    Platform.TIKTOK: TikTokCRDTBuilder(),
    Platform.BLUESKY: BlueskyCRDTBuilder(),
}

# Global converter instance
_converter = DocumentConverter()


def convert_document(
    source: ParsedDocument,
    target_platform: Platform | str,
) -> ConversionResult:
    """Convert a parsed document to another platform format."""
    return _converter.convert(source, target_platform)


def convert_crdt_binary(
    source_platform: str,
    binary_data: bytes,
    target_platform: str,
    fragment_name: str = "default",
) -> ConversionResult:
    """Parse binary CRDT data then convert to target platform."""
    parsed = parse_crdt_binary(source_platform, binary_data, fragment_name)
    return _converter.convert(parsed, target_platform)


def convert_crdt_to_crdt(
    source_platform: str,
    target_platform: str,
    binary_data: bytes,
    fragment_name: str = "default",
) -> tuple[Doc[Any], ConversionResult]:
    """Full pipeline: binary → parse → convert → build new CRDT Doc."""
    parsed = parse_crdt_binary(source_platform, binary_data, fragment_name)
    result = _converter.convert(parsed, target_platform)

    target = Platform(target_platform.lower())
    builder = _BUILDERS[target]
    doc = builder.build_document(result.document, fragment_name)

    return doc, result
