"""Request-scoped context (tenant, actor, memberships, permissions).

The context is held in `contextvars.ContextVar` so it works for both sync and
async views and survives Celery's `task_prerun` propagation (set up in Phase 3).

Code paths MUST read the context via `current_request_context()` rather than
poking at `request.user` directly, so background jobs and CLI commands can
provide their own context without depending on an HTTP request.

Domain-boundary correction (Phase C):
  - ``workspace`` is the active UX shell (navigation, theme).
  - ``active_org_node`` is the active data-scope boundary.
  - Role-based permissions are in ``permissions`` (derived from memberships).
  - These three dimensions are independent.
"""

from __future__ import annotations

import contextlib
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Iterator

    from simorgh.apps.accounts.models import User
    from simorgh.apps.memberships.models import Membership
    from simorgh.apps.organizations.models import OrganizationNode
    from simorgh.apps.tenants.models import Tenant
    from simorgh.apps.workspaces.models import Workspace


@dataclass(frozen=True)
class RequestContext:
    """Immutable snapshot of who/where for the current request or job.

    ``permissions`` is the flat set of permission codenames the actor holds in
    ``tenant``, pre-resolved with role inheritance, for O(1) ``is_allowed`` checks.
    ``org_node_ids`` is the set of ``OrganizationNode.id`` the actor can act on
    (their membership nodes UNION descendants), used by the scoped query engine.
    ``workspace`` is the resolved UX-shell Workspace for this request (from the
    ``X-Workspace`` header), or None.
    ``active_org_node`` is the user's active OrganizationNode (data scope),
    resolved from ``X-Org-Node`` header or ``UserOrgContextPreference``, or None.
    """

    actor: User | None
    tenant: Tenant | None
    memberships: tuple[Membership, ...] = ()
    permissions: frozenset[str] = frozenset()
    org_node_ids: frozenset[int] = frozenset()
    workspace: Workspace | None = None
    active_org_node: OrganizationNode | None = None
    extra: dict[str, object] = field(default_factory=dict)

    @property
    def is_authenticated(self) -> bool:
        return self.actor is not None and getattr(self.actor, "is_authenticated", False)

    @property
    def is_superuser(self) -> bool:
        return bool(self.actor and getattr(self.actor, "is_superuser", False))


_EMPTY = RequestContext(actor=None, tenant=None)

_current_context: ContextVar[RequestContext] = ContextVar("simorgh_request_context", default=_EMPTY)


def current_request_context() -> RequestContext:
    """Return the request context bound to the current execution flow."""

    return _current_context.get()


def current_tenant() -> Tenant | None:
    return _current_context.get().tenant


def current_actor() -> User | None:
    return _current_context.get().actor


def current_workspace() -> Workspace | None:
    return _current_context.get().workspace


def current_org_node() -> OrganizationNode | None:
    return _current_context.get().active_org_node


def set_request_context(ctx: RequestContext) -> object:
    """Bind `ctx`. Returns a token usable with `reset_request_context`."""

    return _current_context.set(ctx)


def reset_request_context(token: object) -> None:
    _current_context.reset(token)  # type: ignore[arg-type]


@contextlib.contextmanager
def use_request_context(ctx: RequestContext) -> Iterator[RequestContext]:
    """Temporarily swap the active context (tests, management commands, jobs)."""

    token = _current_context.set(ctx)
    try:
        yield ctx
    finally:
        _current_context.reset(token)
