"""Statuses mixin: post, delete, reblog, undo_reblog, undo_favourite, post_media, search."""

import asyncio
import json
import logging
import random
import uuid
from datetime import datetime
from typing import IO
from typing import Any

from httpx import HTTPError

from minimal_activitypub import SearchType
from minimal_activitypub import Visibility
from minimal_activitypub import __display_name__
from minimal_activitypub.constants import INSTANCE_TYPE_PLEROMA
from minimal_activitypub.constants import UTC
from minimal_activitypub.exceptions import NetworkError
from minimal_activitypub.types import ActivityPubClass
from minimal_activitypub.utils import _check_exception

logger = logging.getLogger(__display_name__)

Status = dict[str, Any]


class _StatusesMixin:
    """Mixin providing status, media, and search methods."""

    async def delete_status(self: ActivityPubClass, status: str | Status, delete_only_one: bool = False) -> Status:
        """Delete a status.

        :param status: The ID of the status you want to delete or a dict containing the status details
        :param delete_only_one: Set this to True if this delete is the only one. Set it to False if this delete is part
            of a batch of deletes. With this set to False it will introduce a random wait of up to 3 seconds.
            This hopefully should help with instance saturation.
            Defaults to False

        :returns: Status that has just been deleted
        """
        if not delete_only_one:
            # add random delay of up to 3 seconds in case we are deleting many
            # statuses in a batch
            sleep_for = random.SystemRandom().random() * 3
            logger.debug(
                "ActivityPub.delete_status - status_id = %s - sleep_for = %s",
                status if isinstance(status, str) else status["id"],
                sleep_for,
            )
            await asyncio.sleep(delay=sleep_for)

        if isinstance(status, str):
            status_id = status
        elif isinstance(status, dict):
            status_id = status["id"]
            if self.instance_type == INSTANCE_TYPE_PLEROMA and status["reblogged"]:
                undo_reblog_response = await self.undo_reblog(status=status)
                return undo_reblog_response
            if self.instance_type == INSTANCE_TYPE_PLEROMA and status["favourited"]:
                undo_favourite_response = await self.undo_favourite(status=status)
                return undo_favourite_response

        await self._pre_call_checks()

        url = f"{self.instance}/api/v1/statuses/{status_id}"
        try:
            response = await self.client.delete(url=url, headers=self.headers, timeout=self.timeout)
            self._update_ratelimit(response.headers)
            await _check_exception(response=response)
            self._parse_next_prev(links=response.headers.get("Link"))
            result: Status = response.json()
        except HTTPError as error:
            raise NetworkError from error

        logger.debug(
            "ActivityPub.delete_status -> result:\n%s",
            json.dumps(result, indent=4),
        )
        return result

    async def undo_reblog(self: ActivityPubClass, status: str | dict[Any, Any]) -> Status:
        """Remove a reblog.

        :param status: The ID of the status you want to delete or a dict containing the status details

        :returns: The response from the server.
        """
        logger.debug(
            "ActivityPub.undo_reblog(status=%s",
            status,
        )

        if isinstance(status, str):
            status_id = status
        elif isinstance(status, dict):
            status_id = status["reblog"]["id"]

        await self._pre_call_checks()
        url = f"{self.instance}/api/v1/statuses/{status_id}/unreblog"
        try:
            response = await self.client.post(url=url, headers=self.headers, timeout=self.timeout)
            self._update_ratelimit(response.headers)
            await _check_exception(response=response)
            self._parse_next_prev(links=response.headers.get("Link"))
            result: Status = response.json()
        except HTTPError as error:
            raise NetworkError from error

        logger.debug(
            "ActivityPub.undo_reblog -> result:\n%s",
            json.dumps(result, indent=4),
        )
        return result

    async def reblog(self: ActivityPubClass, status_id: str) -> Status:
        """Reblog a status.

        :param status_id: The ID of the status you want to delete or a dict containing the status details

        :returns: The response from the server.
        """
        logger.debug(
            "ActivityPub.reblog(status_id=%s)",
            status_id,
        )

        await self._pre_call_checks()
        url = f"{self.instance}/api/v1/statuses/{status_id}/reblog"
        try:
            response = await self.client.post(url=url, headers=self.headers, timeout=self.timeout)
            self._update_ratelimit(response.headers)
            await _check_exception(response=response)
            self._parse_next_prev(links=response.headers.get("Link"))
            result: Status = response.json()
        except HTTPError as error:
            raise NetworkError from error

        logger.debug(
            "ActivityPub.reblog -> result:\n%s",
            json.dumps(result, indent=4),
        )
        return result

    async def undo_favourite(
        self: ActivityPubClass,
        status: str | dict[Any, Any],
    ) -> Status:
        """Remove a favourite.

        :param status: The ID of the status you want to delete or a dict containing the status details

        :returns: The Status that has just been un-favourited.
        """
        logger.debug(
            "ActivityPub.undo_favourite(status=%s)",
            status,
        )

        if isinstance(status, str):
            status_id = status
        elif isinstance(status, dict):
            status_id = status["id"]

        await self._pre_call_checks()
        url = f"{self.instance}/api/v1/statuses/{status_id}/unfavourite"
        try:
            response = await self.client.post(url=url, headers=self.headers, timeout=self.timeout)
            self._update_ratelimit(response.headers)
            await _check_exception(response=response)
            self._parse_next_prev(links=response.headers.get("Link"))
            result: Status = response.json()
        except HTTPError as error:
            raise NetworkError from error

        logger.debug(
            "ActivityPub.undo_favourite -> result:\n%s",
            json.dumps(result, indent=4),
        )
        return result

    async def post_status(  # noqa: PLR0913 This method has this many arguments
        self: ActivityPubClass,
        status: str,
        visibility: Visibility = Visibility.PUBLIC,
        media_ids: list[str] | None = None,
        sensitive: bool = False,
        spoiler_text: str | None = None,
        scheduled_at: datetime | None = None,
    ) -> Status:
        """Post a status to the fediverse.

        :param status: The text to be posted on the timeline.
        :param visibility: Visibility of the posted status. Enumerable one of `public`, `unlisted`,
            `private`, or `direct`. Defaults to `public`
        :param media_ids: list of ids for media (pictures, videos, etc) to be attached to this post.
            Can be `None` if no media is to be attached. Defaults to `None`
        :param sensitive: Set to true the post is of a sensitive nature and should be marked as such.
            For example overly political or explicit material is often marked as sensitive.
            Applies particularly to attached media. Defaults to `False`
        :param spoiler_text: Text to be shown as a warning or subject before the actual content.
            Statuses are generally collapsed behind this field. Defaults to `None`
        :param scheduled_at: Date and time at which this status should be posted. This is for scheduling
            statuses. This datetime needs to be at least 5 minutes in the future.
            If no timezone is included "UTC" is assumed.

        :return: dict of the status just posted.
        """
        logger.debug(
            "ActivityPub.post_status(status=%s, visibility=%s, media_ids=%s, "
            "sensitive=%s, spoiler_text=%s, scheduled_at=%s)",
            status,
            visibility,
            media_ids,
            sensitive,
            spoiler_text,
            scheduled_at,
        )
        await self._pre_call_checks()

        url = f"{self.instance}/api/v1/statuses"
        headers = self.headers.copy()
        headers.append(("Idempotency-Key", uuid.uuid4().hex))

        logger.debug("ActivityPub.post_status(...) - using URL=%s", url)

        data: dict[str, str | list[str]] = {
            "status": status,
            "visibility": visibility.value,
        }
        if sensitive:
            data["sensitive"] = "true"
        if media_ids:
            data["media_ids[]"] = media_ids
        if spoiler_text:
            data["spoiler_text"] = spoiler_text
        if scheduled_at:
            if not scheduled_at.tzinfo:
                scheduled_at = UTC.localize(scheduled_at)
            data["scheduled_at"] = scheduled_at.isoformat()

        logger.debug("data=%s", data)

        try:
            response = await self.client.post(
                url=url,
                headers=headers,
                data=data,
                timeout=self.timeout,
            )
            self._update_ratelimit(headers=response.headers)
            await _check_exception(response=response)
            result: Status = response.json()
            logger.debug("ActivityPub.post_status - request:\n%s", response.request)
        except HTTPError as error:
            raise NetworkError from error

        logger.debug(
            "ActivityPub.post_status -> result:\n%s",
            json.dumps(result, indent=4),
        )
        return result

    async def post_media(
        self: ActivityPubClass,
        file: IO[bytes],
        mime_type: str,
        description: str | None = None,
        focus: tuple[float, float] | None = None,
    ) -> Any:
        """Post a media file (image or video).

        :param file: The file to be uploaded
        :param mime_type: Mime type
        :param description: A plain-text description of the media, for accessibility purposes
        :param focus: Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0
            (see "Focal points <https://docs.joinmastodon.org/methods/statuses/media/#focal-points>"_)

        :returns: dict containing details for this media on server, such a `id`, `url` etc
        """
        logger.debug(
            "ActivityPub.post_media(file=..., mime_type=%s, description=%s, focus=%s)",
            mime_type,
            description,
            focus,
        )

        await self._pre_call_checks()

        url = f"{self.instance}/api/v1/media"
        upload_file = {"file": (uuid.uuid4().hex, file, mime_type)}
        data = {}
        if description:
            data["description"] = description
        if focus and len(focus) >= 2:
            data["focus"] = f"{focus[0]},{focus[1]}"

        try:
            response = await self.client.post(
                url=url,
                headers=self.headers,
                data=data,
                files=upload_file,
                timeout=self.timeout,
            )
            self._update_ratelimit(headers=response.headers)
            await _check_exception(response=response)
            result = response.json()
        except HTTPError as error:
            raise NetworkError from error

        logger.debug(
            "ActivityPub.post_media -> result:\n%s",
            json.dumps(result, indent=4),
        )
        return result

    async def search(  # noqa PLR0913
        self: ActivityPubClass,
        query: str,
        query_type: SearchType,
        resolve: bool = True,
        following: bool = False,
        account_id: str | None = None,
        exclude_unreviewed: bool = False,
        max_id: str | None = None,
        min_id: str | None = None,
        limit: int | None = None,
        offset: int | None = None,
    ) -> Any:
        """Search for accounts, statuses and hashtags.

        :param query: The search query.
        :param query_type: Specify whether to search for only accounts, hashtags, statuses.
            Use SearchType enum for this.
        :param resolve: Only relevant if type includes accounts. If true and (a) the search query is for a remote
            account (e.g., someaccount@someother.server) and (b) the local server does not know about the account,
            WebFinger is used to try and resolve the account at some other.server. This provides the best recall at
            higher latency. If false only accounts the server knows about are returned. Defaults to True.
        :param following: Only include accounts that the user is following? Defaults to False.
        :param account_id: If provided, will only return statuses authored by this account.
        :param exclude_unreviewed: Filter out unreviewed tags? Defaults to False. Use True when trying to find
            trending tags.
        :param max_id: All results returned will be lesser than this ID. In effect, sets an upper bound on results.
        :param min_id: Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates
        :param limit: Maximum number of results to return, per type. Mastodon servers default this to 20 results per
            category. Max 40 results per category.
        :param offset: Skip the first n results.
        """
        url = f"{self.instance}/api/v2/search"

        params: dict[str, int | str | bool] = {
            "q": query,
            "type": query_type.value,
            "resolve": resolve,
        }
        if following:
            params["following"] = following
        if account_id:
            params["account_id"] = account_id
        if exclude_unreviewed:
            params["exclude_unreviewed"] = exclude_unreviewed
        if max_id:
            params["max_id"] = max_id
        if min_id:
            params["min_id"] = min_id
        if limit:
            params["limit"] = limit
        if offset:
            params["offset"] = offset
        logger.debug("Params=%s", params)
        try:
            response = await self.client.get(
                url=url,
                headers=self.headers,
                params=params,
                timeout=self.timeout,
            )
            self._update_ratelimit(headers=response.headers)
            await _check_exception(response=response)
            result = response.json()
        except HTTPError as error:
            raise NetworkError from error

        logger.debug("ActivityPub.search -> result:\n%s", json.dumps(result, indent=4))

        return result
