"""Provide the Subreddit class."""

from __future__ import annotations

import contextlib
from copy import deepcopy
from csv import writer
from io import StringIO
from json import dumps, loads
from pathlib import Path
from typing import TYPE_CHECKING, Any
from urllib.parse import urljoin

import websocket
from defusedxml import ElementTree
from prawcore import Redirect
from prawcore.exceptions import ServerError
from requests.exceptions import HTTPError

from praw.const import API_PATH, JPEG_HEADER
from praw.exceptions import (
    ClientException,
    InvalidFlairTemplateID,
    MediaPostFailed,
    RedditAPIException,
    TooLargeMediaException,
    WebSocketException,
)
from praw.models.listing.generator import ListingGenerator
from praw.models.listing.mixins import SubredditListingMixin
from praw.models.reddit.base import RedditBase
from praw.models.reddit.emoji import SubredditEmoji
from praw.models.reddit.mixins import FullnameMixin, MessageableMixin
from praw.models.reddit.modmail import ModmailConversation
from praw.models.reddit.removal_reasons import SubredditRemovalReasons
from praw.models.reddit.rules import SubredditRules
from praw.models.reddit.widgets import SubredditWidgets, WidgetEncoder
from praw.models.reddit.wikipage import WikiPage
from praw.models.util import permissions_string, stream_generator
from praw.util import cachedproperty

if TYPE_CHECKING:
    from collections.abc import Generator, Iterator

    from requests import Response

    import praw.models


