# Copyright © 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.

"""Context information needed to check permissions."""

from typing import NamedTuple, Self, TYPE_CHECKING, overload, override

from django.contrib.auth.models import AnonymousUser
from django.db.models import Q

if TYPE_CHECKING:
    from debusine.db.models import Group, Token, User, WorkRequest
    from debusine.db.models.auth import GroupQuerySet

#: Type alias for the user variable used by permission predicates
type PermissionUser = "User | AnonymousUser"


class PermissionContext(NamedTuple):
    """Context information passed to permission predicates."""

    #: The current user.
    user: PermissionUser

    #: True if permission checks should always succeed.
    #: This is used to skip permission checks in situations like management
    #: commands or test fixture setup.
    permission_checks_disabled: bool = False

    #: The current user token, if used
    user_token: "Token | None" = None

    #: The current worker token, if used
    worker_token: "Token | None" = None

    #: The current work request, if used
    work_request: "WorkRequest | None" = None

    #: Extra groups used for additional permissions
    extra_groups: "list[Group] | None" = None

    @override
    def __str__(self) -> str:
        res = str(self.user)
        if self.user_token:
            res += "[user token]"
        if self.worker_token:
            res += f"[{self.worker_token.token_type} token]"
        if self.work_request:
            res += f"[work request {self.work_request}]"
        if self.extra_groups:
            res += (
                f"[extra groups {', '.join(str(g) for g in self.extra_groups)}]"
            )
        return res

    def with_permission_checks_disabled(self) -> Self:
        """Return a copy of this PermissionContext with checks disabled."""
        return self._replace(permission_checks_disabled=True)

    def with_extra_groups(self, *groups: "Group") -> Self:
        """Return a copy of this PermissionContext with extra groups added."""
        extra_groups = self.extra_groups or []
        extra_groups.extend(groups)
        return self._replace(extra_groups=extra_groups)

    def groups(self) -> "GroupQuerySet[Group]":
        """Return a queryset selecting the active groups."""
        # Prevent circular import
        from debusine.db.models import Group

        if not self.user.is_authenticated:
            return Group.objects.none()

        group_q = Q(users=self.user)
        if self.extra_groups:
            group_q |= Q(pk__in=[g.pk for g in self.extra_groups])

        return Group.objects.filter(group_q)

    @classmethod
    def for_ui_anonymous(cls) -> Self:
        """Return a PermissionContext for an anonymous user in the UI."""
        return cls(user=AnonymousUser())

    @classmethod
    def for_ui_user(cls, user: "User") -> Self:
        """Return a PermissionContext for a user authenticating in the UI."""
        return cls(user=user)

    @classmethod
    def for_api_user(cls, token: "Token") -> Self:
        """Return a PermissionContext for a user authenticating in the API."""
        from debusine.db.models import Token

        assert token.token_type == Token.Types.USER
        assert token.enabled
        assert token.user is not None
        return cls(user_token=token, user=token.user)

    @classmethod
    def for_worker(cls, token: "Token") -> Self:
        """Return a PermissionContext for a worker connecting to Debusine."""
        from debusine.db.models import Token

        assert token.token_type == Token.Types.WORKER
        assert token.enabled
        assert token.user is None  # enforced by database constraint
        return cls(worker_token=token, user=AnonymousUser())

    @overload
    @classmethod
    def for_work_request(cls, *, token: "Token") -> Self: ...

    @overload
    @classmethod
    def for_work_request(cls, *, work_request: "WorkRequest") -> Self: ...

    @classmethod
    def for_work_request(
        cls,
        *,
        token: "Token | None" = None,
        work_request: "WorkRequest | None" = None,
    ) -> Self:
        """Return a PermissionContext for a worker processing a work request."""
        from debusine.db.models import Token

        if token is not None:
            assert work_request is None
            assert token.token_type == Token.Types.WORK_REQUEST
            assert token.work_request is not None
            assert token.enabled
            return cls(
                user=token.work_request.created_by,
                work_request=token.work_request,
                worker_token=token,
            )
        elif work_request is not None:
            return cls(user=work_request.created_by, work_request=work_request)
        else:
            raise AssertionError("neither token nor work_request passed")

    @classmethod
    def for_worker_activation(cls, token: "Token") -> Self:
        """Return a PermissionContext for worker activation."""
        from debusine.db.models import Token

        assert token.token_type == Token.Types.WORKER_ACTIVATION
        assert token.enabled
        return cls(user=AnonymousUser(), worker_token=token)

    @classmethod
    def for_maintenance(cls) -> Self:
        """Return a PermissionContext for maintenance commands."""
        from debusine.db.models.auth import system_user

        return cls(user=system_user())
