#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright © Nekoka.tt 2019-2020
#
# This file is part of Hikari.
#
# Hikari is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Hikari is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Hikari. If not, see <https://www.gnu.org/licenses/>.
"""Components and entities related to discord's Oauth2 flow."""

from __future__ import annotations

__all__ = [
    "Application",
    "ApplicationOwner",
    "ConnectionVisibility",
    "OAuth2Scope",
    "OwnConnection",
    "OwnGuild",
    "Team",
    "TeamMember",
    "TeamMembershipState",
]

import typing

import attr

from hikari import bases
from hikari import guilds
from hikari import permissions
from hikari import users
from hikari.internal import marshaller
from hikari.internal import more_enums
from hikari.internal import urls

if typing.TYPE_CHECKING:
    from hikari.internal import more_typing


@more_enums.must_be_unique
class OAuth2Scope(str, more_enums.Enum):
    """OAuth2 Scopes that Discord allows.

    These are categories of permissions for applications using the OAuth2 API
    directly. Most users will only ever need the `BOT` scope when developing
    bots.
    """

    ACTIVITIES_READ = "activities.read"
    """Enable the app to fetch a user's "Now Playing/Recently Played" list.

    !!! note:
        You must be whitelisted to use this scope.
    """

    ACTIVITIES_WRITE = "activities.write"
    """Enable the app to update a user's activity.

    !!! note:
        You must be whitelisted to use this scope.

    !!! note:
        This is not required to use the GameSDK activity manager.
    """

    APPLICATIONS_BUILDS_READ = "applications.builds.read"
    """Enable the app to read build data for a user's applications.

    !!! note:
        You must be whitelisted to use this scope.
    """

    APPLICATIONS_BUILDS_UPLOAD = "applications.builds.upload"
    """Enable the app to upload/update builds for a user's applications.

    !!! note:
        You must be whitelisted to use this scope.
    """

    APPLICATIONS_ENTITLEMENTS = "applications.entitlements"
    """Enable the app to read entitlements for a user's applications."""

    APPLICATIONS_STORE_UPDATE = "applications.store.update"
    """Enable the app to read and update store data for the user's applications.

    This includes store listings, achievements, SKU's, etc.

    !!! note:
        The store API is deprecated and may be removed in the future.
    """

    BOT = "bot"
    """Used to add OAuth2 bots to a guild.

    !!! note:
        This requires you to have set up a bot account for your application.
    """

    CONNECTIONS = "connections"
    """Enable the app to view third-party linked accounts such as Twitch."""

    EMAIL = "email"
    """Enable the app to view the user's email and application info."""

    GROUP_DM_JOIN = "gdm.join"
    """Enable the application to join users into a group DM."""

    GUILDS = "guilds"
    """Enable the app to view the guilds the user is in."""

    GUILDS_JOIN = "guilds.join"
    """Enable the app to add the user to a specific guild.

    !!! note:
        This requires you to have set up a bot account for your application.
    """

    IDENTIFY = "identify"
    """Enable the app to view info about itself.

    !!! note:
        This does not include email address info. Use the `EMAIL` scope instead
        to retrieve this information.
    """

    RELATIONSHIPS_READ = "relationships.read"
    """Enable the app to view a user's friend list.

    !!! note:
        You must be whitelisted to use this scope.
    """

    RPC = "rpc"
    """Enable the RPC app to control the local user's Discord client.

    !!! note:
        You must be whitelisted to use this scope.
    """

    RPC_API = "rpc.api"
    """Enable the RPC app to access the RPC API as the local user.

    !!! note:
        You must be whitelisted to use this scope.
    """

    RPC_MESSAGES_READ = "messages.read"
    """Enable the RPC app to read messages from all channels the user is in."""

    RPC_NOTIFICATIONS_READ = "rpc.notifications.read"
    """Enable the RPC app to read  from all channels the user is in.

    !!! note:
        You must be whitelisted to use this scope.
    """

    WEBHOOK_INCOMING = "webhook.incoming"
    """Used to generate a webhook that is returned in the OAuth2 token response.

    This is used during authorization code grants.
    """


@more_enums.must_be_unique
class ConnectionVisibility(int, more_enums.Enum):
    """Describes who can see a connection with a third party account."""

    NONE = 0
    """Only you can see the connection."""

    EVERYONE = 1
    """Everyone can see the connection."""


def _deserialize_integrations(
    payload: more_typing.JSONArray, **kwargs: typing.Any
) -> typing.Sequence[guilds.GuildIntegration]:
    return [guilds.PartialGuildIntegration.deserialize(integration, **kwargs) for integration in payload]


