# 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 Any, Self, TYPE_CHECKING, overload, override

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

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:
    """Context information passed to permission predicates."""

    def __init__(
        self,
        user: PermissionUser,
        extra_groups: "GroupQuerySet[Group] | None",
        permission_checks_disabled: bool,
        user_token: "Token | None",
        worker_token: "Token | None",
        work_request: "WorkRequest | None",
    ) -> None:
        """Initialize a PermissionContext."""
        #: The current user.
        self._user = user
        #: Extra groups used for additional permissions
        self._extra_groups: GroupQuerySet[Group] | None
        if isinstance(extra_groups, EmptyQuerySet):
            self._extra_groups = None
        else:
            self._extra_groups = extra_groups
        #: True if permission checks should always succeed.
        #: This is used to skip permission checks in situations like management
        #: commands or test fixture setup.
        self._permission_checks_disabled: bool = permission_checks_disabled
        #: The current user token, if used
        self._user_token = user_token
        #: The current worker token, if used
        self._worker_token = worker_token
        #: The current work request, if used
        self._work_request = work_request

    @property
    def user(self) -> PermissionUser:
        """Readonly user access."""
        return self._user

    @property
    def extra_groups(self) -> "GroupQuerySet[Group]":
        """Readonly extra_groups access."""
        # Prevent circular import
        from debusine.db.models import Group

        return self._extra_groups or Group.objects.none()

    @property
    def permission_checks_disabled(self) -> bool:
        """Readonly permission_checks_disabled access."""
        return self._permission_checks_disabled

    @property
    def user_token(self) -> "Token | None":
        """Readonly user_token access."""
        return self._user_token

    @property
    def worker_token(self) -> "Token | None":
        """Readonly worker_token access."""
        return self._worker_token

    @property
    def work_request(self) -> "WorkRequest | None":
        """Readonly work_request access."""
        return self._work_request

    @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}]"
        extra_groups = tuple(self.extra_groups.order_by("pk"))
        if extra_groups:
            res += f"[extra groups {', '.join(str(g) for g in extra_groups)}]"
        return res

    @override
    def __repr__(self) -> str:
        return str(self)

    @override
    def __eq__(self, o: Any) -> bool:
        if not isinstance(o, PermissionContext):
            return NotImplemented
        # Ignore extra_groups, which are derived from work_request
        return (
            self._user,
            self._permission_checks_disabled,
            self._user_token,
            self._worker_token,
            self._work_request,
        ) == (
            o._user,
            o._permission_checks_disabled,
            o._user_token,
            o._worker_token,
            o._work_request,
        )

    def with_permission_checks_disabled(self) -> Self:
        """Return a copy of this PermissionContext with checks disabled."""
        return self.__class__(
            user=self._user,
            extra_groups=self._extra_groups,
            permission_checks_disabled=True,
            user_token=self._user_token,
            worker_token=self._worker_token,
            work_request=self._work_request,
        )

    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()

        if self._extra_groups is None:
            return Group.objects.filter(Q(users=self.user))

        return Group.objects.filter(
            Q(users=self.user) | Q(pk__in=self._extra_groups.values("pk"))
        )

    @classmethod
    def for_ui_anonymous(cls) -> Self:
        """Return a PermissionContext for an anonymous user in the UI."""
        return cls(
            user=AnonymousUser(),
            extra_groups=None,
            permission_checks_disabled=False,
            user_token=None,
            worker_token=None,
            work_request=None,
        )

    @classmethod
    def for_ui_user(cls, user: "User") -> Self:
        """Return a PermissionContext for a user authenticating in the UI."""
        return cls(
            user=user,
            extra_groups=None,
            permission_checks_disabled=False,
            user_token=None,
            worker_token=None,
            work_request=None,
        )

    @classmethod
    def for_api_user(cls, token: "Token") -> Self:
        """Return a PermissionContext for a user authenticating in the API."""
        # Prevent circular import
        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,
            extra_groups=None,
            permission_checks_disabled=False,
            user=token.user,
            worker_token=None,
            work_request=None,
        )

    @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(
            user=AnonymousUser(),
            extra_groups=None,
            permission_checks_disabled=False,
            user_token=None,
            worker_token=token,
            work_request=None,
        )

    @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,
                extra_groups=token.work_request.get_extra_groups(),
                permission_checks_disabled=False,
                user_token=None,
                worker_token=token,
                work_request=token.work_request,
            )
        elif work_request is not None:
            return cls(
                user=work_request.created_by,
                extra_groups=work_request.get_extra_groups(),
                permission_checks_disabled=False,
                user_token=None,
                worker_token=None,
                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(),
            extra_groups=None,
            permission_checks_disabled=False,
            user_token=None,
            worker_token=token,
            work_request=None,
        )

    @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(),
            extra_groups=None,
            permission_checks_disabled=False,
            user_token=None,
            worker_token=None,
            work_request=None,
        )
