"""Provide Collections functionality."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, cast

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.mixins import CreatedMixin
from praw.models.reddit.submission import Submission
from praw.models.reddit.subreddit import Subreddit
from praw.util.cache import cachedproperty

if TYPE_CHECKING:
    import datetime
    from collections.abc import Iterator

    import praw
    from praw import models


class Collection(CreatedMixin, 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/test/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"
    _created_at_attribute = "created_at_utc"

    @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) -> 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 cast("models.Subreddit", next(self._reddit.info(fullnames=[self.subreddit_id])))

    @property
    def updated_datetime(self) -> datetime.datetime:
        """The last update time as a timezone-aware :class:`datetime.datetime`.

        The returned object is localized to the system's timezone.

        """
        return self._to_local_datetime(self.last_update_utc)

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


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

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


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 = {
            "description": description,
            "sr_fullname": self.subreddit_fullname,
            "title": title,
        }
        if display_layout:
            data["display_layout"] = display_layout
        return self._reddit.post(
            API_PATH["collection_create"],
            data=data,
        )


Subreddit._subreddit_collections_class = SubredditCollections