class Modmail:
    """Provides modmail functions for a :class:`.Subreddit`.

    For example, to send a new modmail from r/test to u/spez with the subject ``"test"``
    along with a message body of ``"hello"``:

    .. code-block:: python

        reddit.subreddit("test").modmail.create(subject="test", body="hello", recipient="spez")

    """

    def __call__(self, id: str | None = None, *, mark_read: bool = False) -> ModmailConversation:
        """Return an individual conversation.

        :param id: A reddit base36 conversation ID, e.g., ``"2gmz"``.
        :param mark_read: If ``True``, conversation is marked as read (default:
            ``False``).

        For example:

        .. code-block:: python

            reddit.subreddit("test").modmail("2gmz", mark_read=True)

        To print all messages from a conversation as Markdown source:

        .. code-block:: python

            conversation = reddit.subreddit("test").modmail("2gmz", mark_read=True)
            for message in conversation.messages:
                print(message.body_markdown)

        ``ModmailConversation.user`` is a special instance of :class:`.Redditor` with
        extra attributes describing the non-moderator user's recent posts, comments, and
        modmail messages within the subreddit, as well as information on active bans and
        mutes. This attribute does not exist on internal moderator discussions.

        For example, to print the user's ban status:

        .. code-block:: python

            conversation = reddit.subreddit("test").modmail("2gmz", mark_read=True)
            print(conversation.user.ban_status)

        To print a list of recent submissions by the user:

        .. code-block:: python

            conversation = reddit.subreddit("test").modmail("2gmz", mark_read=True)
            print(conversation.user.recent_posts)

        """
        return ModmailConversation(self.subreddit._reddit, id=id, mark_read=mark_read)

    def __init__(self, subreddit: praw.models.Subreddit) -> None:
        """Initialize a :class:`.Modmail` instance."""
        self.subreddit = subreddit

    def _build_subreddit_list(self, other_subreddits: list[praw.models.Subreddit] | None) -> str:
        """Return a comma-separated list of subreddit display names."""
        subreddits = [self.subreddit] + (other_subreddits or [])
        return ",".join(str(subreddit) for subreddit in subreddits)

    def bulk_read(
        self,
        *,
        other_subreddits: list[praw.models.Subreddit | str] | None = None,
        state: str | None = None,
    ) -> list[ModmailConversation]:
        """Mark conversations for subreddit(s) as read.

        .. note::

            Due to server-side restrictions, r/all is not a valid subreddit for this
            method. Instead, use :meth:`~.Modmail.subreddits` to get a list of
            subreddits using the new modmail.

        :param other_subreddits: A list of :class:`.Subreddit` instances for which to
            mark conversations (default: ``None``).
        :param state: Can be one of: ``"all"``, ``"archived"``, or ``"highlighted"``,
            ``"inprogress"``, ``"join_requests"``, ``"mod"``, ``"new"``,
            ``"notifications"``, or ``"appeals"`` (default: ``"all"``). ``"all"`` does
            not include internal, archived, or appeals conversations.

        :returns: A list of :class:`.ModmailConversation` instances that were marked
            read.

        For example, to mark all notifications for a subreddit as read:

        .. code-block:: python

            subreddit = reddit.subreddit("test")
            subreddit.modmail.bulk_read(state="notifications")

        """
        params = {"entity": self._build_subreddit_list(other_subreddits)}
        if state:
            params["state"] = state
        response = self.subreddit._reddit.post(API_PATH["modmail_bulk_read"], params=params)
        return [self(conversation_id) for conversation_id in response["conversation_ids"]]

    def conversations(
        self,
        *,
        other_subreddits: list[praw.models.Subreddit] | None = None,
        sort: str | None = None,
        state: str | None = None,
        **generator_kwargs: Any,
    ) -> Iterator[ModmailConversation]:
        """Generate :class:`.ModmailConversation` objects for subreddit(s).

        :param other_subreddits: A list of :class:`.Subreddit` instances for which to
            fetch conversations (default: ``None``).
        :param sort: Can be one of: ``"mod"``, ``"recent"``, ``"unread"``, or ``"user"``
            (default: ``"recent"``).
        :param state: Can be one of: ``"all"``, ``"archived"``, ``"highlighted"``,
            ``"inprogress"``, ``"join_requests"``, ``"mod"``, ``"new"``,
            ``"notifications"``, or ``"appeals"`` (default: ``"all"``). ``"all"`` does
            not include internal, archived, or appeals conversations.

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

        For example:

        .. code-block:: python

            conversations = reddit.subreddit("all").modmail.conversations(state="mod")

        """
        params = {}
        if self.subreddit != "all":
            params["entity"] = self._build_subreddit_list(other_subreddits)
        Subreddit._safely_add_arguments(arguments=generator_kwargs, key="params", sort=sort, state=state, **params)
        return ListingGenerator(
            self.subreddit._reddit,
            API_PATH["modmail_conversations"],
            **generator_kwargs,
        )

    def create(
        self,
        *,
        author_hidden: bool = False,
        body: str,
        recipient: str | praw.models.Redditor,
        subject: str,
    ) -> ModmailConversation:
        """Create a new :class:`.ModmailConversation`.

        :param author_hidden: When ``True``, author is hidden from non-moderators
            (default: ``False``).
        :param body: The message body. Cannot be empty.
        :param recipient: The recipient; a username or an instance of
            :class:`.Redditor`.
        :param subject: The message subject. Cannot be empty.

        :returns: A :class:`.ModmailConversation` object for the newly created
            conversation.

        .. code-block:: python

            subreddit = reddit.subreddit("test")
            redditor = reddit.redditor("bboe")
            subreddit.modmail.create(subject="Subject", body="Body", recipient=redditor)

        """
        data = {
            "body": body,
            "isAuthorHidden": author_hidden,
            "srName": self.subreddit,
            "subject": subject,
            "to": recipient,
        }
        return self.subreddit._reddit.post(API_PATH["modmail_conversations"], data=data)

    def subreddits(self) -> Iterator[praw.models.Subreddit]:
        """Yield subreddits using the new modmail that the user moderates.

        For example:

        .. code-block:: python

            subreddits = reddit.subreddit("all").modmail.subreddits()

        """
        response = self.subreddit._reddit.get(API_PATH["modmail_subreddits"])
        for value in response["subreddits"].values():
            subreddit = self.subreddit._reddit.subreddit(value["display_name"])
            subreddit.last_updated = value["lastUpdated"]
            yield subreddit

    def unread_count(self) -> dict[str, int]:
        """Return unread conversation count by conversation state.

        At time of writing, possible states are: ``"archived"``, ``"highlighted"``,
        ``"inprogress"``, ``"join_requests"``, ``"mod"``, ``"new"``,
        ``"notifications"``, or ``"appeals"``.

        :returns: A dict mapping conversation states to unread counts.

        For example, to print the count of unread moderator discussions:

        .. code-block:: python

            subreddit = reddit.subreddit("test")
            unread_counts = subreddit.modmail.unread_count()
            print(unread_counts["mod"])

        """
        return self.subreddit._reddit.get(API_PATH["modmail_unread_count"])


