"""Weave (W&B) attribute mapper.

Weave consumes OpenInference + a small set of Weave-specific keys (display
name, thread id, output value). This mapper layers the latter on top of
OpenInference's vocabulary — compose ``["genai", "openinference", "weave"]``
to feed a Weave backend.
"""

from typing import Callable

from litellm.integrations.otel.mappers.base import AttributeMap, AttrValue, SpanData
from litellm.integrations.otel.mappers.utils import collect, json_or_none
from litellm.integrations.otel.model.payloads import LLMCallSpanData


class WeaveMapper:
    """Maps ``LLMCallSpanData`` to Weave's vendor attributes."""

    _LLM_CALL_ATTRS: dict[str, Callable[[LLMCallSpanData], AttrValue | None]] = {
        # ``display_name`` has the form ``"{operation} {model}"``. The span
        # name already covers that, but Weave reads this attribute too.
        "weave.display_name": lambda d: (
            f"{d.operation.value} {d.request_model}" if d.request_model else None
        ),
        "weave.call_id": lambda d: d.identity.call_id or None,
    }

    # JSON-payload attributes: each builder returns the serialized blob or None.
    _BLOB_ATTRS: dict[str, Callable[[LLMCallSpanData], AttrValue | None]] = {
        # Weave treats the response choices as the "output" payload.
        "weave.output": lambda d: (
            json_or_none(list(d.choices_out)) if d.choices_out else None
        ),
    }

    def map(self, data: SpanData) -> AttributeMap:
        match data:
            case LLMCallSpanData():
                return self._llm_call(data)
            case _:
                return {}

    @classmethod
    def _llm_call(cls, data: LLMCallSpanData) -> AttributeMap:
        return {
            **collect(cls._LLM_CALL_ATTRS, data),
            **collect(cls._BLOB_ATTRS, data),
        }
