# Copyright 2024 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Basic permission check infrastructure."""

import functools
from collections.abc import Callable
from typing import Any, TYPE_CHECKING, TypeAlias, TypeVar, Union

from django.contrib.auth.models import AnonymousUser
from django.db.models import Model, Q, QuerySet, TextChoices

from debusine.db.context import ContextConsistencyError, context

if TYPE_CHECKING:
    from debusine.db.models import User


class Roles(TextChoices):
    """TextChoices used as roles."""


#: Type alias for the user variable used by permission predicates
PermissionUser: TypeAlias = Union["User", AnonymousUser, None]

M = TypeVar("M", bound=Model)
QS = TypeVar("QS", bound=QuerySet[Any, Any])


def ROLES(user: "User", *roles: "Roles") -> Q:
    """Select elements for which user has at least one of the given roles."""
    return Q(roles__group__users=user, roles__role__in=roles)


def permission_check(
    f: Callable[[M, PermissionUser], bool]
) -> Callable[[M, PermissionUser], bool]:
    """Implement common elements of permission checking predicates."""

    @functools.wraps(f)
    def wrapper(self: M, user: PermissionUser) -> bool:
        if context.permission_checks_disabled:
            return True

        # User has not been set in the context: context.user is passed, but it
        # contains None
        if user is None:
            if context.worker_token is None:
                raise ContextConsistencyError("user was not set in context")
            else:
                user = AnonymousUser()

        return f(self, user)

    return wrapper


def permission_filter(
    f: Callable[[QS, PermissionUser], QS]
) -> Callable[[QS, PermissionUser], QS]:
    """Implement common elements of permission filtering predicates."""

    @functools.wraps(f)
    def wrapper(self: QS, user: PermissionUser) -> QS:
        if context.permission_checks_disabled:
            return self

        # User has not been set in the context: context.user is passed, but it
        # contains None
        if user is None:
            if context.worker_token is None:
                raise ContextConsistencyError("user was not set in context")
            else:
                user = AnonymousUser()

        return f(self, user)

    return wrapper
