"""Method-aware, fail-closed DRF capability enforcement (RBAC v2).

Views declare an explicit ``capability_map``. Keys are ``(action, method)``
tuples or an ``action`` string (all methods for that action). Values:

- a capability code string;
- ``{"any_of": [codes]}`` or ``{"all_of": [codes]}``;
- ``PUBLIC`` — explicitly public endpoint (manifest-reviewed);
- ``AUTHENTICATED_PERSONAL`` — owner-scoped endpoint for any active member;
- ``DENY`` — explicitly disabled (e.g. destroy on workflow documents).

An unmapped (action, method) pair is DENIED — there is no fallback to
``view``/read. Missing capability => 403; scope filtering (object outside
scope => 404) belongs to the owning domain's scope policy.

Example::

    class RequisitionViewSet(ModelViewSet):
        permission_classes = [RequiresCapabilities]
        capability_map = {
            ("list", "GET"): "procurement.requisition.read",
            ("retrieve", "GET"): "procurement.requisition.read",
            ("create", "POST"): "procurement.requisition.create",
            ("update", "PUT"): "procurement.requisition.update",
            ("partial_update", "PATCH"): "procurement.requisition.update",
            ("destroy", "DELETE"): DENY,
            ("submit", "POST"): "procurement.requisition.submit",
        }
"""

from __future__ import annotations

from rest_framework.permissions import BasePermission

from . import registry
from .resolver import effective_capabilities


class _Marker:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"<capability marker {self.name}>"


PUBLIC = _Marker("PUBLIC")
AUTHENTICATED_PERSONAL = _Marker("AUTHENTICATED_PERSONAL")
DENY = _Marker("DENY")

_UNMAPPED = _Marker("UNMAPPED")


def resolve_rule(view, action: str | None, method: str):
    """The rule for an (action, method) pair, or _UNMAPPED. Exact
    (action, method) beats the action-wide entry."""
    capability_map = getattr(view, "capability_map", None)
    if not capability_map or action is None:
        return _UNMAPPED
    rule = capability_map.get((action, method.upper()), _UNMAPPED)
    if rule is _UNMAPPED:
        rule = capability_map.get(action, _UNMAPPED)
    return rule


def _codes_in_rule(rule):
    if isinstance(rule, str):
        return [rule]
    if isinstance(rule, dict):
        return list(rule.get("any_of", [])) + list(rule.get("all_of", []))
    return []


def validate_capability_map(view_class) -> list[str]:
    """Meta-test helper: problems in a view's capability_map (empty = valid).

    Checks every rule references registered capability codes and that dict
    rules declare exactly one of any_of/all_of. Enumerating a router's
    (action, method) pairs against the map is done by
    ``unmapped_action_methods`` below.
    """
    problems = []
    capability_map = getattr(view_class, "capability_map", None)
    if capability_map is None:
        return [f"{view_class.__name__} has no capability_map"]
    for key, rule in capability_map.items():
        label = f"{view_class.__name__}.{key}"
        if isinstance(rule, _Marker):
            if rule is _UNMAPPED:
                problems.append(f"{label}: invalid marker")
            continue
        if isinstance(rule, dict):
            if bool(rule.get("any_of")) == bool(rule.get("all_of")):
                problems.append(f"{label}: declare exactly one of any_of/all_of")
        elif not isinstance(rule, str):
            problems.append(f"{label}: unsupported rule type {type(rule).__name__}")
        for code in _codes_in_rule(rule):
            if not registry.is_registered(code):
                problems.append(f"{label}: unknown capability {code!r}")
    return problems


def unmapped_action_methods(viewset_class) -> list[tuple[str, str]]:
    """Meta-test helper: every (action, HTTP method) pair a DefaultRouter would
    expose for this viewset that has no capability_map entry. A migrated
    module's test asserts this returns []."""
    from rest_framework.routers import DefaultRouter

    router = DefaultRouter()
    router.register("probe", viewset_class, basename="probe")
    pairs = set()
    for route in router.get_routes(viewset_class):
        for method, action in route.mapping.items():
            if hasattr(viewset_class, action):
                pairs.add((action, method.upper()))
    view = viewset_class()
    return sorted(
        (action, method) for action, method in pairs
        if resolve_rule(view, action, method) is _UNMAPPED
    )


class RequiresCapabilities(BasePermission):
    """Fail-closed capability gate. See module docstring for capability_map."""

    def has_permission(self, request, view):
        action = getattr(view, "action", None)
        rule = resolve_rule(view, action, request.method)

        if rule is PUBLIC:
            return True
        if rule is DENY or rule is _UNMAPPED:
            # Unmapped pairs fail closed (plan section 2.9). DENY renders 403
            # too; the view should also drop the route (405) where possible.
            return False

        user = request.user
        if not getattr(user, "is_authenticated", False) or not user.is_active:
            return False
        if rule is AUTHENTICATED_PERSONAL:
            return True

        from apps.core.tenancy import get_active_organization

        organization = get_active_organization(request)
        capabilities = effective_capabilities(user, organization)
        if isinstance(rule, str):
            return rule in capabilities
        if isinstance(rule, dict):
            any_of = rule.get("any_of")
            all_of = rule.get("all_of")
            if any_of:
                return any(code in capabilities for code in any_of)
            if all_of:
                return all(code in capabilities for code in all_of)
        return False
