"""BlueSky media upload manager for image and video blobs."""

import asyncio
import contextlib
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

import httpx

from marqetive.core.exceptions import MediaUploadError
from marqetive.platforms.bluesky.exceptions import map_bluesky_error
from marqetive.utils.file_handlers import download_file, read_file_bytes
from marqetive.utils.retry import retry_async_func

BLUESKY_API_BASE = "https://bsky.social/xrpc"
BLUESKY_VIDEO_SERVICE_BASE = "https://video.bsky.app/xrpc"
BLUESKY_VIDEO_JOB_POLL_DELAY_SECONDS = 4
BLUESKY_VIDEO_JOB_POLL_MAX_DELAY_SECONDS = 15
BLUESKY_VIDEO_JOB_MAX_ATTEMPTS = 120
BLUESKY_VIDEO_UPLOAD_TIMEOUT_SECONDS = 120
BLUESKY_ALLOWED_IMAGE_EXTENSIONS = frozenset({".jpg", ".jpeg", ".png", ".webp", ".gif"})
BLUESKY_ALLOWED_VIDEO_EXTENSIONS = frozenset({".mp4"})


def resolve_bluesky_image_mime_type(
    media_url: str,
    *,
    platform: str = "bluesky",
) -> str:
    """Resolve image MIME type based on URL/path extension."""
    parsed = urlparse(media_url)
    extension = Path(parsed.path).suffix.lower()
    if extension not in BLUESKY_ALLOWED_IMAGE_EXTENSIONS:
        raise MediaUploadError(
            f"Unsupported BlueSky image format '{extension}'. "
            f"Allowed: {', '.join(sorted(BLUESKY_ALLOWED_IMAGE_EXTENSIONS))}",
            platform=platform,
        )

    return {
        ".jpg": "image/jpeg",
        ".jpeg": "image/jpeg",
        ".png": "image/png",
        ".webp": "image/webp",
        ".gif": "image/gif",
    }[extension]


def resolve_bluesky_video_mime_type(
    media_url: str,
    *,
    platform: str = "bluesky",
) -> str:
    """Resolve video MIME type based on URL/path extension."""
    parsed = urlparse(media_url)
    extension = Path(parsed.path).suffix.lower()

    if extension not in BLUESKY_ALLOWED_VIDEO_EXTENSIONS:
        allowed = ", ".join(sorted(BLUESKY_ALLOWED_VIDEO_EXTENSIONS))
        raise MediaUploadError(
            f"Unsupported BlueSky video format '{extension}'. Allowed: {allowed}",
            platform=platform,
        )

    return {
        ".mp4": "video/mp4",
    }[extension]


@dataclass
class BlueSkyMediaUploadResult:
    """Result for a BlueSky media upload."""

    media_id: str
    size_bytes: int
    blob: dict[str, Any]


