"""Provides classes for interacting with moderator notes."""

from __future__ import annotations

from itertools import islice
from typing import TYPE_CHECKING, Any, cast

from praw.const import API_PATH
from praw.models.base import PRAWBase
from praw.models.listing.generator import ListingGenerator
from praw.models.reddit.comment import Comment
from praw.models.reddit.submission import Submission

if TYPE_CHECKING:
    from collections.abc import Iterator

    import praw
    from praw import models
    from praw.models.reddit.redditor import Redditor
    from praw.models.reddit.subreddit import Subreddit


class BaseModNotes:
    """Provides base methods to interact with moderator notes."""

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

        :param reddit: An instance of :class:`.Reddit`.

        """
        self._reddit = reddit

    def _all_generator(
        self,
        redditor: Redditor | str,
        subreddit: Subreddit | str,
        **generator_kwargs: Any,
    ) -> Iterator[models.ModNote]:
        PRAWBase._safely_add_arguments(
            arguments=generator_kwargs,
            key="params",
            subreddit=subreddit,
            user=redditor,
        )
        return cast(
            "Iterator[models.ModNote]",
            ListingGenerator(self._reddit, API_PATH["mod_notes"], **generator_kwargs),
        )

    def _bulk_generator(
        self, redditors: list[Redditor | str], subreddits: list[Subreddit | str]
    ) -> Iterator[models.ModNote]:
        subreddits_iter = iter(subreddits)
        redditors_iter = iter(redditors)
        while True:
            subreddits_chunk = list(islice(subreddits_iter, 500))
            users_chunk = list(islice(redditors_iter, 500))
            if not any([subreddits_chunk, users_chunk]):
                break
            params: dict[str, str | int] = {
                "subreddits": ",".join(map(str, subreddits_chunk)),
                "users": ",".join(map(str, users_chunk)),
            }
            response = self._reddit.get(API_PATH["mod_notes_bulk"], params=params)
            for note_dict in response["mod_notes"]:
                yield cast("models.ModNote", self._reddit._objector.objectify(data=note_dict))

    def _ensure_attribute(self, error_message: str, **attributes: Any) -> Any:
        attribute, value_ = attributes.popitem()
        value = value_ or getattr(self, attribute, None)
        if value is None:
            raise TypeError(error_message)
        return value

    def _notes(
        self,
        *,
        all_notes: bool,
        redditors: list[Redditor | str],
        subreddits: list[Subreddit | str],
        **generator_kwargs: Any,
    ) -> Iterator[models.ModNote]:
        if all_notes:
            for subreddit in subreddits:
                for redditor in redditors:
                    yield from self._all_generator(redditor, subreddit, **generator_kwargs)
        else:
            yield from self._bulk_generator(redditors, subreddits)

    def create(
        self,
        *,
        label: str | None = None,
        note: str,
        redditor: Redditor | str | None = None,
        subreddit: Subreddit | str | None = None,
        thing: Comment | Submission | str | None = None,
        **other_settings: Any,
    ) -> models.ModNote:
        """Create a :class:`.ModNote` for a redditor in the specified subreddit.

        :param label: The label for the note. As of this writing, this can be one of the
            following: ``"ABUSE_WARNING"``, ``"BAN"``, ``"BOT_BAN"``,
            ``"HELPFUL_USER"``, ``"PERMA_BAN"``, ``"SOLID_CONTRIBUTOR"``,
            ``"SPAM_WARNING"``, ``"SPAM_WATCH"``, or ``None`` (default: ``None``).
        :param note: The content of the note. As of this writing, this is limited to 250
            characters.
        :param redditor: The redditor to create the note for (default: ``None``).

            .. note::

                This parameter is required if ``thing`` is not provided or this is not
                called from a :class:`.Redditor` instance (e.g.,
                ``reddit.redditor.notes``).

        :param subreddit: The subreddit associated with the note (default: ``None``).

            .. note::

                This parameter is required if ``thing`` is not provided or this is not
                called from a :class:`.Subreddit` instance (e.g.,
                ``reddit.subreddit.mod``).

        :param thing: Either the fullname of a comment/submission, a :class:`.Comment`,
            or a :class:`.Submission` to associate with the note.
        :param other_settings: Additional keyword arguments can be provided to handle
            new parameters as Reddit introduces them.

        :returns: The new :class:`.ModNote` object.

        For example, to create a note for u/spez in r/test:

        .. code-block:: python

            reddit.subreddit("test").mod.notes.create(
                label="HELPFUL_USER", note="Test note", redditor="spez"
            )
            # or
            reddit.redditor("spez").mod.notes.create(
                label="HELPFUL_USER", note="Test note", subreddit="test"
            )
            # or
            reddit.notes.create(
                label="HELPFUL_USER", note="Test note", redditor="spez", subreddit="test"
            )

        """
        reddit_id = None
        if thing:
            resolved: Comment | Submission | Subreddit | str = thing
            if isinstance(thing, str):
                reddit_id = thing
                # this is to minimize the number of requests
                if not (getattr(self, "redditor", redditor) and getattr(self, "subreddit", subreddit)):
                    # only fetch if we are missing either redditor or subreddit
                    resolved = next(self._reddit.info(fullnames=[thing]))
            else:
                reddit_id = thing.fullname
            # ``thing`` is only ever a comment/submission (or their fullname), so
            # ``resolved`` is a Comment/Submission here -- ``info`` resolves a fullname to
            # the same -- and both expose ``author`` and ``subreddit``.
            assert isinstance(resolved, (Comment, Submission))
            redditor = getattr(self, "redditor", redditor) or resolved.author
            subreddit = getattr(self, "subreddit", subreddit) or resolved.subreddit
        redditor = self._ensure_attribute(
            "Either the 'redditor' or 'thing' parameters must be provided or this"
            " method must be called from a Redditor instance (e.g., 'redditor.notes').",
            redditor=redditor,
        )
        subreddit = self._ensure_attribute(
            "Either the 'subreddit' or 'thing' parameters must be provided or this"
            " method must be called from a Subreddit instance (e.g.,"
            " 'subreddit.mod.notes').",
            subreddit=subreddit,
        )
        data = {
            "note": note,
            "subreddit": str(subreddit),
            "user": str(redditor),
        }
        if label:
            data["label"] = label
        if reddit_id:
            data["reddit_id"] = reddit_id
        data.update(other_settings)
        return self._reddit.post(API_PATH["mod_notes"], data=data)

    def delete(
        self,
        *,
        delete_all: bool = False,
        note_id: str | None = None,
        redditor: Redditor | str | None = None,
        subreddit: Subreddit | str | None = None,
    ) -> None:
        """Delete note(s) for a redditor.

        :param delete_all: When ``True``, delete all notes for the specified redditor in
            the specified subreddit (default: ``False``).

            .. note::

                This will make a request for each note.

        :param note_id: The ID of the note to delete. This parameter is ignored if
            ``delete_all`` is ``True``.
        :param redditor: The redditor to delete the note(s) for (default: ``None``). Can
            be a :class:`.Redditor` instance or a redditor name.

            .. note::

                This parameter is required if this method is **not** called from a
                :class:`.Redditor` instance (e.g., ``redditor.notes``).

        :param subreddit: The subreddit to delete the note(s) from (default: ``None``).
            Can be a :class:`.Subreddit` instance or a subreddit name.

            .. note::

                This parameter is required if this method is **not** called from a
                :class:`.Subreddit` instance (e.g., ``reddit.subreddit.mod``).


        For example, to delete a note with the ID
        ``"ModNote_d324b280-5ecc-435d-8159-3e259e84e339"``, try:

        .. code-block:: python

            reddit.subreddit("test").mod.notes.delete(
                note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339", redditor="spez"
            )
            # or
            reddit.redditor("spez").notes.delete(
                note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339", subreddit="test"
            )
            # or
            reddit.notes.delete(
                note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339",
                subreddit="test",
                redditor="spez",
            )

        To delete all notes for u/spez, try:

        .. code-block:: python

            reddit.subreddit("test").mod.notes.delete(delete_all=True, redditor="spez")
            # or
            reddit.redditor("spez").notes.delete(delete_all=True, subreddit="test")
            # or
            reddit.notes.delete(delete_all=True, subreddit="test", redditor="spez")

        """
        redditor_: Redditor | str = self._ensure_attribute(
            "Either the 'redditor' parameter must be provided or this method must be"
            " called from a Redditor instance (e.g., 'redditor.notes').",
            redditor=redditor,
        )
        subreddit_: Subreddit | str = self._ensure_attribute(
            "Either the 'subreddit' parameter must be provided or this method must be"
            " called from a Subreddit instance (e.g., 'subreddit.mod.notes').",
            subreddit=subreddit,
        )
        if not delete_all and note_id is None:
            msg = "Either 'note_id' or 'delete_all' must be provided."
            raise TypeError(msg)
        if delete_all:
            for note in self._notes(all_notes=True, redditors=[redditor_], subreddits=[subreddit_]):
                note.delete()
        else:
            params = {
                "note_id": note_id,
                "subreddit": str(subreddit_),
                "user": str(redditor_),
            }
            self._reddit.delete(API_PATH["mod_notes"], params=params)


class RedditModNotes(BaseModNotes):
    """Provides methods to interact with moderator notes at a global level.

    .. note::

        The authenticated user must be a moderator of the provided subreddit(s).

    For example, the latest note for u/spez in r/redditdev and r/test, and for u/bboe in
    r/redditdev can be iterated through like so:

    .. code-block:: python

        redditor = reddit.redditor("bboe")
        subreddit = reddit.subreddit("redditdev")

        pairs = [(subreddit, "spez"), ("test", "spez"), (subreddit, redditor)]

        for note in reddit.notes(pairs=pairs):
            print(f"{note.label}: {note.note}")

    """

    def __call__(
        self,
        *,
        all_notes: bool = False,
        pairs: list[tuple[Subreddit | str, Redditor | str]] | None = None,
        redditors: list[Redditor | str] | None = None,
        subreddits: list[Subreddit | str] | None = None,
        things: list[Comment | Submission] | None = None,
        **generator_kwargs: Any,
    ) -> Iterator[models.ModNote]:
        """Get note(s) for each subreddit/user pair, or ``None`` if they don't have any.

        :param all_notes: Whether to return all notes or only the latest note for each
            subreddit/redditor pair (default: ``False``).

            .. note::

                Setting this to ``True`` will result in a request for each unique
                subreddit/redditor pair. If ``subreddits`` and ``redditors`` are
                provided, this will make a request equivalent to number of redditors
                multiplied by the number of subreddits.

        :param pairs: A list of subreddit/redditor tuples.

            .. note::

                Required if ``subreddits``, ``redditors``, nor ``things`` are provided.

        :param redditors: A list of redditors to return notes for. This parameter is
            used in tandem with ``subreddits`` to get notes from multiple subreddits for
            each of the provided redditors.

            .. note::

                Required if ``items`` or ``things`` is not provided or if ``subreddits``
                **is** provided.

        :param subreddits: A list of subreddits to return notes for. This parameter is
            used in tandem with ``redditors`` to get notes for multiple redditors from
            each of the provided subreddits.

            .. note::

                Required if ``items`` or ``things`` is not provided or if ``redditors``
                **is** provided.

        :param things: A list of comments and/or submissions to return notes for.
        :param generator_kwargs: Additional keyword arguments passed to the generator.
            This parameter is ignored when ``all_notes`` is ``False``.

        :returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
            if the user doesn't have any notes) per entry in their relative order. If
            ``all_notes`` is ``True``, this will yield all notes for each entry.

        .. note::

            This method will merge the subreddits and redditors provided from ``pairs``,
            ``redditors``, ``subreddits``, and ``things``.

        .. note::

            This method accepts :class:`.Redditor` instances or redditor names and
            :class:`.Subreddit` instances or subreddit names where applicable.

        For example, the latest note for u/spez in r/redditdev and r/test, and for
        u/bboe in r/redditdev can be iterated through like so:

        .. code-block:: python

            redditor = reddit.redditor("bboe")
            subreddit = reddit.subreddit("redditdev")

            pairs = [(subreddit, "spez"), ("test", "spez"), (subreddit, redditor)]

            for note in reddit.notes(pairs=pairs):
                print(f"{note.label}: {note.note}")

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

        .. code-block:: python

            redditor = reddit.redditor("bboe")
            subreddit = reddit.subreddit("redditdev")

            for note in reddit.notes(
                redditors=["spez", redditor],
                subreddits=["announcements", subreddit, "test"],
                all_notes=True,
            ):
                print(f"{note.label}: {note.note}")

        For example, the latest note for the authors of the last 5 comments and
        submissions from r/test can be iterated through like so:

        .. code-block:: python

            submissions = list(reddit.subreddit("test").new(limit=5))
            comments = list(reddit.subreddit("test").comments(limit=5))

            for note in reddit.notes(things=submissions + comments):
                print(f"{note.label}: {note.note}")

        .. note::

            Setting ``all_notes`` to ``True`` will make a request for each redditor and
            subreddit combination. The previous example will make 6 requests.

        """
        if pairs is None:
            pairs = []
        if redditors is None:
            redditors = []
        if subreddits is None:
            subreddits = []
        if things is None:
            things = []
        if not (pairs + redditors + subreddits + things):
            msg = "Either the 'pairs', 'redditors', 'subreddits', or 'things' parameters must be provided."
            raise TypeError(msg)
        if len(redditors) * len(subreddits) == 0 and len(redditors) + len(subreddits) > 0:
            raise TypeError(
                "'redditors' must be non-empty if 'subreddits' is not empty."
                if len(subreddits) > 0
                else "'subreddits' must be non-empty if 'redditors' is not empty."
            )

        merged_redditors = []
        merged_subreddits = []
        items = pairs + [(subreddit, redditor) for redditor in set(redditors) for subreddit in set(subreddits)] + things

        for item in items:
            if isinstance(item, (Comment, Submission)):
                merged_redditors.append(item.author.name)
                merged_subreddits.append(item.subreddit.display_name)
            elif isinstance(item, tuple):
                subreddit, redditor = item
                merged_redditors.append(redditor)
                merged_subreddits.append(subreddit)
            else:
                msg = f"Cannot get subreddit and author fields from type {type(item)}"
                raise TypeError(msg)
        return self._notes(
            all_notes=all_notes,
            redditors=merged_redditors,
            subreddits=merged_subreddits,
            **generator_kwargs,
        )

    def things(
        self,
        *things: Comment | Submission,
        all_notes: bool | None = None,
        **generator_kwargs: Any,
    ) -> Iterator[models.ModNote]:
        """Return notes associated with the author of a :class:`.Comment` or :class:`.Submission`.

        :param things: One or more things to return notes on. Must be a
            :class:`.Comment` or :class:`.Submission`.
        :param all_notes: Whether to return all notes, or only the latest (default:
            ``True`` if only one thing is provided otherwise ``False``).

            .. note::

                Setting this to ``True`` will result in a request for each thing.


        :returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
            if the user doesn't have any notes) per entry in their relative order. If
            ``all_notes`` is ``True``, this will yield all notes for each entry.

        For example, to get the latest note for the authors of the top 25 submissions in
        r/test:

        .. code-block:: python

            submissions = reddit.subreddit("test").top(limit=25)
            for note in reddit.notes.things(*submissions):
                print(f"{note.label}: {note.note}")

        For example, to get the latest note for the authors of the last 25 comments in
        r/test:

        .. code-block:: python

            comments = reddit.subreddit("test").comments(limit=25)
            for note in reddit.notes.things(*comments):
                print(f"{note.label}: {note.note}")

        """
        subreddits = []
        redditors = []
        for thing in things:
            subreddits.append(thing.subreddit)
            redditors.append(thing.author)
        if all_notes is None:
            all_notes = len(things) == 1
        return self._notes(
            all_notes=all_notes,
            redditors=redditors,
            subreddits=subreddits,
            **generator_kwargs,
        )


class RedditorModNotes(BaseModNotes):
    """Provides methods to interact with moderator notes at the redditor level.

    .. note::

        The authenticated user must be a moderator of the provided subreddit(s).

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

    .. code-block:: python

        redditor = reddit.redditor("spez")

        for note in redditor.notes.subreddits("test"):
            print(f"{note.label}: {note.note}")

    """

    def __init__(self, reddit: praw.Reddit, redditor: Redditor | str) -> None:
        """Initialize a :class:`.RedditorModNotes` instance.

        :param reddit: An instance of :class:`.Reddit`.
        :param redditor: An instance of :class:`.Redditor`.

        """
        super().__init__(reddit)
        self.redditor = redditor

    def subreddits(
        self,
        *subreddits: Subreddit | str,
        all_notes: bool | None = None,
        **generator_kwargs: Any,
    ) -> Iterator[models.ModNote]:
        """Return notes for this :class:`.Redditor` from one or more subreddits.

        :param subreddits: One or more subreddits to retrieve the notes from. Must be
            either a :class:`.Subreddit` or a subreddit name.
        :param all_notes: Whether to return all notes or only the latest note (default:
            ``True`` if only one subreddit is provided otherwise ``False``).

            .. note::

                Setting this to ``True`` will result in a request for each subreddit.


        :returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
            if this redditor doesn't have any notes) per subreddit in their relative
            order. If ``all_notes`` is ``True``, this will yield all notes or ``None``
            from each subreddit for this redditor.

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

        .. code-block:: python

            redditor = reddit.redditor("spez")

            for note in redditor.notes.subreddits("test"):
                print(f"{note.label}: {note.note}")

        For example, the latest note for u/spez from r/test and r/redditdev can be
        iterated through like so:

        .. code-block:: python

            redditor = reddit.redditor("spez")
            subreddit = reddit.subreddit("redditdev")

            for note in redditor.notes.subreddits("test", subreddit):
                print(f"{note.label}: {note.note}")

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

        .. code-block:: python

            redditor = reddit.redditor("spez")
            subreddit = reddit.subreddit("redditdev")

            for note in redditor.notes.subreddits("test", subreddit, all_notes=True):
                print(f"{note.label}: {note.note}")

        """
        if len(subreddits) == 0:
            msg = "At least 1 subreddit must be provided."
            raise ValueError(msg)
        if all_notes is None:
            all_notes = len(subreddits) == 1
        return self._notes(
            all_notes=all_notes,
            redditors=[self.redditor] * len(subreddits),
            subreddits=list(subreddits),
            **generator_kwargs,
        )


class SubredditModNotes(BaseModNotes):
    """Provides methods to interact with moderator notes at the subreddit level.

    .. note::

        The authenticated user must be a moderator of 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}")

    """

    def __init__(self, reddit: praw.Reddit, subreddit: Subreddit | str) -> None:
        """Initialize a :class:`.SubredditModNotes` instance.

        :param reddit: An instance of :class:`.Reddit`.
        :param subreddit: An instance of :class:`.Subreddit`.

        """
        super().__init__(reddit)
        self.subreddit = subreddit

    def redditors(
        self,
        *redditors: Redditor | str,
        all_notes: bool | None = None,
        **generator_kwargs: Any,
    ) -> Iterator[models.ModNote]:
        """Return notes from this :class:`.Subreddit` for one or more redditors.

        :param redditors: One or more redditors to retrieve notes for. Must be either a
            :class:`.Redditor` or a redditor name.
        :param all_notes: Whether to return all notes or only the latest note (default:
            ``True`` if only one redditor is provided otherwise ``False``).

            .. note::

                Setting this to ``True`` will result in a request for each redditor.


        :returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
            if the user doesn't have any notes in this subreddit) per redditor in their
            relative order. If ``all_notes`` is ``True``, this will yield all notes for
            each redditor.

        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}")

        For example, the latest note for u/spez and u/bboe from r/test can be iterated
        through like so:

        .. code-block:: python

            subreddit = reddit.subreddit("test")
            redditor = reddit.redditor("bboe")

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

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

        .. code-block:: python

            subreddit = reddit.subreddit("test")
            redditor = reddit.redditor("bboe")

            for note in subreddit.mod.notes.redditors("spez", redditor, all_notes=True):
                print(f"{note.label}: {note.note}")

        """
        if len(redditors) == 0:
            msg = "At least 1 redditor must be provided."
            raise ValueError(msg)
        if all_notes is None:
            all_notes = len(redditors) == 1
        return self._notes(
            all_notes=all_notes,
            redditors=list(redditors),
            subreddits=[self.subreddit] * len(redditors),
            **generator_kwargs,
        )
