# SPDX-License-Identifier: MIT

from __future__ import annotations

from typing import TYPE_CHECKING, List, Optional, Union

from .appinfo import PartialAppInfo
from .asset import Asset
from .enums import ChannelType, InviteTarget, InviteType, NSFWLevel, VerificationLevel, try_enum
from .guild_scheduled_event import GuildScheduledEvent
from .mixins import Hashable
from .object import Object
from .utils import _get_as_snowflake, parse_time, snowflake_time
from .welcome_screen import WelcomeScreen

__all__ = (
    "PartialInviteChannel",
    "PartialInviteGuild",
    "Invite",
)

if TYPE_CHECKING:
    import datetime

    from typing_extensions import Self

    from .abc import GuildChannel
    from .guild import Guild
    from .state import ConnectionState
    from .types.channel import (
        GroupInviteRecipient as GroupInviteRecipientPayload,
        InviteChannel as InviteChannelPayload,
    )
    from .types.gateway import InviteCreateEvent, InviteDeleteEvent
    from .types.guild import GuildFeature
    from .types.invite import Invite as InvitePayload, InviteGuild as InviteGuildPayload
    from .user import User

    GatewayInvitePayload = Union[InviteCreateEvent, InviteDeleteEvent]
    InviteGuildType = Union[Guild, "PartialInviteGuild", Object]
    InviteChannelType = Union[GuildChannel, "PartialInviteChannel", Object]


class PartialInviteChannel:
    """Represents a "partial" invite channel.

    This model will be given when the user is not part of the
    guild the :class:`Invite` resolves to.


    .. 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.

            .. versionchanged:: 2.5
                if the channel is of type :attr:`ChannelType.group`,
                returns the name that's rendered by the official client.

    Attributes
    ----------
    name: Optional[:class:`str`]
        The partial channel's name.
    id: :class:`int`
        The partial channel's ID.
    type: :class:`ChannelType`
        The partial channel's type.
    """

    __slots__ = (
        "id",
        "name",
        "type",
        "_recipients",
        "_icon",
        "_state",
    )

    def __init__(self, *, state: ConnectionState, data: InviteChannelPayload) -> None:
        self._state = state
        self.id: int = int(data["id"])
        self.name: Optional[str] = data.get("name")
        self.type: ChannelType = try_enum(ChannelType, data["type"])
        if self.type is ChannelType.group:
            self._recipients: List[GroupInviteRecipientPayload] = data.get("recipients", [])
        else:
            self._recipients = []
        self._icon: Optional[str] = data.get("icon")

    def __str__(self) -> str:
        if self.name:
            return self.name
        if self.type is ChannelType.group:
            return ", ".join([recipient["username"] for recipient in self._recipients]) or "Unnamed"
        return ""

    def __repr__(self) -> str:
        return f"<PartialInviteChannel id={self.id} name={self.name} type={self.type!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)

    @property
    def icon(self) -> Optional[Asset]:
        """Optional[:class:`Asset`]: Returns the channel's icon asset if available.

        .. versionadded:: 2.6
        """
        if self._icon is None:
            return None
        return Asset._from_icon(self._state, self.id, self._icon, path="channel")


