# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-reconcile-agent-card-capability-claims-with-runtime-task-lifecycle-reality-in-a2a-v1-0.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-reconcile-agent-card-capability-claims-with-runtime-task-lifecycle-reality-in-a2a-v1-0.warrant.json
# Markdown SHA-256:   7d162cd75b0822efd02e25f0bbed368c4b16006f7449ebc2d468a3acd1e7e8a3
# Code AST SHA-256:   fec5123dcdae7cd8717f5df90dd91b42cdd36b77efd9e13279b564a7a4d55613
# Verified Classes:   LifecycleContractViolation, PeerLifecycleProfile
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class LifecycleContractViolation(RuntimeError):
    """Raised when a remote peer's task lifecycle diverges from what its
    Agent Card implied, in a way the consumer cannot safely paper over."""
    pass


TERMINAL = {"completed", "failed", "canceled"}
FORWARD = ["submitted", "working", "input-required", "completed"]


class PeerLifecycleProfile:
    def __init__(self, agent_url: str, card_version: str):
        self.agent_url = agent_url
        self.card_version = card_version
        self.resubscribe_replays = None      # True=replay, False=resume, None=unknown
        self.task_survives_input_required = None
        self.resend_same_taskid_resumes = None

    def record_resubscribe(self, replayed_from_start: bool):
        self.resubscribe_replays = replayed_from_start

    def record_resend(self, resumed_in_place: bool):
        self.resend_same_taskid_resumes = resumed_in_place


def reconcile_resubscribe(profile: PeerLifecycleProfile,
                          card: dict,
                          observed_events: list[dict],
                          last_known_state: str) -> list[dict]:
    """Given a resubscribe response, return only the events the consumer
    has not already applied. Raises if the peer's behavior contradicts a
    lifecycle guarantee the consumer depends on and cannot recover from."""
    if not card.get("capabilities", {}).get("streaming", False):
        raise LifecycleContractViolation(
            f"{profile.agent_url}: resubscribe attempted but Card never "
            f"advertised streaming; consumer opened an SSE channel anyway"
        )

    seen = set()
    deduped = []
    for ev in observed_events:
        key = (ev["taskId"], ev["status"]["state"], ev["status"].get("timestamp"))
        if key in seen:
            continue
        seen.add(key)
        deduped.append(ev)

    # Detect backward state movement (Go runtime duplicate `submitted` case).
    order = {s: i for i, s in enumerate(FORWARD)}
    prev = order.get(last_known_state, -1)
    for ev in deduped:
        cur = order.get(ev["status"]["state"], -1)
        if cur < prev and ev["status"]["state"] not in TERMINAL:
            raise LifecycleContractViolation(
                f"{profile.agent_url}: resubscribe moved state backward "
                f"{last_known_state} -> {ev['status']['state']}; peer "
                f"replayed instead of resumed and consumer cannot reconcile"
            )
        prev = cur

    if deduped and deduped[0]["status"]["state"] == "submitted" \
            and last_known_state != "submitted":
        profile.record_resubscribe(replayed_from_start=True)
    else:
        profile.record_resubscribe(replayed_from_start=False)

    return deduped


def dispatch_followup(profile: PeerLifecycleProfile,
                      card: dict,
                      task_id: str,
                      answer_text: str) -> dict:
    """Send an input-required answer. Handles the -32001 case per the
    empirically learned profile rather than per the Card."""
    survives = profile.task_survives_input_required
    if survives is None:
        # Unknown peer: probe with tasks/get before sending.
        raise LifecycleContractViolation(
            f"{profile.agent_url}: no lifecycle profile for task {task_id}; "
            f"probe tasks/get before follow-up or risk orphaned task"
        )
    if not survives:
        # Peer reaped the task; re-dispatch from scratch.
        return {"action": "redispatch", "reason": "task reaped after input-required"}
    return {"action": "send_same_taskid", "taskId": task_id, "text": answer_text}
