"""RBAC permission classes.

Apply to a ViewSet by setting `rbac_domain = "<domain>"` and including
`HasMenuPermission` in `permission_classes`. Action mapping handles standard
ModelViewSet methods; custom @action methods use the action name as the
permission action (e.g. `approve` -> `<domain>.approve`).

ViewSets without a `rbac_domain` attribute are not gated by this class —
fall back to `IsAuthenticated` (or whatever else is in `permission_classes`).
"""

from rest_framework.permissions import SAFE_METHODS, BasePermission


def user_has_full_scope(user, domain: str) -> bool:
    """True if `user` may see/modify ALL records in `domain` (manager-level),
    otherwise they are scoped to their own records.

    Uses the `<domain>.delete` permission as the manager-level proxy: in the
    seeded role matrix only the manager role gets `delete`. Superuser always.
    """
    if not getattr(user, "is_authenticated", False):
        return False
    if user.is_superuser:
        return True
    if not domain:
        return False
    return user.has_menu_perm(f"{domain}.delete")


_ACTION_MAP = {
    "list": "view",
    "retrieve": "view",
    "create": "create",
    "update": "update",
    "partial_update": "update",
    "destroy": "delete",
}


class HasMenuPermission(BasePermission):
    """Gate a ViewSet by the user's RBAC role permissions.

    Custom @action methods that need a non-`view` mapping (e.g. `approve`)
    should set `rbac_action_map = {"action_name": "approve"}` on the ViewSet.
    Unmapped custom @actions default to `view`, so read-only sub-resources work
    out of the box.
    """

    def has_permission(self, request, view):
        user = request.user
        if not user.is_authenticated:
            return False
        if user.is_superuser:
            return True
        domain = getattr(view, "rbac_domain", None)
        if not domain:
            return True
        # Actions explicitly opened to any authenticated user (e.g. employees
        # filling/answering a survey) bypass the menu-permission gate.
        if view.action in getattr(view, "rbac_public_actions", ()):
            return True
        action = _ACTION_MAP.get(view.action)
        if action is None:
            action = getattr(view, "rbac_action_map", {}).get(view.action, "view")
        return user.has_menu_perm(f"{domain}.{action}")


class IsOwnerOrFullScope(BasePermission):
    """Object-level write guard. Reads are open (queryset already scopes them).

    Writes (non-safe methods) allowed if the user has full scope in the
    ViewSet's `rbac_domain`, or owns the object via a known ownership field.
    Pair with a `get_queryset` that scopes list/retrieve — this stops a scoped
    user editing another user's record even if it leaks into the queryset.
    """

    OWNER_FIELDS = (
        "created_by_id", "requester_id", "assignee_id", "user_id",
        "organizer_id", "uploaded_by_id",
    )

    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True
        user = request.user
        if user_has_full_scope(user, getattr(view, "rbac_domain", "")):
            return True
        uid = getattr(user, "id", None)
        return any(getattr(obj, f, None) == uid for f in self.OWNER_FIELDS)


class IsRbacAdmin(BasePermission):
    """Allows access only to RBAC admins (superuser or has `rbac.manage`)."""

    def has_permission(self, request, view):
        user = request.user
        if not user.is_authenticated:
            return False
        return user.is_superuser or user.has_menu_perm("rbac.manage")
