"""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
from django.utils import timezone
from django.db import models


def user_has_full_scope(user, domain: str, organization) -> 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 or getattr(user, "organization_role", "staff") == "director":
        return True
    if not domain:
        return False
    return user.has_menu_perm(f"{domain}.delete", organization)


def user_managed_department_ids(user):
    if not getattr(user, "is_authenticated", False):
        return []
    return list(user.department_memberships.filter(role="manager", is_active=True).filter(models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now())).values_list("department_id", flat=True))


def user_is_authority_member(user, config_field, organization):
    if not getattr(user, "is_authenticated", False):
        return False
    if user.is_superuser or getattr(user, "organization_role", "staff") == "director":
        return True
    from .models import AppConfig
    config = AppConfig.objects.first()
    department_id = getattr(config, f"{config_field}_id", None) if config else None
    return bool(department_id and user.department_memberships.filter(department_id=department_id, is_active=True).filter(models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now())).exists())


def user_is_hr_authority(user, organization):
    """True if `user` may read confidential HR employee detail beyond their own record.

    Deliberately does NOT reuse `user_is_authority_member`'s superuser bypass:
    platform-superuser status alone must not grant confidential HR read
    access (see CONTEXT.md's Superadmin/Director distinction — Superadmin is
    the platform superuser flag, Director is a separate fixed organization
    authority tier). A director is still an automatic HR authority.
    """
    if not getattr(user, "is_authenticated", False):
        return False
    if getattr(user, "organization_role", "staff") == "director":
        return True
    from .models import AppConfig
    config = AppConfig.objects.first()
    department_id = getattr(config, "hr_department_id", None) if config else None
    return bool(
        department_id
        and user.department_memberships.filter(department_id=department_id, is_active=True)
        .filter(models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now()))
        .exists()
    )


def user_is_procurement_authority(user, organization):
    return user_is_authority_member(user, "procurement_department", organization) or user.has_menu_perm("procurements.approve", organization)


def user_is_finance_authority(user, organization):
    return user_is_authority_member(user, "finance_department", organization) or user.has_menu_perm("finance.approve", organization) or user.user_roles.filter(organization=organization, role__slug__in=["finance-staff", "fat-manager", "finance-manager"]).exists()


def user_is_accounting_authority(user, organization):
    return user.has_menu_perm("accounting.approve", organization) or user.user_roles.filter(organization=organization, role__slug__in=["accounting-staff", "fat-manager"]).exists()


def user_is_administrations_approver(user, organization):
    """Authority to decide self-service Administration workflows.

    This is deliberately separate from operational scope: permission to see
    or delete every request must not implicitly grant approval authority.
    """
    if not getattr(user, "is_authenticated", False):
        return False
    if user.is_superuser or getattr(user, "organization_role", "staff") == "director":
        return True
    return user.has_menu_perm("administrations.approve", organization)


_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 or getattr(user, "organization_role", "staff") == "director":
            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")
        from .tenancy import get_active_organization
        organization = get_active_organization(request)
        # Visibility is constrained by each domain queryset. Every active user
        # may enter read-only operational screens for records in that scope.
        if action == "view":
            if domain == "finance":
                return user_is_finance_authority(user, organization)
            if domain == "accounting":
                return user_is_accounting_authority(user, organization)
            return True
        if domain == "asset_management" and user_is_finance_authority(user, organization):
            return True
        return user.has_menu_perm(f"{domain}.{action}", organization)


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
        from .tenancy import get_active_organization
        organization = get_active_organization(request)
        if user_has_full_scope(user, getattr(view, "rbac_domain", ""), organization):
            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
        if user.is_superuser:
            return True
        from .tenancy import get_active_organization
        return user.has_menu_perm("rbac.manage", get_active_organization(request))
