# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-a-stale-agent-card-capability-advertisement-during-a2a-v1-0-task-lifecycle-negotiation-when-the-remote-agent-s-skills-arra.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-a-stale-agent-card-capability-advertisement-during-a2a-v1-0-task-lifecycle-negotiation-when-the-remote-agent-s-skills-arra.warrant.json
# Markdown SHA-256:   8d34ac835e241c7fffd09ce1c08078b27e7300910dc865219558d0690c0e1ee1
# Code AST SHA-256:   12b55670a46f06ba494bf6969fb2a3c25148a5f990f7de559d6c40fb03fee25e
# Verified Classes:   StaleAgentCardError, ArtifactSchemaDriftError, CachedCard
# Defensive Guards:   3 fail-closed invariants (zero external deps)

import hashlib
import json
from dataclasses import dataclass, field
from typing import Any


class StaleAgentCardError(RuntimeError):
    """Raised when a negotiated task fails in a way that indicates the cached
    Agent Card's skills[] no longer matches the remote's current advertisement."""


class ArtifactSchemaDriftError(RuntimeError):
    """Raised when a task succeeds but the returned artifact type is not a member
    of the outputModes advertised in the card used at negotiation time."""


@dataclass
class CachedCard:
    raw: dict[str, Any]
    skill_fingerprint: str
    version: str


def skill_fingerprint(card: dict[str, Any]) -> str:
    """Canonical hash over the fields that affect negotiation, excluding `version`."""
    skills = sorted(
        (
            {
                "id": s.get("id", ""),
                "inputModes": sorted(s.get("inputModes", [])),
                "outputModes": sorted(s.get("outputModes", [])),
            }
            for s in card.get("skills", [])
        ),
        key=lambda s: s["id"],
    )
    caps = card.get("capabilities", {})
    material = {
        "skills": skills,
        "streaming": bool(caps.get("streaming", False)),
        "pushNotifications": bool(caps.get("pushNotifications", False)),
    }
    blob = json.dumps(material, sort_keys=True, separators=(",", ":")).encode()
    return hashlib.sha256(blob).hexdigest()


def cache_card(card: dict[str, Any]) -> CachedCard:
    return CachedCard(
        raw=card,
        skill_fingerprint=skill_fingerprint(card),
        version=card.get("version", ""),
    )


def negotiate(
    cached: CachedCard,
    skill_id: str,
    task_response: dict[str, Any],
) -> None:
    """Validate a task response against the card used at negotiation time.

    Raises StaleAgentCardError on lifecycle-level 'unknown skill' failures and
    ArtifactSchemaDriftError on silent output-mode drift.
    """
    status = task_response.get("status", {})
    state = status.get("state", "")
    message_parts = status.get("message", {}).get("parts", [])
    message_text = " ".join(
        p.get("text", "") for p in message_parts if p.get("type") == "text"
    ).lower()

    if state == "failed" and ("unknown skill" in message_text or "unsupported mode" in message_text):
        raise StaleAgentCardError(
            f"remote rejected skillId={skill_id!r} after submission; "
            f"cached fingerprint={cached.skill_fingerprint[:12]}… "
            f"version={cached.version!r} — re-fetch Agent Card before retry"
        )

    if state == "completed":
        advertised = {
            s["id"]: set(s.get("outputModes", []))
            for s in cached.raw.get("skills", [])
        }
        allowed = advertised.get(skill_id)
        if allowed is None:
            raise StaleAgentCardError(
                f"skillId={skill_id!r} not present in cached card but task completed"
            )
        for artifact in task_response.get("artifacts", []):
            for part in artifact.get("parts", []):
                ptype = part.get("type", "")
                if ptype and ptype not in allowed:
                    raise ArtifactSchemaDriftError(
                        f"skillId={skill_id!r} returned part type {ptype!r} "
                        f"not in advertised outputModes {sorted(allowed)} — "
                        f"remote may have reused skill id for a different capability"
                    )
