"""Threads API client implementation.

This is an initial Threads platform setup built around the Threads Graph API.
"""

import asyncio
import logging
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
from typing import Any, cast
from urllib.parse import urlparse

import httpx
from pydantic import HttpUrl

from marqetive.core.base import ProgressCallback, SocialMediaPlatform
from marqetive.core.exceptions import (
    PlatformAuthError,
    PlatformError,
    PostNotFoundError,
    ThreadCancelledException,
    ValidationError,
)
from marqetive.core.models import (
    AuthCredentials,
    Comment,
    MediaAttachment,
    MediaType,
    Post,
    PostCreateRequest,
    PostStatus,
    PostUpdateRequest,
    ProgressStatus,
)
from marqetive.utils.retry import BackoffConfig, retry_async

logger = logging.getLogger(__name__)

THREADS_API_BASE = "https://graph.threads.net/v1.0"

# Container status polling config
CONTAINER_STATUS_MAX_POLLS = 12  # 12 × 5s = 60s max wait per container
CONTAINER_STATUS_POLL_INTERVAL = 5.0  # seconds between polls
CONTAINER_STATUSES_READY = {"FINISHED", "PUBLISHED"}
CONTAINER_STATUSES_FAILED = {"ERROR", "EXPIRED"}

# Retry config for transient errors
THREADS_TRANSIENT_CODES = {2, 1, 341}
THREADS_MAX_RETRIES = 3
THREADS_RETRY_BASE_DELAY = 2.0  # seconds — backoff: 2s, 4s, 8s
THREADS_RETRY_CONFIG = BackoffConfig(
    max_attempts=THREADS_MAX_RETRIES,
    base_delay=THREADS_RETRY_BASE_DELAY,
    max_delay=THREADS_RETRY_BASE_DELAY * (2 ** (THREADS_MAX_RETRIES - 1)),
    jitter=False,
)