class SubredditFilters:
    """Provide functions to interact with the special :class:`.Subreddit`'s filters.

    Members of this class should be utilized via :meth:`.Subreddit.filters`. For
    example, to add a filter, run:

    .. code-block:: python

        reddit.subreddit("all").filters.add("test")

    """

    def __init__(self, subreddit: praw.models.Subreddit) -> None:
        """Initialize a :class:`.SubredditFilters` instance.

        :param subreddit: The special subreddit whose filters to work with.

        As of this writing filters can only be used with the special subreddits ``all``
        and ``mod``.

        """
        self.subreddit = subreddit

    def __iter__(self) -> Iterator[praw.models.Subreddit]:
        """Iterate through the special :class:`.Subreddit`'s filters.

        This method should be invoked as:

        .. code-block:: python

            for subreddit in reddit.subreddit("test").filters:
                ...

        """
        url = API_PATH["subreddit_filter_list"].format(special=self.subreddit, user=self.subreddit._reddit.user.me())
        params = {"unique": self.subreddit._reddit._next_unique}
        response_data = self.subreddit._reddit.get(url, params=params)
        yield from response_data.subreddits

    def add(self, subreddit: praw.models.Subreddit | str) -> None:
        """Add ``subreddit`` to the list of filtered subreddits.

        :param subreddit: The subreddit to add to the filter list.

        Items from subreddits added to the filtered list will no longer be included when
        obtaining listings for r/all.

        Alternatively, you can filter a subreddit temporarily from a special listing in
        a manner like so:

        .. code-block:: python

            reddit.subreddit("all-redditdev-learnpython")

        :raises: ``prawcore.NotFound`` when calling on a non-special subreddit.

        """
        url = API_PATH["subreddit_filter"].format(
            special=self.subreddit,
            user=self.subreddit._reddit.user.me(),
            subreddit=subreddit,
        )
        self.subreddit._reddit.put(url, data={"model": dumps({"name": str(subreddit)})})

    def remove(self, subreddit: praw.models.Subreddit | str) -> None:
        """Remove ``subreddit`` from the list of filtered subreddits.

        :param subreddit: The subreddit to remove from the filter list.

        :raises: ``prawcore.NotFound`` when calling on a non-special subreddit.

        """
        url = API_PATH["subreddit_filter"].format(
            special=self.subreddit,
            user=self.subreddit._reddit.user.me(),
            subreddit=str(subreddit),
        )
        self.subreddit._reddit.delete(url)


