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

from datetime import date, timedelta

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, F
from django.utils import timezone
from note.models import Note, NoteUser, NoteClub, NoteSpecial
from note_kfet.middlewares import get_current_request
from member.models import Membership, Club

from .decorators import memoize
from .models import Permission


class PermissionBackend(ModelBackend):
    """
    Manage permissions of users
    """
    supports_object_permissions = True
    supports_anonymous_user = False
    supports_inactive_user = False

    @staticmethod
    @memoize
    def get_raw_permissions(request, t):  # noqa: C901
        """
        Query permissions of a certain type for a user, then memoize it.
        :param request: The current request
        :param t: The type of the permissions: view, change, add or delete
        :return: The queryset of the permissions of the user (memoized) grouped by clubs
        """
        # Permission for auth
        if hasattr(request, 'oauth2') and request.oauth2 is not None and 'scope' in request.oauth2:
            # OAuth2 Authentication
            user = request.oauth2['user']

            def permission_filter(membership_obj):
                query = Q(pk=-1)
                for scope in request.oauth2['scope']:
                    if scope == "openid":
                        continue
                    permission_id, club_id = scope.split('_')
                    if int(club_id) == membership_obj.club_id:
                        query |= Q(pk=permission_id, mask__rank__lte=request.oauth2['mask'])
                return query

        # Restreint token permission to his scope
        elif hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
            user = request.auth.user

            def permission_filter(membership_obj):
                query = Q(pk=-1)
                for scope in request.auth.scope.split(' '):
                    if scope == "openid" or scope == "0_0":
                        continue
                    permission_id, club_id = scope.split('_')
                    if int(club_id) == membership_obj.club_id:
                        query |= Q(pk=permission_id)
                return query

        else:
            user = request.user

            def permission_filter(membership_obj):
                return Q(mask__rank__lte=request.session.get("permission_mask", 42))

        if user.is_anonymous:
            # Unauthenticated users have no permissions
            return Permission.objects.none()

        memberships = Membership.objects.filter(user=user).all()

        perms = []

        for membership in memberships:
            for role in membership.roles.all():
                for perm in role.permissions.filter(permission_filter(membership), type=t).all():
                    if not perm.permanent:
                        if membership.date_start > date.today() or membership.date_end < date.today():
                            continue
                    perm.membership = membership
                    perms.append(perm)
        return perms

    @staticmethod
    def permissions(request, model, type):
        """
        List all permissions of the given user that applies to a given model and a give type
        :param request: The current request
        :param model: The model that the permissions shoud apply
        :param type: The type of the permissions: view, change, add or delete
        :return: A generator of the requested permissions
        """
        if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
            # OAuth2 Authentication
            user = request.auth.user
        else:
            user = request.user

        for permission in PermissionBackend.get_raw_permissions(request, type):
            if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
                continue

            membership = permission.membership
            club = membership.club

            permission = permission.about(
                user=user,
                club=club,
                membership=membership,
                User=User,
                Club=Club,
                Membership=Membership,
                Note=Note,
                NoteUser=NoteUser,
                NoteClub=NoteClub,
                NoteSpecial=NoteSpecial,
                F=F,
                Q=Q,
                now=timezone.now(),
                today=date.today(),
                week=timedelta(days=7),
            )
            yield permission

    @staticmethod
    @memoize
    def filter_queryset(request, model, t, field=None):
        """
        Filter a queryset by considering the permissions of a given user.
        :param request: The current request
        :param model: The concerned model of the queryset
        :param t: The type of modification (view, add, change, delete)
        :param field: The field of the model to test, if concerned
        :return: A query that corresponds to the filter to give to a queryset
        """
        if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
            # OAuth2 Authentication
            user = request.auth.user
        else:
            user = request.user

        if user is None or user.is_anonymous:
            # Anonymous users can't do anything
            return Q(pk=-1)

        if user.is_superuser and request.session.get("permission_mask", -1) >= 42:
            # Superusers have all rights
            return Q()

        if not isinstance(model, ContentType):
            model = ContentType.objects.get_for_model(model)

        # Never satisfied
        query = Q(pk=-1)
        perms = PermissionBackend.permissions(request, model, t)
        for perm in perms:
            if perm.field and field != perm.field:
                continue
            if perm.type != t or perm.model != model:
                continue
            perm.update_query()
            query = query | perm.query
        return query

    @staticmethod
    @memoize
    def check_perm(request, perm, obj=None):
        """
        Check is the given user has the permission over a given object.
        The result is then memoized.
        Exception: for add permissions, since the object is not hashable since it doesn't have any
        primary key, the result is not memoized. Moreover, the right could change
        (e.g. for a transaction, the balance of the user could change)
        """
        # Requested by a shell
        if request is None:
            return False

        user_obj = request.user
        sess = request.session

        if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
            # OAuth2 Authentication
            user_obj = request.auth.user

        if user_obj is None or user_obj.is_anonymous:
            return False

        if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
            return True

        if obj is None:
            return True

        perm = perm.split('.')[-1].split('_', 2)
        perm_type = perm[0]
        perm_field = perm[2] if len(perm) == 3 else None

        ct = ContentType.objects.get_for_model(obj)
        if any(permission.applies(obj, perm_type, perm_field)
               for permission in PermissionBackend.permissions(request, ct, perm_type)):
            return True
        return False

    def has_perm(self, user_obj, perm, obj=None):
        # Warning: this does not check that user_obj has the permission,
        # but if the current request has the permission.
        # This function is implemented for backward compatibility, and should not be used.
        return PermissionBackend.check_perm(get_current_request(), perm, obj)

    def has_module_perms(self, user_obj, app_label):
        return False

    @staticmethod
    @memoize
    def has_model_perm(request, model, type):
        """
        Check is the given user has the permission over a given model for a given action.
        The result is then memoized.
        :param request: The current request
        :param model: The model that the permissions shoud apply
        :param type: The type of the permissions: view, change, add or delete
        For view action, it is consider possible if user can view or change the model
        """
        # Requested by a shell
        if request is None:
            return False

        user_obj = request.user
        sess = request.session

        if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
            # OAuth2 Authentication
            user_obj = request.auth.user

        if user_obj is None or user_obj.is_anonymous:
            return False

        if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
            return True

        ct = ContentType.objects.get_for_model(model)
        if any(PermissionBackend.permissions(request, ct, type)):
            return True
        if type == "view" and any(PermissionBackend.permissions(request, ct, "change")):
            return True
        return False

    def get_all_permissions(self, user_obj, obj=None):
        ct = ContentType.objects.get_for_model(obj)
        return list(self.permissions(get_current_request(), ct, "view"))
