"""Shared helpers for the attribute mappers.

Small, mapper-agnostic utilities — JSON serialization, message extraction, and
extractor-table application — pulled out of the individual mapper modules so
they live in one place.
"""

import json
from typing import Callable, Mapping, Sequence

from litellm.integrations.otel.mappers.base import AttributeMap, AttrValue
from litellm.integrations.otel.model.payloads import LLMCallSpanData


def drop_none(values: Mapping[str, AttrValue | None]) -> AttributeMap:
    """Return ``values`` with ``None``-valued entries removed."""
    return {k: v for k, v in values.items() if v is not None}


def collect(table: Mapping[str, Callable], source: object) -> AttributeMap:
    """Apply an extractor table to ``source``, dropping ``None`` results."""
    return drop_none({key: extract(source) for key, extract in table.items()})


def json_if(payload: Mapping[str, object]) -> str | None:
    """JSON-serialize ``payload`` only when it's non-empty; else ``None``."""
    return json.dumps(payload) if payload else None


def json_or_none(value: object) -> str | None:
    """JSON-serialize ``value`` (falling back to ``str``); ``None`` on failure."""
    try:
        return json.dumps(value, default=str)
    except Exception:
        return None


def stringify_message(message: object) -> str | None:
    """JSON-serialize a chat message dict; ``None`` if not a dict or on failure."""
    if not isinstance(message, dict):
        return None
    try:
        return json.dumps(message, default=str)
    except Exception:
        return None


def serialize_messages(messages: Sequence[object]) -> str | None:
    """Round-trip a sequence of message dicts through ``stringify_message``."""
    serialized = [
        json.loads(s) for s in (stringify_message(m) for m in messages) if s is not None
    ]
    return json.dumps(serialized) if serialized else None


def message_content(message: object) -> str | None:
    """Extract the textual ``content`` from a chat message dict."""
    if not isinstance(message, dict):
        return None
    content = message.get("content")
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        # multimodal: concatenate text parts only
        parts = [
            part.get("text", "")
            for part in content
            if isinstance(part, dict) and part.get("type") == "text"
        ]
        return "".join(p for p in parts if isinstance(p, str)) or None
    return None


def output_messages(data: LLMCallSpanData) -> list:
    """The ``message`` payload of each response choice."""
    return [c.get("message") for c in data.choices_out if isinstance(c, dict)]
