"""Provide Collections functionality."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from praw.const import API_PATH
from praw.exceptions import ClientException
from praw.models.base import PRAWBase
from praw.models.reddit.base import RedditBase
from praw.models.reddit.submission import Submission
from praw.models.reddit.subreddit import Subreddit
from praw.util.cache import cachedproperty

if TYPE_CHECKING:
    from collections.abc import Iterator

    import praw.models


class CollectionModeration(PRAWBase):
    """Class to support moderation actions on a :class:`.Collection`.

    Obtain an instance via:

    .. code-block:: python

        reddit.subreddit("test").collections("some_uuid").mod

    """

    def __init__(self, reddit: praw.Reddit, collection_id: str) -> None:
        """Initialize a :class:`.CollectionModeration` instance.

        :param collection_id: The ID of a :class:`.Collection`.

        """
        super().__init__(reddit, _data=None)
        self.collection_id = collection_id

    def _post_fullname(self, post: str | praw.models.Submission) -> str:
        """Get a post's fullname.

        :param post: A fullname, a :class:`.Submission`, a permalink, or an ID.

        :returns: The fullname of the post.

        """
        if isinstance(post, Submission):
            return post.fullname
        if not isinstance(post, str):
            msg = f"Cannot get fullname from object of type {type(post)}."
            raise TypeError(msg)
        if post.startswith(f"{self._reddit.config.kinds['submission']}_"):
            return post
        try:
            return self._reddit.submission(url=post).fullname
        except ClientException:
            return self._reddit.submission(post).fullname

    def add_post(self, submission: praw.models.Submission) -> None:
        """Add a post to the collection.

        :param submission: The post to add, a :class:`.Submission`, its permalink as a
            ``str``, its fullname as a ``str``, or its ID as a ``str``.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            collection.mod.add_post("bgibu9")

        .. seealso::

            :meth:`.remove_post`

        """
        link_fullname = self._post_fullname(submission)

        self._reddit.post(
            API_PATH["collection_add_post"],
            data={"collection_id": self.collection_id, "link_fullname": link_fullname},
        )

    def delete(self) -> None:
        """Delete this collection.

        Example usage:

        .. code-block:: python

            reddit.subreddit("test").collections("some_uuid").mod.delete()

        .. seealso::

            :meth:`~.SubredditCollectionsModeration.create`

        """
        self._reddit.post(API_PATH["collection_delete"], data={"collection_id": self.collection_id})

    def remove_post(self, submission: praw.models.Submission) -> None:
        """Remove a post from the collection.

        :param submission: The post to remove, a :class:`.Submission`, its permalink as
            a ``str``, its fullname as a ``str``, or its ID as a ``str``.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            collection.mod.remove_post("bgibu9")

        .. seealso::

            :meth:`.add_post`

        """
        link_fullname = self._post_fullname(submission)

        self._reddit.post(
            API_PATH["collection_remove_post"],
            data={"collection_id": self.collection_id, "link_fullname": link_fullname},
        )

    def reorder(self, links: list[str | praw.models.Submission]) -> None:
        r"""Reorder posts in the collection.

        :param links: A list of :class:`.Submission`\ s or a ``str`` that is either a
            fullname or an ID.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            current_order = collection.link_ids
            new_order = reversed(current_order)
            collection.mod.reorder(new_order)

        """
        link_ids = ",".join(self._post_fullname(post) for post in links)
        self._reddit.post(
            API_PATH["collection_reorder"],
            data={"collection_id": self.collection_id, "link_ids": link_ids},
        )

    def update_description(self, description: str) -> None:
        """Update the collection's description.

        :param description: The new description.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            collection.mod.update_description("Please enjoy these links!")

        .. seealso::

            :meth:`.update_title`

        """
        self._reddit.post(
            API_PATH["collection_desc"],
            data={"collection_id": self.collection_id, "description": description},
        )

    def update_display_layout(self, display_layout: str) -> None:
        """Update the collection's display layout.

        :param display_layout: Either ``"TIMELINE"`` for events or discussions or
            ``"GALLERY"`` for images or memes. Passing ``None`` will clear the set
            layout and ``collection.display_layout`` will be ``None``, however, the
            collection will appear on Reddit as if ``display_layout`` is set to
            ``"TIMELINE"``.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            collection.mod.update_display_layout("GALLERY")

        """
        self._reddit.post(
            API_PATH["collection_layout"],
            data={
                "collection_id": self.collection_id,
                "display_layout": display_layout,
            },
        )

    def update_title(self, title: str) -> None:
        """Update the collection's title.

        :param title: The new title.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            collection.mod.update_title("Titley McTitleface")

        .. seealso::

            :meth:`.update_description`

        """
        self._reddit.post(
            API_PATH["collection_title"],
            data={"collection_id": self.collection_id, "title": title},
        )


class SubredditCollectionsModeration(PRAWBase):
    r"""Class to represent moderator actions on a :class:`.Subreddit`'s :class:`.Collection`\ s.

    Obtain an instance via:

    .. code-block:: python

        reddit.subreddit("test").collections.mod

    """

    def __init__(
        self,
        reddit: praw.Reddit,
        sub_fullname: str,
        _data: dict[str, Any] | None = None,
    ) -> None:
        """Initialize a :class:`.SubredditCollectionsModeration` instance."""
        super().__init__(reddit, _data)
        self.subreddit_fullname = sub_fullname

    def create(self, *, description: str, display_layout: str | None = None, title: str) -> Collection:
        """Create a new :class:`.Collection`.

        The authenticated account must have appropriate moderator permissions in the
        subreddit this collection belongs to.

        :param description: The description, up to 500 characters.
        :param display_layout: Either ``"TIMELINE"`` for events or discussions or
            ``"GALLERY"`` for images or memes. Passing ``""`` or ``None`` will make the
            collection appear on Reddit as if this is set to ``"TIMELINE"`` (default:
            ``None``).
        :param title: The title of the collection, up to 300 characters.

        :returns: The newly created :class:`.Collection`.

        Example usage:

        .. code-block:: python

            my_sub = reddit.subreddit("test")
            new_collection = my_sub.collections.mod.create(title="Title", description="desc")
            new_collection.mod.add_post("bgibu9")

        To specify the display layout as ``"GALLERY"`` when creating the collection:

        .. code-block:: python

            my_sub = reddit.subreddit("test")
            new_collection = my_sub.collections.mod.create(
                title="Title", description="desc", display_layout="GALLERY"
            )
            new_collection.mod.add_post("bgibu9")

        .. seealso::

            :meth:`~.CollectionModeration.delete`

        """
        data = {
            "sr_fullname": self.subreddit_fullname,
            "title": title,
            "description": description,
        }
        if display_layout:
            data["display_layout"] = display_layout
        return self._reddit.post(
            API_PATH["collection_create"],
            data=data,
        )


class SubredditCollections(PRAWBase):
    r"""Class to represent a :class:`.Subreddit`'s :class:`.Collection`\ s.

    Obtain an instance via:

    .. code-block:: python

        reddit.subreddit("test").collections

    """

    @cachedproperty
    def mod(self) -> SubredditCollectionsModeration:
        """Get an instance of :class:`.SubredditCollectionsModeration`.

        Provides :meth:`~SubredditCollectionsModeration.create`:

        .. code-block:: python

            my_sub = reddit.subreddit("test")
            new_collection = my_sub.collections.mod.create(title="Title", description="desc")

        """
        return SubredditCollectionsModeration(self._reddit, self.subreddit.fullname)

    def __call__(
        self,
        collection_id: str | None = None,
        permalink: str | None = None,
    ) -> Collection:
        """Return the :class:`.Collection` with the specified ID.

        :param collection_id: The ID of a :class:`.Collection` (default: ``None``).
        :param permalink: The permalink of a collection (default: ``None``).

        :returns: The specified :class:`.Collection`.

        Exactly one of ``collection_id`` or ``permalink`` is required.

        Example usage:

        .. code-block:: python

            subreddit = reddit.subreddit("test")

            uuid = "847e4548-a3b5-4ad7-afb4-edbfc2ed0a6b"
            collection = subreddit.collections(uuid)
            print(collection.title)
            print(collection.description)

            permalink = "https://www.reddit.com/r/SUBREDDIT/collection/" + uuid
            collection = subreddit.collections(permalink=permalink)
            print(collection.title)
            print(collection.description)

        """
        if (collection_id is None) == (permalink is None):
            msg = "Exactly one of 'collection_id' or 'permalink' must be provided."
            raise TypeError(msg)
        return Collection(self._reddit, collection_id=collection_id, permalink=permalink)

    def __init__(
        self,
        reddit: praw.Reddit,
        subreddit: praw.models.Subreddit,
        _data: dict[str, Any] | None = None,
    ) -> None:
        """Initialize a :class:`.SubredditCollections` instance."""
        super().__init__(reddit, _data)
        self.subreddit = subreddit

    def __iter__(self) -> Iterator[Collection]:
        r"""Iterate over the :class:`.Subreddit`'s :class:`.Collection`\ s.

        Example usage:

        .. code-block:: python

            for collection in reddit.subreddit("test").collections:
                print(collection.permalink)

        """
        request = self._reddit.get(
            API_PATH["collection_subreddit"],
            params={"sr_fullname": self.subreddit.fullname},
        )
        yield from request


class Collection(RedditBase):
    """Class to represent a :class:`.Collection`.

    Obtain an instance via:

    .. code-block:: python

        collection = reddit.subreddit("test").collections("some_uuid")

    or

    .. code-block:: python

        collection = reddit.subreddit("test").collections(
            permalink="https://reddit.com/r/SUBREDDIT/collection/some_uuid"
        )

    .. include:: ../../typical_attributes.rst

    =================== =============================================================
    Attribute           Description
    =================== =============================================================
    ``author``          The :class:`.Redditor` who created the collection.
    ``collection_id``   The UUID of the collection.
    ``created_at_utc``  Time the collection was created, represented in `Unix Time`_.
    ``description``     The collection description.
    ``display_layout``  The collection display layout.
    ``last_update_utc`` Time the collection was last updated, represented in `Unix
                        Time`_.
    ``link_ids``        A list of :class:`.Submission` fullnames.
    ``permalink``       The collection's permalink (to view on the web).
    ``sorted_links``    An iterable listing of the posts in this collection.
    ``title``           The title of the collection.
    =================== =============================================================

    .. _unix time: https://en.wikipedia.org/wiki/Unix_time

    """

    STR_FIELD = "collection_id"

    @cachedproperty
    def mod(self) -> CollectionModeration:
        """Get an instance of :class:`.CollectionModeration`.

        Provides access to various methods, including
        :meth:`~.CollectionModeration.add_post`, :meth:`~.CollectionModeration.delete`,
        :meth:`~.CollectionModeration.reorder`, and
        :meth:`~.CollectionModeration.update_title`.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            collection.mod.update_title("My new title!")

        """
        return CollectionModeration(self._reddit, self.collection_id)

    @cachedproperty
    def subreddit(self) -> praw.models.Subreddit:
        """Get the subreddit that this collection belongs to.

        For example:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            subreddit = collection.subreddit

        """
        return next(self._reddit.info(fullnames=[self.subreddit_id]))

    def __init__(
        self,
        reddit: praw.Reddit,
        _data: dict[str, Any] | None = None,
        collection_id: str | None = None,
        permalink: str | None = None,
    ) -> None:
        """Initialize a :class:`.Collection` instance.

        :param reddit: An instance of :class:`.Reddit`.
        :param _data: Any data associated with the :class:`.Collection`.
        :param collection_id: The ID of the :class:`.Collection`.
        :param permalink: The permalink of the :class:`.Collection`.

        """
        if sum(1 for value in (_data, collection_id, permalink) if value is not None) != 1:
            msg = "Exactly one of '_data', 'collection_id', or 'permalink' must be provided."
            raise TypeError(msg)

        if permalink:
            collection_id = self._url_parts(permalink)[4]

        if collection_id:
            self.collection_id = collection_id  # set from _data otherwise

        super().__init__(reddit, _data)

        self._info_params = {
            "collection_id": self.collection_id,
            "include_links": True,
        }

    def __iter__(self) -> Iterator[praw.models.Submission]:
        """Provide a way to iterate over the posts in this :class:`.Collection`.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            for submission in collection:
                print(submission.title, submission.permalink)

        """
        yield from self.sorted_links

    def __len__(self) -> int:
        """Get the number of posts in this :class:`.Collection`.

        Example usage:

        .. code-block:: python

            collection = reddit.subreddit("test").collections("some_uuid")
            print(len(collection))

        """
        return len(self.link_ids)

    def __setattr__(self, attribute: str, value: Any) -> None:
        """Objectify author, subreddit, and sorted_links attributes."""
        if attribute == "author_name":
            self.author = self._reddit.redditor(value)
        elif attribute == "sorted_links":
            value = self._reddit._objector.objectify(data=value)
        super().__setattr__(attribute, value)

    def _fetch(self) -> None:
        data = self._fetch_data()
        try:
            self._reddit._objector.check_error(data)
        except ClientException:
            # A well-formed but invalid Collections ID during fetch time
            # causes Reddit to return something that looks like an error
            # but with no content.
            msg = f"Error during fetch. Check collection ID {self.collection_id!r} is correct."
            raise ClientException(msg) from None

        other = type(self)(self._reddit, _data=data)
        self.__dict__.update(other.__dict__)
        super()._fetch()

    def _fetch_info(self) -> tuple[str, dict, dict[str, bool | str]]:
        return "collection", {}, self._info_params

    def follow(self) -> None:
        """Follow this :class:`.Collection`.

        Example usage:

        .. code-block:: python

            reddit.subreddit("test").collections("some_uuid").follow()

        .. seealso::

            :meth:`.unfollow`

        """
        self._reddit.post(
            API_PATH["collection_follow"],
            data={"collection_id": self.collection_id, "follow": True},
        )

    def unfollow(self) -> None:
        """Unfollow this :class:`.Collection`.

        Example usage:

        .. code-block:: python

            reddit.subreddit("test").collections("some_uuid").unfollow()

        .. seealso::

            :meth:`.follow`

        """
        self._reddit.post(
            API_PATH["collection_follow"],
            data={"collection_id": self.collection_id, "follow": False},
        )


Subreddit._subreddit_collections_class = SubredditCollections
