"""Component to allow users to login and get tokens.

# POST /auth/token

This is an OAuth2 endpoint for granting tokens. We currently support the grant
types "authorization_code" and "refresh_token". Because we follow the OAuth2
spec, data should be send in formatted as x-www-form-urlencoded. Examples will
be in JSON as it's more readable.

## Grant type authorization_code

Exchange the authorization code retrieved from the login flow for tokens.

{
    "client_id": "https://hassbian.local:8123/",
    "grant_type": "authorization_code",
    "code": "411ee2f916e648d691e937ae9344681e"
}

Return value will be the access and refresh tokens. The access token will have
a limited expiration. New access tokens can be requested using the refresh
token.

{
    "access_token": "ABCDEFGH",
    "expires_in": 1800,
    "refresh_token": "IJKLMNOPQRST",
    "token_type": "Bearer"
}

## Grant type refresh_token

Request a new access token using a refresh token.

{
    "client_id": "https://hassbian.local:8123/",
    "grant_type": "refresh_token",
    "refresh_token": "IJKLMNOPQRST"
}

Return value will be a new access token. The access token will have
a limited expiration.

{
    "access_token": "ABCDEFGH",
    "expires_in": 1800,
    "token_type": "Bearer"
}

## Revoking a refresh token

It is also possible to revoke a refresh token and all access tokens that have
ever been granted by that refresh token. Response code will ALWAYS be 200.

{
    "token": "IJKLMNOPQRST",
    "action": "revoke"
}

# Websocket API

## Get current user

Send websocket command `auth/current_user` will return current user of the
active websocket connection.

{
    "id": 10,
    "type": "auth/current_user",
}

The result payload likes

{
    "id": 10,
    "type": "result",
    "success": true,
    "result": {
        "id": "USER_ID",
        "name": "John Doe",
        "is_owner": true,
        "credentials": [{
            "auth_provider_type": "homeassistant",
            "auth_provider_id": null
        }],
        "mfa_modules": [{
            "id": "totp",
            "name": "TOTP",
            "enabled": true
        }]
    }
}

## Create a long-lived access token

Send websocket command `auth/long_lived_access_token` will create
a long-lived access token for current user. Access token will not be saved in
Home Assistant. User need to record the token in secure place.

{
    "id": 11,
    "type": "auth/long_lived_access_token",
    "client_name": "GPS Logger",
    "lifespan": 365
}

Result will be a long-lived access token:

{
    "id": 11,
    "type": "result",
    "success": true,
    "result": "ABCDEFGH"
}

"""
from datetime import timedelta
from typing import Union
import uuid

from aiohttp import web
import voluptuous as vol

from homeassistant.auth import InvalidAuthError
from homeassistant.auth.models import (
    TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
    Credentials,
    User,
)
from homeassistant.components import websocket_api
from homeassistant.component
s.http.auth import async_sign_path
from homeassistant.components.http.ban import log_invalid_auth
f
rom homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_OK
from homeassistant.core import HomeAssistant, callback
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util

from . import indieauth, login_flow, mfa_setup_flow

DOMAIN = "auth"
WS_TYPE_CURRENT_USER = "auth/current_user"
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {vol.Required("type"): WS_TYPE_CURRENT_USER}
)

WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token"
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {
        vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
        vol.Required("lifespan"): int,  # days
        vol.Required("client_name"): str,
        vol.Optional("client_icon"): str,
    }
)

WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens"
SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {vol.Required("type"): WS_TYPE_REFRESH_TOKENS}
)

WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token"
SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {
        vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN,
        vol.Required("refresh_token_id"): str,
    }
)

WS_TYPE_SIGN_PATH = "auth/sign_path"
SCHEMA_WS_SIGN_PATH = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {
        vol.Required("type"): WS_TYPE_SIGN_PATH,
        vol.Required("path"): str,
        vol.Optional("expires", default=30): int,
    }
)

RESULT_TYPE_CREDENTIALS = "credentials"
RESULT_TYPE_USER = "user"


@bind_hass
def create_auth_code(
    hass, client_id: str, credential_or_user: Union[Credentials, User]
) -> str:
    """Create an authorization code to fetch tokens."""
    return hass.data[DOMAIN](client_id, credential_or_user)


async def async_setup(hass, config):
    """Component to allow users to login."""
    store_result, retrieve_result = _create_auth_code_store()

    hass.data[DOMAIN] = store_result

    hass.http.register_view(TokenView(retrieve_result))
    hass.http.register_view(LinkUserView(retrieve_result))

    hass.components.websocket_api.async_register_command(
        WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER
    )
    hass.components.websocket_api.async_register_command(
        WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
        websocket_create_long_lived_access_token,
        SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN,
    )
    hass.components.websocket_api.async_register_command(
        WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS
    )
    hass.components.websocket_api.async_register_command(
        WS_TYPE_DELETE_REFRESH_TOKEN,
        websocket_delete_refresh_token,
        SCHEMA_WS_DELETE_REFRESH_TOKEN,
    )
    hass.components.websocket_api.async_register_command(
        WS_TYPE_SIGN_PATH, websocket_sign_path, SCHEMA_WS_SIGN_PATH
    )

    await login_flow.async_setup(hass, store_result)
    await mfa_setup_flow.async_setup(hass)

    return True


class TokenView(HomeAssistantView):
    """View to issue or revoke tokens."""

    url = "/auth/token"
    name = "api:auth:token"
    requires_auth = False
    cors_allowed = True

    def __init__(self, retrieve_auth):
        """Initialize the token view."""
        self._retrieve_auth = retrieve_auth

    @log_invalid_auth
    async def post(self, request):
        """Grant a token."""
        hass = request.app["hass"]
        data = await request.post()

        grant_type = data.get("grant_type")

        # IndieAuth 6.3.5
        # The revocation endpoint is the same as the token endpoint.
        # The revocation request includes an additional parameter,
        # action=revoke.
        if data.get("action") == "revoke":
            return await self._async_handle_revoke_token(hass, data)

        if grant_type == "authorization_code":
            return await self._async_handle_auth_code(hass, data, request.remote)

        if grant_type == "refresh_token":
            return await self._async_handle_refresh_token(hass, data, request.remote)

        return self.json(
            {"error": "unsupported_grant_type"}, status_code=HTTP_BAD_REQUEST
        )

    async def _async_handle_revoke_token(self, hass, data):
        """Handle revoke token request."""
        # OAuth 2.0 Token Revocation [RFC7009]
        # 2.2 The authorization server responds with HTTP status code 200
        # if the token has been revoked successfully or if the client
        # submitted an invalid token.
        token = data.get("token")

        if token is None:
            return web.Response(status=HTTP_OK)

        refresh_token = await hass.auth.async_get_refresh_token_by_token(token)

        if refresh_token is None:
            return web.Response(status=HTTP_OK)

        await hass.auth.async_remove_refresh_token(refresh_token)
        return web.Response(status=HTTP_OK)

    async def _async_h