# Copyright (C) 2020-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.

"""
This module provides an ``Auth`` class to authenticate an app on the platform.

The authentication is based on some credentials object and will create an
access token. It can be checked if the token is still valid, and it can be
refreshed, too.
"""


import urllib.parse
from datetime import datetime, timedelta
from typing import Optional

from here.platform.api.aaa_oauth2_api import AAAOauth2Api
from here.platform.credentials import CredentialsType, PlatformCredentials
from requests_oauthlib import OAuth1


class Auth:
    """
    This class is responsible for authenticating with the HERE platform.

    It requires PlatformCredentials, AAAOauth2BaseApi object.
    """

    def __init__(
        self, credentials: PlatformCredentials, aaa_oauth2_api: Optional[AAAOauth2Api] = None
    ):
        """
        Instantiate authentication token.

        :param credentials: an instance of PlatformCredentials
        :param aaa_oauth2_api: an instance of AAAOauth2Api required in case of Credentials type.
        """
        self.credentials = credentials
        self.aaa_oauth2_api = aaa_oauth2_api

        self._token: Optional[str] = None
        self._token_type: Optional[str] = None
        self._token_expires_in: Optional[int] = None
        self._token_requested_at: Optional[datetime] = None
        self._token_expires_at: Optional[datetime] = None
        self.set_scope(
            credentials.cred_properties["scope"]
            if "scope" in credentials.cred_properties
            else None
        )

    @property
    def token(self) -> Optional[str]:
        """
        Return the current token or requests a new one if needed.

        :return: a valid token
        """
        if (
            self.credentials.cred_type == CredentialsType.Token
            and self.credentials.cred_properties
        ):
            self._token = self.credentials.cred_properties["access_token"]
            self._token_expires_in = self.credentials.cred_properties["expires_in"]
            self._token_expires_at = datetime.fromtimestamp(
                self.credentials.cred_properties["exp"]
            )

        if not self.token_still_valid():
            self.generate_token()
        return self._token

    def token_still_valid(self) -> bool:
        """
        Check whether the auth token is still valid or expired.

        :return: a boolean indicating if a token is still valid.
        """
        if not self._token or not self._token_expires_at:
            return False
        return datetime.now() < (self._token_expires_at - timedelta(seconds=60))

    def generate_token(self):
        """
        Authenticate with the HERE account service and retrieve a new token.
        """
        oauth = OAuth1(
            self.credentials.cred_properties["key"],
            client_secret=self.credentials.cred_properties["secret"],
            signature_method="HMAC-SHA256",
        )

        query = {"grant_type": "client_credentials"}
        if self._scope:
            query["scope"] = self._scope

        response_json = self.aaa_oauth2_api.request_scoped_access_token(
            oauth=oauth, data=urllib.parse.urlencode(query, doseq=False)
        )

        self._token = response_json.get("access_token")
        self._token_type = response_json.get("token_type")
        self._token_expires_in = int(response_json.get("expires_in"))
        self._token_requested_at = datetime.now()
        self._token_expires_at = self._token_requested_at + timedelta(
            seconds=self._token_expires_in
        )

    def set_scope(self, project_hrn: str):
        """
        Explicitly sets the project scope if not set yet or overwrites the
        project scope from the configuration.

        :param project_hrn: the project HRN string
        """
        self._scope = project_hrn
        self._invalidate_token()

    def _invalidate_token(self):
        self._token = None
