# SPDX-License-Identifier: MIT

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from .activity import BaseActivity, Spotify, create_activity
from .asset import Asset
from .enums import Status, WidgetStyle, try_enum
from .invite import Invite
from .user import BaseUser
from .utils import MISSING, _get_as_snowflake, resolve_invite, snowflake_time

if TYPE_CHECKING:
    import datetime

    from .abc import GuildChannel, Snowflake
    from .guild import Guild
    from .state import ConnectionState
    from .types.widget import (
        Widget as WidgetPayload,
        WidgetMember as WidgetMemberPayload,
        WidgetSettings as WidgetSettingsPayload,
    )

__all__ = (
    "WidgetChannel",
    "WidgetMember",
    "WidgetSettings",
    "Widget",
)


class WidgetChannel:
    """Represents a "partial" widget channel.

    .. collapse:: operations

        .. describe:: x == y

            Checks if two partial channels are the same.

        .. describe:: x != y

            Checks if two partial channels are not the same.

        .. describe:: hash(x)

            Return the partial channel's hash.

        .. describe:: str(x)

            Returns the partial channel's name.

    Attributes
    ----------
    id: :class:`int`
        The channel's ID.
    name: :class:`str`
        The channel's name.
    position: :class:`int`
        The channel's position
    """

    __slots__ = ("id", "name", "position")

    def __init__(self, id: int, name: str, position: int) -> None:
        self.id: int = id
        self.name: str = name
        self.position: int = position

    def __str__(self) -> str:
        return self.name

    def __repr__(self) -> str:
        return f"<WidgetChannel id={self.id} name={self.name!r} position={self.position!r}>"

    @property
    def mention(self) -> str:
        """:class:`str`: The string that allows you to mention the channel."""
        return f"<#{self.id}>"

    @property
    def created_at(self) -> datetime.datetime:
        """:class:`datetime.datetime`: Returns the channel's creation time in UTC."""
        return snowflake_time(self.id)


class WidgetMember(BaseUser):
    """Represents a "partial" member of the widget's guild.

    .. collapse:: operations

        .. describe:: x == y

            Checks if two widget members are the same.

        .. describe:: x != y

            Checks if two widget members are not the same.

        .. describe:: hash(x)

            Return the widget member's hash.

        .. describe:: str(x)

            Returns the widget member's name.

    Attributes
    ----------
    id: :class:`int`
        The member's anonymized ID.
    name: :class:`str`
        The member's name.
    discriminator: :class:`str`
        The member's anonymized discriminator.

        .. note::
            This is being phased out by Discord; the username system is moving away from ``username#discriminator``
            to users having a globally unique username.
            See the `help article <https://dis.gd/app-usernames>`__ for details.
    status: :class:`Status`
        The member's status.
    activity: Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]
        The member's activity. This generally only has the ``name`` set.
    deafened: Optional[:class:`bool`]
        Whether the member is currently deafened.
    muted: Optional[:class:`bool`]
        Whether the member is currently muted.
    suppress: Optional[:class:`bool`]
        Whether the member is currently being suppressed.
    connected_channel: Optional[:class:`WidgetChannel`]
        Which channel the member is connected to.
    """

    __slots__ = (
        "status",
        "activity",
        "deafened",
        "suppress",
        "muted",
        "connected_channel",
        "_avatar_url",
    )

    def __init__(
        self,
        *,
        state: ConnectionState,
        data: WidgetMemberPayload,
        connected_channel: Optional[WidgetChannel] = None,
    ) -> None:
        super().__init__(state=state, data=data)
        self.status: Status = try_enum(Status, data.get("status"))
        self.deafened: Optional[bool] = data.get("deaf", False) or data.get("self_deaf", False)
        self.muted: Optional[bool] = data.get("mute", False) or data.get("self_mute", False)
        self.suppress: Optional[bool] = data.get("suppress", False)
        self._avatar_url: Optional[str] = data.get("avatar_url")

        self.activity: Optional[Union[BaseActivity, Spotify]] = None
        if activity := (data.get("activity") or data.get("game")):
            self.activity = create_activity(activity, state=state)

        self.connected_channel: Optional[WidgetChannel] = connected_channel

    def __repr__(self) -> str:
        return f"<WidgetMember name={self.name!r} discriminator={self.discriminator!r}"

    # overwrite base type's @property since widget members always seem to have `avatar: null`,
    # and instead a separate `avatar_url` field with a full url
    @property
    def avatar(self) -> Optional[Asset]:
        """Optional[:class:`Asset`]: The user's avatar.
        The size can be chosen using :func:`Asset.with_size`, however the format is always
        static and cannot be changed through :func:`Asset.with_format` or similar methods.
        """
        if (url := self._avatar_url) is not None:
            return Asset(self._state, url=url, key=url, animated=False)
        return None

    @property
    def display_name(self) -> str:
        """:class:`str`: Returns the member's name."""
        return self.name


