"""Source-preserving effective-access resolver (RBAC v2).

The only module allowed to combine baseline, role, Director, wildcard
migration, and cache logic. Entitlements bind capabilities to their source so
object/queryset authorization can evaluate "one entitlement carries both the
capability and the scope" — never a cross-product of a capability from one
source with a scope from another (plan section 4.7).

`effective_capabilities()` is a flattened union for menu/route presentation
only; object authorization keeps the entitlement list.

Deliberate migration rules:
- Legacy `*` / `<domain>.*` / `<domain>.<action>` grants NEVER expand into v2
  capability codes (plan section 10.1). Only Permission rows whose `code` is a
  registered capability count.
- Superusers keep the temporary runtime bypass (all registered capabilities)
  until the audited break-glass path ships (plan section 4.1).
"""

from __future__ import annotations

from dataclasses import dataclass, field

from django.core.cache import cache

from . import registry
from .cache import CACHE_TTL, effective_access_key

SOURCE_MEMBERSHIP = "membership_baseline"
SOURCE_ROLE = "role_assignment"
SOURCE_DIRECTOR = "director"
SOURCE_SUPERUSER = "superuser_migration_bypass"

# Scope kinds. Baseline capabilities bind to own/participating records only;
# role-assignment scopes widen via OperationalScopeGrant (Slice 2) — until
# then a role assignment carries "assignment" scope (own + future grants).
SCOPE_OWN = "own"
SCOPE_ASSIGNMENT = "assignment"
SCOPE_ORGANIZATION = "organization"


@dataclass(frozen=True)
class Entitlement:
    source: str
    capabilities: frozenset[str]
    scope: str
    role_assignment_id: int | None = None
    role_slug: str = ""


def _is_active_member(user, organization) -> bool:
    if not getattr(user, "is_authenticated", False) or not user.is_active:
        return False
    return user.organization_memberships.filter(organization=organization).exists()


def _role_entitlements(user, organization) -> list[Entitlement]:
    """One entitlement per UserRole assignment, carrying only registered
    v2 capability codes from that role's permissions."""
    from apps.core.models import UserRole

    assignments = (
        UserRole.objects.filter(user=user, organization=organization)
        .select_related("role")
        .prefetch_related("role__permissions")
    )
    result = []
    for assignment in assignments:
        codes = frozenset(
            p.code for p in assignment.role.permissions.all()
            if p.code and registry.is_registered(p.code)
        )
        if codes:
            result.append(Entitlement(
                source=SOURCE_ROLE,
                capabilities=codes,
                scope=SCOPE_ASSIGNMENT,
                role_assignment_id=assignment.pk,
                role_slug=assignment.role.slug,
            ))
    return result


def entitlements(user, organization) -> list[Entitlement]:
    """Source-preserving entitlement list for object/queryset authorization."""
    if organization is None or not getattr(user, "is_authenticated", False):
        return []
    if not user.is_active:
        return []

    result: list[Entitlement] = []
    all_registered = frozenset(d.code for d in registry.all_capabilities())

    if user.is_superuser:
        # Temporary migration bypass (plan 4.1) — replaced by audited
        # break-glass before contract cleanup.
        result.append(Entitlement(
            source=SOURCE_SUPERUSER,
            capabilities=all_registered,
            scope=SCOPE_ORGANIZATION,
        ))

    member = _is_active_member(user, organization)

    if member and getattr(user, "organization_role", "staff") == "director":
        result.append(Entitlement(
            source=SOURCE_DIRECTOR,
            capabilities=all_registered,
            scope=SCOPE_ORGANIZATION,
        ))

    if member:
        result.append(Entitlement(
            source=SOURCE_MEMBERSHIP,
            capabilities=registry.baseline_capability_codes(),
            scope=SCOPE_OWN,
        ))
        result.extend(_role_entitlements(user, organization))

    return result


def effective_capabilities(user, organization) -> frozenset[str]:
    """Flattened capability union for menu/route presentation ONLY.

    Cached per (user, organization) in the Django cache (TTL + explicit
    invalidation via authorization.cache) and memoized on the user instance
    for the request lifetime.
    """
    if organization is None or not getattr(user, "is_authenticated", False):
        return frozenset()

    memo = getattr(user, "_rbacv2_caps_memo", None)
    if memo is None:
        memo = user._rbacv2_caps_memo = {}
    if organization.id in memo:
        return memo[organization.id]

    key = effective_access_key(user.pk, organization.id)
    cached = cache.get(key)
    if cached is None:
        sources = entitlements(user, organization)
        cached = frozenset().union(*(e.capabilities for e in sources)) if sources else frozenset()
        cache.set(key, cached, CACHE_TTL)
    memo[organization.id] = cached
    return cached


def has_capability(user, organization, code: str) -> bool:
    return code in effective_capabilities(user, organization)