@marshaller.marshallable()
@attr.s(slots=True, kw_only=True)
class OwnConnection(bases.HikariEntity, marshaller.Deserializable):
    """Represents a user's connection with a third party account.

    Returned by the `GET Current User Connections` endpoint.
    """

    id: str = marshaller.attrib(deserializer=str)
    """The string ID of the third party connected account.

    !!! warning
        Seeing as this is a third party ID, it will not be a snowflake.
    """

    name: str = marshaller.attrib(deserializer=str, repr=True)
    """The username of the connected account."""

    type: str = marshaller.attrib(deserializer=str, repr=True)
    """The type of service this connection is for."""

    is_revoked: bool = marshaller.attrib(raw_name="revoked", deserializer=bool, if_undefined=False, default=False)
    """Whether the connection has been revoked."""

    integrations: typing.Sequence[guilds.PartialGuildIntegration] = marshaller.attrib(
        deserializer=_deserialize_integrations, if_undefined=list, factory=list, inherit_kwargs=True,
    )
    """A sequence of the partial guild integration objects this connection has."""

    is_verified: bool = marshaller.attrib(raw_name="verified", deserializer=bool)
    """Whether the connection has been verified."""

    is_friend_syncing: bool = marshaller.attrib(raw_name="friend_sync", deserializer=bool)
    """Whether friends should be added based on this connection."""

    is_showing_activity: bool = marshaller.attrib(raw_name="show_activity", deserializer=bool)
    """Whether this connection's activities are shown in the user's presence."""

    visibility: ConnectionVisibility = marshaller.attrib(deserializer=ConnectionVisibility, repr=True)
    """The visibility of the connection."""


@marshaller.marshallable()
@attr.s(slots=True, kw_only=True)
class OwnGuild(guilds.PartialGuild):
    """Represents a user bound partial guild object."""

    is_owner: bool = marshaller.attrib(raw_name="owner", deserializer=bool, repr=True)
    """Whether the current user owns this guild."""

    my_permissions: permissions.Permission = marshaller.attrib(
        raw_name="permissions", deserializer=permissions.Permission
    )
    """The guild level permissions that apply to the current user or bot."""


@more_enums.must_be_unique
class TeamMembershipState(int, more_enums.Enum):
    """Represents the state of a user's team membership."""

    INVITED = 1
    """Denotes the user has been invited to the team but has yet to accept."""

    ACCEPTED = 2
    """Denotes the user has accepted the invite and is now a member."""


@marshaller.marshallable()
@attr.s(slots=True, kw_only=True)
class TeamMember(bases.HikariEntity, marshaller.Deserializable):
    """Represents a member of a Team."""

    membership_state: TeamMembershipState = marshaller.attrib(deserializer=TeamMembershipState)
    """The state of this user's membership."""

    permissions: typing.Set[str] = marshaller.attrib(deserializer=set)
    """This member's permissions within a team.

    Will always be `["*"]` until Discord starts using this.
    """

    team_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True)
    """The ID of the team this member belongs to."""

    user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True)
    """The user object of this team member."""


def _deserialize_members(
    payload: more_typing.JSONArray, **kwargs: typing.Any
) -> typing.Mapping[bases.Snowflake, TeamMember]:
    return {bases.Snowflake(member["user"]["id"]): TeamMember.deserialize(member, **kwargs) for member in payload}


@marshaller.marshallable()
@attr.s(slots=True, kw_only=True)
class Team(bases.UniqueEntity, marshaller.Deserializable):
    """Represents a development team, along with all its members."""

    icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str)
    """The hash of this team's icon, if set."""

    members: typing.Mapping[bases.Snowflake, TeamMember] = marshaller.attrib(
        deserializer=_deserialize_members, inherit_kwargs=True
    )
    """The member's that belong to this team."""

    owner_user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True)
    """The ID of this team's owner."""

    @property
    def icon_url(self) -> typing.Optional[str]:
        """URL of this team's icon, if set."""
        return self.format_icon_url()

    def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]:
        """Generate the icon URL for this team if set.

        Parameters
        ----------
        fmt : str
            The format to use for this URL, defaults to `png`.
            Supports `png`, `jpeg`, `jpg` and `webp`.
        size : int
            The size to set for the URL, defaults to `4096`. Can be any power
            of two between `16` and `4096` inclusive.

        Returns
        -------
        str, optional
            The string URL.

        Raises
        ------
        ValueError
            If `size` is not a power of two or not between 16 and 4096.
        """
        if self.icon_hash:
            return urls.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size)
        return None


@marshaller.marshallable()
@attr.s(slots=True, kw_only=True)
class ApplicationOwner(users.User):
    """Represents the user who owns an application, may be a team user."""

    flags: int = marshaller.attrib(deserializer=users.UserFlag, repr=True)
    """This user's flags."""

    @property
    def is_team_user(self) -> bool:
        """If this user is a Team user (the owner of an application that's owned by a team)."""
        return bool((self.flags >> 10) & 1)


