"""Provide the LiveThread class."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from praw.const import API_PATH
from praw.models.list.redditor import RedditorList
from praw.models.listing.generator import ListingGenerator
from praw.models.reddit.base import RedditBase
from praw.models.reddit.mixins import FullnameMixin
from praw.models.reddit.redditor import Redditor
from praw.models.util import stream_generator
from praw.util.cache import cachedproperty

if TYPE_CHECKING:
    from collections.abc import Iterable, Iterator

    import praw.models


class LiveContributorRelationship:
    """Provide methods to interact with live threads' contributors."""

    @staticmethod
    def _handle_permissions(permissions: Iterable[str]) -> str:
        permissions = {"all"} if permissions is None else set(permissions)
        return ",".join(f"+{x}" for x in permissions)

    def __call__(self) -> list[praw.models.Redditor]:
        """Return a :class:`.RedditorList` for live threads' contributors.

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            for contributor in thread.contributor():
                print(contributor)

        """
        url = API_PATH["live_contributors"].format(id=self.thread.id)
        temp = self.thread._reddit.get(url)
        return temp if isinstance(temp, RedditorList) else temp[0]

    def __init__(self, thread: praw.models.LiveThread) -> None:
        """Initialize a :class:`.LiveContributorRelationship` instance.

        :param thread: An instance of :class:`.LiveThread`.

        .. note::

            This class should not be initialized directly. Instead, obtain an instance
            via: :meth:`.LiveThread.contributor`.

        """
        self.thread = thread

    def accept_invite(self) -> None:
        """Accept an invite to contribute the live thread.

        Usage:

        .. code-block:: python

            thread = reddit.live("ydwwxneu7vsa")
            thread.contributor.accept_invite()

        """
        url = API_PATH["live_accept_invite"].format(id=self.thread.id)
        self.thread._reddit.post(url)

    def invite(
        self,
        redditor: str | praw.models.Redditor,
        *,
        permissions: list[str] | None = None,
    ) -> None:
        """Invite a redditor to be a contributor of the live thread.

        :param redditor: A redditor name or :class:`.Redditor` instance.
        :param permissions: When provided (not ``None``), permissions should be a list
            of strings specifying which subset of permissions to grant. An empty list
            ``[]`` indicates no permissions, and when not provided (``None``), indicates
            full permissions.

        :raises: :class:`.RedditAPIException` if the invitation already exists.

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            redditor = reddit.redditor("spez")

            # "manage" and "settings" permissions
            thread.contributor.invite(redditor, permissions=["manage", "settings"])

        .. seealso::

            :meth:`.LiveContributorRelationship.remove_invite` to remove the invite for
            redditor.

        """
        url = API_PATH["live_invite"].format(id=self.thread.id)
        data = {
            "name": str(redditor),
            "type": "liveupdate_contributor_invite",
            "permissions": self._handle_permissions(permissions),
        }
        self.thread._reddit.post(url, data=data)

    def leave(self) -> None:
        """Abdicate the live thread contributor position (use with care).

        Usage:

        .. code-block:: python

            thread = reddit.live("ydwwxneu7vsa")
            thread.contributor.leave()

        """
        url = API_PATH["live_leave"].format(id=self.thread.id)
        self.thread._reddit.post(url)

    def remove(self, redditor: str | praw.models.Redditor) -> None:
        """Remove the redditor from the live thread contributors.

        :param redditor: A redditor fullname (e.g., ``"t2_1w72"``) or :class:`.Redditor`
            instance.

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            redditor = reddit.redditor("spez")
            thread.contributor.remove(redditor)
            thread.contributor.remove("t2_1w72")  # with fullname

        """
        fullname = redditor.fullname if isinstance(redditor, Redditor) else redditor
        data = {"id": fullname}
        url = API_PATH["live_remove_contrib"].format(id=self.thread.id)
        self.thread._reddit.post(url, data=data)

    def remove_invite(self, redditor: str | praw.models.Redditor) -> None:
        """Remove the invite for redditor.

        :param redditor: A redditor fullname (e.g., ``"t2_1w72"``) or :class:`.Redditor`
            instance.

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            redditor = reddit.redditor("spez")
            thread.contributor.remove_invite(redditor)
            thread.contributor.remove_invite("t2_1w72")  # with fullname

        .. seealso::

            :meth:`.LiveContributorRelationship.invite` to invite a redditor to be a
            contributor of the live thread.

        """
        fullname = redditor.fullname if isinstance(redditor, Redditor) else redditor
        data = {"id": fullname}
        url = API_PATH["live_remove_invite"].format(id=self.thread.id)
        self.thread._reddit.post(url, data=data)

    def update(
        self,
        redditor: str | praw.models.Redditor,
        *,
        permissions: list[str] | None = None,
    ) -> None:
        """Update the contributor permissions for ``redditor``.

        :param redditor: A redditor name or :class:`.Redditor` instance.
        :param permissions: When provided (not ``None``), permissions should be a list
            of strings specifying which subset of permissions to grant (other
            permissions are removed). An empty list ``[]`` indicates no permissions, and
            when not provided (``None``), indicates full permissions.

        For example, to grant all permissions to the contributor, try:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            thread.contributor.update("spez")

        To grant ``"access"`` and ``"edit"`` permissions (and to remove other
        permissions), try:

        .. code-block:: python

            thread.contributor.update("spez", permissions=["access", "edit"])

        To remove all permissions from the contributor, try:

        .. code-block:: python

            subreddit.moderator.update("spez", permissions=[])

        """
        url = API_PATH["live_update_perms"].format(id=self.thread.id)
        data = {
            "name": str(redditor),
            "type": "liveupdate_contributor",
            "permissions": self._handle_permissions(permissions),
        }
        self.thread._reddit.post(url, data=data)

    def update_invite(
        self,
        redditor: str | praw.models.Redditor,
        *,
        permissions: list[str] | None = None,
    ) -> None:
        """Update the contributor invite permissions for ``redditor``.

        :param redditor: A redditor name or :class:`.Redditor` instance.
        :param permissions: When provided (not ``None``), permissions should be a list
            of strings specifying which subset of permissions to grant (other
            permissions are removed). An empty list ``[]`` indicates no permissions, and
            when not provided (``None``), indicates full permissions.

        For example, to set all permissions to the invitation, try:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            thread.contributor.update_invite("spez")

        To set ``"access"`` and ``"edit"`` permissions (and to remove other permissions)
        to the invitation, try:

        .. code-block:: python

            thread.contributor.update_invite("spez", permissions=["access", "edit"])

        To remove all permissions from the invitation, try:

        .. code-block:: python

            thread.contributor.update_invite("spez", permissions=[])

        """
        url = API_PATH["live_update_perms"].format(id=self.thread.id)
        data = {
            "name": str(redditor),
            "type": "liveupdate_contributor_invite",
            "permissions": self._handle_permissions(permissions),
        }
        self.thread._reddit.post(url, data=data)