class PartialInviteGuild:
    """Represents a "partial" invite guild.

    This model will be given when the user is not part of the
    guild the :class:`Invite` resolves to.

    .. collapse:: operations

        .. describe:: x == y

            Checks if two partial guilds are the same.

        .. describe:: x != y

            Checks if two partial guilds are not the same.

        .. describe:: hash(x)

            Return the partial guild's hash.

        .. describe:: str(x)

            Returns the partial guild's name.

    Attributes
    ----------
    name: :class:`str`
        The partial guild's name.
    id: :class:`int`
        The partial guild's ID.
    description: Optional[:class:`str`]
        The partial guild's description.
    features: List[:class:`str`]
        A list of features the partial guild has. See :attr:`Guild.features` for more information.
    nsfw_level: :class:`NSFWLevel`
        The partial guild's nsfw level.

        .. versionadded:: 2.4

    vanity_url_code: Optional[:class:`str`]
        The partial guild's vanity url code, if any.

        .. versionadded:: 2.4

    verification_level: :class:`VerificationLevel`
        The partial guild's verification level.
    premium_subscription_count: :class:`int`
        The number of "boosts" this guild currently has.

        .. versionadded:: 2.5
    """

    __slots__ = (
        "_state",
        "features",
        "_icon",
        "_banner",
        "id",
        "name",
        "_splash",
        "description",
        "nsfw_level",
        "vanity_url_code",
        "verification_level",
        "premium_subscription_count",
    )

    def __init__(self, state: ConnectionState, data: InviteGuildPayload, id: int) -> None:
        self._state: ConnectionState = state
        self.id: int = id
        self.name: str = data["name"]
        self.features: List[GuildFeature] = data.get("features", [])
        self._icon: Optional[str] = data.get("icon")
        self._banner: Optional[str] = data.get("banner")
        self._splash: Optional[str] = data.get("splash")
        self.nsfw_level: NSFWLevel = try_enum(NSFWLevel, data.get("nsfw_level", 0))
        self.vanity_url_code: Optional[str] = data.get("vanity_url_code")
        self.verification_level: VerificationLevel = try_enum(
            VerificationLevel, data.get("verification_level")
        )
        self.description: Optional[str] = data.get("description")
        self.premium_subscription_count: int = data.get("premium_subscription_count") or 0

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

    def __repr__(self) -> str:
        return (
            f"<{self.__class__.__name__} id={self.id} name={self.name!r} features={self.features} "
            f"description={self.description!r}>"
        )

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

    @property
    def icon(self) -> Optional[Asset]:
        """Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
        if self._icon is None:
            return None
        return Asset._from_guild_icon(self._state, self.id, self._icon)

    @property
    def banner(self) -> Optional[Asset]:
        """Optional[:class:`Asset`]: Returns the guild's banner asset, if available."""
        if self._banner is None:
            return None
        return Asset._from_banner(self._state, self.id, self._banner)

    @property
    def splash(self) -> Optional[Asset]:
        """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available."""
        if self._splash is None:
            return None
        return Asset._from_guild_image(self._state, self.id, self._splash, path="splashes")