def _deserialize_verify_key(payload: str) -> bytes:
    return bytes(payload, "utf-8")


@marshaller.marshallable()
@attr.s(slots=True, kw_only=True)
class Application(bases.UniqueEntity, marshaller.Deserializable):
    """Represents the information of an Oauth2 Application."""

    name: str = marshaller.attrib(deserializer=str, repr=True)
    """The name of this application."""

    description: str = marshaller.attrib(deserializer=str)
    """The description of this application, will be an empty string if unset."""

    is_bot_public: typing.Optional[bool] = marshaller.attrib(
        raw_name="bot_public", deserializer=bool, if_undefined=None, default=None, repr=True
    )
    """Whether the bot associated with this application is public.

    Will be `None` if this application doesn't have an associated bot.
    """

    is_bot_code_grant_required: typing.Optional[bool] = marshaller.attrib(
        raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None, default=None
    )
    """Whether this application's bot is requiring code grant for invites.

    Will be `None` if this application doesn't have a bot.
    """

    owner: typing.Optional[ApplicationOwner] = marshaller.attrib(
        deserializer=ApplicationOwner.deserialize, if_undefined=None, default=None, inherit_kwargs=True, repr=True
    )
    """The object of this application's owner.

    This should always be `None` in application objects retrieved outside
    Discord's oauth2 flow.
    """

    rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib(deserializer=set, if_undefined=None, default=None)
    """A collection of this application's rpc origin URLs, if rpc is enabled."""

    summary: str = marshaller.attrib(deserializer=str)
    """This summary for this application's primary SKU if it's sold on Discord.

    Will be an empty string if unset.
    """

    verify_key: typing.Optional[bytes] = marshaller.attrib(
        deserializer=_deserialize_verify_key, if_undefined=None, default=None
    )
    """The base64 encoded key used for the GameSDK's `GetTicket`."""

    icon_hash: typing.Optional[str] = marshaller.attrib(
        raw_name="icon", deserializer=str, if_undefined=None, default=None
    )
    """The hash of this application's icon, if set."""

    team: typing.Optional[Team] = marshaller.attrib(
        deserializer=Team.deserialize, if_undefined=None, if_none=None, default=None, inherit_kwargs=True
    )
    """This application's team if it belongs to one."""

    guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib(
        deserializer=bases.Snowflake, if_undefined=None, default=None
    )
    """The ID of the guild this application is linked to if sold on Discord."""

    primary_sku_id: typing.Optional[bases.Snowflake] = marshaller.attrib(
        deserializer=bases.Snowflake, if_undefined=None, default=None
    )
    """The ID of the primary "Game SKU" of a game that's sold on Discord."""

    slug: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None)
    """The URL slug that links to this application's store page.

    Only applicable to applications sold on Discord.
    """

    cover_image_hash: typing.Optional[str] = marshaller.attrib(
        raw_name="cover_image", deserializer=str, if_undefined=None, default=None
    )
    """The hash of this application's cover image on it's store, if set."""

    @property
    def icon_url(self) -> typing.Optional[str]:
        """URL for this team's icon, if set."""
        return self.format_icon_url()

    def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]:
        """Generate the icon URL for this application if set.

        Parameters
        ----------
        fmt : str
            The format to use for this URL, defaults to `png`.
            Supports `png`, `jpeg`, `jpg` and `webp`.
        size : int
            The size to set for the URL, defaults to `4096`.
            Can be any power of two between 16 and 4096.

        Returns
        -------
        str, optional
            The string URL.

        Raises
        ------
        ValueError
            If `size` is not a power of two or not between 16 and 4096.
        """
        if self.icon_hash:
            return urls.generate_cdn_url("app-icons", str(self.id), self.icon_hash, fmt=fmt, size=size)
        return None

    @property
    def cover_image_url(self) -> typing.Optional[str]:
        """URL for this icon's store cover image, if set."""
        return self.format_cover_image_url()

    def format_cover_image_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]:
        """Generate the URL for this application's store page's cover image is set and applicable.

        Parameters
        ----------
        fmt : str
            The format to use for this URL, defaults to `png`.
            Supports `png`, `jpeg`, `jpg` and `webp`.
        size : int
            The size to set for the URL, defaults to `4096`.
            Can be any power of two between 16 and 4096.

        Returns
        -------
        str, optional
            The string URL.

        Raises
        ------
        ValueError
            If `size` is not a power of two or not between 16 and 4096.
        """
        if self.cover_image_hash:
            return urls.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, fmt=fmt, size=size)
        return None