class WidgetSettings:
    """Represents a :class:`Guild`'s widget settings.

    .. versionadded:: 2.5

    Attributes
    ----------
    guild: :class:`Guild`
        The widget's guild.
    enabled: :class:`bool`
        Whether the widget is enabled.
    channel_id: Optional[:class:`int`]
        The widget channel ID. If set, an invite link for this channel will be generated,
        which allows users to join the guild from the widget.
    """

    __slots__ = ("_state", "guild", "enabled", "channel_id")

    def __init__(
        self, *, state: ConnectionState, guild: Guild, data: WidgetSettingsPayload
    ) -> None:
        self._state: ConnectionState = state
        self.guild: Guild = guild
        self.enabled: bool = data["enabled"]
        self.channel_id: Optional[int] = _get_as_snowflake(data, "channel_id")

    def __repr__(self) -> str:
        return f"<WidgetSettings enabled={self.enabled!r} channel_id={self.channel_id!r} guild={self.guild!r}>"

    @property
    def channel(self) -> Optional[GuildChannel]:
        """Optional[:class:`abc.GuildChannel`]: The widget channel, if set."""
        return self.guild.get_channel(self.channel_id) if self.channel_id is not None else None

    async def edit(
        self,
        *,
        enabled: bool = MISSING,
        channel: Optional[Snowflake] = MISSING,
        reason: Optional[str] = None,
    ) -> WidgetSettings:
        """|coro|

        Edits the widget.

        You must have :attr:`~Permissions.manage_guild` permission to
        do this.

        Parameters
        ----------
        enabled: :class:`bool`
            Whether to enable the widget.
        channel: Optional[:class:`~disnake.abc.Snowflake`]
            The new widget channel. Pass ``None`` to remove the widget channel.
            If set, an invite link for this channel will be generated,
            which allows users to join the guild from the widget.
        reason: Optional[:class:`str`]
            The reason for editing the widget. Shows up on the audit log.

        Raises
        ------
        Forbidden
            You do not have permission to edit the widget.
        HTTPException
            Editing the widget failed.

        Returns
        -------
        :class:`WidgetSettings`
            The new widget settings.
        """
        return await self.guild.edit_widget(enabled=enabled, channel=channel, reason=reason)


