# Copyright (C) 2022 Collabora Limited
#
# Author: Sjoerd Simons <sjoerd@collabora.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later

import logging

from django.utils.module_loading import import_string
from mozilla_django_oidc.auth import OIDCAuthenticationBackend


class OIDCAuthenticationBackendUsernameFromEmail(OIDCAuthenticationBackend):
    def create_user(self, claims):
        # TODO try multiple options and fallbacks
        logger = logging.getLogger("mozilla_django_oidc")
        email = claims.get("email")
        logger.info(f"Creating new user for e-mail: {email}")
        # On Azure AD the username is not part of the claims
        username = email.split("@")[0]
        return self.UserModel.objects.create_user(username, email=email)


def split_username_algo(preferred_username):
    """Generate username for the Django user

    :arg str/unicode preferred_username: the `preferred_username`
         present in the OIDC claims, which is to be used to generate
         the local username.

    This is the default algorithm for the preferred_username-based
    backend. In some environments, this claim will have the form of
    an email address. Since django-allauth unconditionally splits
    this kind of usernames, do the same for consistency:
    https://github.com/pennersr/django-allauth/blob/30a3473b8010264143f031/allauth/utils.py#L50

    :returns: str/unicode

    """
    return preferred_username.split("@")[0]


class OIDCAuthenticationBackendUsernameFromPreferred(OIDCAuthenticationBackend):
    """This is an OIDC backend that identifies users based on
    `preferred_username` rather than email. We do this because the UPN
    (user principal name) is stored in the standard preferred_username
    claim by Active Directory. It isn't really ideal to use preferred
    username, it would be better to identify from `sub` in the long
    run, since this is the only field really approved for that usage
    in the claims. mozilla_django_oidc does not do this however, and
    when working with AD as the authorization server, UPN is
    preferable to `email` because its form is more tightly
    controlled.

    Note that this class customises the default
    OIDCAuthenticationBackend in mozilla-django-oidc to use the
    `preferred_username` claim instead of the `email` claim, and sets
    a different default username generation algorithm (the default is
    to base64 encode the SHA of the email to avoid leaking the user's
    email address). There are no other substantive changes to this
    code compared to the OIDCAuthenticationBackend provided by
    mozilla-django-oidc.

    """

    def describe_user_by_claims(self, claims):
        """Produce a short text description of a user from the claims
        given. Used for logging.

        This is an override of the parent method, which just uses
        preferred_username rather than email.

        """
        username = claims.get("preferred_username")
        return f"preferred_username {username}"

    def filter_users_by_claims(self, claims):
        """Return all users matching the specified preferred username

        This is an override of the parent method, which just uses
        preferred_username rather than email.

        """
        logger = logging.getLogger("mozilla_django_oidc")

        username = claims.get("preferred_username")
        if not username:
            return self.UserModel.objects.none()

        return self.UserModel.objects.filter(username=self.get_username(claims))

    def get_userinfo(self, access_token, id_token, payload):
        # the OpenID-Connect spec is very lax about what the userinfo endpoint
        # MUST return and some platforms (*cough* Azure *cough*) have chosen to
        # return a very minimal set so let's also use the payload of the ID
        # token as a base
        # https://learn.microsoft.com/EN-US/azure/active-directory/develop/userinfo#userinfo-response
        userinfo = super().get_userinfo(access_token, id_token, payload)
        userinfo = dict(payload, **userinfo)
        return userinfo

    def verify_claims(self, claims):
        """Verify the provided claims to decide if authentication
        should be allowed.

        This is an override of the parent method, and is largely the
        same, except that it substitutes preferred_username for
        email. Note that the preferred_username claim is only present
        when OIDC_RP_SCOPES contains "profile", which is not the
        default (unlike email). That means if you enable this backend,
        you must also update OIDC_RP_SCOPES, and you will get a
        warning whenever this method is called to remind you to do
        so. The default accept behaviour matches upstream, although it
        is surprising.

        """
        scopes = self.get_settings("OIDC_RP_SCOPES", "openid email").split()
        if "profile" in scopes:
            return "preferred_username" in claims

        logger = logging.getLogger("mozilla_django_oidc")
        logger.warning(
            'OIDC_RP_SCOPES does not include "profile". '
            "You need to override `verify_claims` for claims verification."
        )

        return True

    def get_username(self, claims):
        """Generate username based on claims.

        This is an override of the parent method, with
        preferred_username substituted for email. We also change the
        default algorithm to extract the username part of an
        email-form preferred_username, rather than to base64 encode
        the SHA of the claim, as the parent does.

        """
        username_algo = self.get_settings("OIDC_USERNAME_ALGO", None)
        if username_algo:
            if isinstance(username_algo, str):
                username_algo = import_string(username_algo)
            return username_algo(claims.get("preferred_username"))
        return split_username_algo(claims.get("preferred_username"))
