"""Closed-box helpers — ``get_logger`` + span-manipulation conveniences.

Closes CB-08 (consumers don't ``import logging`` for the
canonical logger pattern), CB-12 (no ``import opentelemetry`` to
manipulate spans mid-operation), CB-13 (OTel resource
configuration via Settings, not env vars only).

Every helper is no-op-safe when OTel isn't installed — the lazy
imports + ``is_recording()`` checks short-circuit cleanly. This
keeps the default-install code path light AND keeps consumer
call sites uniform whether OTel is wired or not.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from mgf.common.observability._otel_reexports import Span, SpanStatus


def get_logger(name: str | None = None) -> logging.Logger:
    """Return a :class:`logging.Logger` (closes CB-08).

    Identical to ``logging.getLogger(name)``, but exposed through
    ``mgf.common.observability`` so consumers don't reach past
    us into stdlib for the canonical logger pattern. The
    convention is::

        from mgf.common.observability import get_logger

        LOG = get_logger(__name__)
        LOG.info("hello", extra={"user": "alice"})

    Returns a stdlib :class:`logging.Logger` because that IS the
    interface — wrapping it in our own type would lose
    interoperability with every test fixture, IDE plug-in, and
    log-aggregation library that knows about ``logging.Logger``.
    The closed-box value here is the import path: consumers grow
    the habit of "everything logging-related lives in
    ``mgf.common.observability``".

    A future iteration MAY wrap in a ``SafeLogger`` that
    intercepts reserved-name ``extra={...}`` collisions (closes
    CB-10 mechanically — see CLOSED_BOX_REDESIGN.md §10 Q8).
    For Phase 2 we ship the simpler shape; the wrapper class can
    swap in without source-compat impact since both forms ARE
    ``logging.Logger`` subclasses.

    Threading: thread-safe (stdlib ``logging.getLogger`` is).
    """
    return logging.getLogger(name)


def get_current_span() -> Span | None:
    """Return the currently-active OpenTelemetry :class:`Span`,
    or ``None`` when no span is active or OTel is not installed.

    Closes CB-12 (consumers don't ``import opentelemetry`` to
    grab the active span).

    Threading: thread-safe — OTel resolves the active span via
    contextvars; the lookup is lock-free.
    """
    try:
        from opentelemetry import trace
    except ImportError:
        return None
    span = trace.get_current_span()
    # OTel returns a NonRecordingSpan when no real span is active.
    # Treat that as "no span" for the closed-box semantics.
    if not span.is_recording():
        return None
    return span


def set_attribute(key: str, value: Any) -> None:
    """Set ``key`` on the current span. No-op when OTel disabled
    or no span is active.

    Closes CB-12 — consumers no longer reach past us::

        # v0.1 (closed-box leak):
        from opentelemetry import trace
        trace.get_current_span().set_attribute("step", "validate")

        # v0.3:
        from mgf.common.observability import set_attribute
        set_attribute("step", "validate")

    Threading: thread-safe.
    """
    span = get_current_span()
    if span is None:
        return
    span.set_attribute(key, value)


def add_event(
    name: str,
    attributes: dict[str, Any] | None = None,
    timestamp: int | None = None,
) -> None:
    """Add a named event to the current span. No-op when OTel
    disabled or no span is active.

    Closes CB-12 (consumers no longer ``import opentelemetry`` to
    add a span event).

    Threading: thread-safe.
    """
    span = get_current_span()
    if span is None:
        return
    span.add_event(name, attributes=attributes or {}, timestamp=timestamp)


def set_status(status: SpanStatus, description: str = "") -> None:
    """Set the current span's status. No-op when OTel disabled
    or no span is active.

    ``status`` is a :class:`SpanStatus` value (re-exported as
    ``SpanStatus`` from ``mgf.common.observability``). Common
    usage::

        from mgf.common.observability import (
            operation_span, set_status, SpanStatus, StatusCode,
        )

        with operation_span("vm.create", vm_name=name):
            try:
                ...
            except SomeError as e:
                set_status(SpanStatus(StatusCode.ERROR, str(e)))
                raise

    Closes CB-12 — consumers no longer ``import opentelemetry``
    to mark a span errored.

    Threading: thread-safe.
    """
    span = get_current_span()
    if span is None:
        return
    span.set_status(status, description)


__all__ = [
    "add_event",
    "get_current_span",
    "get_logger",
    "set_attribute",
    "set_status",
]