class LiveThread(RedditBase):
    """An individual :class:`.LiveThread` object.

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

    ==================== =========================================================
    Attribute            Description
    ==================== =========================================================
    ``created_utc``      The creation time of the live thread, in `Unix Time`_.
    ``description``      Description of the live thread, as Markdown.
    ``description_html`` Description of the live thread, as HTML.
    ``id``               The ID of the live thread.
    ``nsfw``             A ``bool`` representing whether or not the live thread is
                         marked as NSFW.
    ==================== =========================================================

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

    """

    STR_FIELD = "id"

    @cachedproperty
    def contrib(self) -> praw.models.reddit.live.LiveThreadContribution:
        """Provide an instance of :class:`.LiveThreadContribution`.

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            thread.contrib.add("### update")

        """
        return LiveThreadContribution(self)

    @cachedproperty
    def contributor(self) -> praw.models.reddit.live.LiveContributorRelationship:
        """Provide an instance of :class:`.LiveContributorRelationship`.

        You can call the instance to get a list of contributors which is represented as
        :class:`.RedditorList` instance consists of :class:`.Redditor` instances. Those
        :class:`.Redditor` instances have ``permissions`` attributes as contributors:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            for contributor in thread.contributor():
                # prints `Redditor(name="Acidtwist") ["all"]`
                print(contributor, contributor.permissions)

        """
        return LiveContributorRelationship(self)

    @cachedproperty
    def stream(self) -> praw.models.reddit.live.LiveThreadStream:
        """Provide an instance of :class:`.LiveThreadStream`.

        Streams are used to indefinitely retrieve new updates made to a live thread,
        like:

        .. code-block:: python

            for live_update in reddit.live("ta535s1hq2je").stream.updates():
                print(live_update.body)

        Updates are yielded oldest first as :class:`.LiveUpdate`. Up to 100 historical
        updates will initially be returned. To only retrieve new updates starting from
        when the stream is created, pass ``skip_existing=True``:

        .. code-block:: python

            live_thread = reddit.live("ta535s1hq2je")
            for live_update in live_thread.stream.updates(skip_existing=True):
                print(live_update.author)

        """
        return LiveThreadStream(self)

    def __eq__(self, other: str | praw.models.LiveThread) -> bool:
        """Return whether the other instance equals the current.

        .. note::

            This comparison is case sensitive.

        """
        if isinstance(other, str):
            return other == str(self)
        return isinstance(other, self.__class__) and str(self) == str(other)

    def __getitem__(self, update_id: str) -> praw.models.LiveUpdate:
        """Return a lazy :class:`.LiveUpdate` instance.

        :param update_id: A live update ID, e.g.,
            ``"7827987a-c998-11e4-a0b9-22000b6a88d2"``.

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
            update.thread  # LiveThread(id="ukaeu1ik4sw5")
            update.id  # "7827987a-c998-11e4-a0b9-22000b6a88d2"
            update.author  # "umbrae"

        """
        return LiveUpdate(self._reddit, self.id, update_id)

    def __hash__(self) -> int:
        """Return the hash of the current instance."""
        return hash(self.__class__.__name__) ^ hash(str(self))

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

        :param reddit: An instance of :class:`.Reddit`.
        :param id: A live thread ID, e.g., ``"ukaeu1ik4sw5"``

        """
        if (id, _data).count(None) != 1:
            msg = "Either 'id' or '_data' must be provided."
            raise TypeError(msg)
        if id:
            self.id = id
        super().__init__(reddit, _data=_data)

    def _fetch(self) -> None:
        data = self._fetch_data()
        data = data["data"]
        other = type(self)(self._reddit, _data=data)
        self.__dict__.update(other.__dict__)
        super()._fetch()

    def _fetch_info(self) -> tuple[str, dict[str, str], None]:
        return "liveabout", {"id": self.id}, None

    def discussions(self, **generator_kwargs: str | int | dict[str, str]) -> Iterator[praw.models.Submission]:
        """Get submissions linking to the thread.

        :param generator_kwargs: keyword arguments passed to :class:`.ListingGenerator`
            constructor.

        :returns: A :class:`.ListingGenerator` object which yields :class:`.Submission`
            objects.

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

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            for submission in thread.discussions(limit=None):
                print(submission.title)

        """
        url = API_PATH["live_discussions"].format(id=self.id)
        return ListingGenerator(self._reddit, url, **generator_kwargs)

    def report(self, type: str) -> None:  # noqa: A002
        """Report the thread violating the Reddit rules.

        :param type: One of ``"spam"``, ``"vote-manipulation"``,
            ``"personal-information"``, ``"sexualizing-minors"``, or
            ``"site-breaking"``.

        Usage:

        .. code-block:: python

            thread = reddit.live("xyu8kmjvfrww")
            thread.report("spam")

        """
        url = API_PATH["live_report"].format(id=self.id)
        self._reddit.post(url, data={"type": type})

    def updates(self, **generator_kwargs: str | int | dict[str, str]) -> Iterator[praw.models.LiveUpdate]:
        """Return a :class:`.ListingGenerator` yields :class:`.LiveUpdate` s.

        :param generator_kwargs: keyword arguments passed to :class:`.ListingGenerator`
            constructor.

        :returns: A :class:`.ListingGenerator` object which yields :class:`.LiveUpdate`
            objects.

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

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            after = "LiveUpdate_fefb3dae-7534-11e6-b259-0ef8c7233633"
            for submission in thread.updates(limit=5, params={"after": after}):
                print(submission.body)

        """
        url = API_PATH["live_updates"].format(id=self.id)
        for update in ListingGenerator(self._reddit, url, **generator_kwargs):
            update._thread = self
            yield update


class LiveThreadContribution:
    """Provides a set of contribution functions to a :class:`.LiveThread`."""

    def __init__(self, thread: praw.models.LiveThread) -> None:
        """Initialize a :class:`.LiveThreadContribution` instance.

        :param thread: An instance of :class:`.LiveThread`.

        This instance can be retrieved through ``thread.contrib`` where thread is a
        :class:`.LiveThread` instance. E.g.,

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            thread.contrib.add("### update")

        """
        self.thread = thread

    def add(self, body: str) -> None:
        """Add an update to the live thread.

        :param body: The Markdown formatted content for the update.

        Usage:

        .. code-block:: python

            thread = reddit.live("ydwwxneu7vsa")
            thread.contrib.add("test `LiveThreadContribution.add()`")

        """
        url = API_PATH["live_add_update"].format(id=self.thread.id)
        self.thread._reddit.post(url, data={"body": body})

    def close(self) -> None:
        """Close the live thread permanently (cannot be undone).

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            thread.contrib.close()

        """
        url = API_PATH["live_close"].format(id=self.thread.id)
        self.thread._reddit.post(url)

    def update(
        self,
        *,
        description: str | None = None,
        nsfw: bool | None = None,
        resources: str | None = None,
        title: str | None = None,
        **other_settings: str | None,
    ) -> None:
        """Update settings of the live thread.

        :param description: The live thread's description (default: ``None``).
        :param nsfw: Indicate whether this thread is not safe for work (default:
            ``None``).
        :param resources: Markdown formatted information that is useful for the live
            thread (default: ``None``).
        :param title: The title of the live thread (default: ``None``).

        Does nothing if no arguments are provided.

        Each setting will maintain its current value if ``None`` is specified.

        Additional keyword arguments can be provided to handle new settings as Reddit
        introduces them.

        Usage:

        .. code-block:: python

            thread = reddit.live("xyu8kmjvfrww")

            # update 'title' and 'nsfw'
            updated_thread = thread.contrib.update(title=new_title, nsfw=True)

        If Reddit introduces new settings, you must specify ``None`` for the setting you
        want to maintain:

        .. code-block:: python

            # update 'nsfw' and maintain new setting 'foo'
            thread.contrib.update(nsfw=True, foo=None)

        """
        settings = {
            "title": title,
            "description": description,
            "nsfw": nsfw,
            "resources": resources,
        }
        settings.update(other_settings)
        if all(value is None for value in settings.values()):
            return
        # get settings from Reddit (not cache)
        thread = LiveThread(self.thread._reddit, self.thread.id)
        data = {key: getattr(thread, key) if value is None else value for key, value in settings.items()}

        url = API_PATH["live_update_thread"].format(id=self.thread.id)
        # prawcore (0.7.0) Session.request() modifies `data` kwarg
        self.thread._reddit.post(url, data=data.copy())
        self.thread._reset_attributes(*data.keys())


class LiveThreadStream:
    """Provides a :class:`.LiveThread` stream.

    Usually used via:

    .. code-block:: python

        for live_update in reddit.live("ta535s1hq2je").stream.updates():
            print(live_update.body)

    """

    def __init__(self, live_thread: praw.models.LiveThread) -> None:
        """Initialize a :class:`.LiveThreadStream` instance.

        :param live_thread: The live thread associated with the stream.

        """
        self.live_thread = live_thread

    def updates(self, **stream_options: dict[str, Any]) -> Iterator[praw.models.LiveUpdate]:
        """Yield new updates to the live thread as they become available.

        :param skip_existing: Set to ``True`` to only fetch items created after the
            stream (default: ``False``).

        As with :meth:`.LiveThread.updates()`, updates are yielded as
        :class:`.LiveUpdate`.

        Updates are yielded oldest first. Up to 100 historical updates will initially be
        returned.

        Keyword arguments are passed to :func:`.stream_generator`.

        For example, to retrieve all new updates made to the ``"ta535s1hq2je"`` live
        thread, try:

        .. code-block:: python

            for live_update in reddit.live("ta535s1hq2je").stream.updates():
                print(live_update.body)

        To only retrieve new updates starting from when the stream is created, pass
        ``skip_existing=True``:

        .. code-block:: python

            live_thread = reddit.live("ta535s1hq2je")
            for live_update in live_thread.stream.updates(skip_existing=True):
                print(live_update.author)

        """
        return stream_generator(self.live_thread.updates, **stream_options)


class LiveUpdateContribution:
    """Provides a set of contribution functions to :class:`.LiveUpdate`."""

    def __init__(self, update: praw.models.LiveUpdate) -> None:
        """Initialize a :class:`.LiveUpdateContribution` instance.

        :param update: An instance of :class:`.LiveUpdate`.

        This instance can be retrieved through ``update.contrib`` where update is a
        :class:`.LiveUpdate` instance. E.g.,

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
            update.contrib  # LiveUpdateContribution instance
            update.contrib.remove()

        """
        self.update = update

    def remove(self) -> None:
        """Remove a live update.

        Usage:

        .. code-block:: python

            thread = reddit.live("ydwwxneu7vsa")
            update = thread["6854605a-efec-11e6-b0c7-0eafac4ff094"]
            update.contrib.remove()

        """
        url = API_PATH["live_remove_update"].format(id=self.update.thread.id)
        data = {"id": self.update.fullname}
        self.update.thread._reddit.post(url, data=data)

    def strike(self) -> None:
        """Strike a content of a live update.

        .. code-block:: python

            thread = reddit.live("xyu8kmjvfrww")
            update = thread["cb5fe532-dbee-11e6-9a91-0e6d74fabcc4"]
            update.contrib.strike()

        To check whether the update is stricken or not, use ``update.stricken``
        attribute.

        .. note::

            Accessing lazy attributes on updates (includes ``update.stricken``) may
            raise :py:class:`AttributeError`. See :class:`.LiveUpdate` for details.

        """
        url = API_PATH["live_strike"].format(id=self.update.thread.id)
        data = {"id": self.update.fullname}
        self.update.thread._reddit.post(url, data=data)


class LiveUpdate(FullnameMixin, RedditBase):
    """An individual :class:`.LiveUpdate` object.

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

    =============== ===================================================================
    Attribute       Description
    =============== ===================================================================
    ``author``      The :class:`.Redditor` who made the update.
    ``body``        Body of the update, as Markdown.
    ``body_html``   Body of the update, as HTML.
    ``created_utc`` The time the update was created, as `Unix Time`_.
    ``stricken``    A ``bool`` representing whether or not the update was stricken (see
                    :meth:`.strike`).
    =============== ===================================================================

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

    """

    STR_FIELD = "id"
    _kind = "LiveUpdate"

    @cachedproperty
    def contrib(self) -> praw.models.reddit.live.LiveUpdateContribution:
        """Provide an instance of :class:`.LiveUpdateContribution`.

        Usage:

        .. code-block:: python

            thread = reddit.live("ukaeu1ik4sw5")
            update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
            update.contrib  # LiveUpdateContribution instance

        """
        return LiveUpdateContribution(self)

    @property
    def thread(self) -> LiveThread:
        """Return :class:`.LiveThread` object the update object belongs to."""
        return self._thread

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

        Either ``thread_id`` and ``update_id``, or ``_data`` must be provided.

        :param reddit: An instance of :class:`.Reddit`.
        :param thread_id: A live thread ID, e.g., ``"ukaeu1ik4sw5"``.
        :param update_id: A live update ID, e.g.,
            ``"7827987a-c998-11e4-a0b9-22000b6a88d2"``.

        Usage:

        .. code-block:: python

            update = LiveUpdate(reddit, "ukaeu1ik4sw5", "7827987a-c998-11e4-a0b9-22000b6a88d2")
            update.thread  # LiveThread(id="ukaeu1ik4sw5")
            update.id  # "7827987a-c998-11e4-a0b9-22000b6a88d2"
            update.author  # "umbrae"

        """
        if _data is not None:
            # Since _data (part of JSON returned from reddit) have no thread ID,
            # self._thread must be set by the caller of LiveUpdate(). See the code of
            # LiveThread.updates() for example.
            super().__init__(reddit, _data=_data, _fetched=True)
        elif thread_id and update_id:
            self.id = update_id
            super().__init__(reddit, _data=None)
            self._thread = LiveThread(self._reddit, thread_id)
        else:
            msg = "Either 'thread_id' and 'update_id', or '_data' must be provided."
            raise TypeError(msg)

    def __setattr__(self, attribute: str, value: Any) -> None:
        """Objectify author."""
        if attribute == "author":
            value = Redditor(self._reddit, name=value)
        super().__setattr__(attribute, value)

    def _fetch(self) -> None:
        url = API_PATH["live_focus"].format(thread_id=self.thread.id, update_id=self.id)
        other = self._reddit.get(url)[0]
        self.__dict__.update(other.__dict__)
        super()._fetch()