class Invite(Hashable):
    """Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.

    Depending on the way this object was created, some of the attributes can
    have a value of ``None`` (see table below).

    .. collapse:: operations

        .. describe:: x == y

            Checks if two invites are equal.

        .. describe:: x != y

            Checks if two invites are not equal.

        .. describe:: hash(x)

            Returns the invite hash.

        .. describe:: str(x)

            Returns the invite URL.

    .. _invite_attr_table:

    The following table illustrates what methods will obtain the attributes:

    +------------------------------------+---------------------------------------------------------------------+
    |             Attribute              |                          Method                                     |
    +====================================+=====================================================================+
    | :attr:`max_age`                    | :meth:`abc.GuildChannel.invites`\\, :meth:`Guild.invites`            |
    +------------------------------------+---------------------------------------------------------------------+
    | :attr:`max_uses`                   | :meth:`abc.GuildChannel.invites`\\, :meth:`Guild.invites`            |
    +------------------------------------+---------------------------------------------------------------------+
    | :attr:`created_at`                 | :meth:`abc.GuildChannel.invites`\\, :meth:`Guild.invites`            |
    +------------------------------------+---------------------------------------------------------------------+
    | :attr:`temporary`                  | :meth:`abc.GuildChannel.invites`\\, :meth:`Guild.invites`            |
    +------------------------------------+---------------------------------------------------------------------+
    | :attr:`uses`                       | :meth:`abc.GuildChannel.invites`\\, :meth:`Guild.invites`            |
    +------------------------------------+---------------------------------------------------------------------+
    | :attr:`approximate_member_count`   | :meth:`Client.fetch_invite` with ``with_counts`` enabled            |
    +------------------------------------+---------------------------------------------------------------------+
    | :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` with ``with_counts`` enabled            |
    +------------------------------------+---------------------------------------------------------------------+
    | :attr:`expires_at`                 | :meth:`Client.fetch_invite` with ``with_expiration`` enabled        |
    +------------------------------------+---------------------------------------------------------------------+
    | :attr:`guild_scheduled_event`      | :meth:`Client.fetch_invite` with valid ``guild_scheduled_event_id`` |
    |                                    | or valid event ID in URL or invite object                           |
    +------------------------------------+---------------------------------------------------------------------+

    If something is not in the table above, then it's available by all methods.

    Attributes
    ----------
    code: :class:`str`
        The URL fragment used for the invite.
    type: :class:`InviteType`
        The type of the invite.

        .. versionadded:: 2.10

    guild: Optional[Union[:class:`Guild`, :class:`Object`, :class:`PartialInviteGuild`]]
        The guild the invite is for. Can be ``None`` if it's not a guild invite (see :attr:`type`).
    max_age: Optional[:class:`int`]
        How long before the invite expires in seconds.
        A value of ``0`` indicates that it doesn't expire.

        Optional according to the :ref:`table <invite_attr_table>` above.
    max_uses: Optional[:class:`int`]
        How many times the invite can be used.
        A value of ``0`` indicates that it has unlimited uses.

        Optional according to the :ref:`table <invite_attr_table>` above.
    created_at: Optional[:class:`datetime.datetime`]
        An aware UTC datetime object denoting the time the invite was created.

        Optional according to the :ref:`table <invite_attr_table>` above.
    temporary: Optional[:class:`bool`]
        Whether the invite grants temporary membership.
        If ``True``, members who joined via this invite will be kicked upon disconnect.

        Optional according to the :ref:`table <invite_attr_table>` above.
    uses: Optional[:class:`int`]
        How many times the invite has been used.

        Optional according to the :ref:`table <invite_attr_table>` above.
    approximate_member_count: Optional[:class:`int`]
        The approximate number of members in the guild.

        Optional according to the :ref:`table <invite_attr_table>` above.
    approximate_presence_count: Optional[:class:`int`]
        The approximate number of members currently active in the guild.
        This includes idle, dnd, online, and invisible members. Offline members are excluded.

        Optional according to the :ref:`table <invite_attr_table>` above.
    expires_at: Optional[:class:`datetime.datetime`]
        The expiration date of the invite. If the value is ``None`` when received through
        :meth:`Client.fetch_invite` with ``with_expiration`` enabled, the invite will never expire.

        .. versionadded:: 2.0

    inviter: Optional[:class:`User`]
        The user who created the invite, if any.

        This is ``None`` in vanity invites, for example.
    channel: Optional[Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`]]
        The channel the invite is for.
    target_type: :class:`InviteTarget`
        The type of target for the voice channel invite.

        .. versionadded:: 2.0

    target_user: Optional[:class:`User`]
        The user whose stream to display for this invite, if any.

        .. versionadded:: 2.0

    target_application: Optional[:class:`PartialAppInfo`]
        The embedded application the invite targets, if any.

        .. versionadded:: 2.0

    guild_scheduled_event: Optional[:class:`GuildScheduledEvent`]
        The guild scheduled event included in the invite, if any.

        .. versionadded:: 2.3

    guild_welcome_screen: Optional[:class:`WelcomeScreen`]
        The partial guild's welcome screen, if any.

        .. versionadded:: 2.5
    """

    __slots__ = (
        "max_age",
        "code",
        "type",
        "guild",
        "created_at",
        "uses",
        "temporary",
        "max_uses",
        "inviter",
        "channel",
        "target_user",
        "target_type",
        "approximate_member_count",
        "approximate_presence_count",
        "target_application",
        "expires_at",
        "guild_scheduled_event",
        "guild_welcome_screen",
        "_state",
    )

    BASE = "https://discord.gg"

    def __init__(
        self,
        *,
        state: ConnectionState,
        data: Union[InvitePayload, GatewayInvitePayload],
        guild: Optional[Union[PartialInviteGuild, Guild]] = None,
        channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
    ) -> None:
        self._state: ConnectionState = state
        self.code: str = data["code"]
        self.type: InviteType = try_enum(InviteType, data.get("type", 0))
        self.guild: Optional[InviteGuildType] = self._resolve_guild(data.get("guild"), guild)

        self.max_age: Optional[int] = data.get("max_age")
        self.max_uses: Optional[int] = data.get("max_uses")
        self.created_at: Optional[datetime.datetime] = parse_time(data.get("created_at"))
        self.temporary: Optional[bool] = data.get("temporary")
        self.uses: Optional[int] = data.get("uses")
        self.approximate_presence_count: Optional[int] = data.get("approximate_presence_count")
        self.approximate_member_count: Optional[int] = data.get("approximate_member_count")

        expires_at = data.get("expires_at", None)
        self.expires_at: Optional[datetime.datetime] = (
            parse_time(expires_at) if expires_at else None
        )

        inviter_data = data.get("inviter")
        self.inviter: Optional[User] = None if inviter_data is None else self._state.create_user(inviter_data)  # type: ignore

        self.channel: Optional[InviteChannelType] = self._resolve_channel(
            data.get("channel"), channel
        )

        # this is stored here due to disnake.Guild not storing a welcome screen
        # if it was stored on the Guild object, we would be throwing away this data from the api request
        if (
            self.guild is not None
            and (guild_data := data.get("guild"))
            and "welcome_screen" in guild_data
        ):
            self.guild_welcome_screen: Optional[WelcomeScreen] = WelcomeScreen(
                state=self._state,
                data=guild_data["welcome_screen"],
                guild=self.guild,  # type: ignore
            )
        else:
            self.guild_welcome_screen: Optional[WelcomeScreen] = None

        target_user_data = data.get("target_user")
        self.target_user: Optional[User] = None if target_user_data is None else self._state.create_user(target_user_data)  # type: ignore

        self.target_type: InviteTarget = try_enum(InviteTarget, data.get("target_type", 0))

        application = data.get("target_application")
        self.target_application: Optional[PartialAppInfo] = (
            PartialAppInfo(data=application, state=state) if application else None
        )

        if scheduled_event := data.get("guild_scheduled_event"):
            self.guild_scheduled_event: Optional[GuildScheduledEvent] = GuildScheduledEvent(
                state=state, data=scheduled_event
            )
        else:
            self.guild_scheduled_event: Optional[GuildScheduledEvent] = None

    @classmethod
    def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self:
        guild: Optional[Union[Guild, PartialInviteGuild]]
        try:
            guild_data = data["guild"]
        except KeyError:
            # If we're here, then this is a group DM
            guild = None
        else:
            guild_id = int(guild_data["id"])
            guild = state._get_guild(guild_id)
            if guild is None:
                # If it's not cached, then it has to be a partial guild
                guild = PartialInviteGuild(state, guild_data, guild_id)

        channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None
        if channel_data := data.get("channel"):
            channel = PartialInviteChannel(data=channel_data, state=state)
            if guild is not None and not isinstance(guild, PartialInviteGuild):
                # Upgrade the partial data if applicable
                channel = guild.get_channel(channel.id) or channel

        return cls(state=state, data=data, guild=guild, channel=channel)

    @classmethod
    def from_gateway(cls, *, state: ConnectionState, data: GatewayInvitePayload) -> Self:
        guild_id: Optional[int] = _get_as_snowflake(data, "guild_id")
        guild: Optional[Union[Guild, Object]] = state._get_guild(guild_id)
        channel_id = int(data["channel_id"])
        if guild is not None:
            channel = guild.get_channel(channel_id) or Object(id=channel_id)
        else:
            guild = Object(id=guild_id) if guild_id is not None else None
            channel = Object(id=channel_id)

        return cls(
            state=state,
            data=data,
            # objects may be partial due to missing cache
            guild=guild,  # type: ignore
            channel=channel,  # type: ignore
        )

    def _resolve_guild(
        self,
        data: Optional[InviteGuildPayload],
        guild: Optional[Union[Guild, PartialInviteGuild]] = None,
    ) -> Optional[InviteGuildType]:
        if guild is not None:
            return guild

        if data is None:
            return None

        guild_id = int(data["id"])
        return PartialInviteGuild(self._state, data, guild_id)

    def _resolve_channel(
        self,
        data: Optional[InviteChannelPayload],
        channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
    ) -> Optional[InviteChannelType]:
        if channel is not None:
            return channel

        if data is None:
            return None

        return PartialInviteChannel(data=data, state=self._state)

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

    def __repr__(self) -> str:
        s = f"<Invite code={self.code!r} type={self.type!r}"
        if self.type is InviteType.guild:
            s += f" guild={self.guild!r} online={self.approximate_presence_count}"
        if self.type is not InviteType.friend:
            s += f" members={self.approximate_member_count}"
        s += ">"
        return s

    def __hash__(self) -> int:
        return hash(self.code)

    @property
    def id(self) -> str:
        """:class:`str`: Returns the proper code portion of the invite."""
        return self.code

    @property
    def url(self) -> str:
        """:class:`str`: A property that retrieves the invite URL."""
        url = f"{self.BASE}/{self.code}"
        if self.guild_scheduled_event:
            url += f"?event={self.guild_scheduled_event.id}"
        return url

    async def delete(self, *, reason: Optional[str] = None) -> None:
        """|coro|

        Revokes the instant invite.

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

        Parameters
        ----------
        reason: Optional[:class:`str`]
            The reason for deleting this invite. Shows up on the audit log.

        Raises
        ------
        Forbidden
            You do not have permissions to revoke invites.
        NotFound
            The invite is invalid or expired.
        HTTPException
            Revoking the invite failed.
        """
        await self._state.http.delete_invite(self.code, reason=reason)
