"""BlueSky API client implementation using XRPC endpoints."""

import asyncio
import contextlib
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime, timedelta
from typing import Any

import httpx
from pydantic import HttpUrl

from marqetive.core.base import ProgressCallback, SocialMediaPlatform
from marqetive.core.exceptions import (
    MediaUploadError,
    PlatformAuthError,
    PlatformError,
    PostNotFoundError,
    ThreadCancelledException,
    ValidationError,
)
from marqetive.core.models import (
    AuthCredentials,
    Comment,
    CommentStatus,
    MediaAttachment,
    MediaType,
    Post,
    PostCreateRequest,
    PostStatus,
    PostUpdateRequest,
    ProgressStatus,
)
from marqetive.platforms.bluesky.exceptions import map_bluesky_error
from marqetive.platforms.bluesky.media import (
    BlueSkyMediaManager,
    resolve_bluesky_image_mime_type,
)
from marqetive.utils.retry import retry_async_func

BLUESKY_API_BASE = "https://bsky.social/xrpc"
BLUESKY_POST_COLLECTION = "app.bsky.feed.post"
BLUESKY_REPLY_REFERENCE_MAX_ATTEMPTS = 3
BLUESKY_REPLY_REFERENCE_BASE_DELAY_SECONDS = 0.25


class BlueSkyClient(SocialMediaPlatform):
    """BlueSky API client using direct XRPC calls."""

    def __init__(
        self,
        credentials: AuthCredentials,
        timeout: float = 30.0,
        progress_callback: ProgressCallback | None = None,
    ) -> None:
        super().__init__(
            platform_name="bluesky",
            credentials=credentials,
            base_url=BLUESKY_API_BASE,
            timeout=timeout,
            progress_callback=progress_callback,
        )
        self.repo_identifier = self._get_repo_identifier()
        self._media_manager: BlueSkyMediaManager | None = None

    async def __aenter__(self) -> "BlueSkyClient":
        await super().__aenter__()
        self._media_manager = BlueSkyMediaManager(
            access_token=self.credentials.access_token,
            token_type=self.credentials.token_type,
            timeout=self.timeout,
        )
        await self._media_manager.__aenter__()
        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        if self._media_manager:
            await self._media_manager.__aexit__(exc_type, exc_val, exc_tb)
            self._media_manager = None
        await super().__aexit__(exc_type, exc_val, exc_tb)

    def _get_repo_identifier(self) -> str:
        """Resolve BlueSky repo identifier (DID or handle)."""
        did = self.credentials.additional_data.get("did")
        if isinstance(did, str) and did:
            return did
        if self.credentials.user_id:
            return self.credentials.user_id
        if self.credentials.username:
            return self.credentials.username

        raise PlatformAuthError(
            "BlueSky requires user_id (DID/handle), username, or 'did' in additional_data",
            platform=self.platform_name,
        )

    async def authenticate(self) -> AuthCredentials:
        """Validate current BlueSky credentials."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")
        if await self.is_authenticated():
            return self.credentials

        raise PlatformAuthError(
            "Invalid or expired credentials. Please re-authenticate via BlueSky.",
            platform=self.platform_name,
        )

    async def refresh_token(self) -> AuthCredentials:
        """Refresh BlueSky access token using refresh JWT."""
        if not self.credentials.refresh_token:
            raise PlatformAuthError(
                "No refresh token available",
                platform=self.platform_name,
            )

        try:

            async def _refresh_session() -> dict[str, Any]:
                async with httpx.AsyncClient(timeout=self.timeout) as client:
                    response = await client.post(
                        f"{BLUESKY_API_BASE}/com.atproto.server.refreshSession",
                        headers={
                            "Authorization": f"Bearer {self.credentials.refresh_token}"
                        },
                    )
                    response.raise_for_status()
                    return response.json()

            data = await retry_async_func(_refresh_session)

        except httpx.HTTPStatusError as e:
            response_data: dict[str, Any]
            try:
                response_data = e.response.json()
            except Exception:
                response_data = {}
            mapped = map_bluesky_error(
                e.response.status_code,
                response_data=response_data,
                error_message=e.response.text,
            )
            if isinstance(mapped, Exception):
                raise mapped from e
            raise PlatformAuthError(
                f"Failed to refresh BlueSky token: {e.response.text}",
                platform=self.platform_name,
                status_code=e.response.status_code,
            ) from e

        except httpx.HTTPError as e:
            raise PlatformAuthError(
                f"Network error refreshing token: {e}",
                platform=self.platform_name,
            ) from e

        self.credentials.access_token = data["accessJwt"]
        if "refreshJwt" in data:
            self.credentials.refresh_token = data["refreshJwt"]
        if "did" in data:
            self.credentials.user_id = data["did"]
            self.repo_identifier = data["did"]
            self.credentials.additional_data["did"] = data["did"]
        if "handle" in data:
            self.credentials.username = data["handle"]
        expires_at_raw = data.get("expiresAt")
        expires_in = data.get("expiresIn")

        if isinstance(expires_at_raw, str):
            with contextlib.suppress(ValueError):
                self.credentials.expires_at = datetime.fromisoformat(
                    expires_at_raw.replace("Z", "+00:00")
                )
        elif isinstance(expires_at_raw, int | float):
            ts = float(expires_at_raw)
            if ts > 1_000_000_000_000:
                ts /= 1000.0
            self.credentials.expires_at = datetime.fromtimestamp(ts, tz=UTC)
        elif isinstance(expires_in, int | float):
            self.credentials.expires_at = datetime.now(UTC) + timedelta(
                seconds=float(expires_in)
            )
        else:
            self.credentials.expires_at = datetime.now(UTC) + timedelta(
                hours=1, minutes=50
            )

        if self._media_manager:
            self._media_manager.update_credentials(
                access_token=self.credentials.access_token,
                token_type=self.credentials.token_type,
            )

        return self.credentials

    async def is_authenticated(self) -> bool:
        """Check whether the current access token is valid."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        try:
            response = await retry_async_func(
                self.api_client.get,
                "/com.atproto.server.getSession",
            )
            return bool(response.data.get("did"))
        except (httpx.HTTPError, TypeError):
            return False

    def _validate_create_post_request(self, request: PostCreateRequest) -> None:
        """Validate BlueSky post creation request."""
        if not request.content:
            raise ValidationError(
                "BlueSky posts require content",
                platform=self.platform_name,
                field="content",
            )

        if request.media_ids:
            raise ValidationError(
                "BlueSky media_ids are not supported in BlueSkyClient. Use media_urls.",
                platform=self.platform_name,
                field="media_ids",
            )

    def _get_image_mime_type(self, media_url: str) -> str:
        """Get MIME type for an image URL after extension validation."""
        return resolve_bluesky_image_mime_type(
            media_url,
            platform=self.platform_name,
        )

    def _normalize_post_uri(self, post_id: str) -> str:
        """Normalize post identifier to an AT URI."""
        if post_id.startswith("at://"):
            return post_id

        if post_id.startswith("https://bsky.app/profile/"):
            parts = post_id.replace("https://bsky.app/profile/", "").split("/")
            if len(parts) >= 3 and parts[1] == "post":
                profile = parts[0]
                rkey = parts[2]
                return f"at://{profile}/{BLUESKY_POST_COLLECTION}/{rkey}"

        return f"at://{self.repo_identifier}/{BLUESKY_POST_COLLECTION}/{post_id}"

    def _extract_rkey_from_uri(self, uri: str) -> str:
        """Extract record key from an AT URI."""
        parts = uri.split("/")
        if len(parts) < 5:
            raise ValidationError(
                f"Invalid BlueSky post URI: {uri}",
                platform=self.platform_name,
                field="post_id",
            )
        return parts[-1]

    def _build_post_url(self, uri: str) -> HttpUrl | None:
        """Convert AT URI to public bsky.app URL."""
        parts = uri.split("/")
        if len(parts) < 5:
            return None
        profile = parts[2]
        rkey = parts[4]
        return HttpUrl(f"https://bsky.app/profile/{profile}/post/{rkey}")

    async def _resolve_reply_reference(
        self, post_uri: str
    ) -> dict[str, dict[str, str]]:
        """Resolve reply root/parent reference for replying to a post."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        post: dict[str, Any] | None = None
        for attempt in range(BLUESKY_REPLY_REFERENCE_MAX_ATTEMPTS):
            response = await retry_async_func(
                self.api_client.get,
                "/app.bsky.feed.getPosts",
                params={"uris": post_uri},
            )
            posts = response.data.get("posts", [])
            if posts:
                post = posts[0]
                break

            if attempt < BLUESKY_REPLY_REFERENCE_MAX_ATTEMPTS - 1:
                delay_seconds = BLUESKY_REPLY_REFERENCE_BASE_DELAY_SECONDS * (
                    attempt + 1
                )
                await asyncio.sleep(delay_seconds)

        if not post:
            raise PostNotFoundError(post_uri, self.platform_name)

        parent_uri = post.get("uri")
        parent_cid = post.get("cid")
        if not parent_uri or not parent_cid:
            raise PlatformError(
                "BlueSky reply target is missing uri/cid",
                platform=self.platform_name,
            )

        root = post.get("record", {}).get("reply", {}).get("root", {})
        root_uri = root.get("uri", parent_uri)
        root_cid = root.get("cid", parent_cid)

        return {
            "root": {"uri": root_uri, "cid": root_cid},
            "parent": {"uri": parent_uri, "cid": parent_cid},
        }

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

        self._validate_create_post_request(request)

        langs: list[str] | None = None
        if (
            isinstance(request.additional_data.get("langs"), list)
            and request.additional_data["langs"]
        ):
            langs = [
                str(lang)
                for lang in request.additional_data["langs"]
                if isinstance(lang, str)
            ]

        reply_to_post_id = request.reply_to_post_id or request.additional_data.get(
            "reply_to_uri"
        )
        record: dict[str, Any] = {
            "$type": BLUESKY_POST_COLLECTION,
            "text": request.content,
            "createdAt": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
        }
        if langs:
            record["langs"] = langs

        if reply_to_post_id:
            if request.media_urls:
                raise MediaUploadError(
                    "BlueSky reply posts with media are not supported in this flow",
                    platform=self.platform_name,
                )

            reply_uri = self._normalize_post_uri(str(reply_to_post_id))
            record["reply"] = await self._resolve_reply_reference(reply_uri)

        if request.media_urls:
            alt_texts = request.additional_data.get("alt_texts", [])
            images: list[dict[str, Any]] = []
            for idx, media_url in enumerate(request.media_urls):
                self._get_image_mime_type(media_url)
                alt_text = (
                    alt_texts[idx]
                    if isinstance(alt_texts, list)
                    and idx < len(alt_texts)
                    and isinstance(alt_texts[idx], str)
                    else ""
                )
                upload_result = await self._media_manager.upload_image(media_url)
                images.append({"alt": alt_text, "image": upload_result.blob})
            record["embed"] = {"$type": "app.bsky.embed.images", "images": images}

        payload = {
            "repo": self.repo_identifier,
            "collection": BLUESKY_POST_COLLECTION,
            "record": record,
        }

        try:
            response = await retry_async_func(
                self.api_client.post,
                "/com.atproto.repo.createRecord",
                data=payload,
            )
        except httpx.HTTPStatusError as e:
            response_data: dict[str, Any]
            try:
                response_data = e.response.json()
            except Exception:
                response_data = {}
            raise map_bluesky_error(
                e.response.status_code,
                response_data=response_data,
                error_message=e.response.text,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Failed to create BlueSky post: {e}",
                platform=self.platform_name,
            ) from e

        uri = response.data["uri"]

        return Post(
            post_id=uri,
            platform=self.platform_name,
            content=request.content,
            status=PostStatus.PUBLISHED,
            created_at=datetime.now(),
            author_id=self.repo_identifier,
            url=self._build_post_url(uri),
            raw_data=response.data,
        )

    async def create_thread(
        self,
        posts: list[PostCreateRequest],
        cancellation_check: Callable[[], Awaitable[bool]] | None = None,
    ) -> list[Post]:
        """Create a BlueSky thread by chaining replies to the thread head."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

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

            self._validate_create_post_request(post_request)

            final_request = post_request
            if reply_to_id is not None:
                final_request = post_request.model_copy(
                    update={"reply_to_post_id": reply_to_id}
                )

            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_posts[-1].post_id,
            )

        return created_posts

    def _parse_post(self, post_data: dict[str, Any]) -> Post:
        """Parse BlueSky feed post payload to Post model."""
        record = post_data.get("record", {})
        created_at_raw = record.get("createdAt")

        created_at = datetime.now()
        if isinstance(created_at_raw, str):
            try:
                created_at = datetime.fromisoformat(
                    created_at_raw.replace("Z", "+00:00")
                )
            except ValueError:
                created_at = datetime.now()

        uri = post_data.get("uri", "")

        return Post(
            post_id=uri,
            platform=self.platform_name,
            content=record.get("text"),
            status=PostStatus.PUBLISHED,
            url=self._build_post_url(uri),
            created_at=created_at,
            author_id=post_data.get("author", {}).get("did"),
            likes_count=int(post_data.get("likeCount", 0) or 0),
            comments_count=int(post_data.get("replyCount", 0) or 0),
            shares_count=int(post_data.get("repostCount", 0) or 0),
            views_count=0,  # BlueSky API doesn't expose view counts
            raw_data=post_data,
        )

    async def _get_post_via_xrpc(self, post_uri: str) -> Post:
        """Retrieve a post through direct XRPC."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        try:
            response = await retry_async_func(
                self.api_client.get,
                "/app.bsky.feed.getPosts",
                params={"uris": post_uri},
            )
            posts = response.data.get("posts", [])
            if posts and isinstance(posts[0], dict):
                return self._parse_post(posts[0])
            raise PostNotFoundError(post_uri, self.platform_name, status_code=404)
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise PostNotFoundError(
                    post_uri, self.platform_name, status_code=404
                ) from e
            response_data: dict[str, Any]
            try:
                response_data = e.response.json()
            except Exception:
                response_data = {}
            raise map_bluesky_error(
                e.response.status_code,
                response_data=response_data,
                error_message=e.response.text,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Failed to fetch BlueSky post via XRPC: {e}",
                platform=self.platform_name,
            ) from e

    async def _delete_post_via_xrpc(self, post_uri: str) -> bool:
        """Delete a post through XRPC."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        parts = post_uri.split("/")
        if len(parts) < 5:
            raise ValidationError(
                f"Invalid BlueSky post URI: {post_uri}",
                platform=self.platform_name,
                field="post_id",
            )

        repo = parts[2]
        collection = parts[3]
        rkey = parts[4]
        try:
            await retry_async_func(
                self.api_client.post,
                "/com.atproto.repo.deleteRecord",
                data={
                    "repo": repo,
                    "collection": collection,
                    "rkey": rkey,
                },
            )
            return True
        except httpx.HTTPStatusError as e:
            response_data: dict[str, Any]
            try:
                response_data = e.response.json()
            except Exception:
                response_data = {}
            mapped = map_bluesky_error(
                e.response.status_code,
                response_data=response_data,
                error_message=e.response.text,
            )
            if isinstance(mapped, Exception):
                raise mapped from e
            raise PlatformError(
                f"Failed to delete BlueSky post via XRPC: {e.response.text}",
                platform=self.platform_name,
                status_code=e.response.status_code,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Failed to delete BlueSky post via XRPC: {e}",
                platform=self.platform_name,
            ) from e
        except Exception as e:
            raise PlatformError(
                f"Failed to delete BlueSky post via XRPC: {e}",
                platform=self.platform_name,
            ) from e

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

        post_uri = self._normalize_post_uri(post_id)
        return await self._get_post_via_xrpc(post_uri)

    async def delete_post(self, post_id: str) -> bool:
        """Delete a BlueSky post by URI or record key."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        post_uri = self._normalize_post_uri(post_id)
        return await self._delete_post_via_xrpc(post_uri)

    async def update_post(
        self,
        post_id: str,  # noqa: ARG002
        request: PostUpdateRequest,  # noqa: ARG002
    ) -> Post:
        """Update a BlueSky post.

        Note: BlueSky does not provide an edit flow in this client implementation.
        """
        raise PlatformError(
            "BlueSky post editing is not supported by this client.",
            platform=self.platform_name,
        )

    def _parse_comment(self, post_data: dict[str, Any], root_post_id: str) -> Comment:
        """Parse BlueSky reply post into Comment model."""
        record = post_data.get("record", {})
        created_at_raw = record.get("createdAt")

        created_at = datetime.now()
        if isinstance(created_at_raw, str):
            try:
                created_at = datetime.fromisoformat(
                    created_at_raw.replace("Z", "+00:00")
                )
            except ValueError:
                created_at = datetime.now()

        return Comment(
            comment_id=post_data.get("uri", ""),
            post_id=root_post_id,
            platform=self.platform_name,
            content=record.get("text", ""),
            author_id=post_data.get("author", {}).get("did", ""),
            author_username=post_data.get("author", {}).get("handle"),
            created_at=created_at,
            likes_count=int(post_data.get("likeCount", 0) or 0),
            replies_count=int(post_data.get("replyCount", 0) or 0),
            status=CommentStatus.VISIBLE,
            raw_data=post_data,
        )

    async def get_comments(
        self,
        post_id: str,
        limit: int = 50,
        offset: int = 0,
    ) -> list[Comment]:
        """Retrieve comments (replies) for a BlueSky post."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        post_uri = self._normalize_post_uri(post_id)

        try:
            response = await retry_async_func(
                self.api_client.get,
                "/app.bsky.feed.getPostThread",
                params={"uri": post_uri, "depth": 2},
            )
            thread = response.data.get("thread", {})
            replies = thread.get("replies", []) if isinstance(thread, dict) else []

            comments: list[Comment] = []
            for reply in replies:
                if not isinstance(reply, dict):
                    continue
                reply_post = reply.get("post")
                if isinstance(reply_post, dict):
                    comments.append(self._parse_comment(reply_post, post_uri))

            return comments[offset : offset + limit]
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise PostNotFoundError(
                    post_id, self.platform_name, status_code=404
                ) from e
            response_data: dict[str, Any]
            try:
                response_data = e.response.json()
            except Exception:
                response_data = {}
            raise map_bluesky_error(
                e.response.status_code,
                response_data=response_data,
                error_message=e.response.text,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Failed to fetch BlueSky comments: {e}",
                platform=self.platform_name,
            ) from e

    async def create_comment(self, post_id: str, content: str) -> Comment:
        """Create a reply comment on a BlueSky post."""
        if not self.api_client:
            raise RuntimeError("Client must be used as async context manager")

        if not content:
            raise ValidationError(
                "Comment content cannot be empty",
                platform=self.platform_name,
                field="content",
            )

        post_uri = self._normalize_post_uri(post_id)
        reply_ref = await self._resolve_reply_reference(post_uri)

        payload = {
            "repo": self.repo_identifier,
            "collection": BLUESKY_POST_COLLECTION,
            "record": {
                "$type": BLUESKY_POST_COLLECTION,
                "text": content,
                "createdAt": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
                "reply": reply_ref,
            },
        }

        try:
            response = await retry_async_func(
                self.api_client.post,
                "/com.atproto.repo.createRecord",
                data=payload,
            )
        except httpx.HTTPStatusError as e:
            response_data: dict[str, Any]
            try:
                response_data = e.response.json()
            except Exception:
                response_data = {}
            raise map_bluesky_error(
                e.response.status_code,
                response_data=response_data,
                error_message=e.response.text,
            ) from e
        except httpx.HTTPError as e:
            raise PlatformError(
                f"Failed to create BlueSky comment: {e}",
                platform=self.platform_name,
            ) from e

        return Comment(
            comment_id=response.data["uri"],
            post_id=post_uri,
            platform=self.platform_name,
            content=content,
            author_id=self.repo_identifier,
            created_at=datetime.now(),
            status=CommentStatus.VISIBLE,
            raw_data=response.data,
        )

    async def delete_comment(self, comment_id: str) -> bool:
        """Delete a BlueSky comment (reply post)."""
        return await self.delete_post(comment_id)

    async def upload_media(
        self,
        media_url: str,
        media_type: str,
        alt_text: str | None = None,
    ) -> MediaAttachment:
        """Upload image media to BlueSky and return blob metadata."""
        if not self._media_manager:
            raise RuntimeError("Client must be used as async context manager")

        if media_type.lower() != "image":
            raise MediaUploadError(
                "BlueSkyClient currently supports image uploads only",
                platform=self.platform_name,
            )

        try:
            result = await self._media_manager.upload_image(media_url)
        except MediaUploadError:
            raise
        except Exception as e:
            raise MediaUploadError(
                f"Failed to upload media to BlueSky: {e}",
                platform=self.platform_name,
            ) from e

        return MediaAttachment(
            media_id=result.media_id,
            media_type=MediaType.IMAGE,
            url=HttpUrl(media_url),
            size_bytes=result.size_bytes,
            alt_text=alt_text,
        )
