"""
Class for OAuth device flow authentication.

https://docs.github.com/en/developers/apps/authorizing-oauth-apps#device-flow
"""

from __future__ import annotations

import asyncio
from datetime import datetime

import aiohttp

from ..common.const import (
    LOGGER,
    OAUTH_ACCESS_TOKEN,
    OAUTH_DEVICE_LOGIN,
    DeviceFlowError,
    HttpMethod,
)
from ..common.exceptions import AIOGitHubAPIException
from ..objects.login.device import AIOGitHubAPILoginDevice
from ..objects.login.oauth import AIOGitHubAPILoginOauth
from .helpers import async_call_api

HEADERS = {"Accept": "application/json"}


class AIOGitHubAPIDeviceLogin:
    _close_session = False

    def __init__(
        self,
        client_id: str,
        scope: str = "",
        session: aiohttp.ClientSession = None,
    ):
        """
        Initialises a GitHub API OAuth device flow.

        param | required | description
        -- | -- | --
        `client_id` | True | The client ID of your OAuth app.
        `scope` | False | [Scope(s)](https://docs.github.com/en/developers/apps/scopes-for-oauth-apps) that will be requested.
        `session` | False | `aiohttp.ClientSession` to be used by this package.
        """
        self.client_id = client_id
        self.scope = scope
        self._interval = 5
        self._expires_in = None
        self._expires = None
        self._device_code = None

        if session is None:
            self.session = aiohttp.ClientSession()
            self._close_session = True
        else:
            self.session = session

    async def __aenter__(self) -> "AIOGitHubAPIDeviceLogin":
        """Async enter."""
        return self

    async def __aexit__(self, *exc_info) -> None:
        """Async exit."""
        await self._close()

    async def async_register_device(self) -> AIOGitHubAPILoginDevice:
        """Register the device and return an object that contains the user code for authorization."""
        params = {"client_id": self.client_id, "scope": self.scope}
        response = await async_call_api(
            session=self.session,
            method=HttpMethod.POST,
            url=OAUTH_DEVICE_LOGIN,
            params=params,
            headers=HEADERS,
        )
        device = AIOGitHubAPILoginDevice(response.data)
        self._device_code = device.device_code
        self._interval = device.interval
        self._expires = datetime.timestamp(datetime.now()) + device.expires_in

        return device

    async def async_device_activation(self) -> AIOGitHubAPILoginOauth:
        """Wait for the user to enter the code and activate the device."""
        _activation = None
        while _activation is None:
            if self._expires is None or self._device_code is None:
                await asyncio.sleep(self._interval)

            params = {
                "client_id": self.client_id,
                "device_code": self._device_code,
                "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
            }

            if self._expires < datetime.timestamp(datetime.now()):
                raise AIOGitHubAPIException("User took too long to enter key")

            try:
                response = await async_call_api(
                    session=self.session,
                    method=HttpMethod.POST,
                    url=OAUTH_ACCESS_TOKEN,
                    params=params,
                    headers=HEADERS,
                )
                if response.data.get("error"):
                    if response.data["error"] == DeviceFlowError.AUTHORIZATION_PENDING:
                        LOGGER.debug(response.data["error_description"])
                        await asyncio.sleep(self._interval)
                    else:
                        raise AIOGitHubAPIException(response.data["error_description"])
                else:
                    _activation = AIOGitHubAPILoginOauth(response.data)
                    break

            except AIOGitHubAPIException as exception:
                raise AIOGitHubAPIException(exception) from exception

        return _activation

    async def _close(self) -> None:
        """Close open client session."""
        if self.session and self._close_session:
            await self.session.close()
