"""
Scope and context management.

Provides per-request isolation using ``contextvars`` so that tags,
user info, breadcrumbs, and extra data are automatically associated
with the correct request in concurrent / async applications.

Two scope tiers:
* **Global scope** – process-wide data (release, environment, SDK info).
* **Isolation scope** – per-request data (user, tags, breadcrumbs).
"""

import copy
import contextvars
from collections import deque
from datetime import datetime, timezone

from overcast_sdk.consts import MAX_BREADCRUMBS
from overcast_sdk.utils import capture_internal, safe_str

# ── Context variable for per-request scope ────────────────────────────────────
_isolation_scope_var = contextvars.ContextVar("overcast_isolation_scope", default=None)


class Scope:
    """Holds contextual data that is merged into every event."""

    __slots__ = (
        "_tags", "_user", "_extra", "_contexts",
        "_breadcrumbs", "_max_breadcrumbs", "_event_processors",
        "_before_breadcrumb",
    )

    def __init__(self, max_breadcrumbs=MAX_BREADCRUMBS, before_breadcrumb=None):
        self._tags = {}
        self._user = {}
        self._extra = {}
        self._contexts = {}
        self._breadcrumbs = deque(maxlen=max_breadcrumbs)
        self._max_breadcrumbs = max_breadcrumbs
        self._event_processors = []
        self._before_breadcrumb = before_breadcrumb

    # ── Tags ──────────────────────────────────────────────────────────────

    def set_tag(self, key, value):
        self._tags[safe_str(key)] = safe_str(value)

    def remove_tag(self, key):
        self._tags.pop(key, None)

    # ── User ──────────────────────────────────────────────────────────────

    def set_user(self, user_data):
        """Set user context.  Pass ``None`` to clear."""
        self._user = dict(user_data) if user_data else {}

    # ── Extra ─────────────────────────────────────────────────────────────

    def set_extra(self, key, value):
        self._extra[safe_str(key)] = value

    # ── Contexts ──────────────────────────────────────────────────────────

    def set_context(self, key, value):
        self._contexts[safe_str(key)] = dict(value) if isinstance(value, dict) else value

    # ── Breadcrumbs ───────────────────────────────────────────────────────

    def add_breadcrumb(self, crumb, hint=None):
        """Append a breadcrumb to the scope.

        The optional *before_breadcrumb* hook may modify or discard it.
        """
        with capture_internal:
            if not isinstance(crumb, dict):
                return

            # Apply hook
            if self._before_breadcrumb is not None:
                crumb = self._before_breadcrumb(crumb, hint)
                if crumb is None:
                    return

            crumb.setdefault(
                "timestamp", datetime.now(timezone.utc).isoformat()
            )
            crumb.setdefault("level", "info")
            self._breadcrumbs.append(crumb)

    def clear_breadcrumbs(self):
        self._breadcrumbs.clear()

    # ── Event processors ──────────────────────────────────────────────────

    def add_event_processor(self, fn):
        """Register a callable ``fn(event) -> event | None``."""
        self._event_processors.append(fn)

    # ── Apply scope data to an event ──────────────────────────────────────

    def apply_to_event(self, event):
        """Merge scope data into *event* and run processors.

        Returns the (possibly modified) event, or ``None`` if a processor
        drops it.
        """
        if self._tags:
            event.setdefault("tags", {}).update(self._tags)
        if self._user:
            event["user"] = dict(self._user)
        if self._extra:
            event.setdefault("extra", {}).update(self._extra)
        if self._contexts:
            event.setdefault("contexts", {}).update(self._contexts)
        if self._breadcrumbs:
            event["breadcrumbs"] = list(self._breadcrumbs)

        for processor in self._event_processors:
            with capture_internal:
                event = processor(event)
                if event is None:
                    return None
        return event

    # ── Copy / clear ──────────────────────────────────────────────────────

    def copy(self):
        """Return a shallow copy of this scope."""
        new = Scope.__new__(Scope)
        new._tags = dict(self._tags)
        new._user = dict(self._user)
        new._extra = dict(self._extra)
        new._contexts = dict(self._contexts)
        new._breadcrumbs = deque(self._breadcrumbs, maxlen=self._max_breadcrumbs)
        new._max_breadcrumbs = self._max_breadcrumbs
        new._event_processors = list(self._event_processors)
        new._before_breadcrumb = self._before_breadcrumb
        return new

    def clear(self):
        self._tags.clear()
        self._user.clear()
        self._extra.clear()
        self._contexts.clear()
        self._breadcrumbs.clear()
        self._event_processors.clear()


# ── Global scope (shared across all events) ───────────────────────────────────
_global_scope = None


def get_global_scope():
    """Return the process-wide global scope (created lazily)."""
    global _global_scope
    if _global_scope is None:
        _global_scope = Scope()
    return _global_scope


def set_global_scope(scope):
    global _global_scope
    _global_scope = scope


# ── Isolation scope (per-request / per-task) ──────────────────────────────────

def get_isolation_scope():
    """Return the current request/task scope, creating one if needed."""
    scope = _isolation_scope_var.get()
    if scope is None:
        scope = Scope()
        _isolation_scope_var.set(scope)
    return scope


def push_isolation_scope(scope=None):
    """Set a new isolation scope, returning the token for ``pop``."""
    if scope is None:
        scope = Scope()
    return _isolation_scope_var.set(scope)


def pop_isolation_scope(token):
    """Restore the previous isolation scope."""
    _isolation_scope_var.reset(token)


# ── Convenience: "current scope" is always the isolation scope ────────────────

def get_current_scope():
    """Alias for the isolation scope (the one most integrations use)."""
    return get_isolation_scope()
