import boto3
import time
from .cookie_utils import read_cookie
from .envs import Envs
from .jwt_utils import jwt_decode, jwt_encode


# Authentication and authorization related functions.
#
class Auth:

    def __init__(self, auth0_client_id: str, auth0_secret: str, envs: Envs):
        self.auth0_client_id = auth0_client_id
        self.auth0_secret = auth0_secret
        self.envs = envs

    class Cache:
        aws_credentials = {}

    def authorize(self, request: dict, env: str) -> dict:
        """
        Verifies that the given request is authenticated AND authorized, based on the authtoken
        cookie (a JWT-signed-encoded value) in the given request. If so, returns the verified and
        decoded authtoken as a dictionary. If not, returns a dictionary indicating not authorized
        and/or not authenticated, and the basic info contained in the authtoken.
        """
        try:

            # Read the authtoken cookie.

            authtoken = read_cookie(request, "authtoken")
            if not authtoken:
                return self._create_not_authenticated_response(request, "no-authtoken")

            # Decode the authtoken cookie.

            authtoken_decoded = self.decode_authtoken(authtoken)
            if not authtoken_decoded:
                return self._create_not_authenticated_response(request, "invalid-authtoken")

            # Sanity check the decoded authtoken.

            if authtoken_decoded["authorized"] is not True or authtoken_decoded["authenticated"] is not True:
                return self._create_not_authenticated_response(request, "invalid-authtoken-auth", authtoken_decoded)

            if self.auth0_client_id != authtoken_decoded["aud"]:
                return self._create_not_authenticated_response(request, "invalid-authtoken-aud", authtoken_decoded)

            domain = self._get_domain(request)
            if domain != authtoken_decoded["domain"]:
                return self._create_not_authenticated_response(request, "invalid-authtoken-domain", authtoken_decoded)

            # Check the authtoken expiration time (its expiration time must be in the future i.e. greater than now).

            authtoken_expires_time_t = authtoken_decoded["authenticated_until"]
            current_time_t = int(time.time())
            if authtoken_expires_time_t <= current_time_t:
                return self._create_not_authenticated_response(request, "authtoken-expired", authtoken_decoded)

            # Check that the specified environment is allowed, i.e. that the request is authorized.
            # Note that if not, we end up returning HTTP 403 (not authorized) and, NOT 401 (not authenticated),
            # as we would do (above) if not authenticated (done in react_routes/route_requires_authorization);
            # the UI acts differently for these two cases.

            allowed_envs = authtoken_decoded["allowed_envs"]
            if not self.envs.is_allowed_env(env, allowed_envs):
                status = "not-authorized-env" if self.envs.is_known_env(env) else "not-authorized-unknown-env"
                return self._create_not_authorized_response(request, status, authtoken_decoded)

            return authtoken_decoded

        except Exception as e:
            print("Authorize exception: " + str(e))
            return self._create_not_authenticated_response(request, "exception: " + str(e))

    def create_authtoken(self, jwt: str, jwt_expires_at: int, env: str, domain: str) -> str:
        """
        Creates and returns a new signed JWT, to be used as the login authtoken (cookie), from
        the given AUTHENTICATED and signend and encoded JWT, which will contain the following:
        - Booleans indicating authenticated AND authorized.
        - The user name (from the "email" field of the given JWT).
        - The list of known environments.
        - The default environment.
        - The initial environment (i.e. the environment in which the user was first authenticated).
        - The list of allowed (authorized) environments for the user associated with the given JWT.
        - The first/last name of the user associated with the given JWT.
        - The timestamp of when the given JWT was issued (UTC time_t/epoch format).
        - The timestamp of when the given JWT expires issued (UTC time_t/epoch format).
        - The audience (aka "aud" aka Auth0 client ID).
        The allowed environments and first/last name are obtained via the users ElasticSearch store;
        the first/last names are just for informational/display purposes in the client.
        Returns the JWT-signed-encoded authtoken value as a string.
        """
        jwt_decoded = jwt_decode(jwt, self.auth0_client_id, self.auth0_secret)
        email = jwt_decoded.get("email")
        allowed_envs, first_name, last_name = self.envs.get_user_auth_info(email)
        print('xyzzy;create_authtoken')
        print(jwt_decoded.get("exp"))
        from .datetime_utils import convert_time_t_to_useastern_datetime_string
        print('xyzzy;create_authtoken;jwt-expires-at')
        print(jwt_expires_at)
        print(type(jwt_expires_at))
        print(convert_time_t_to_useastern_datetime_string(jwt_expires_at))
        print('xyzzy;create_authtoken;jwt-exp')
        print(convert_time_t_to_useastern_datetime_string(jwt_decoded.get("exp")))
        authtoken_decoded = {
            "authenticated": True,
            "authenticated_at": jwt_decoded.get("iat"),
            "authenticated_until": jwt_expires_at,
            "authorized": True,
            "user": email,
            "user_verified": jwt_decoded.get("email_verified"),
            "first_name": first_name,
            "last_name": last_name,
            "allowed_envs": allowed_envs,
            "known_envs": self.envs.get_known_envs(),
            "default_env": self.envs.get_default_env(),
            "initial_env": env,
            "domain": domain
        }
        # JWT-sign-encode the authtoken using our Auth0 client ID (aka audience aka "aud") and
        # secret. This *required* audience is added to the JWT before encoding (done in the
        # jwt_encode function), set to the value we pass here, namely, self.auth0_client_id;
        # it was also in the given JWT (i.e. jwt_decoded["aud"]), and these should match (no
        # need to check); it came from the original Auth0 invocation on the client-side (in the
        # Auth0Lock box); we communicate it to the client-side via the non-protected /header endpoint.
        return jwt_encode(authtoken_decoded, audience=self.auth0_client_id, secret=self.auth0_secret)

    def decode_authtoken(self, authtoken: str) -> dict:
        """
        Fully verifies AND decodes and returns the given JWT-signed-encoded authtoken (cookie).
        If not verified (by the jwt_decode function) then None will be returned.
        See create_authtoken (above) for an enumeration of the contents of the authtoken.
        Returns the verified/decoded JWT as a dictionary.
        """
        return jwt_decode(authtoken, self.auth0_client_id, self.auth0_secret)

    def _create_not_authorized_response(self, request: dict, status: str, authtoken_decoded: dict, authenticated: bool = True) -> dict:
        """
        Creates a response suitable for a request which is NOT authorized, or NOT authenticated,
        depending on authenticated argument. Note that we still want to return some basic info,
        i.e. list of known environments, default environment, domain, and aud (Auth0 client ID)
        is required for the Auth0 login box (Auth0Lock) on the client-side (i.e. React UI). This
        info is gotten from the given decoded authtoken or if not set then sets this info explicitly.
        """
        if authtoken_decoded:
            response = authtoken_decoded
        else:
            response = {
                "known_envs": self.envs.get_known_envs(),
                "default_env": self.envs.get_default_env(),
                "domain": self._get_domain(request),
                "aud": self.auth0_client_id  # Needed for Auth0Lock login box on client-side.
            }
        response["authenticated"] = authenticated
        response["authorized"] = False
        response["status"] = status
        return response

    def _create_not_authenticated_response(self, request: dict, status: str, authtoken_decoded: dict = None) -> dict:
        """
        Creates a response suitable for a request which is NOT authenticated.
        """
        return self._create_not_authorized_response(request, status, authtoken_decoded, False)

    @staticmethod
    def _get_domain(request: dict) -> str:
        if request:
            try:
                return request.get("headers", {}).get("host")
            except:
                pass
        return ""

    def get_aws_credentials(self, env: str) -> dict:
        """
        Returns basic AWS credentials info (NOT the secret).
        This is just for informational/display purposes in the UI.
        This has nothing to do with the rest of the authentication
        and authorization stuff here but vaguely related so here seems fine.
        """
        aws_credentials = Auth.Cache.aws_credentials.get(env)
        if not aws_credentials:
            try:
                session = boto3.session.Session()
                credentials = session.get_credentials()
                access_key_id = credentials.access_key
                region_name = session.region_name
                caller_identity = boto3.client("sts").get_caller_identity()
                user_arn = caller_identity["Arn"]
                account_number = caller_identity["Account"]
                aws_credentials = {
                    "aws_account_number": account_number,
                    "aws_user_arn": user_arn,
                    "aws_access_key_id": access_key_id,
                    "aws_region": region_name,
                    "auth0_client_id": self.auth0_client_id
                }
                # Try getting the account name though probably no permission at the moment.
                aws_account_name = None
                try:
                    aws_credentials["aws_account_name"] = \
                        boto3.client('iam').list_account_aliases()['AccountAliases'][0]
                except:
                    pass
                if not aws_account_name:
                    try:
                        aws_credentials["aws_account_name"] = \
                            boto3.client('organizations').describe_account(AccountId=account_number).get('Account').get('Name')
                    except:
                        pass
                Auth.Cache.aws_credentials[env] = aws_credentials
            except:
                return {}
        return aws_credentials
