from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied

from apps.core.permissions.pending_review import (
    WRITE_METHODS as _PENDING_WRITE_METHODS,
    _is_pending as _is_pending_review,
    _redirect_with_flash as _pending_redirect,
)
from apps.core.permissions.roles import UserRole


class RoleRequiredMixin(LoginRequiredMixin):
    """Require authenticated user with one of `allowed_roles` (or superuser).

    Staff status alone does not grant access — use explicit roles or Django permissions.

    Also blocks write-method requests for users with ``pending_admin_review=True``
    so any view inheriting this mixin enforces the review gate without changes.
    Reads pass through.
    """

    allowed_roles: tuple[str, ...] | list[str] = ()

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return self.handle_no_permission()
        if (
            request.method in _PENDING_WRITE_METHODS
            and _is_pending_review(request.user)
        ):
            return _pending_redirect(request)
        if request.user.is_superuser:
            return super().dispatch(request, *args, **kwargs)
        if self.allowed_roles and getattr(request.user, "role", "") not in self.allowed_roles:
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)


class RimsAccessMixin(LoginRequiredMixin):
    """RIMS routes: superuser, or any listed Django permission, or any allowed IILMP role.

    Also blocks write-method requests for users with ``pending_admin_review=True``.
    """

    allowed_roles: tuple[str, ...] | list[str] = ()
    rims_permissions: tuple[str, ...] | list[str] = ()

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return self.handle_no_permission()
        if (
            request.method in _PENDING_WRITE_METHODS
            and _is_pending_review(request.user)
        ):
            return _pending_redirect(request)
        u = request.user
        if u.is_superuser:
            return super().dispatch(request, *args, **kwargs)
        perm_ok = bool(
            self.rims_permissions and any(u.has_perm(p) for p in self.rims_permissions),
        )
        role_ok = bool(
            self.allowed_roles and getattr(u, "role", "") in self.allowed_roles,
        )
        if self.rims_permissions and self.allowed_roles:
            if perm_ok or role_ok:
                return super().dispatch(request, *args, **kwargs)
        elif self.rims_permissions:
            if perm_ok:
                return super().dispatch(request, *args, **kwargs)
        elif self.allowed_roles:
            if role_ok:
                return super().dispatch(request, *args, **kwargs)
        else:
            return super().dispatch(request, *args, **kwargs)
        raise PermissionDenied


class AdminRoleRequiredMixin(RoleRequiredMixin):
    allowed_roles = (UserRole.ADMIN,)


# ---------------------------------------------------------------------------
# P0 RBAC — SRS §4.3.1, §5.4. The four mixins below enforce role separation
# (NFRFM005 two-eyes, FRFM028–035 segregation of duties, NFRFA011 audit
# read-only). Bundled here so any view can pick the right gate without
# re-deriving the role membership each time.
# ---------------------------------------------------------------------------

_SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}


class AuditorReadOnlyMixin(RimsAccessMixin):
    """SRS NFRFA011 / FRFA-GPS025 — read-only audit-trail access.

    Allowed roles read; everyone else is denied. Write methods are blocked
    even for matching roles (auditor view is observe-only by definition).
    """

    allowed_roles = (
        UserRole.AUDITOR,
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
    )

    def dispatch(self, request, *args, **kwargs):
        if request.method not in _SAFE_METHODS:
            raise PermissionDenied("Audit log is read-only.")
        return super().dispatch(request, *args, **kwargs)


class FinanceApprovalMixin(RimsAccessMixin):
    """SRS §5.4 FRFM006/016/024/034 + NFRFM005 — disbursement approver tier.

    Distinct from FinanceStaffMixin which also allows GRANTS_MANAGER /
    FINANCE_OFFICER; approval requires Finance Manager or higher.
    """

    allowed_roles = (
        UserRole.FINANCE_MANAGER,
        UserRole.FINANCE_DIRECTOR,
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
    )


class ProgramCoordinatorMixin(RimsAccessMixin):
    """SRS §5.4 FRFM011/029 — budget-revision approver."""

    allowed_roles = (
        UserRole.PROGRAM_COORDINATOR,
        UserRole.PROGRAM_DIRECTOR,
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
    )


class DisbursementCreateMixin(RimsAccessMixin):
    """SRS §5.4 FRFM028/029/030/035 — disbursement request creators.

    PMs raise the request, finance staff may also create on their behalf.
    """

    allowed_roles = (
        UserRole.PROJECT_MANAGER,
        UserRole.FINANCE_OFFICER,
        UserRole.GRANTS_MANAGER,
        UserRole.PROGRAM_MANAGER,
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
    )


class PermissionPolicyMixin:
    """CBV: after object is loaded, require guardian permission on it."""

    object_permission: str | None = None

    def get_object(self, queryset=None):
        obj = super().get_object(queryset)
        if self.object_permission and not self.request.user.has_perm(self.object_permission, obj):
            raise PermissionDenied
        return obj
