import unicodedata
import logging

from django.conf import settings
from django.contrib.auth.models import Group
from django.core.exceptions import SuspiciousOperation
from django.urls import reverse

from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from mozilla_django_oidc.utils import absolutify, import_from_settings

from ..models.member import Member

logger = logging.getLogger(__name__)


def generate_username(email):

    return unicodedata.normalize("NFKC", email.split("@")[0][:150])


def store_tokens(session, access_token, id_token, refresh_token):
    if import_from_settings("OIDC_STORE_ACCESS_TOKEN", False):
        session["oidc_access_token"] = access_token

    if import_from_settings("OIDC_STORE_ID_TOKEN", False):
        session["oidc_id_token"] = id_token

    if import_from_settings("OIDC_STORE_REFRESH_TOKEN", True):
        session["oidc_refresh_token"] = refresh_token


class OIDCAuthenticationBackendWithRefresh(OIDCAuthenticationBackend):
    """
    Modified version based on https://github.com/mozilla/mozilla-django-oidc/pull/377
    to add refresh token storage as part of the authentication process.

    They can be used later in the session refresh middleware

    """

    def authenticate(self, request, **kwargs):
        """Authenticates a user based on the OIDC code flow."""

        self.request = request
        if not self.request:
            return None

        state = self.request.GET.get("state")
        code = self.request.GET.get("code")
        nonce = kwargs.pop("nonce", None)
        code_verifier = kwargs.pop("code_verifier", None)

        if not code or not state:
            return None

        reverse_url = self.get_settings(
            "OIDC_AUTHENTICATION_CALLBACK_URL", "oidc_authentication_callback"
        )

        token_payload = {
            "client_id": self.OIDC_RP_CLIENT_ID,
            "client_secret": self.OIDC_RP_CLIENT_SECRET,
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": absolutify(self.request, reverse(reverse_url)),
        }

        if code_verifier is not None:
            token_payload.update({"code_verifier": code_verifier})

        # Get the token
        token_info = self.get_token(token_payload)
        id_token = token_info.get("id_token")
        access_token = token_info.get("access_token")
        refresh_token = token_info.get("refresh_token")

        # Validate the token
        payload = self.verify_token(id_token, nonce=nonce)
        if payload:
            session = self.request.session
            store_tokens(session, access_token, id_token, refresh_token)
            try:
                return self.get_or_create_user(access_token, id_token, payload)
            except SuspiciousOperation as exc:
                logger.warning("Failed to get or create user: %s", exc)
                return None

        return None


class MemberOIDCAuthenticationBackend(OIDCAuthenticationBackendWithRefresh):

    def __init__(self, *args, **kwargs):
        self.default_scopes = ("openid", "email", "profile")
        super().__init__(self, args, kwargs)

    def get_scopes(self):
        env_settings = self.get_settings("OIDC_RP_SCOPES", None)
        if env_settings:
            return env_settings.split()
        return self.default_scopes

    def verify_claims(self, claims):
        if "email" not in claims:
            return False
        return True

    def get_username(self, claims):
        if "preferred_username" in claims:
            return claims["preferred_username"]
        return generate_username(claims.get("email"))

    def create_user(self, claims):
        user = super().create_user(claims)
        user.first_name = claims.get("given_name", "")
        user.last_name = claims.get("family_name", "")

        # Create an associated Member
        member = Member(user_ptr_id=user.pk)
        member.__dict__.update(user.__dict__)

        if settings.AUTO_ASSIGNED_USER_GROUP:

            # If there's an auto-assign group assign them
            group = Group.objects.filter(name=settings.AUTO_ASSIGNED_USER_GROUP).first()
            if group:
                member.groups.add(group)
            member.save()

        return user
