"""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.

Uses a strategy-based approach with 4 conversion strategies based on
platform shape (thread vs single):
  - thread → thread: map cards 1:1
  - thread → single: merge text, pool & dedup media
  - single → thread: wrap in single card
  - single → single: direct mapping
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any, cast

from pycrdt import Doc

from marqetive.utils.crdt.builders.base import BaseCRDTBuilder
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.threads import ThreadsCRDTBuilder
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_SHAPES,
    AutoNumberConfig,
    BlueSkyCardContent,
    ContentType,
    ConversionExtras,
    ConversionResult,
    ConversionWarning,
    InstagramPostContent,
    LinkedInPostContent,
    OriginalCard,
    OriginalCardStructure,
    ParsedBlueSkyDocument,
    ParsedDocument,
    ParsedInstagramDocument,
    ParsedLinkedInDocument,
    ParsedThreadsDocument,
    ParsedTikTokDocument,
    ParsedTwitterDocument,
    Platform,
    PlatformShape,
    PostVisibility,
    ThreadsCardContent,
    TikTokPostContent,
    TwitterCardContent,
)
from marqetive.utils.crdt.parser import parse_crdt_binary

_PLATFORM_MEDIA_LIMITS: dict[Platform, int] = {
    Platform.TWITTER: 4,
    Platform.BLUESKY: 4,
    Platform.LINKEDIN: 9,
    Platform.INSTAGRAM: 10,
    Platform.TIKTOK: 1,
    Platform.THREADS: 20,
}

_LOCATION_MUSIC_PLATFORMS: frozenset[Platform] = frozenset(
    {Platform.INSTAGRAM, Platform.TIKTOK}
)

_THREAD_CARD_TYPES: dict[
    Platform, type[TwitterCardContent | BlueSkyCardContent | ThreadsCardContent]
] = {
    Platform.TWITTER: TwitterCardContent,
    Platform.BLUESKY: BlueSkyCardContent,
    Platform.THREADS: ThreadsCardContent,
}


def _trim_media(
    media: list[str],
    target: Platform,
    warnings: list[ConversionWarning],
) -> list[str]:
    """Trim media list to platform limit, appending a warning if trimmed."""
    limit = _PLATFORM_MEDIA_LIMITS.get(target)
    if limit is not None and len(media) > limit:
        warnings.append(
            ConversionWarning(
                code="media_trimmed",
                field="media",
                message=(
                    f"Media trimmed from {len(media)} to {limit} "
                    f"(max for {target.value})"
                ),
            )
        )
        return media[:limit]
    return media


def _infer_instagram_content_type(
    media: list[str],
    warnings: list[ConversionWarning],
) -> ContentType:
    """Infer Instagram content type from media count."""
    if len(media) > 1:
        content_type = ContentType.CAROUSEL
        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
    return ContentType.POST


# ==================== Intermediate Representation ====================


@dataclass(frozen=True)
class _CardRepr:
    """Intermediate representation of a single card from a thread source."""

    id: str | None = None
    text: str = ""
    media: list[str] = field(default_factory=list)


@dataclass(frozen=True)
class _IntermediateRepr:
    """Unified decomposed representation of any source document."""

    text: str = ""
    media: list[str] = field(default_factory=list)
    cards: list[_CardRepr] | None = None
    auto_number_config: AutoNumberConfig | None = None
    location: str | None = None
    music: str | None = None
    share_to_feed: bool = True
    location_name: str | None = None
    cover_url: str | None = None
    comment_text: str | None = None


# ==================== Converter ====================


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,
            )

        if source_platform not in PLATFORM_SHAPES:
            raise CRDTConversionError(
                f"Unsupported source platform: {source_platform.value}",
                source_platform=source_platform.value,
                target_platform=target_platform.value,
            )
        if target_platform not in PLATFORM_SHAPES:
            raise CRDTConversionError(
                f"Unsupported target platform: {target_platform.value}",
                source_platform=source_platform.value,
                target_platform=target_platform.value,
            )

        source_shape = PLATFORM_SHAPES[source_platform]
        target_shape = PLATFORM_SHAPES[target_platform]
        ir = self._decompose(source)

        match (source_shape, target_shape):
            case (PlatformShape.THREAD, PlatformShape.THREAD):
                doc, extra_warnings, original_cards = self._assemble_thread_to_thread(
                    ir, target_platform
                )
            case (PlatformShape.THREAD, PlatformShape.SINGLE):
                doc, extra_warnings, original_cards = self._assemble_thread_to_single(
                    ir, source_platform, target_platform
                )
            case (PlatformShape.SINGLE, PlatformShape.THREAD):
                doc, extra_warnings, original_cards = self._assemble_single_to_thread(
                    ir, target_platform
                )
            case (PlatformShape.SINGLE, PlatformShape.SINGLE):
                doc, extra_warnings, original_cards = self._assemble_single_to_single(
                    ir, source_platform, target_platform
                )
            case _:
                raise CRDTConversionError(
                    f"No conversion path from {source_platform.value} "
                    f"to {target_platform.value}",
                    source_platform=source_platform.value,
                    target_platform=target_platform.value,
                )

        return self._make_result(
            doc, source, target_platform, extra_warnings, original_cards
        )

    # ==================== Source Detection ====================

    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
            case ParsedThreadsDocument():
                return Platform.THREADS
            case _:
                raise CRDTConversionError("Unknown document type")

    # ==================== Decompose ====================

    def _decompose(self, source: ParsedDocument) -> _IntermediateRepr:
        """Extract a unified intermediate representation from any source."""
        match source:
            case ParsedTwitterDocument(cards=cards, auto_number_config=anc):
                card_reprs = [
                    _CardRepr(
                        id=c.id,
                        text=c.text,
                        media=list(c.media),
                    )
                    for c in cards
                ]
                text = "\n\n".join(c.text for c in cards if c.text)
                # dict.fromkeys preserves insertion order while deduplicating
                media = list(dict.fromkeys(m for c in cards for m in c.media))
                return _IntermediateRepr(
                    text=text,
                    media=media,
                    cards=card_reprs,
                    auto_number_config=anc,
                )

            case (
                ParsedBlueSkyDocument(cards=cards, auto_number_config=anc)
                | ParsedThreadsDocument(cards=cards, auto_number_config=anc)
            ):
                card_reprs = [
                    _CardRepr(
                        id=c.id,
                        text=c.text,
                        media=list(c.media),
                    )
                    for c in cards
                ]
                text = "\n\n".join(c.text for c in cards if c.text)
                # dict.fromkeys preserves insertion order while deduplicating
                media = list(dict.fromkeys(m for c in cards for m in c.media))
                return _IntermediateRepr(
                    text=text,
                    media=media,
                    cards=card_reprs,
                    auto_number_config=anc,
                )

            case ParsedLinkedInDocument(post=post):
                return _IntermediateRepr(text=post.text, media=list(post.media))

            case ParsedInstagramDocument(post=post, comment=comment):
                return _IntermediateRepr(
                    text=post.caption,
                    media=list(post.media),
                    location=post.location,
                    music=post.music,
                    share_to_feed=post.share_to_feed,
                    location_name=post.location_name,
                    cover_url=post.cover_url,
                    comment_text=comment.text if comment is not None else None,
                )

            case ParsedTikTokDocument(post=post):
                return _IntermediateRepr(
                    text=post.title,
                    media=list(post.media),
                    location=post.location,
                    music=post.music,
                )

            case _:
                raise CRDTConversionError("Unknown document type")

    # ==================== Assembly Strategies ====================

    def _assemble_thread_to_thread(
        self,
        ir: _IntermediateRepr,
        target: Platform,
    ) -> tuple[
        ParsedDocument,
        list[ConversionWarning],
        OriginalCardStructure | None,
    ]:
        """Map cards 1:1 between thread platforms."""
        warnings: list[ConversionWarning] = []
        source_cards = ir.cards or []
        card_type = _THREAD_CARD_TYPES[target]

        built_cards = []
        for c in source_cards:
            media = _trim_media(list(c.media), target, warnings)
            built_cards.append(
                card_type(
                    id=c.id,
                    text=c.text,
                    media=media,
                )
            )

        match target:
            case Platform.TWITTER:
                doc: ParsedDocument = ParsedTwitterDocument(
                    cards=cast(list[TwitterCardContent], built_cards),
                    auto_number_config=ir.auto_number_config,
                )
            case Platform.BLUESKY:
                doc = ParsedBlueSkyDocument(
                    cards=cast(list[BlueSkyCardContent], built_cards),
                    auto_number_config=ir.auto_number_config,
                )
            case Platform.THREADS:
                doc = ParsedThreadsDocument(
                    cards=cast(list[ThreadsCardContent], built_cards),
                    auto_number_config=ir.auto_number_config,
                )
            case _:
                raise CRDTConversionError(f"Unsupported thread target: {target.value}")

        return doc, warnings, None

    def _assemble_thread_to_single(
        self,
        ir: _IntermediateRepr,
        source: Platform,
        target: Platform,
    ) -> tuple[
        ParsedDocument,
        list[ConversionWarning],
        OriginalCardStructure | None,
    ]:
        """Merge thread cards into a single post."""
        warnings: list[ConversionWarning] = []
        cards = ir.cards or []

        if len(cards) > 1:
            warnings.append(
                ConversionWarning(
                    code="thread_merged",
                    field="cards",
                    message=(
                        f"Merged {len(cards)} "
                        f"{source.value.title()} cards into "
                        f"single {target.value.title()} post"
                    ),
                )
            )

        media = _trim_media(list(ir.media), target, warnings)

        doc = self._build_single_doc(target, ir.text, media, warnings)

        original_cards = (
            OriginalCardStructure(
                cards=[
                    OriginalCard(
                        id=c.id,
                        text=c.text,
                        media=list(c.media),
                    )
                    for c in cards
                ],
                auto_number_config=ir.auto_number_config,
            )
            if len(cards) > 1
            else None
        )

        return doc, warnings, original_cards

    def _assemble_single_to_thread(
        self,
        ir: _IntermediateRepr,
        target: Platform,
    ) -> tuple[
        ParsedDocument,
        list[ConversionWarning],
        OriginalCardStructure | None,
    ]:
        """Wrap single post content in a single card."""
        warnings: list[ConversionWarning] = []
        card_type = _THREAD_CARD_TYPES[target]
        media = _trim_media(list(ir.media), target, warnings)

        card = card_type(text=ir.text, media=media)

        match target:
            case Platform.TWITTER:
                doc: ParsedDocument = ParsedTwitterDocument(
                    cards=cast(list[TwitterCardContent], [card]),
                )
            case Platform.BLUESKY:
                doc = ParsedBlueSkyDocument(
                    cards=cast(list[BlueSkyCardContent], [card]),
                )
            case Platform.THREADS:
                doc = ParsedThreadsDocument(
                    cards=cast(list[ThreadsCardContent], [card]),
                )
            case _:
                raise CRDTConversionError(f"Unsupported thread target: {target.value}")

        return doc, warnings, None

    def _assemble_single_to_single(
        self,
        ir: _IntermediateRepr,
        source_platform: Platform,
        target: Platform,
    ) -> tuple[
        ParsedDocument,
        list[ConversionWarning],
        OriginalCardStructure | None,
    ]:
        """Direct mapping between single-post platforms."""
        warnings: list[ConversionWarning] = []
        media = _trim_media(list(ir.media), target, warnings)

        carry_loc_music = (
            source_platform in _LOCATION_MUSIC_PLATFORMS
            and target in _LOCATION_MUSIC_PLATFORMS
        )
        location = ir.location if carry_loc_music else None
        music = ir.music if carry_loc_music else None

        # Carry Instagram-specific fields only between Instagram sources/targets
        share_to_feed = ir.share_to_feed if target == Platform.INSTAGRAM else True
        location_name = ir.location_name if carry_loc_music else None
        cover_url = ir.cover_url if target == Platform.INSTAGRAM else None

        doc = self._build_single_doc(
            target,
            ir.text,
            media,
            warnings,
            location,
            music,
            share_to_feed,
            location_name,
            cover_url,
        )
        return doc, warnings, None

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

    def _build_single_doc(
        self,
        target: Platform,
        text: str,
        media: list[str],
        warnings: list[ConversionWarning],
        location: str | None = None,
        music: str | None = None,
        share_to_feed: bool = True,
        location_name: str | None = None,
        cover_url: str | None = None,
    ) -> ParsedDocument:
        """Construct a single-post document for the target platform."""
        match target:
            case Platform.LINKEDIN:
                return ParsedLinkedInDocument(
                    post=LinkedInPostContent(text=text, media=media),
                )
            case Platform.INSTAGRAM:
                content_type = _infer_instagram_content_type(media, warnings)
                return ParsedInstagramDocument(
                    post=InstagramPostContent(
                        caption=text,
                        media=media,
                        content_type=content_type,
                        location=location,
                        music=music,
                        share_to_feed=share_to_feed,
                        location_name=location_name,
                        cover_url=cover_url,
                    ),
                )
            case Platform.TIKTOK:
                return ParsedTikTokDocument(
                    post=TikTokPostContent(
                        title=text,
                        media=media,
                        location=location,
                        music=music,
                    ),
                )
            case _:
                raise CRDTConversionError(f"Unsupported single target: {target.value}")

    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)
        target_shape = PLATFORM_SHAPES[target]

        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_shape != PlatformShape.THREAD:
                    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:
                    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 (
                ParsedBlueSkyDocument(cards=cards, auto_number_config=anc)
                | ParsedThreadsDocument(cards=cards, auto_number_config=anc)
            ):
                if target_shape != PlatformShape.THREAD:
                    if anc is not None:
                        _drop("auto_number_config", anc.model_dump())
                    if len(cards) > 1:
                        _drop("thread_card_count", len(cards))
                for i, card in enumerate(cards):
                    if card.og is not None:
                        _drop(f"card_{i}_og", card.og)

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

            case ParsedInstagramDocument(post=post, comment=comment):
                if target != Platform.INSTAGRAM:
                    if (
                        post.location is not None
                        and target not in _LOCATION_MUSIC_PLATFORMS
                    ):
                        _drop("location", post.location)
                    if (
                        post.music is not None
                        and target not in _LOCATION_MUSIC_PLATFORMS
                    ):
                        _drop("music", post.music)
                    if post.content_type != ContentType.POST:
                        _drop("content_type", post.content_type.value)
                    if not post.share_to_feed:
                        _drop("share_to_feed", post.share_to_feed)
                    if (
                        post.location_name is not None
                        and target not in _LOCATION_MUSIC_PLATFORMS
                    ):
                        _drop("location_name", post.location_name)
                    if post.cover_url is not None:
                        _drop("cover_url", post.cover_url)
                    if comment is not None:
                        _drop("comment", comment.model_dump())

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

        return extras, warnings

    def _make_result(
        self,
        document: ParsedDocument,
        source: ParsedDocument,
        target: Platform,
        extra_warnings: list[ConversionWarning] | None = None,
        original_cards: OriginalCardStructure | None = None,
    ) -> ConversionResult:
        """Build a ConversionResult with extras and validation warnings."""
        source_platform = self._detect_source_platform(source)

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

        all_warnings = (extra_warnings or []) + extras_warnings

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


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

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

assert set(_BUILDERS) == set(Platform), (
    f"Missing builders: {set(Platform) - set(_BUILDERS)}"
)

# 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
