# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-opentelemetry-genai-tool-call-span-attribute-coercion-and-the-silent-loss-of-gen-ai-tool-call-arguments-under-non-jso.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-opentelemetry-genai-tool-call-span-attribute-coercion-and-the-silent-loss-of-gen-ai-tool-call-arguments-under-non-jso.warrant.json
# Markdown SHA-256:   6689f3ada99c17df66229754cdae5f6acf1c980e04e7b753614cb18ea27d3b61
# Code AST SHA-256:   10efea470c62665e909429b899dbd3f90681beb38a9ca37544178a62aca36fc4
# Verified Classes:   ToolCallArgumentFidelityError
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import json
from datetime import datetime
from decimal import Decimal


class ToolCallArgumentFidelityError(RuntimeError):
    """Raised when a GenAI tool-call span's arguments cannot be faithfully reconstructed."""


def reconstruct_tool_arguments(span_attributes: dict, tool_requires_args: bool) -> dict:
    """
    Attempt to reconstruct tool-call arguments from an OTel GenAI span.

    Enforces the operational invariant: a tool-call span for a tool that
    requires arguments MUST carry a JSON-round-trippable
    `gen_ai.tool.call.arguments` attribute. Absence or non-parseability is a
    hard failure, not a skip.
    """
    raw = span_attributes.get("gen_ai.tool.call.arguments")

    if raw is None:
        if tool_requires_args:
            raise ToolCallArgumentFidelityError(
                "gen_ai.tool.call.arguments absent for a tool that requires "
                "arguments; cannot distinguish 'no args' from 'serialization "
                "failure' (span status is not a reliable signal here)."
            )
        return {}

    try:
        parsed = json.loads(raw)
    except (TypeError, json.JSONDecodeError) as exc:
        raise ToolCallArgumentFidelityError(
            f"gen_ai.tool.call.arguments is not JSON-parseable: {exc!r}. "
            "Likely emitted via str() or default=str fallback; fidelity is lossy."
        ) from exc

    if not isinstance(parsed, dict):
        raise ToolCallArgumentFidelityError(
            f"Expected object arguments, got {type(parsed).__name__}; "
            "cannot replay tool invocation safely."
        )

    # Detect known lossy encodings that round-trip as strings but are not
    # the original type. This is a heuristic; tagged encoders avoid it.
    for key, value in parsed.items():
        if isinstance(value, str) and _looks_like_lossy_timestamp(value):
            raise ToolCallArgumentFidelityError(
                f"Argument {key!r} appears to be a str()-coerced datetime "
                f"({value!r}); original type is unrecoverable."
            )

    return parsed


def _looks_like_lossy_timestamp(value: str) -> bool:
    # A str(datetime) has a space separator; ISO-8601 uses 'T'.
    try:
        if " " in value and "T" not in value:
            datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
            return True
    except ValueError:
        pass
    return False


# Example: a span whose arguments contained a Decimal, emitted with default=str.
_lossy_span = {
    "gen_ai.tool.name": "compute_invoice_total",
    "gen_ai.tool.call.arguments": json.dumps({"amount": Decimal("0.1")}, default=str),
}

try:
    reconstruct_tool_arguments(_lossy_span, tool_requires_args=True)
except ToolCallArgumentFidelityError as err:
    # Quarantine, do not silently replay.
    print(f"QUARANTINE: {err}")
