# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-genai-tool-call-span-as-a-child-of-the-model-invocation-span-rather-than-the-enclosing-agent-span.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-genai-tool-call-span-as-a-child-of-the-model-invocation-span-rather-than-the-enclosing-agent-span.warrant.json
# Markdown SHA-256:   084c864cda335cd74ab3fa63f47bf41097b15d11f7d335d432548e4a4abfbc1b
# Code AST SHA-256:   de8491913ac99862b82837a97d4f5581dc055b2fee64e649c938644303e13db0
# Verified Classes:   ToolSpanParentingError, ModelInvocation
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

from opentelemetry import trace


class ToolSpanParentingError(RuntimeError):
    """Raised when a tool-call span cannot be parented per GenAI conventions."""


@dataclass
class ModelInvocation:
    span: trace.Span
    context: object  # captured SpanContext-bearing context
    closed: bool = False


def parent_tool_span(
    tracer: trace.Tracer,
    tool_name: str,
    model: Optional[ModelInvocation],
    agent_context: object,
    *,
    framework_synthesized: bool = False,
    ambiguous_owner: bool = False,
) -> trace.Span:
    """Create a tool-call span with the convention-correct parent.

    Parent under the model invocation span only when the call is causally
    bound to exactly one same-segment model response whose context is
    available. Otherwise parent under the agent span and link the model span.
    """
    if framework_synthesized:
        # No model turn produced this call; agent span is the correct parent.
        return tracer.start_span(f"tool.{tool_name}", context=agent_context)

    if model is None:
        raise ToolSpanParentingError(
            f"tool '{tool_name}' claims model derivation but no model invocation given"
        )

    if ambiguous_owner:
        # Cannot attribute to a single invocation: parent to agent, link model.
        span = tracer.start_span(f"tool.{tool_name}", context=agent_context)
        span.add_link(model.span.get_span_context())
        return span

    if model.closed and model.context is None:
        # Model span ended and context discarded: do NOT fabricate a parent.
        raise ToolSpanParentingError(
            f"tool '{tool_name}': model span closed with no captured context; "
            "cannot parent per convention (use a link, not a parent)"
        )

    # Correct path: parent under the captured model context.
    return tracer.start_span(f"tool.{tool_name}", context=model.context)


def close_model_span(model: ModelInvocation) -> None:
    """End the model span only after capturing its context for children."""
    if model.context is None:
        raise ToolSpanParentingError(
            "refusing to close model span before capturing its context"
        )
    model.span.end()
    model.closed = True
