"""Tracing protocol for kernel instrumentation — stdlib only (ADR-007).

Defines the interface that tracing backends must implement.
The kernel uses this protocol; concrete backends live outside kernel.
"""

from __future__ import annotations

import contextlib
import functools
import time
from collections.abc import Callable, Generator
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable


@dataclass
class SpanContext:
    """Lightweight span data container — stdlib only."""

    name: str
    start_time: float = field(default_factory=time.time)
    end_time: float = 0.0
    attributes: dict[str, Any] = field(default_factory=dict)
    status: str = "ok"
    error: str = ""

    @property
    def duration_ms(self) -> float:
        if self.end_time <= 0:
            return 0.0
        return (self.end_time - self.start_time) * 1000


@runtime_checkable
class TracingBackend(Protocol):
    """Protocol for tracing backends (OTel, console, null)."""

    def start_span(self, name: str, attributes: dict[str, Any] | None = None) -> SpanContext: ...

    def end_span(self, span: SpanContext) -> None: ...

    def record_exception(self, span: SpanContext, error: Exception) -> None: ...


class NullTracer:
    """No-op tracer for when no backend is configured."""

    def start_span(self, name: str, attributes: dict[str, Any] | None = None) -> SpanContext:
        return SpanContext(name=name, attributes=attributes or {})

    def end_span(self, span: SpanContext) -> None:
        span.end_time = time.time()

    def record_exception(self, span: SpanContext, error: Exception) -> None:
        span.status = "error"
        span.error = str(error)


class ConsoleTracer:
    """Tracer that prints spans to stdout — for development."""

    def __init__(self) -> None:
        self._spans: list[SpanContext] = []

    def start_span(self, name: str, attributes: dict[str, Any] | None = None) -> SpanContext:
        return SpanContext(name=name, attributes=attributes or {})

    def end_span(self, span: SpanContext) -> None:
        span.end_time = time.time()
        self._spans.append(span)

    def record_exception(self, span: SpanContext, error: Exception) -> None:
        span.status = "error"
        span.error = str(error)
        span.end_time = time.time()
        self._spans.append(span)

    @property
    def spans(self) -> list[SpanContext]:
        return list(self._spans)


# Global tracer registry
_active_tracer: TracingBackend = NullTracer()


def set_tracer(tracer: TracingBackend) -> None:
    """Set the global active tracer."""
    global _active_tracer  # noqa: PLW0603
    _active_tracer = tracer


def get_tracer() -> TracingBackend:
    """Get the global active tracer."""
    return _active_tracer


@contextlib.contextmanager
def trace_span(name: str, **attributes: Any) -> Generator[SpanContext, None, None]:
    """Context manager for creating traced spans."""
    tracer = get_tracer()
    span = tracer.start_span(name, attributes=dict(attributes))
    try:
        yield span
    except Exception as e:
        tracer.record_exception(span, e)
        raise
    else:
        tracer.end_span(span)


def traced(name: str | None = None, **extra_attrs: Any) -> Callable[..., Callable[..., Any]]:
    """Decorator to trace function execution."""

    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        span_name = name or f"{func.__module__}.{func.__qualname__}"

        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            with trace_span(span_name, **extra_attrs):
                return func(*args, **kwargs)

        return wrapper

    return decorator
