from collections.abc import Iterator
from collections.abc import Mapping
from collections.abc import Sequence
from contextlib import contextmanager
from typing import TYPE_CHECKING
from typing import Optional

from opentelemetry import version
from opentelemetry.context import Context as OtelContext  # noqa:F401
from opentelemetry.trace import SpanKind as OtelSpanKind
from opentelemetry.trace import Tracer as OtelTracer
from opentelemetry.trace import TracerProvider as OtelTracerProvider
from opentelemetry.trace import use_span
from opentelemetry.trace.propagation import get_current_span
from opentelemetry.trace.span import DEFAULT_TRACE_OPTIONS
from opentelemetry.trace.span import INVALID_SPAN

from ddtrace._trace.provider import ActiveTrace as DDActiveTrace
from ddtrace.internal.constants import SPAN_API_OTEL
from ddtrace.internal.logger import get_logger
from ddtrace.propagation.http import _TraceContext
from ddtrace.trace import tracer as ddtracer

from .span import Span


if TYPE_CHECKING:
    from opentelemetry.trace import Link as OtelLink  # noqa:F401
    from opentelemetry.trace.span import Span as OtelSpan  # noqa:F401
    from opentelemetry.util.types import AttributeValue as OtelAttributeValue  # noqa:F401

    from ddtrace.context import Context as DDContext  # noqa:F401


log = get_logger(__name__)


try:
    from opentelemetry.util._decorator import _agnosticcontextmanager as contextmanager  # type: ignore[no-redef]
except ImportError:
    log.warning(
        "opentelemetry.util._decorator not found, using contextlib.contextmanager instead. "
        "Using @tracer.start_as_current_span decorator in generators and async functions "
        "will result in inaccurate durations. For async support upgrade to opentelemetry-api>=1.24."
    )
    from contextlib import contextmanager


OTEL_VERSION = tuple(int(x) for x in version.__version__.split(".")[:3])


def _otel_to_dd_span_context(otel_span: "OtelSpan") -> "DDContext":
    trace_id, span_id, _, tf, ts, _ = otel_span.get_span_context()
    if tf is DEFAULT_TRACE_OPTIONS:
        # If a SpanContext is created with specificing the trace flags field it is set to DEFAULT_TRACE_OPTIONS.
        # This is analogous to setting SamplingPriority to None. The span will be sampled lazily.
        tf = None
    trace_state = ts.to_header()
    return _TraceContext._get_context(trace_id, span_id, tf, trace_state)


class TracerProvider(OtelTracerProvider):
    """
    Entry point of the OpenTelemetry API and provides access to OpenTelemetry compatible Tracers.
    One TracerProvider should be initialized and set per application.
    """

    if OTEL_VERSION >= (1, 26):
        # OpenTelemetry 1.26+ has a new get_tracer signature
        # https://github.com/open-telemetry/opentelemetry-python/commit/d4e13bdf95190314b0d21a9357f850fa2e6a4cd3
        # The new signature includes an `attributes` parameter which is used by opentelemetry internals.
        def get_tracer(
            self,
            instrumenting_module_name: str,
            instrumenting_library_version: Optional[str] = None,
            schema_url: Optional[str] = None,
            attributes: Optional[dict] = None,
        ) -> OtelTracer:
            """Returns an opentelemetry compatible Tracer."""
            return Tracer()

    else:

        def get_tracer(  # type: ignore[misc]
            self,
            instrumenting_module_name: str,
            instrumenting_library_version: Optional[str] = None,
            schema_url: Optional[str] = None,
        ) -> OtelTracer:
            """Returns an opentelemetry compatible Tracer."""
            return Tracer()


class Tracer(OtelTracer):
    """Starts and/or activates OpenTelemetry compatible Spans using the global Datadog Tracer."""

    def start_span(
        self,
        name: str,
        context: Optional[OtelContext] = None,
        kind: OtelSpanKind = OtelSpanKind.INTERNAL,
        attributes: Optional[Mapping[str, "OtelAttributeValue"]] = None,
        links: Optional[Sequence["OtelLink"]] = None,
        start_time: Optional[int] = None,
        record_exception: bool = True,
        set_status_on_exception: bool = True,
    ) -> "OtelSpan":
        """Creates and starts an opentelemetry span."""
        # Get active otel span
        curr_otel_span = get_current_span(context)
        if curr_otel_span is INVALID_SPAN:
            # There is no active datadog/otel span
            dd_active: Optional[DDActiveTrace] = None
        elif isinstance(curr_otel_span, Span):
            # Get underlying ddtrace span from the active otel span
            dd_active = curr_otel_span._ddspan
        else:
            # Otel span was not generated by the ddtrace library and does not have an underlying ddtrace span.
            # Convert otel span to a ddtrace context object.
            dd_active = _otel_to_dd_span_context(curr_otel_span)

        # Create a new Datadog span (not activated), then return a valid OTel span
        dd_span = ddtracer._start_span(name, child_of=dd_active, activate=False, span_api=SPAN_API_OTEL)

        if links:
            for link in links:
                dd_span.set_link(
                    link.context.trace_id,
                    link.context.span_id,
                    link.context.trace_state.to_header(),
                    link.context.trace_flags,
                    link.attributes,
                )
        return Span(
            dd_span,
            kind=kind,
            attributes=attributes,
            start_time=start_time,
            record_exception=record_exception,
            set_status_on_exception=set_status_on_exception,
        )

    @contextmanager
    def start_as_current_span(
        self,
        name: str,
        context: Optional[OtelContext] = None,
        kind: OtelSpanKind = OtelSpanKind.INTERNAL,
        attributes: Optional[Mapping[str, "OtelAttributeValue"]] = None,
        links: Optional[Sequence["OtelLink"]] = None,
        start_time: Optional[int] = None,
        record_exception: bool = True,
        set_status_on_exception: bool = True,
        end_on_exit: bool = True,
    ) -> Iterator["OtelSpan"]:
        """Context manager for creating and activating a new opentelemetry span."""
        # Create a new non-active OTel span wrapper
        span = self.start_span(
            name,
            context=context,
            kind=kind,
            attributes=attributes,
            links=links,
            start_time=start_time,
            record_exception=record_exception,
            set_status_on_exception=set_status_on_exception,
        )

        with use_span(
            span,
            end_on_exit=end_on_exit,
            record_exception=record_exception,
            set_status_on_exception=set_status_on_exception,
        ) as span:
            yield span
