import aiohttp
import asyncio

from weakref import WeakValueDictionary
from typing import List, Optional

from .http import AsyncHTTP
from .token import AccessToken
from .user import User, PartialUser

__all__ = ("Client",)


class Client:
    """Represents a client that offers interaction to the discord OAuth2 API.

    Parameters
    -----------
    client_id: :class:`int`
        The OAuth2 application client id.
    client_secret: :class:`str`
        The OAuth2 application client secret.
    redirect_uri: :class:`str`
        The OAuth2 redirect url.
        This url must configurated from uri's at discord developer portal.
    scopes: Optional[List[:class:`str`]]
        List of OAuth2 scopes.
        Default ``None``.
    connector: Optional[:class:`aiohttp.BaseConnector`]
        A connector for aiohttp client API.
        `aiohttp documentation <https://docs.aiohttp.org/en/stable/client_reference.html#aiohttp.BaseConnector>`.
        Default ``None``.
    loop: Optional[:class:`asyncio.AbstractEventLoop`]
        The event loop of asyncio.
        This loop will be only used at the instance of :class:`aiohttp.ClientSession`.
        Default ``None``.
    proxy: Optional[:class:`str`]
        The proxy URL.
        Default ``None``.
    proxy_auth: Optional[:class:`aiohttp.BasicAuth`]
        Object that represents proxy HTTP Basic Authorization.
        `aiohttp documentation <https://docs.aiohttp.org/en/stable/client_reference.html#aiohttp.BasicAuth>`.
        Default ``None``.
    http_trace: Optional[:class:`aiohttp.TraceConfig`]
        The trace configuration to use for tracking HTTP requests the library does using ``aiohttp``.
        `aiohttp documentation <https://docs.aiohttp.org/en/stable/client_advanced.html#client-tracing>`.
        Default ``None``.
    """

    def __init__(
        self,
        *,
        client_id: int,
        client_secret: str,
        redirect_uri: str,
        scopes: Optional[List[str]] = None,
        connector: Optional[aiohttp.BaseConnector] = None,
        loop: Optional[asyncio.AbstractEventLoop] = None,
        proxy: Optional[str] = None,
        proxy_auth: Optional[aiohttp.BasicAuth] = None,
        http_trace: Optional[aiohttp.TraceConfig] = None,
    ) -> None:
        self.client_id: int = client_id
        self._client_secret: str = client_secret
        self.redirect_uri: str = redirect_uri
        self.scopes: Optional[List[str]] = scopes
        self.http: AsyncHTTP = AsyncHTTP(
            client_id=client_id,
            client_secret=client_secret,
            redirect_uri=redirect_uri,
            connector=connector,
            loop=loop,
            proxy=proxy,
            proxy_auth=proxy_auth,
            http_trace=http_trace,
        )

        self._users: WeakValueDictionary[int, User] = WeakValueDictionary()

    async def exchange_code(self, code: str) -> AccessToken:
        """Exchanges the code you receive from the OAuth2 redirect.

        Parameters
        ---------
        code: :class:`str`
            The code from the querystring.

        Returns
        -------
        :class:`AccessToken`
        """
        data = await self.http.exchange_code(code, scopes=self.scopes)
        return AccessToken(data)

    async def refresh_token(self, refresh_token: str) -> AccessToken:
        """Refreshes an access token.
        This can be useful to anticipate the expiration and refresh the token.

        Parameters
        ---------
        refresh_toke: :class:`str`
            The user's refresh token.

        Returns
        -------
        :class:`AccessToken`
        """
        data = await self.http.refresh_token(refresh_token)
        return AccessToken(data)

    async def fetch_user(self, access_token: AccessToken) -> User:
        """Fetch user's information from discord api using user's access token.

        You must have the scope `identify` to use this.
        To access user's email you must have the scope `email`.

        Parameters
        ----------
        access_token: :class:`AccessToken`
            The user's access token.

        Returns
        ------
        :class:`User`
        """
        data = await self.http.get_user(access_token.access_token)
        user = User(http=self.http, access_token=access_token, data=data)
        self._users[user.id] = user
        return user

    def get_user(self, id: int, /) -> Optional[User]:
        """Returns a user with the given ID

        Parameters
        ----------
        id: :class:`int`
            The user's id that you
            want to find.

        Returns
        -------
        Optional[:class:`User`]
            Returns ``None`` when there's no user with that id
            in the cache
        """
        return self._users.get(id)

    def get_partial_user(self, *, access_token: AccessToken) -> PartialUser:
        """Returns a partial user.

        This is useful if you have a access_token but don't want to do an API call
        to fetch it again.

        Note that this partial user class will not let you use methods that requires
        user information.

        Parameters
        ----------
        access_token: :class:`AccessToken`
            The user's access token.

        Returns
        ------
        :class:`PartialUser`
        """
        return PartialUser(http=self.http, access_token=access_token)

    async def close(self):
        """Closes and cleanup operations on the client."""
        await self.http.close()
