# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-retried-span-as-a-new-causal-root-rather-than-a-continuation-of-the-original-trace.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-retried-span-as-a-new-causal-root-rather-than-a-continuation-of-the-original-trace.warrant.json
# Markdown SHA-256:   ec13de0182715ecb66a3246c10a3d99048712e996b94389466da44d2cc9ab7a1
# Code AST SHA-256:   6c7a7aa17af9bd58f0e74ae7a41edb36d17f452fff4cc5169304062a1641addd
# Verified Classes:   CausalStitchingError, StitchMode, SpanContext, RetryAttempt
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


class CausalStitchingError(RuntimeError):
    """Raised when a retried/sub-agent/async span cannot be validly stitched."""


class StitchMode(Enum):
    CONTINUATION = "continuation"   # parent reference, live parent in same process tree
    NEW_ROOT_LINKED = "new_root_linked"  # new root + span link + correlation key


@dataclass
class SpanContext:
    trace_id: str
    span_id: str
    process_id: str
    is_open: bool
    correlation_key: Optional[str] = None


@dataclass
class RetryAttempt:
    original: SpanContext
    retry_process_id: str
    retry_has_live_parent: bool
    correlation_key: Optional[str] = None
    attempt_number: int = 1
    links: list[str] = field(default_factory=list)


def decide_stitch_mode(attempt: RetryAttempt) -> StitchMode:
    """
    Enforce the invariant: span parentage requires a live, resolvable parent
    in the same process tree. Otherwise the retry is a new causal root that
    must be stitched via correlation key + span link.
    """
    same_process = attempt.retry_process_id == attempt.original.process_id
    parent_resolvable = attempt.original.is_open and same_process

    if parent_resolvable and attempt.retry_has_live_parent:
        return StitchMode.CONTINUATION

    # Boundary crossed: parent is not resolvable at export time.
    if not attempt.correlation_key:
        raise CausalStitchingError(
            f"Retry attempt {attempt.attempt_number} of span "
            f"{attempt.original.span_id} (trace {attempt.original.trace_id}) "
            f"crossed the causal boundary (same_process={same_process}, "
            f"parent_open={attempt.original.is_open}) but carries no "
            f"correlation_key. Cannot stitch without a stable key; refusing "
            f"to emit a parent reference to an unresolvable span."
        )
    return StitchMode.NEW_ROOT_LINKED


def emit_retry_span(attempt: RetryAttempt) -> dict:
    mode = decide_stitch_mode(attempt)
    if mode is StitchMode.CONTINUATION:
        return {
            "trace_id": attempt.original.trace_id,
            "parent_span_id": attempt.original.span_id,
            "retry.attempt": attempt.attempt_number,
            "links": attempt.links,
        }
    return {
        "trace_id": f"root-{attempt.correlation_key}",
        "parent_span_id": None,
        "correlation_key": attempt.correlation_key,
        "links": [attempt.original.span_id, *attempt.links],
        "retry.attempt": attempt.attempt_number,
        "causal.root": True,
    }
