"""
Trace context — prevents double-counting when orchestration frameworks
(LangChain, LlamaIndex) call lower-level provider SDKs (OpenAI, Anthropic).

Uses Python ``contextvars`` so the context propagates correctly through
``asyncio`` tasks and thread-local storage without leaking.

How it works
~~~~~~~~~~~~
When a LangChain callback handler begins tracing an LLM call, it enters
the ``trace_scope()`` context manager.  Inside that scope, any raw
provider wrapper (``TracedOpenAI``, ``TracedAnthropic``, etc.) that
calls ``is_inside_trace()`` sees ``True`` and quietly skips its own
trace to avoid duplicates.

Usage (framework integrations)::

    from syntropy.trace_context import trace_scope

    with trace_scope("langchain"):
        # … underlying OpenAI call
        result = original_fn()

Usage (provider wrappers)::

    from syntropy.trace_context import is_inside_trace

    if is_inside_trace():
        return original_fn()  # skip our trace wrapper
"""

from __future__ import annotations

import contextvars
from contextlib import contextmanager
from typing import Generator, Optional

_active_trace: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
    "syntropy_active_trace", default=None
)


def is_inside_trace() -> bool:
    """Return True if the current context is inside a traced framework call."""
    return _active_trace.get() is not None


def active_trace_source() -> Optional[str]:
    """Return the source name of the current trace scope, or None."""
    return _active_trace.get()


@contextmanager
def trace_scope(source: str) -> Generator[None, None, None]:
    """
    Context manager that marks the current execution as being inside
    an orchestration-level trace (e.g. ``"langchain"``).

    Provider-level wrappers should check ``is_inside_trace()`` and
    skip their own trace dispatch when True to avoid double-counting.
    """
    token = _active_trace.set(source)
    try:
        yield
    finally:
        _active_trace.reset(token)