class BlueSkyMediaManager:
    """Manager for BlueSky media uploads."""

    def __init__(
        self,
        *,
        access_token: str,
        token_type: str = "Bearer",
        timeout: float = 30.0,
        pds_base_url: str = BLUESKY_API_BASE,
    ) -> None:
        self.access_token = access_token
        self.token_type = token_type
        self.timeout = timeout
        self.pds_base_url = pds_base_url
        self._client = httpx.AsyncClient(timeout=httpx.Timeout(timeout))

    async def __aenter__(self) -> "BlueSkyMediaManager":
        return self

    async def __aexit__(self, *args: object) -> None:
        await self._client.aclose()

    def update_credentials(
        self, *, access_token: str, token_type: str | None = None
    ) -> None:
        """Update auth credentials used for subsequent uploads."""
        self.access_token = access_token
        if token_type is not None:
            self.token_type = token_type

    def update_pds_base_url(self, pds_base_url: str) -> None:
        """Update the PDS base URL used for service auth."""
        self.pds_base_url = pds_base_url

    def get_image_mime_type(self, media_url: str) -> str:
        """Resolve image MIME type based on URL/path extension."""
        return resolve_bluesky_image_mime_type(media_url, platform="bluesky")

    def get_video_mime_type(self, media_url: str) -> str:
        """Resolve video MIME type based on URL/path extension."""
        return resolve_bluesky_video_mime_type(media_url, platform="bluesky")

    async def upload_image(self, media_url: str) -> BlueSkyMediaUploadResult:
        """Upload an image to BlueSky and return blob metadata."""
        mime_type = self.get_image_mime_type(media_url)
        media_path = media_url
        temp_path: str | None = None

        try:
            try:
                if media_url.startswith(("http://", "https://")):
                    media_path = await download_file(
                        media_url,
                        timeout=self.timeout,
                    )
                    temp_path = media_path
                elif not Path(media_url).exists():
                    raise FileNotFoundError(f"Media file not found: {media_url}")

                content = await read_file_bytes(media_path)
            except httpx.HTTPError as e:
                raise MediaUploadError(
                    f"Failed to download media from URL: {e}",
                    platform="bluesky",
                ) from e
            except OSError as e:
                raise MediaUploadError(
                    f"Failed to read media file: {e}",
                    platform="bluesky",
                ) from e

            headers = {
                "Authorization": f"{self.token_type} {self.access_token}",
                "Content-Type": mime_type,
            }

            try:

                async def _upload_blob() -> dict[str, Any]:
                    response = await self._client.post(
                        f"{BLUESKY_API_BASE}/com.atproto.repo.uploadBlob",
                        content=content,
                        headers=headers,
                    )
                    response.raise_for_status()
                    return response.json()

                data = await retry_async_func(_upload_blob)
            except httpx.HTTPStatusError as e:
                response_data: dict
                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 MediaUploadError(
                    f"Failed to upload media to BlueSky: {e}",
                    platform="bluesky",
                ) from e

            blob = data.get("blob")
            if not blob:
                raise MediaUploadError(
                    "BlueSky uploadBlob response missing blob field",
                    platform="bluesky",
                )

            blob_ref = blob.get("ref", {})
            media_id = blob_ref.get("$link") or blob.get("cid") or "unknown_blob"

            return BlueSkyMediaUploadResult(
                media_id=str(media_id),
                size_bytes=len(content),
                blob=blob,
            )
        finally:
            if temp_path:
                with contextlib.suppress(OSError):
                    Path(temp_path).unlink()

    async def upload_video(
        self,
        media_url: str,
        *,
        did: str,
        upload_name: str | None = None,
    ) -> BlueSkyMediaUploadResult:
        """Upload a video to BlueSky and return blob metadata."""
        mime_type = self.get_video_mime_type(media_url)
        media_path = media_url
        temp_path: str | None = None
        resolved_name = upload_name or ""

        try:
            try:
                if media_url.startswith(("http://", "https://")):
                    parsed_url = urlparse(media_url)
                    resolved_name = resolved_name or Path(parsed_url.path).name
                    media_path = await download_file(
                        media_url,
                        timeout=self.timeout,
                    )
                    temp_path = media_path
                else:
                    resolved_name = resolved_name or Path(media_url).name

                content = await read_file_bytes(media_path)
            except (httpx.HTTPError, FileNotFoundError) as e:
                raise MediaUploadError(
                    f"Failed to download media file: {e}",
                    platform="bluesky",
                ) from e
            except OSError as e:
                raise MediaUploadError(
                    f"Failed to read media file: {e}",
                    platform="bluesky",
                ) from e

            if not did.startswith("did:"):
                raise MediaUploadError(
                    "BlueSky video uploads require a DID.",
                    platform="bluesky",
                )

            try:
                pds_host = urlparse(self.pds_base_url).hostname or "bsky.social"
                auth_payload = {
                    "aud": f"did:web:{pds_host}",
                    "lxm": "com.atproto.repo.uploadBlob",
                    "exp": int(datetime.now(UTC).timestamp()) + 60 * 30,
                }

                async def _get_service_auth() -> dict[str, Any]:
                    response = await self._client.get(
                        f"{self.pds_base_url}/com.atproto.server.getServiceAuth",
                        params=auth_payload,
                        headers={
                            "Authorization": f"{self.token_type} {self.access_token}",
                        },
                    )
                    response.raise_for_status()
                    return response.json()

                auth_data = await retry_async_func(_get_service_auth)
            except httpx.HTTPStatusError as e:
                response_data: dict
                try:
                    response_data = e.response.json()
                except Exception:
                    response_data = {}
                raise map_bluesky_error(
                    e.response.status_code,
                    response_data=response_data,
                ) from e
            except httpx.HTTPError as e:
                raise MediaUploadError(
                    f"Failed to get BlueSky service auth token: {e}",
                    platform="bluesky",
                ) from e

            token = auth_data.get("token")
            if not token:
                raise MediaUploadError(
                    "BlueSky service auth token missing in response",
                    platform="bluesky",
                )

            headers = {
                "Authorization": f"Bearer {token}",
                "Content-Type": mime_type,
                "Content-Length": str(len(content)),
            }
            upload_url = f"{BLUESKY_VIDEO_SERVICE_BASE}/app.bsky.video.uploadVideo"
            if resolved_name and not Path(resolved_name).suffix:
                resolved_name = f"{resolved_name}.mp4"

            async def _upload_video() -> dict[str, Any]:
                async with httpx.AsyncClient(
                    timeout=httpx.Timeout(BLUESKY_VIDEO_UPLOAD_TIMEOUT_SECONDS),
                    follow_redirects=True,
                ) as video_client:
                    response = await video_client.post(
                        upload_url,
                        params={"did": did, "name": resolved_name or "video.mp4"},
                        content=content,
                        headers=headers,
                    )
                    response.raise_for_status()
                    return response.json()

            try:
                job_status = await retry_async_func(_upload_video)
            except httpx.HTTPStatusError as e:
                response_data = {}
                response_text = e.response.text
                with contextlib.suppress(Exception):
                    response_data = e.response.json()
                if e.response.status_code == 409 and response_data.get("jobId"):
                    job_status = response_data
                else:
                    raise map_bluesky_error(
                        e.response.status_code,
                        response_data=response_data,
                        error_message=response_text,
                    ) from e
            except httpx.HTTPError as e:
                raise MediaUploadError(
                    f"Failed to upload video to BlueSky: {e!r}",
                    platform="bluesky",
                ) from e

            job_id = job_status.get("jobId") or job_status.get("jobStatus", {}).get(
                "jobId"
            )
            if not job_id:
                raise MediaUploadError(
                    "BlueSky video upload response missing jobId",
                    platform="bluesky",
                )

            blob = job_status.get("blob")
            if blob:
                blob_ref = blob.get("ref", {})
                media_id = blob_ref.get("$link") or blob.get("cid") or "unknown_blob"
                return BlueSkyMediaUploadResult(
                    media_id=str(media_id),
                    size_bytes=len(content),
                    blob=blob,
                )

            status_url = f"{BLUESKY_VIDEO_SERVICE_BASE}/app.bsky.video.getJobStatus"
            delay_seconds = BLUESKY_VIDEO_JOB_POLL_DELAY_SECONDS
            for _ in range(BLUESKY_VIDEO_JOB_MAX_ATTEMPTS):
                status_response = await self._client.get(
                    status_url,
                    params={"jobId": job_id},
                    headers={"Authorization": f"Bearer {token}"},
                )
                if status_response.status_code >= 400:
                    status_data = {}
                    with contextlib.suppress(Exception):
                        status_data = status_response.json()
                    status = status_data.get("jobStatus", status_data)
                    blob = status.get("blob") if isinstance(status, dict) else None
                    if blob:
                        blob_ref = blob.get("ref", {})
                        media_id = (
                            blob_ref.get("$link") or blob.get("cid") or "unknown_blob"
                        )
                        return BlueSkyMediaUploadResult(
                            media_id=str(media_id),
                            size_bytes=len(content),
                            blob=blob,
                        )
                    raise map_bluesky_error(
                        status_response.status_code,
                        response_data=(
                            status_data if isinstance(status_data, dict) else None
                        ),
                    )
                status_data = status_response.json()
                status = status_data.get("jobStatus", status_data)
                blob = status.get("blob")
                if blob:
                    blob_ref = blob.get("ref", {})
                    media_id = (
                        blob_ref.get("$link") or blob.get("cid") or "unknown_blob"
                    )
                    return BlueSkyMediaUploadResult(
                        media_id=str(media_id),
                        size_bytes=len(content),
                        blob=blob,
                    )

                state = status.get("state") or ""
                error = status.get("error")
                message = status.get("message")
                if error or (isinstance(state, str) and "FAILED" in state.upper()):
                    detail = message or error or "video processing failed"
                    raise MediaUploadError(
                        f"BlueSky video processing failed: {detail}",
                        platform="bluesky",
                    )

                await asyncio.sleep(delay_seconds)
                delay_seconds = min(
                    delay_seconds * 1.5, BLUESKY_VIDEO_JOB_POLL_MAX_DELAY_SECONDS
                )

            raise MediaUploadError(
                "BlueSky video processing timed out",
                platform="bluesky",
            )
        finally:
            if temp_path:
                with contextlib.suppress(OSError):
                    Path(temp_path).unlink()