class SubredditFlair:
    """Provide a set of functions to interact with a :class:`.Subreddit`'s flair."""

    @cachedproperty
    def link_templates(
        self,
    ) -> praw.models.reddit.subreddit.SubredditLinkFlairTemplates:
        """Provide an instance of :class:`.SubredditLinkFlairTemplates`.

        Use this attribute for interacting with a :class:`.Subreddit`'s link flair
        templates. For example to list all the link flair templates for a subreddit
        which you have the ``flair`` moderator permission on try:

        .. code-block:: python

            for template in reddit.subreddit("test").flair.link_templates:
                print(template)

        """
        return SubredditLinkFlairTemplates(self.subreddit)

    @cachedproperty
    def templates(
        self,
    ) -> praw.models.reddit.subreddit.SubredditRedditorFlairTemplates:
        """Provide an instance of :class:`.SubredditRedditorFlairTemplates`.

        Use this attribute for interacting with a :class:`.Subreddit`'s flair templates.
        For example to list all the flair templates for a subreddit which you have the
        ``flair`` moderator permission on try:

        .. code-block:: python

            for template in reddit.subreddit("test").flair.templates:
                print(template)

        """
        return SubredditRedditorFlairTemplates(self.subreddit)

    def __call__(
        self,
        redditor: praw.models.Redditor | str | None = None,
        **generator_kwargs: Any,
    ) -> Iterator[praw.models.Redditor]:
        """Return a :class:`.ListingGenerator` for Redditors and their flairs.

        :param redditor: When provided, yield at most a single :class:`.Redditor`
            instance (default: ``None``).

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

        Usage:

        .. code-block:: python

            for flair in reddit.subreddit("test").flair(limit=None):
                print(flair)

        """
        Subreddit._safely_add_arguments(arguments=generator_kwargs, key="params", name=redditor)
        generator_kwargs.setdefault("limit", None)
        url = API_PATH["flairlist"].format(subreddit=self.subreddit)
        return ListingGenerator(self.subreddit._reddit, url, **generator_kwargs)

    def __init__(self, subreddit: praw.models.Subreddit) -> None:
        """Initialize a :class:`.SubredditFlair` instance.

        :param subreddit: The subreddit whose flair to work with.

        """
        self.subreddit = subreddit

    def configure(
        self,
        *,
        link_position: str = "left",
        link_self_assign: bool = False,
        position: str = "right",
        self_assign: bool = False,
        **settings: Any,
    ) -> None:
        """Update the :class:`.Subreddit`'s flair configuration.

        :param link_position: One of ``"left"``, ``"right"``, or ``False`` to disable
            (default: ``"left"``).
        :param link_self_assign: Permit self assignment of link flair (default:
            ``False``).
        :param position: One of ``"left"``, ``"right"``, or ``False`` to disable
            (default: ``"right"``).
        :param self_assign: Permit self assignment of user flair (default: ``False``).

        .. code-block:: python

            subreddit = reddit.subreddit("test")
            subreddit.flair.configure(link_position="right", self_assign=True)

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

        """
        data = {
            "flair_enabled": bool(position),
            "flair_position": position or "right",
            "flair_self_assign_enabled": self_assign,
            "link_flair_position": link_position or "",
            "link_flair_self_assign_enabled": link_self_assign,
        }
        data.update(settings)
        url = API_PATH["flairconfig"].format(subreddit=self.subreddit)
        self.subreddit._reddit.post(url, data=data)

    def delete(self, redditor: praw.models.Redditor | str) -> None:
        """Delete flair for a :class:`.Redditor`.

        :param redditor: A redditor name or :class:`.Redditor` instance.

        .. seealso::

            :meth:`~.SubredditFlair.update` to delete the flair of many redditors at
            once.

        """
        url = API_PATH["deleteflair"].format(subreddit=self.subreddit)
        self.subreddit._reddit.post(url, data={"name": str(redditor)})

    def delete_all(self) -> list[dict[str, str | bool | dict[str, str]]]:
        """Delete all :class:`.Redditor` flair in the :class:`.Subreddit`.

        :returns: List of dictionaries indicating the success or failure of each delete.

        """
        return self.update(x["user"] for x in self())

    def set(
        self,
        redditor: praw.models.Redditor | str,
        *,
        css_class: str = "",
        flair_template_id: str | None = None,
        text: str = "",
    ) -> None:
        """Set flair for a :class:`.Redditor`.

        :param redditor: A redditor name or :class:`.Redditor` instance.
        :param text: The flair text to associate with the :class:`.Redditor` or
            :class:`.Submission` (default: ``""``).
        :param css_class: The css class to associate with the flair html (default:
            ``""``). Use either this or ``flair_template_id``.
        :param flair_template_id: The ID of the flair template to be used (default:
            ``None``). Use either this or ``css_class``.

        This method can only be used by an authenticated user who is a moderator of the
        associated :class:`.Subreddit`.

        For example:

        .. code-block:: python

            reddit.subreddit("test").flair.set("bboe", text="PRAW author", css_class="mods")
            template = "6bd28436-1aa7-11e9-9902-0e05ab0fad46"
            reddit.subreddit("test").flair.set(
                "spez", text="Reddit CEO", flair_template_id=template
            )

        """
        if css_class and flair_template_id is not None:
            msg = "Parameter 'css_class' cannot be used in conjunction with 'flair_template_id'."
            raise TypeError(msg)
        data = {"name": str(redditor), "text": text}
        if flair_template_id is not None:
            data["flair_template_id"] = flair_template_id
            url = API_PATH["select_flair"].format(subreddit=self.subreddit)
        else:
            data["css_class"] = css_class
            url = API_PATH["flair"].format(subreddit=self.subreddit)
        self.subreddit._reddit.post(url, data=data)

    def update(
        self,
        flair_list: Iterator[str | praw.models.Redditor | dict[str, str | praw.models.Redditor]],
        *,
        text: str = "",
        css_class: str = "",
    ) -> list[dict[str, str | bool | dict[str, str]]]:
        """Set or clear the flair for many redditors at once.

        :param flair_list: Each item in this list should be either:

            - The name of a redditor.
            - An instance of :class:`.Redditor`.
            - A dictionary mapping keys ``"user"``, ``"flair_text"``, and
              ``"flair_css_class"`` to their respective values. The ``"user"`` key
              should map to a redditor, as described above. When a dictionary isn't
              provided, or the dictionary is missing either ``"flair_text"`` or
              ``"flair_css_class"`` keys, the default values will come from the other
              arguments.
        :param css_class: The css class to use when not explicitly provided in
            ``flair_list`` (default: ``""``).
        :param text: The flair text to use when not explicitly provided in
            ``flair_list`` (default: ``""``).

        :returns: List of dictionaries indicating the success or failure of each update.

        For example, to clear the flair text, and set the ``"praw"`` flair css class on
        a few users try:

        .. code-block:: python

            subreddit.flair.update(["bboe", "spez", "spladug"], css_class="praw")

        """
        temp_lines = StringIO()
        for item in flair_list:
            if isinstance(item, dict):
                writer(temp_lines).writerow([
                    str(item["user"]),
                    item.get("flair_text", text),
                    item.get("flair_css_class", css_class),
                ])
            else:
                writer(temp_lines).writerow([str(item), text, css_class])

        lines = temp_lines.getvalue().splitlines()
        temp_lines.close()
        response = []
        url = API_PATH["flaircsv"].format(subreddit=self.subreddit)
        while lines:
            data = {"flair_csv": "\n".join(lines[:100])}
            response.extend(self.subreddit._reddit.post(url, data=data))
            lines = lines[100:]
        return response


