# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.scopes import BaseScopes
from member.models import Club
from note.models import Alias
from note_kfet.middlewares import get_current_request

from .backends import PermissionBackend
from .models import Permission

from django.utils.translation import gettext_lazy as _


class PermissionScopes(BaseScopes):
    """
    An OAuth2 scope is defined by a permission object and a club.
    A token will have a subset of permissions from the owner of the application,
    and can be useful to make queries through the API with limited privileges.
    """

    def get_all_scopes(self, **kwargs):
        scopes = {}
        if 'scopes' in kwargs:
            for scope in kwargs['scopes']:
                if scope == 'openid':
                    scopes['openid'] = _("OpenID Connect (username and email)")
                elif scope == '0_0':
                    scopes['0_0'] = _("Useless scope which do nothing")
                else:
                    p = Permission.objects.get(id=scope.split('_')[0])
                    club = Club.objects.get(id=scope.split('_')[1])
                    scopes[scope] = f"{p.description} (club {club.name})"
            return scopes

        scopes = {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
                  for p in Permission.objects.all() for club in Club.objects.all()}
        scopes['openid'] = _("OpenID Connect (username and email)")
        scopes['0_0'] = _("Useless scope which do nothing")
        return scopes

    def get_available_scopes(self, application=None, request=None, *args, **kwargs):
        if not application:
            return []
        scopes = [f"{p.id}_{p.membership.club.id}"
                  for t in Permission.PERMISSION_TYPES
                  for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
        scopes.append('0_0')  # always available
        return scopes

    def get_default_scopes(self, application=None, request=None, *args, **kwargs):
        if not application:
            return []
        scopes = [f"{p.id}_{p.membership.club.id}"
                  for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
        scopes = ['0_0']  # always default
        return scopes


class PermissionOAuth2Validator(OAuth2Validator):
    oidc_claim_scope = OAuth2Validator.oidc_claim_scope
    oidc_claim_scope.update({"name": 'openid',
                             "normalized_name": 'openid',
                             "email": 'openid',
                             })

    def get_additional_claims(self, request):
        return {
            "name": request.user.username,
            "normalized_name": Alias.normalize(request.user.username),
            "email": request.user.email,
        }

    def get_userinfo_claims(self, request):
        claims = super().get_userinfo_claims(request)
        claims['is_active'] = request.user.is_active
        return claims

    def get_discovery_claims(self, request):
        claims = super().get_discovery_claims(self)
        return claims + ["name", "normalized_name", "email"]

    def validate_client_credentials_scopes(self, client_id, scopes, client, request, *args, **kwargs):
        """
        For client credentials valid scopes are scope of the app owner
        """
        valid_scopes = set()
        request.oauth2 = {}
        request.oauth2['user'] = client.user
        request.oauth2['user'].is_anomymous = False
        request.oauth2['scope'] = scopes
        # mask implementation
        if hasattr(request.decoded_body, 'mask'):
            try:
                request.oauth2['mask'] = int(request.decoded_body['mask'])
            except ValueError:
                request.oauth2['mask'] = 42
        else:
            request.oauth2['mask'] = 42

        for t in Permission.PERMISSION_TYPES:
            for p in PermissionBackend.get_raw_permissions(request, t[0]):
                scope = f"{p.id}_{p.membership.club.id}"
                if scope in scopes:
                    valid_scopes.add(scope)

        # Always give one scope to generate token
        if not valid_scopes:
            valid_scopes.add('0_0')

        request.scopes = valid_scopes
        return valid_scopes

    def validate_ropb_scopes(self, client_id, scopes, client, request, *args, **kwargs):
        """
        For ROPB valid scopes are scope of the user
        """
        valid_scopes = set()
        request.oauth2 = {}
        request.oauth2['user'] = request.user
        request.oauth2['user'].is_anomymous = False
        request.oauth2['scope'] = scopes
        # mask implementation
        if hasattr(request.decoded_body, 'mask'):
            try:
                request.oauth2['mask'] = int(request.decoded_body['mask'])
            except ValueError:
                request.oauth2['mask'] = 42
        else:
            request.oauth2['mask'] = 42

        for t in Permission.PERMISSION_TYPES:
            for p in PermissionBackend.get_raw_permissions(request, t[0]):
                scope = f"{p.id}_{p.membership.club.id}"
                if scope in scopes:
                    valid_scopes.add(scope)

        # Always give one scope to generate token
        if not valid_scopes:
            valid_scopes.add('0_0')

        request.scopes = valid_scopes
        return valid_scopes

    def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
        """
        User can request as many scope as he wants, including invalid scopes,
        but it will have only the permissions he has.

        This allows clients to request more permission to get finally a
        subset of permissions.
        """
        valid_scopes = set()
        if hasattr(request, 'grant_type') and request.grant_type == 'client_credentials':
            return self.validate_client_credentials_scopes(client_id, scopes, client, request, args, kwargs)
        if hasattr(request, 'grant_type') and request.grant_type == 'password':
            return self.validate_ropb_scopes(client_id, scopes, client, request, args, kwargs)

        # Authorization code and Implicit are the same for scope, OIDC it's only a layer

        valid_scopes = set()
        req = get_current_request()
        request.oauth2 = {}
        request.oauth2['user'] = req.user
        request.oauth2['scope'] = scopes
        # mask implementation
        request.oauth2['mask'] = req.session.load()['permission_mask']

        for t in Permission.PERMISSION_TYPES:
            for p in PermissionBackend.get_raw_permissions(request, t[0]):
                scope = f"{p.id}_{p.membership.club.id}"
                if scope in scopes:
                    valid_scopes.add(scope)

        # We grant openid scope if user is active
        if 'openid' in scopes and req.user.is_active:
            valid_scopes.add('openid')

        # Always give one scope to generate token
        if not valid_scopes:
            valid_scopes.add('0_0')

        request.scopes = valid_scopes
        return valid_scopes
