# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-cross-vendor-a2a-task-lifecycle-corruption-when-agent-cards-are-discovered-asynchronously.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-cross-vendor-a2a-task-lifecycle-corruption-when-agent-cards-are-discovered-asynchronously.warrant.json
# Markdown SHA-256:   67af369670a8e294f933046496228d7d28b38081371e68bc193059616297d664
# Code AST SHA-256:   0286961ee07ed498c3a9881ccfd318422827c88aee591c208c2a94f97d3402c1
# Verified Classes:   TaskOrphanedError, CardSnapshot
# Defensive Guards:   3 fail-closed invariants (zero external deps)

import hashlib
import json
from dataclasses import dataclass


class TaskOrphanedError(RuntimeError):
    """Raised when a task's originating card revision is no longer resolvable."""


@dataclass(frozen=True)
class CardSnapshot:
    revision_hash: str
    skills: dict          # skill_id -> input schema hash
    endpoint: str


def snapshot_card(card: dict) -> CardSnapshot:
    skills = {
        s["id"]: hashlib.sha256(
            json.dumps(s.get("inputSchema", {}), sort_keys=True).encode()
        ).hexdigest()
        for s in card.get("skills", [])
    }
    body = json.dumps(
        {"skills": sorted(skills), "url": card["url"]}, sort_keys=True
    ).encode()
    return CardSnapshot(
        revision_hash=hashlib.sha256(body).hexdigest(),
        skills=skills,
        endpoint=card["url"],
    )


def resume_task(task: dict, recorded: CardSnapshot, current_card: dict) -> str:
    """Validate a task against the live card before resuming. Returns endpoint."""
    live = snapshot_card(current_card)
    skill_id = task.get("metadata", {}).get("skillId")

    if skill_id is None:
        raise TaskOrphanedError(
            f"task {task.get('id')} carries no skillId; cannot reconcile against "
            f"card revision {live.revision_hash}"
        )

    if skill_id not in live.skills:
        raise TaskOrphanedError(
            f"task {task.get('id')} minted under card {recorded.revision_hash} "
            f"references skill {skill_id!r}, absent from current card "
            f"{live.revision_hash}; task is orphaned, do not resume"
        )

    if recorded.skills.get(skill_id) != live.skills[skill_id]:
        raise TaskOrphanedError(
            f"skill {skill_id!r} input schema changed between card "
            f"{recorded.revision_hash} and {live.revision_hash}; resuming would "
            f"route task {task.get('id')} under a different contract"
        )

    return live.endpoint