class SubredditFlairTemplates:
    """Provide functions to interact with a :class:`.Subreddit`'s flair templates."""

    @staticmethod
    def flair_type(*, is_link: bool) -> str:
        """Return ``"LINK_FLAIR"`` or ``"USER_FLAIR"`` depending on ``is_link`` value."""
        return "LINK_FLAIR" if is_link else "USER_FLAIR"

    def __init__(self, subreddit: praw.models.Subreddit) -> None:
        """Initialize a :class:`.SubredditFlairTemplates` instance.

        :param subreddit: The subreddit whose flair templates to work with.

        .. note::

            This class should not be initialized directly. Instead, obtain an instance
            via: ``reddit.subreddit("test").flair.templates`` or
            ``reddit.subreddit("test").flair.link_templates``.

        """
        self.subreddit = subreddit

    def __iter__(self) -> Iterator[None]:
        """Abstract method to return flair templates."""
        raise NotImplementedError

    def _add(
        self,
        *,
        allowable_content: str | None = None,
        background_color: str | None = None,
        css_class: str = "",
        is_link: bool | None = None,
        max_emojis: int | None = None,
        mod_only: bool | None = None,
        text: str,
        text_color: str | None = None,
        text_editable: bool = False,
    ) -> None:
        url = API_PATH["flairtemplate_v2"].format(subreddit=self.subreddit)
        data = {
            "allowable_content": allowable_content,
            "background_color": background_color,
            "css_class": css_class,
            "flair_type": self.flair_type(is_link=is_link),
            "max_emojis": max_emojis,
            "mod_only": bool(mod_only),
            "text": text,
            "text_color": text_color,
            "text_editable": bool(text_editable),
        }
        self.subreddit._reddit.post(url, data=data)

    def _clear(self, *, is_link: bool | None = None) -> None:
        url = API_PATH["flairtemplateclear"].format(subreddit=self.subreddit)
        self.subreddit._reddit.post(url, data={"flair_type": self.flair_type(is_link=is_link)})

    def _reorder(self, flair_list: list, *, is_link: bool | None = None) -> None:
        url = API_PATH["flairtemplatereorder"].format(subreddit=self.subreddit)
        self.subreddit._reddit.patch(
            url,
            params={
                "flair_type": self.flair_type(is_link=is_link),
                "subreddit": self.subreddit.display_name,
            },
            json=flair_list,
        )

    def delete(self, template_id: str) -> None:
        """Remove a flair template provided by ``template_id``.

        For example, to delete the first :class:`.Redditor` flair template listed, try:

        .. code-block:: python

            template_info = list(subreddit.flair.templates)[0]
            subreddit.flair.templates.delete(template_info["id"])

        """
        url = API_PATH["flairtemplatedelete"].format(subreddit=self.subreddit)
        self.subreddit._reddit.post(url, data={"flair_template_id": template_id})

    def update(
        self,
        template_id: str,
        *,
        allowable_content: str | None = None,
        background_color: str | None = None,
        css_class: str | None = None,
        fetch: bool = True,
        max_emojis: int | None = None,
        mod_only: bool | None = None,
        text: str | None = None,
        text_color: str | None = None,
        text_editable: bool | None = None,
    ) -> None:
        """Update the flair template provided by ``template_id``.

        :param template_id: The flair template to update. If not valid then an exception
            will be thrown.
        :param allowable_content: If specified, most be one of ``"all"``, ``"emoji"``,
            or ``"text"`` to restrict content to that type. If set to ``"emoji"`` then
            the ``"text"`` param must be a valid emoji string, for example,
            ``":snoo:"``.
        :param background_color: The flair template's new background color, as a hex
            color.
        :param css_class: The flair template's new css_class (default: ``""``).
        :param fetch: Whether PRAW will fetch existing information on the existing flair
            before updating (default: ``True``).
        :param max_emojis: Maximum emojis in the flair (Reddit defaults this value to
            ``10``).
        :param mod_only: Indicate if the flair can only be used by moderators.
        :param text: The flair template's new text.
        :param text_color: The flair template's new text color, either ``"light"`` or
            ``"dark"``.
        :param text_editable: Indicate if the flair text can be modified for each
            redditor that sets it (default: ``False``).

        .. warning::

            If parameter ``fetch`` is set to ``False``, all parameters not provided will
            be reset to their default (``None`` or ``False``) values.

        For example, to make a user flair template text editable, try:

        .. code-block:: python

            template_info = list(subreddit.flair.templates)[0]
            subreddit.flair.templates.update(
                template_info["id"],
                text=template_info["flair_text"],
                text_editable=True,
            )

        """
        url = API_PATH["flairtemplate_v2"].format(subreddit=self.subreddit)
        data = {
            "allowable_content": allowable_content,
            "background_color": background_color,
            "css_class": css_class,
            "flair_template_id": template_id,
            "max_emojis": max_emojis,
            "mod_only": mod_only,
            "text": text,
            "text_color": text_color,
            "text_editable": text_editable,
        }
        if fetch:
            existing_data_ = [template for template in iter(self) if template["id"] == template_id]
            if len(existing_data_) != 1:
                raise InvalidFlairTemplateID(template_id)
            existing_data = existing_data_[0]
            for key, value in existing_data.items():
                if data.get(key) is None:
                    data[key] = value
        self.subreddit._reddit.post(url, data=data)