class ThreadsClient(SocialMediaPlatform):
    """Threads Graph API client.

    Supports single posts, carousel posts (multiple media), and threaded
    multi-post sequences via create_thread.
    """

    def __init__(
        self,
        credentials: AuthCredentials,
        timeout: float = 30.0,
        progress_callback: ProgressCallback | None = None,
    ) -> None:
        super().__init__(
            platform_name="threads",
            credentials=credentials,
            base_url=THREADS_API_BASE,
            timeout=timeout,
            progress_callback=progress_callback,
        )
        self.threads_user_id = self._resolve_threads_user_id()

    # Auth

    def _resolve_threads_user_id(self) -> str:
        """Resolve Threads user ID from credentials."""
        additional_user_id = self.credentials.additional_data.get("threads_user_id")
        if isinstance(additional_user_id, str) and additional_user_id:
            return additional_user_id
        if self.credentials.user_id:
            return self.credentials.user_id
        raise PlatformAuthError(
            "Threads requires user_id or 'threads_user_id' in additional_data",
            platform=self.platform_name,
        )

    async def authenticate(self) -> AuthCredentials:
        """Validate Threads credentials."""
        if await self.is_authenticated():
            return self.credentials
        raise PlatformAuthError(
            "Invalid or expired credentials. Please re-authenticate via Threads OAuth.",
            platform=self.platform_name,
        )

    async def refresh_token(self) -> AuthCredentials:
        """Refresh Threads access token (not yet implemented)."""
        raise PlatformAuthError(
            "Threads token refresh is not implemented yet. Reconnect the account.",
            platform=self.platform_name,
            requires_reconnection=True,
        )

    async def is_authenticated(self) -> bool:
        """Check whether current Threads credentials are valid."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")
        try:
            await self.api_client.get(
                "/me",
                params={
                    "fields": "id,username",
                    "access_token": self.credentials.access_token,
                },
            )
            return True
        except httpx.HTTPError:
            return False

    # Validation

    def _normalize_media_request(self, request: PostCreateRequest) -> PostCreateRequest:
        """Normalize media inputs for Threads.

        Threads supports media URLs or Threads media container IDs. For scheduled
        posts, container IDs can expire, so prefer URLs when both are present.
        Also allow media URLs passed via additional_data for compatibility.
        """
        additional = request.additional_data or {}
        media_urls = list(request.media_urls)
        media_ids = list(request.media_ids)

        if not media_urls:
            extra_urls: list[str] = []
            raw_urls = additional.get("media_urls")
            if isinstance(raw_urls, str):
                extra_urls = [raw_urls]
            elif isinstance(raw_urls, list):
                extra_urls = [str(url) for url in raw_urls if url]
            else:
                raw_url = additional.get("media_url")
                if raw_url:
                    extra_urls = [str(raw_url)]
            media_urls = extra_urls

        if media_urls and media_ids:
            logger.warning(
                "Threads request provided both media_urls and media_ids; "
                "preferring media_urls to avoid stale container IDs."
            )
            media_ids = []

        if media_urls != request.media_urls or media_ids != request.media_ids:
            return request.model_copy(
                update={"media_urls": media_urls, "media_ids": media_ids}
            )

        return request

    def _validate_create_post_request(self, request: PostCreateRequest) -> None:
        """Validate Threads post creation request."""
        pass

    @staticmethod
    def _is_media_not_found_error(error: httpx.HTTPStatusError) -> bool:
        try:
            body = error.response.json()
        except Exception:
            return False
        api_error = body.get("error", {})
        return bool(
            api_error.get("error_subcode") == 4279009
            or api_error.get("error_user_title") == "Media Not Found"
            or api_error.get("error_user_msg") == "The media cannot be found."
        )

    # helpers
    def _is_transient_threads_error(self, error: Exception) -> bool:
        if isinstance(error, httpx.HTTPStatusError):
            if error.response.status_code >= 500:
                return True
            try:
                body = error.response.json()
                api_error = body.get("error", {})
                return bool(
                    api_error.get("is_transient")
                    or api_error.get("code") in THREADS_TRANSIENT_CODES
                )
            except Exception:
                return False
        return isinstance(error, httpx.HTTPError)

    def _log_retry_warning(
        self,
        error: Exception,
        operation: str,
        attempt: int,
        max_attempts: int,
        delay: float,
    ) -> None:
        if isinstance(error, httpx.HTTPStatusError):
            logger.warning(
                "Threads %s transient error (attempt %d/%d), retrying in %.1fs: %s",
                operation,
                attempt + 1,
                max_attempts,
                delay,
                error,
            )
        else:
            logger.warning(
                "Threads %s network error (attempt %d/%d), retrying in %.1fs: %s",
                operation,
                attempt + 1,
                max_attempts,
                delay,
                str(error),
            )

    def _threads_retry(
        self, operation: str
    ) -> Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]:
        def on_retry(
            error: Exception,
            attempt: int,
            max_attempts: int,
            delay: float,
        ) -> None:
            self._log_retry_warning(
                error=error,
                operation=operation,
                attempt=attempt,
                max_attempts=max_attempts,
                delay=delay,
            )

        return retry_async(
            config=THREADS_RETRY_CONFIG,
            error_classifier=self._is_transient_threads_error,
            on_retry=on_retry,
            log_attempts=False,
            log_errors=False,
        )

    async def _post_with_retry(
        self,
        endpoint: str,
        data: dict[str, Any],
        operation: str = "request",
    ) -> Any:
        """POST to Threads API with exponential backoff on transient errors."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        @self._threads_retry(operation)
        async def _post() -> Any:
            if not self.api_client:
                raise RuntimeError("Client must be used as async context manager")

            return await self.api_client.post(endpoint, data=data)

        return await _post()

    async def _get_with_retry(
        self,
        endpoint: str,
        params: dict[str, Any] | None = None,
        operation: str = "request",
    ) -> Any:
        """GET from Threads API with exponential backoff on transient errors."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        @self._threads_retry(operation)
        async def _get() -> Any:
            if not self.api_client:
                raise RuntimeError("Client must be used as async context manager")

            return await self.api_client.get(endpoint, params=params)

        return await _get()

    async def _wait_for_container_ready(
        self,
        container_id: str,
        operation: str,
    ) -> dict[str, Any]:
        """Poll a Threads container until it is ready to publish."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        params = {
            "fields": "id,status,error_message",
            "access_token": self.credentials.access_token,
        }
        last_status: str | None = None
        last_error: str | None = None

        for attempt in range(1, CONTAINER_STATUS_MAX_POLLS + 1):
            response = await self._get_with_retry(
                f"/{container_id}",
                params=params,
                operation=f"{operation}_status",
            )
            data = response.data

            status = data.get("status")
            last_status = status if isinstance(status, str) else None
            last_error = data.get("error_message")

            status_normalized = last_status.upper() if last_status else None
            if status_normalized in CONTAINER_STATUSES_READY:
                return data
            if status_normalized in CONTAINER_STATUSES_FAILED:
                raise PlatformError(
                    (
                        "Threads container processing failed: "
                        f"id={container_id}, status={status_normalized}, "
                        f"error={last_error or 'unknown'}"
                    ),
                    platform=self.platform_name,
                )

            if attempt < CONTAINER_STATUS_MAX_POLLS:
                await asyncio.sleep(CONTAINER_STATUS_POLL_INTERVAL)

        raise PlatformError(
            (
                "Threads container did not become ready before timeout: "
                f"id={container_id}, status={last_status or 'unknown'}, "
                f"error={last_error or 'none'}"
            ),
            platform=self.platform_name,
        )

    # Post creation

    async def create_post(self, request: PostCreateRequest) -> Post:
        """Create and publish a Threads post (single or carousel)."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        request = self._normalize_media_request(request)
        self._validate_create_post_request(request)

        # Route multi-media requests to carousel flow before building payload
        if len(request.media_urls) >= 2:
            try:
                return await self._create_carousel_post(request)
            except httpx.HTTPStatusError as e:
                raise PlatformError(
                    f"Failed to create Threads post: {e.response.text}",
                    platform=self.platform_name,
                    status_code=e.response.status_code,
                ) from e
            except httpx.HTTPError as e:
                raise PlatformError(
                    f"Network error creating Threads post: {e}",
                    platform=self.platform_name,
                ) from e

        # Single-post payload
        payload: dict[str, Any] = {}
        if request.content:
            payload["text"] = request.content

        # Thread chaining — reply to previous post if set
        reply_to_post_id = request.reply_to_post_id or request.additional_data.get(
            "reply_to_post_id"
        )
        if reply_to_post_id:
            payload["reply_to_id"] = reply_to_post_id

        try:
            waited_for_container = False
            if request.media_ids:
                if len(request.media_ids) > 1:
                    raise ValidationError(
                        "Threads supports only one uploaded media ID per post",
                        platform=self.platform_name,
                        field="media_ids",
                    )
                # Pre-created container — publish directly
                if request.content:
                    raise ValidationError(
                        "Threads media_ids represent prepared containers. "
                        "Text must be provided during media container creation.",
                        platform=self.platform_name,
                        field="content",
                    )
                creation_id = request.media_ids[0]
                container_data: dict[str, Any] = {"id": creation_id}
                await self._wait_for_container_ready(
                    creation_id,
                    operation="container",
                )
                waited_for_container = True

            else:
                # Build container from URL or text
                if request.media_urls:
                    media_url = str(request.media_urls[0])
                    media_type = self._detect_media_type(media_url)
                    payload["media_type"] = media_type
                    if media_type == "IMAGE":
                        payload["image_url"] = media_url
                    else:
                        payload["video_url"] = media_url
                    if request.additional_data.get("alt_text"):
                        payload["alt_text"] = request.additional_data["alt_text"]
                else:
                    payload["media_type"] = "TEXT"

                container_response = await self._post_with_retry(
                    f"/{self.threads_user_id}/threads",
                    data=payload,
                    operation="container",
                )
                creation_id = container_response.data.get("id")
                if not creation_id:
                    raise PlatformError(
                        "Threads API did not return a creation container id",
                        platform=self.platform_name,
                    )
                container_data = container_response.data
                if request.media_urls:
                    await self._wait_for_container_ready(
                        creation_id,
                        operation="container",
                    )
                    waited_for_container = True

            publish_response = None
            for attempt in range(2):
                try:
                    publish_response = await self._post_with_retry(
                        f"/{self.threads_user_id}/threads_publish",
                        data={"creation_id": creation_id},
                        operation="publish",
                    )
                    break
                except httpx.HTTPStatusError as e:
                    if (
                        self._is_media_not_found_error(e)
                        and not waited_for_container
                        and attempt == 0
                    ):
                        # Container may not be ready yet, wait then retry publish once
                        await self._wait_for_container_ready(
                            creation_id,
                            operation="container",
                        )
                        waited_for_container = True
                        continue
                    raise

            post_id = publish_response.data.get("id")  # type: ignore
            if not post_id:
                raise PlatformError(
                    "Threads API did not return a published post id",
                    platform=self.platform_name,
                )

            return Post(
                post_id=post_id,
                platform=self.platform_name,
                content=request.content,
                status=PostStatus.PUBLISHED,
                created_at=datetime.now(UTC),
                raw_data={
                    "container": container_data,
                    "publish": publish_response.data,  # type: ignore
                },
            )

        except httpx.HTTPStatusError as e:
            if self._is_media_not_found_error(e):
                raise PlatformError(
                    (
                        "Threads media container not found or expired. "
                        "If you are scheduling posts, avoid pre-uploaded media IDs "
                        "and pass media_urls at publish time or call upload_media "
                        "immediately before publishing."
                    ),
                    platform=self.platform_name,
                    status_code=e.response.status_code,
                ) from e
            raise PlatformError(
                f"Failed to create Threads post: {e.response.text}",
                platform=self.platform_name,
                status_code=e.response.status_code,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Network error creating Threads post: {e}",
                platform=self.platform_name,
            ) from e

    async def _create_carousel_post(self, request: PostCreateRequest) -> Post:
        """Create a multi-image/video carousel post (3-step Threads API flow).

        Step 1: Create one item container per media URL, poll each until FINISHED.
        Step 2: Create carousel wrapper container referencing all child IDs,
                poll until FINISHED.
        Step 3: Publish the carousel container.
        """
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        # Step 1 — item containers
        child_ids: list[str] = []
        for media_url in request.media_urls:
            url_str = str(media_url)
            media_type = self._detect_media_type(url_str)

            item_payload: dict[str, Any] = {
                "is_carousel_item": "true",
                "media_type": media_type,
            }
            if media_type == "IMAGE":
                item_payload["image_url"] = url_str
            else:
                item_payload["video_url"] = url_str

            alt_text = request.additional_data.get("alt_text")
            if alt_text:
                item_payload["alt_text"] = alt_text

            item_response = await self._post_with_retry(
                f"/{self.threads_user_id}/threads",
                data=item_payload,
                operation="carousel_item",
            )
            child_id = item_response.data.get("id")
            if not child_id:
                raise PlatformError(
                    "Threads API did not return a carousel item container id",
                    platform=self.platform_name,
                )

            # Poll until this item is ready before proceeding to next
            await self._wait_for_container_ready(
                child_id,
                operation="carousel_item",
            )
            child_ids.append(child_id)

        # Step 2 — carousel wrapper container
        carousel_payload: dict[str, Any] = {
            "media_type": "CAROUSEL",
            "children": ",".join(child_ids),
        }

        # Thread chaining
        reply_to_id = request.reply_to_post_id or request.additional_data.get(
            "reply_to_post_id"
        )
        if reply_to_id:
            carousel_payload["reply_to_id"] = reply_to_id

        if request.content:
            carousel_payload["text"] = request.content

        carousel_response = await self._post_with_retry(
            f"/{self.threads_user_id}/threads",
            data=carousel_payload,
            operation="carousel_container",
        )
        creation_id = carousel_response.data.get("id")
        if not creation_id:
            raise PlatformError(
                "Threads API did not return a carousel container id",
                platform=self.platform_name,
            )

        # Poll carousel container before publishing
        await self._wait_for_container_ready(
            creation_id,
            operation="carousel_container",
        )

        # Step 3 — publish
        publish_response = await self._post_with_retry(
            f"/{self.threads_user_id}/threads_publish",
            data={"creation_id": creation_id},
            operation="carousel_publish",
        )
        post_id = publish_response.data.get("id")
        if not post_id:
            raise PlatformError(
                "Threads API did not return a published carousel post id",
                platform=self.platform_name,
            )

        return Post(
            post_id=post_id,
            platform=self.platform_name,
            content=request.content,
            status=PostStatus.PUBLISHED,
            created_at=datetime.now(UTC),
            raw_data={
                "carousel_children": child_ids,
                "carousel_container": carousel_response.data,
                "publish": publish_response.data,
            },
        )

    async def create_thread(
        self,
        posts: list[PostCreateRequest],
        cancellation_check: Callable[[], Awaitable[bool]] | None = None,
    ) -> list[Post]:
        """Create a Threads thread (multiple linked posts).

        Posts are published sequentially, each replying to the previous one.
        """
        if not posts:
            raise ValidationError(
                "At least one post is required for thread creation",
                platform=self.platform_name,
            )

        created_posts: list[Post] = []
        reply_to_id: str | None = None

        for idx, post_request in enumerate(posts):
            if cancellation_check is not None and await cancellation_check():
                raise ThreadCancelledException(
                    f"Thread cancelled after {len(created_posts)} of {len(posts)} posts",
                    platform=self.platform_name,
                    posted_tweets=created_posts,
                )

            # Inject reply_to_post_id so each post chains to the previous
            additional_data = dict(post_request.additional_data)
            if reply_to_id is not None:
                additional_data["reply_to_post_id"] = reply_to_id

            update: dict[str, Any] = {"additional_data": additional_data}
            if reply_to_id is not None:
                update["reply_to_post_id"] = reply_to_id

            final_request = post_request.model_copy(update=update)
            created_post = await self.create_post(final_request)
            created_posts.append(created_post)
            reply_to_id = created_post.post_id

            await self._emit_progress(
                operation="create_thread",
                status=ProgressStatus.PROCESSING,
                progress=idx + 1,
                total=len(posts),
                message=f"Post {idx + 1}/{len(posts)} created",
                entity_id=created_post.post_id,
            )

        return created_posts

    # Read / delete

    async def get_post(self, post_id: str) -> Post:
        """Retrieve a Threads post by ID."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        try:
            response = await self.api_client.get(
                f"/{post_id}",
                params={
                    "fields": (
                        "id,text,permalink,timestamp,media_type,username,"
                        "like_count,reply_count,repost_count"
                    ),
                    "access_token": self.credentials.access_token,
                },
            )

            data = response.data
            created_at = self._parse_timestamp(data.get("timestamp"))
            repost_count = int(data.get("repost_count", 0) or 0)

            return Post(
                post_id=str(data.get("id", post_id)),
                platform=self.platform_name,
                content=data.get("text"),
                status=PostStatus.PUBLISHED,
                url=data.get("permalink"),
                created_at=created_at,
                author_id=data.get("username") or self.credentials.user_id,
                likes_count=int(data.get("like_count", 0) or 0),
                comments_count=int(data.get("reply_count", 0) or 0),
                shares_count=repost_count,
                raw_data=data,
            )

        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise PostNotFoundError(
                    post_id=post_id,
                    platform=self.platform_name,
                    status_code=404,
                ) from e
            raise PlatformError(
                f"Failed to retrieve Threads post: {e.response.text}",
                platform=self.platform_name,
                status_code=e.response.status_code,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Network error retrieving Threads post: {e}",
                platform=self.platform_name,
            ) from e

    async def update_post(self, post_id: str, request: PostUpdateRequest) -> Post:
        """Update a Threads post (not supported)."""
        del post_id, request
        raise PlatformError(
            "Threads post editing is not supported in this client yet",
            platform=self.platform_name,
        )

    async def delete_post(self, post_id: str) -> bool:
        """Delete a Threads post/media object."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        try:
            response = await self.api_client.delete(
                f"{THREADS_API_BASE}/{post_id}",
                params={"access_token": self.credentials.access_token},
            )
            data = response.data
            if isinstance(data, dict) and "success" in data:
                return bool(data["success"])
            return True
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise PostNotFoundError(
                    post_id=post_id,
                    platform=self.platform_name,
                    status_code=404,
                ) from e
            raise PlatformError(
                f"Failed to delete Threads post: {e.response.text}",
                platform=self.platform_name,
                status_code=e.response.status_code,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Network error deleting Threads post: {e}",
                platform=self.platform_name,
            ) from e

    # Comments (not implemented)

    async def get_comments(
        self,
        post_id: str,
        limit: int = 50,
        offset: int = 0,
    ) -> list[Comment]:
        """Retrieve comments (not yet implemented)."""
        del post_id, limit, offset
        raise PlatformError(
            "Threads comment retrieval is not supported in this client yet",
            platform=self.platform_name,
        )

    async def create_comment(self, post_id: str, content: str) -> Comment:
        """Create a comment (not yet implemented)."""
        del post_id, content
        raise PlatformError(
            "Threads comment creation is not supported in this client yet",
            platform=self.platform_name,
        )

    async def delete_comment(self, comment_id: str) -> bool:
        """Delete a comment (not yet implemented)."""
        del comment_id
        raise PlatformError(
            "Threads comment deletion is not supported in this client yet",
            platform=self.platform_name,
        )

    # repost helper
    async def repost(self, post_id: str):
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        try:
            await self.api_client.delete(
                f"{THREADS_API_BASE}/{post_id}/repost",
                params={"access_token": self.credentials.access_token},
            )
            return True
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise PostNotFoundError(
                    post_id=post_id,
                    platform=self.platform_name,
                    status_code=404,
                ) from e
            raise PlatformError(
                f"Failed to respost Threads post: {e.response.text}",
                platform=self.platform_name,
                status_code=e.response.status_code,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Network error respost Threads post: {e}",
                platform=self.platform_name,
            ) from e

    # ------------------------------------------------------------------
    # Media upload
    # ------------------------------------------------------------------

    async def upload_media(
        self,
        media_url: str,
        media_type: str,
        alt_text: str | None = None,
    ) -> MediaAttachment:
        """Create a Threads media container from a public media URL."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        normalized_type = media_type.strip().lower()
        threads_media_type = "IMAGE"
        model_media_type = MediaType.IMAGE
        media_field = "image_url"
        if normalized_type in {"image", "photo"}:
            threads_media_type = "IMAGE"
            model_media_type = MediaType.IMAGE
            media_field = "image_url"
        elif normalized_type in {"video", "reel"}:
            threads_media_type = "VIDEO"
            model_media_type = MediaType.VIDEO
            media_field = "video_url"

        payload: dict[str, Any] = {
            "media_type": threads_media_type,
            media_field: media_url,
        }
        if alt_text:
            payload["alt_text"] = alt_text

        try:
            response = await self.api_client.post(
                f"/{self.threads_user_id}/threads",
                data=payload,
            )
        except httpx.HTTPStatusError as e:
            raise PlatformError(
                f"Failed to upload Threads media: {e.response.text}",
                platform=self.platform_name,
                status_code=e.response.status_code,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Network error uploading Threads media: {e}",
                platform=self.platform_name,
            ) from e

        container_id = response.data.get("id")
        if not container_id:
            raise PlatformError(
                "Threads API did not return a media container id",
                platform=self.platform_name,
            )

        return MediaAttachment(
            media_id=container_id,
            media_type=model_media_type,
            url=cast(HttpUrl, media_url),
            alt_text=alt_text,
        )

    # Static helpers

    @staticmethod
    def _detect_media_type(media_url: str) -> str:
        """Detect Threads media type from URL extension."""
        parsed_path = urlparse(media_url).path.lower()
        video_exts = (".mp4", ".mov", ".m4v", ".webm")
        if parsed_path.endswith(video_exts):
            return "VIDEO"
        return "IMAGE"

    @staticmethod
    def _parse_timestamp(raw_timestamp: Any) -> datetime:
        """Parse timestamp from Threads API responses."""
        if isinstance(raw_timestamp, str):
            try:
                return datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00"))
            except ValueError:
                pass
        return datetime.now(UTC)
