"""Provides the User class."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from prawcore import Conflict

from praw.const import API_PATH
from praw.exceptions import ReadOnlyException
from praw.models import Preferences
from praw.models.base import PRAWBase
from praw.models.listing.generator import ListingGenerator
from praw.models.reddit.redditor import Redditor
from praw.models.reddit.subreddit import Subreddit
from praw.util.cache import cachedproperty

if TYPE_CHECKING:
    from collections.abc import Iterator

    import praw
    from praw import models


class User(PRAWBase):
    """The :class:`.User` class provides methods for the currently authenticated user."""

    @cachedproperty
    def preferences(self) -> models.Preferences:
        """Get an instance of :class:`.Preferences`.

        The preferences can be accessed as a ``dict`` like so:

        .. code-block:: python

            preferences = reddit.user.preferences()
            print(preferences["show_link_flair"])

        Preferences can be updated via:

        .. code-block:: python

            reddit.user.preferences.update(show_link_flair=True)

        The :meth:`.Preferences.update` method returns the new state of the preferences
        as a ``dict``, which can be used to check whether a change went through. Changes
        with invalid types or parameter names fail silently.

        .. code-block:: python

            original_preferences = reddit.user.preferences()
            new_preferences = reddit.user.preferences.update(invalid_param=123)
            print(original_preferences == new_preferences)  # True, no change

        """
        return Preferences(self._reddit)

    def __init__(self, reddit: praw.Reddit) -> None:
        """Initialize an :class:`.User` instance.

        This class is intended to be interfaced with through ``reddit.user``.

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

    def blocked(self) -> list[models.Redditor]:
        r"""Return a :class:`.RedditorList` of blocked :class:`.Redditor`\ s."""
        return self._reddit.get(API_PATH["blocked"])

    def contributor_subreddits(self, **generator_kwargs: Any) -> Iterator[models.Subreddit]:
        r"""Return a :class:`.ListingGenerator` of contributor :class:`.Subreddit`\ s.

        These are subreddits in which the user is an approved user.

        Additional keyword arguments are passed in the initialization of
        :class:`.ListingGenerator`.

        To print a list of the subreddits that you are an approved user in, try:

        .. code-block:: python

            for subreddit in reddit.user.contributor_subreddits(limit=None):
                print(str(subreddit))

        """
        return ListingGenerator(self._reddit, API_PATH["my_contributor"], **generator_kwargs)

    def friends(self, *, user: str | models.Redditor | None = None) -> list[models.Redditor] | models.Redditor:
        r"""Return a :class:`.RedditorList` of friends or a :class:`.Redditor` in the friends list.

        :param user: Checks to see if you are friends with the redditor. Either an
            instance of :class:`.Redditor` or a string can be given.

        :returns: A list of :class:`.Redditor`\ s, or a single :class:`.Redditor` if
            ``user`` is specified. The :class:`.Redditor` instance(s) returned also has
            friend attributes.

        :raises: An instance of :class:`.RedditAPIException` if you are not friends with
            the specified :class:`.Redditor`.

        """
        endpoint = API_PATH["friends"] if user is None else API_PATH["friend_v1"].format(user=str(user))
        return self._reddit.get(endpoint)

    def karma(self) -> dict[models.Subreddit, dict[str, int]]:
        r"""Return a dictionary mapping :class:`.Subreddit`\ s to their karma.

        The returned dict contains subreddits as keys. Each subreddit key contains a
        sub-dict that have keys for ``comment_karma`` and ``link_karma``. The dict is
        sorted in descending karma order.

        .. note::

            Each key of the main dict is an instance of :class:`.Subreddit`. It is
            recommended to iterate over the dict in order to retrieve the values,
            preferably through :py:meth:`dict.items`.

        """
        karma_map = {}
        for row in self._reddit.get(API_PATH["karma"])["data"]:
            subreddit = Subreddit(self._reddit, row["sr"])
            del row["sr"]
            karma_map[subreddit] = row
        return karma_map

    def me(self, *, use_cache: bool = True) -> models.Redditor | None:
        """Return a :class:`.Redditor` instance for the authenticated user.

        :param use_cache: When ``True``, and if this function has been previously
            called, returned the cached version (default: ``True``).

        .. note::

            If you change the :class:`.Reddit` instance's authorization, you might want
            to refresh the cached value. Prefer using separate :class:`.Reddit`
            instances, however, for distinct authorizations.

        """
        if self._reddit.read_only:
            msg = "`user.me()` does not work in read_only mode"
            raise ReadOnlyException(msg)
        if "_me" not in self.__dict__ or not use_cache:
            user_data = self._reddit.get(API_PATH["me"])
            self._me = Redditor(self._reddit, _data=user_data)
        return self._me

    def moderator_subreddits(self, **generator_kwargs: Any) -> Iterator[models.Subreddit]:
        """Return a :class:`.ListingGenerator` subreddits that the user moderates.

        Additional keyword arguments are passed in the initialization of
        :class:`.ListingGenerator`.

        To print a list of the names of the subreddits you moderate, try:

        .. code-block:: python

            for subreddit in reddit.user.moderator_subreddits(limit=None):
                print(str(subreddit))

        .. seealso::

            :meth:`.Redditor.moderated`

        """
        return ListingGenerator(self._reddit, API_PATH["my_moderator"], **generator_kwargs)

    def multireddits(self) -> list[models.Multireddit]:
        r"""Return a list of :class:`.Multireddit`\ s belonging to the user."""
        return self._reddit.get(API_PATH["my_multireddits"])

    def pin(
        self, submission: models.Submission, *, num: int | None = None, state: bool = True
    ) -> models.Submission | None:
        """Set the pin state of a submission on the authenticated user's profile.

        :param submission: An instance of :class:`.Submission` that will be
            pinned/unpinned.
        :param num: If specified, the slot in which the submission will be pinned into.
            If there is a submission already in the specified slot, it will be replaced.
            If ``None`` or there is not a submission in the specified slot, the first
            available slot will be used (default: ``None``). If all slots are used the
            following will occur:

            - Old Reddit:

              1. The submission in the last slot will be unpinned.
              2. The remaining pinned submissions will be shifted down a slot.
              3. The new submission will be pinned in the first slot.

            - New Reddit:

              1. The submission in the first slot will be unpinned.
              2. The remaining pinned submissions will be shifted up a slot.
              3. The new submission will be pinned in the last slot.

            .. note::

                At the time of writing (10/22/2021), there are 4 pin slots available and
                pins are in reverse order on old Reddit. If ``num`` is an invalid value,
                Reddit will ignore it and the same behavior will occur as if ``num`` is
                ``None``.

        :param state: ``True`` pins the submission, ``False`` unpins (default:
            ``True``).

        :returns: The pinned submission.

        :raises: ``prawcore.BadRequest`` when pinning a removed or deleted submission.
        :raises: ``prawcore.Forbidden`` when pinning a submission the authenticated user
            is not the author of.

        .. code-block:: python

            submission = next(reddit.user.me().submissions.new())
            reddit.user.pin(submission)

        """
        data = {
            "id": submission.fullname,
            "num": num,
            "state": state,
            "to_profile": True,
        }
        try:
            return self._reddit.post(API_PATH["sticky_submission"], data=data)
        except Conflict:
            pass

    def subreddits(self, **generator_kwargs: Any) -> Iterator[models.Subreddit]:
        r"""Return a :class:`.ListingGenerator` of :class:`.Subreddit`\ s the user is subscribed to.

        Additional keyword arguments are passed in the initialization of
        :class:`.ListingGenerator`.

        To print a list of the subreddits that you are subscribed to, try:

        .. code-block:: python

            for subreddit in reddit.user.subreddits(limit=None):
                print(str(subreddit))

        """
        return ListingGenerator(self._reddit, API_PATH["my_subreddits"], **generator_kwargs)

    def trusted(self) -> list[models.Redditor]:
        r"""Return a :class:`.RedditorList` of trusted :class:`.Redditor`\ s.

        To display the usernames of your trusted users and the times at which you
        decided to trust them, try:

        .. code-block:: python

            trusted_users = reddit.user.trusted()
            for user in trusted_users:
                print(f"User: {user.name}, time: {user.date}")

        """
        return self._reddit.get(API_PATH["trusted"])