class SubredditModeration:
    """Provides a set of moderation functions to a :class:`.Subreddit`.

    For example, to accept a moderation invite from r/test:

    .. code-block:: python

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

    """

    @staticmethod
    def _handle_only(*, generator_kwargs: dict[str, Any], only: str | None) -> None:
        if only is not None:
            if only == "submissions":
                only = "links"
            RedditBase._safely_add_arguments(arguments=generator_kwargs, key="params", only=only)

    @cachedproperty
    def notes(self) -> praw.models.SubredditModNotes:
        """Provide an instance of :class:`.SubredditModNotes`.

        This provides an interface for managing moderator notes for this subreddit.

        For example, all the notes for u/spez in r/test can be iterated through like so:

        .. code-block:: python

            subreddit = reddit.subreddit("test")

            for note in subreddit.mod.notes.redditors("spez"):
                print(f"{note.label}: {note.note}")

        """
        from praw.models.mod_notes import SubredditModNotes  # noqa: PLC0415

        return SubredditModNotes(self.subreddit._reddit, subreddit=self.subreddit)

    @cachedproperty
    def removal_reasons(self) -> SubredditRemovalReasons:
        """Provide an instance of :class:`.SubredditRemovalReasons`.

        Use this attribute for interacting with a :class:`.Subreddit`'s removal reasons.
        For example to list all the removal reasons for a subreddit which you have the
        ``posts`` moderator permission on, try:

        .. code-block:: python

            for removal_reason in reddit.subreddit("test").mod.removal_reasons:
                print(removal_reason)

        A single removal reason can be lazily retrieved via:

        .. code-block:: python

            reddit.subreddit("test").mod.removal_reasons["reason_id"]

        .. note::

            Attempting to access attributes of an nonexistent removal reason will result
            in a :class:`.ClientException`.

        """
        return SubredditRemovalReasons(self.subreddit)

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

        Streams can be used to indefinitely retrieve Moderator only items from
        :class:`.SubredditModeration` made to moderated subreddits, like:

        .. code-block:: python

            for log in reddit.subreddit("mod").mod.stream.log():
                print(f"Mod: {log.mod}, Subreddit: {log.subreddit}")

        """
        return SubredditModerationStream(self.subreddit)

    def __init__(self, subreddit: praw.models.Subreddit) -> None:
        """Initialize a :class:`.SubredditModeration` instance.

        :param subreddit: The subreddit to moderate.

        """
        self.subreddit = subreddit
        self._stream = None

    def accept_invite(self) -> None:
        """Accept an invitation as a moderator of the community."""
        url = API_PATH["accept_mod_invite"].format(subreddit=self.subreddit)
        self.subreddit._reddit.post(url)

    def edited(
        self, *, only: str | None = None, **generator_kwargs: Any
    ) -> Iterator[praw.models.Comment | praw.models.Submission]:
        """Return a :class:`.ListingGenerator` for edited comments and submissions.

        :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
            only results of that type.

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

        To print all items in the edited queue try:

        .. code-block:: python

            for item in reddit.subreddit("mod").mod.edited(limit=None):
                print(item)

        """
        self._handle_only(generator_kwargs=generator_kwargs, only=only)
        return ListingGenerator(
            self.subreddit._reddit,
            API_PATH["about_edited"].format(subreddit=self.subreddit),
            **generator_kwargs,
        )

    def log(
        self,
        *,
        action: str | None = None,
        mod: praw.models.Redditor | str | None = None,
        **generator_kwargs: Any,
    ) -> Iterator[praw.models.ModAction]:
        """Return a :class:`.ListingGenerator` for moderator log entries.

        :param action: If given, only return log entries for the specified action.
        :param mod: If given, only return log entries for actions made by the passed in
            redditor.

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

        To print the moderator and subreddit of the last 5 modlog entries try:

        .. code-block:: python

            for log in reddit.subreddit("mod").mod.log(limit=5):
                print(f"Mod: {log.mod}, Subreddit: {log.subreddit}")

        """
        params = {"mod": str(mod) if mod else mod, "type": action}
        Subreddit._safely_add_arguments(arguments=generator_kwargs, key="params", **params)
        return ListingGenerator(
            self.subreddit._reddit,
            API_PATH["about_log"].format(subreddit=self.subreddit),
            **generator_kwargs,
        )

