"""Canonical ``mesh.*`` OpenTelemetry span attribute schema — Python SDK.

Mirrors the TypeScript ``MESH_ATTR`` constants from
``src/observability/mesh-span-attributes.ts``. Keep both files in sync
whenever the schema evolves.

Usage::

    from meshpocalypse_semantic.mesh_span_attributes import MeshAttr, set_mesh_attributes

    # With OpenTelemetry SDK installed:
    from opentelemetry import trace

    tracer = trace.get_tracer("my-agent")
    with tracer.start_as_current_span("mesh.job.run") as span:
        set_mesh_attributes(span, agent_id="did:mesh:agent-1",
                            job_id="550e8400-e29b-41d4-a716-446655440000",
                            job_type="audit", job_state="running")

    # Without OpenTelemetry (no-op gracefully):
    span = NonRecordingSpan()
    set_mesh_attributes(span, agent_id="did:mesh:agent-1")
"""

from __future__ import annotations

from dataclasses import dataclass


# ─── Attribute key constants ──────────────────────────────────────────────────


@dataclass(frozen=True)
class _MeshAttrConstants:
    """Frozen dataclass holding all canonical ``mesh.*`` span attribute keys.

    Access via the module-level :data:`MeshAttr` singleton::

        from meshpocalypse_semantic.mesh_span_attributes import MeshAttr

        span.set_attribute(MeshAttr.TENANT_ID, "acme")
    """

    #: Tenant namespace — identifies the logical tenant boundary.
    TENANT_ID: str = "mesh.tenant_id"

    #: Emitting agent's IRI (e.g. ``did:mesh:my-agent``).
    AGENT_ID: str = "mesh.agent_id"

    #: Job UUID — uniquely identifies a unit of work.
    JOB_ID: str = "mesh.job_id"

    #: Job type label. Examples: ``audit``, ``crawl``.
    JOB_TYPE: str = "mesh.job_type"

    #: Job lifecycle state. Valid: ``queued`` | ``running`` | ``succeeded`` | ``failed``.
    JOB_STATE: str = "mesh.job_state"

    #: ATF trust level. Valid: ``INTERN`` | ``JUNIOR`` | ``SENIOR`` | ``LEAD`` | ``SYSTEM``.
    ATF_LEVEL: str = "mesh.atf_level"

    #: Capability IRI — for auction and routing spans.
    CAPABILITY: str = "mesh.capability"

    #: NATS subject — for transport spans.
    SUBJECT: str = "mesh.subject"

    #: Winning agent IRI — for auction spans.
    AUCTION_WINNER: str = "mesh.auction_winner"


#: Module-level singleton — import this and use its fields as attribute keys.
MeshAttr = _MeshAttrConstants()


# ─── Span protocol ────────────────────────────────────────────────────────────


class _SpanLike:
    """Structural protocol: any object with ``set_attribute(key, value)``."""

    def set_attribute(self, key: str, value: str) -> None:  # pragma: no cover
        ...


# ─── Helper ───────────────────────────────────────────────────────────────────


def set_mesh_attributes(
    span: object,
    *,
    tenant_id: str | None = None,
    agent_id: str | None = None,
    job_id: str | None = None,
    job_type: str | None = None,
    job_state: str | None = None,
    atf_level: str | None = None,
    capability: str | None = None,
    subject: str | None = None,
    auction_winner: str | None = None,
) -> None:
    """Set ``mesh.*`` span attributes on *span* in a single call.

    Accepts any object that exposes ``set_attribute(key: str, value: str)``,
    including OpenTelemetry ``Span`` objects and no-op stubs.

    Only attributes whose value is **not** ``None`` are forwarded to the span;
    ``None`` values are silently skipped to prevent ``"None"`` strings from
    appearing in traces.

    Args:
        span:          Any span-like object with a ``set_attribute`` method.
        tenant_id:     Tenant namespace.
        agent_id:      Emitting agent IRI.
        job_id:        Job UUID.
        job_type:      Job type label (e.g. ``"audit"``, ``"crawl"``).
        job_state:     Job lifecycle state.
        atf_level:     ATF trust level string.
        capability:    Capability IRI.
        subject:       NATS subject.
        auction_winner: Winning agent IRI.

    Example::

        with tracer.start_as_current_span("mesh.job.run") as span:
            set_mesh_attributes(
                span,
                agent_id="did:mesh:agent-1",
                job_id="abc123",
                job_type="audit",
                job_state="running",
            )
    """
    set_attr = getattr(span, "set_attribute", None)
    if set_attr is None:
        return

    _maybe_set(set_attr, MeshAttr.TENANT_ID, tenant_id)
    _maybe_set(set_attr, MeshAttr.AGENT_ID, agent_id)
    _maybe_set(set_attr, MeshAttr.JOB_ID, job_id)
    _maybe_set(set_attr, MeshAttr.JOB_TYPE, job_type)
    _maybe_set(set_attr, MeshAttr.JOB_STATE, job_state)
    _maybe_set(set_attr, MeshAttr.ATF_LEVEL, atf_level)
    _maybe_set(set_attr, MeshAttr.CAPABILITY, capability)
    _maybe_set(set_attr, MeshAttr.SUBJECT, subject)
    _maybe_set(set_attr, MeshAttr.AUCTION_WINNER, auction_winner)


def _maybe_set(set_attr: object, key: str, value: str | None) -> None:
    """Call ``set_attr(key, value)`` only when *value* is not ``None``."""
    if value is not None and callable(set_attr):
        set_attr(key, value)