class Widget:
    """Represents a :class:`Guild` widget.

    .. collapse:: operations

        .. describe:: x == y

            Checks if two widgets are the same.

        .. describe:: x != y

            Checks if two widgets are not the same.

        .. describe:: str(x)

            Returns the widget's JSON URL.

    Attributes
    ----------
    id: :class:`int`
        The guild's ID.
    name: :class:`str`
        The guild's name.
    channels: List[:class:`WidgetChannel`]
        The publicly accessible voice and stage channels in the guild.
    members: List[:class:`WidgetMember`]
        The online members in the server. Offline members
        do not appear in the widget.

        .. note::

            Due to a Discord limitation, if this data is available
            the users will be "anonymized" with linear IDs.
            Likewise, the number of members retrieved is capped.

    presence_count: :class:`int`
        The number of online members in the server.

        .. versionadded:: 2.6
    """

    __slots__ = ("_state", "channels", "_invite", "id", "members", "name", "presence_count")

    def __init__(self, *, state: ConnectionState, data: WidgetPayload) -> None:
        self._state = state
        self._invite = data.get("instant_invite")
        self.name: str = data["name"]
        self.id: int = int(data["id"])
        self.presence_count: int = data["presence_count"]

        self.channels: List[WidgetChannel] = []
        for channel in data.get("channels", []):
            _id = int(channel["id"])
            self.channels.append(
                WidgetChannel(id=_id, name=channel["name"], position=channel["position"])
            )

        self.members: List[WidgetMember] = []
        channels = {channel.id: channel for channel in self.channels}
        for member in data.get("members", []):
            connected_channel = _get_as_snowflake(member, "channel_id")
            if connected_channel in channels:
                connected_channel = channels[connected_channel]
            elif connected_channel:
                connected_channel = WidgetChannel(id=connected_channel, name="", position=0)

            self.members.append(WidgetMember(state=self._state, data=member, connected_channel=connected_channel))  # type: ignore

    def __str__(self) -> str:
        return self.json_url

    def __eq__(self, other: Any) -> bool:
        if isinstance(other, Widget):
            return self.id == other.id
        return False

    def __repr__(self) -> str:
        return (
            f"<Widget id={self.id} name={self.name!r}"
            f" invite_url={self.invite_url!r} presence_count={self.presence_count!r}>"
        )

    @property
    def created_at(self) -> datetime.datetime:
        """:class:`datetime.datetime`: Returns the member's creation time in UTC."""
        return snowflake_time(self.id)

    @property
    def json_url(self) -> str:
        """:class:`str`: The JSON URL of the widget."""
        return f"https://discord.com/api/guilds/{self.id}/widget.json"

    @property
    def invite_url(self) -> Optional[str]:
        """Optional[:class:`str`]: The invite URL for the guild, if available."""
        return self._invite

    async def fetch_invite(self, *, with_counts: bool = True) -> Optional[Invite]:
        """|coro|

        Retrieves an :class:`Invite` from the widget's invite URL.
        This is the same as :meth:`Client.fetch_invite`; the invite
        code is abstracted away.

        .. versionchanged:: 2.6
            This may now return ``None`` if the widget does not have
            an attached invite URL.

        Parameters
        ----------
        with_counts: :class:`bool`
            Whether to include count information in the invite. This fills the
            :attr:`Invite.approximate_member_count` and :attr:`Invite.approximate_presence_count`
            fields.

        Returns
        -------
        Optional[:class:`Invite`]
            The invite from the widget's invite URL, if available.
        """
        if not self._invite:
            return None

        invite_id = resolve_invite(self._invite)
        data = await self._state.http.get_invite(invite_id, with_counts=with_counts)
        return Invite.from_incomplete(state=self._state, data=data)

    async def edit(
        self,
        *,
        enabled: bool = MISSING,
        channel: Optional[Snowflake] = MISSING,
        reason: Optional[str] = None,
    ) -> None:
        """|coro|

        Edits the widget.

        You must have :attr:`~Permissions.manage_guild` permission to
        do this

        .. versionadded:: 2.4

        Parameters
        ----------
        enabled: :class:`bool`
            Whether to enable the widget.
        channel: Optional[:class:`~disnake.abc.Snowflake`]
            The new widget channel. Pass ``None`` to remove the widget channel.
        reason: Optional[:class:`str`]
            The reason for editing the widget. Shows up on the audit log.

        Raises
        ------
        Forbidden
            You do not have permission to edit the widget.
        HTTPException
            Editing the widget failed.
        """
        payload: Dict[str, Any] = {}
        if enabled is not MISSING:
            payload["enabled"] = enabled
        if channel is not MISSING:
            payload["channel_id"] = None if channel is None else channel.id

        await self._state.http.edit_widget(self.id, payload, reason=reason)

    def image_url(self, style: WidgetStyle = WidgetStyle.shield) -> str:
        """Returns an URL to the widget's .png image.

        .. versionadded:: 2.5

        Parameters
        ----------
        style: :class:`WidgetStyle`
            The widget style.

        Returns
        -------
        :class:`str`
            The widget image URL.

        """
        return self._state.http.widget_image_url(self.id, style=str(style))
