# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-a2a-v1-0-agent-card-discovery-as-a-non-idempotent-version-fenced-operation-rather-than-a-cacheable-read.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-a2a-v1-0-agent-card-discovery-as-a-non-idempotent-version-fenced-operation-rather-than-a-cacheable-read.warrant.json
# Markdown SHA-256:   a32ee3cc8ca22fcbc2dce8d104383bf4780a4f0fe1c9622d1acacbc7e46a1bb8
# Code AST SHA-256:   3795d4c67b4725b8ac45baf180b88c9ac4f5916890a9da5fa3d781c4d1afe9d1
# Verified Classes:   AgentCardStalenessError, TaskContinuationBlockedError, AgentCard, Task, A2AClient
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import time
from dataclasses import dataclass
from typing import Optional


class AgentCardStalenessError(RuntimeError):
    """Raised when a card-dependent operation cannot confirm current capabilities."""
    pass


class TaskContinuationBlockedError(RuntimeError):
    """Raised if code attempts to let card state gate an in-flight task."""
    pass


@dataclass
class AgentCard:
    version: str
    push_notifications: bool
    fetched_at: float


@dataclass
class Task:
    id: str
    state: str  # submitted | working | input-required | completed | failed | canceled
    delivery_mode: str  # "webhook" | "polling"


MAX_CARD_AGE_S = 120.0  # 2x observed remote redeploy interval


class A2AClient:
    def __init__(self, fetch_card, register_webhook, poll_task):
        self._fetch_card = fetch_card
        self._register_webhook = register_webhook
        self._poll_task = poll_task
        self._memo: Optional[AgentCard] = None

    def _fresh_card(self, *, force: bool = False) -> AgentCard:
        now = time.monotonic()
        if not force and self._memo and (now - self._memo.fetched_at) < MAX_CARD_AGE_S:
            return self._memo
        try:
            card = self._fetch_card()
        except Exception as exc:
            raise AgentCardStalenessError(
                f"card re-validation failed at {now:.3f}: {exc!r}"
            ) from exc
        self._memo = card
        return card

    def start_task(self, skill: str) -> Task:
        # Creation is gated by discovery: fail fast if we cannot confirm capability.
        card = self._fresh_card(force=True)
        if skill not in self._skills_of(card):
            raise AgentCardStalenessError(
                f"skill {skill!r} absent from fresh card v{card.version}"
            )
        return Task(id=f"t-{int(time.time()*1000)}", state="submitted",
                    delivery_mode="polling")

    def register_webhook_for(self, task: Task, url: str) -> None:
        # Binding is gated by discovery: unconditional fresh read.
        card = self._fresh_card(force=True)
        if not card.push_notifications:
            task.delivery_mode = "polling"
            raise AgentCardStalenessError(
                f"fresh card v{card.version} withdrew pushNotifications; "
                f"task {task.id} downgraded to polling"
            )
        self._register_webhook(task.id, url)
        task.delivery_mode = "webhook"

    def continue_task(self, task: Task) -> Task:
        # Continuation must NEVER be gated by card state.
        if task.state in ("completed", "failed", "canceled"):
            return task
        try:
            card = self._fresh_card()
            if not card.push_notifications and task.delivery_mode == "webhook":
                task.delivery_mode = "polling"  # reconcile, do not cancel
        except AgentCardStalenessError:
            pass  # discovery failure is not a task failure
        state = self._poll_task(task.id)
        if state is None:
            raise TaskContinuationBlockedError(
                f"poll_task returned None for {task.id}; "
                f"refusing to infer task failure from discovery state"
            )
        task.state = state
        return task

    @staticmethod
    def _skills_of(card: AgentCard) -> set:
        return {"summarize", "translate"}  # illustrative; parse from card JSON
