"""Authorization engine — combines RBAC (role permissions) with ABAC (policies)."""

from __future__ import annotations

from collections.abc import Iterable
from typing import TYPE_CHECKING, Any

from simorgh.apps.iam import cache as iam_cache
from simorgh.apps.iam.evaluator import evaluate
from simorgh.apps.iam.models import Policy, Role

if TYPE_CHECKING:
    from simorgh.core.context import RequestContext

def collect_role_chain(role: Role) -> list[Role]:
    """Return ``[role, parent, grandparent, ...]`` stopping on cycles."""

    chain: list[Role] = []
    seen: set[int] = set()
    cur: Role | None = role
    while cur is not None and cur.pk not in seen:
        chain.append(cur)
        seen.add(cur.pk)
        cur = cur.parent_role
    return chain


def resolved_permissions(role: Role) -> set[str]:
    """Flat permission codename set for `role` (with parent inheritance)."""

    codenames: set[str] = set()
    for r in collect_role_chain(role):
        codenames.update(r.permissions.values_list("codename", flat=True))
    return codenames


def collect_permissions_for_roles(roles: Iterable[Role]) -> set[str]:
    out: set[str] = set()
    for r in roles:
        out |= resolved_permissions(r)
    return out


def is_allowed(
    ctx: RequestContext,
    action: str,
    resource: Any | None = None,
    *,
    extra: dict[str, Any] | None = None,
) -> bool:
    """Return True if `ctx.actor` may perform `action` on `resource`.

    Decision order:
      1. Unauthenticated → deny.
      2. Superuser → allow.
      3. `action` not in `ctx.permissions` (RBAC miss) → deny.
      4. If any policy on a held role targets this action, all attached
         policies on the matched role(s) must evaluate to True (ABAC).

    Results are memoised per request keyed on ``(actor_id, action, obj_pk)``.
    Pass ``extra`` to bypass the cache for that call.
    """

    actor_id = getattr(ctx.actor, "pk", None) if ctx.actor else None
    # Only memoise when we can derive a stable identity for the resource —
    # anonymous resources (no pk) often carry transient ABAC attributes and
    # would otherwise collide on the (actor, action, None) key.
    resource_pk = getattr(resource, "pk", None) if resource is not None else None
    cacheable = extra is None and (resource is None or resource_pk is not None)
    cache_key = iam_cache.cache_key(actor_id, action, resource) if cacheable else None
    if cache_key is not None:
        cached = iam_cache.get(cache_key)
        if cached is not None:
            return cached

    result = _evaluate_decision(ctx, action, resource, extra)
    if cache_key is not None:
        iam_cache.set(cache_key, result)
    return result


def _evaluate_decision(
    ctx: RequestContext,
    action: str,
    resource: Any | None,
    extra: dict[str, Any] | None,
) -> bool:
    if not ctx.is_authenticated:
        return False
    if ctx.is_superuser:
        return True
    if action not in ctx.permissions:
        return False

    role_ids = {m.role_id for m in ctx.memberships}
    if not role_ids:
        return True  # RBAC passed and no roles attached (shouldn't normally happen)

    policies = list(
        Policy.objects.filter(
            role_id__in=role_ids,
            permission__codename=action,
            is_active=True,
        )
    )
    if not policies:
        return True  # No ABAC constraints attached.

    eval_ctx: dict[str, Any] = {
        "actor": ctx.actor,
        "tenant": ctx.tenant,
        "resource": resource,
        "extra": extra or {},
    }
    return all(evaluate(p.conditions, eval_ctx) for p in policies)


def is_allowed_object(
    ctx: RequestContext,
    action: str,
    obj: Any,
    *,
    extra: dict[str, Any] | None = None,
) -> bool:
    """Object-level convenience wrapper.

    Augments ``extra`` with the most common ABAC inputs derived from ``obj``:
    ``object_tenant_id``, ``object_org_node_id``, ``object_owner_id``. Modules
    can reference these in their policies via ``{"attr": "extra.object_owner_id"}``.
    """

    merged: dict[str, Any] = dict(extra or {})
    merged.setdefault("object_tenant_id", getattr(obj, "tenant_id", None))
    merged.setdefault("object_org_node_id", getattr(obj, "organization_node_id", None))
    owner_id = getattr(obj, "owner_id", None)
    if owner_id is None:
        owner = getattr(obj, "owner", None) or getattr(obj, "created_by", None)
        if owner is not None and hasattr(owner, "pk"):
            owner_id = owner.pk
    merged.setdefault("object_owner_id", owner_id)
    return is_allowed(ctx, action, obj, extra=merged)
