# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-treat-a-terminal-agent-card-as-authority-over-in-flight-task-state-in-a2a-v1-0.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-treat-a-terminal-agent-card-as-authority-over-in-flight-task-state-in-a2a-v1-0.warrant.json
# Markdown SHA-256:   a4c2ead07076d820ed56be1452c537c6be86a7c6d40262eb7edbb0b9147a823e
# Code AST SHA-256:   992e90a5d395862369150ff10909ae226cf097fd70a212d086fd54276ff68114
# Verified Classes:   CardTaskBoundaryError, TaskState, AgentCard, Task
# Defensive Guards:   7 fail-closed invariants (zero external deps)

from __future__ import annotations

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


class CardTaskBoundaryError(RuntimeError):
    """Raised when Card state is used to adjudicate Task disposition."""


class TaskState(str, Enum):
    SUBMITTED = "submitted"
    WORKING = "working"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELED = "canceled"


TERMINAL = {TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELED}


@dataclass
class AgentCard:
    skills: set[str]
    reachable: bool = True


@dataclass
class Task:
    task_id: str
    required_skill: str
    state: TaskState = TaskState.SUBMITTED
    artifacts: list[str] = field(default_factory=list)
    terminal_locked: bool = False


def admit_new_task(card: AgentCard, required_skill: str) -> None:
    """Card governs admission of NEW tasks only."""
    if not card.reachable:
        raise CardTaskBoundaryError(
            f"cannot admit task: Agent Card unreachable for skill {required_skill!r}"
        )
    if required_skill not in card.skills:
        raise CardTaskBoundaryError(
            f"cannot admit task: skill {required_skill!r} not advertised by card"
        )


def apply_status_event(task: Task, new_state: TaskState) -> None:
    """Only server status events may move a Task toward terminal."""
    if task.terminal_locked:
        raise CardTaskBoundaryError(
            f"task {task.task_id!r} is terminal-locked; refusing transition to {new_state}"
        )
    task.state = new_state
    if new_state in TERMINAL:
        task.terminal_locked = True


def reconcile_with_card(task: Task, card: AgentCard) -> None:
    """The boundary: Card deltas must NOT rewrite Task disposition."""
    if task.terminal_locked:
        # Terminal state wins; Card is irrelevant to disposition.
        return
    if not card.reachable or task.required_skill not in card.skills:
        # Skill revoked mid-flight: do NOT locally fail the task.
        # Wait for a server status event or a transport timeout.
        if task.state in TERMINAL:  # defensive; terminal_locked should catch
            raise CardTaskBoundaryError(
                f"task {task.task_id!r} terminal but not locked; state machine corrupt"
            )
        return  # hold, await status event


def retry_task(old: Task, card: AgentCard, new_task_id: str) -> Task:
    """Retries must mint a fresh taskId; never reuse a terminal one."""
    if not old.terminal_locked:
        raise CardTaskBoundaryError(
            f"cannot retry task {old.task_id!r}: not terminal"
        )
    if new_task_id == old.task_id:
        raise CardTaskBoundaryError(
            "retry must allocate a new taskId; reusing terminal taskId corrupts artifact correlation"
        )
    admit_new_task(card, old.required_skill)
    return Task(task_id=new_task_id, required_skill=old.required_skill)


def merge_artifact(task: Task, artifact_id: str) -> None:
    if task.terminal_locked and task.state is TaskState.CANCELED:
        raise CardTaskBoundaryError(
            f"refusing to merge artifact {artifact_id!r} into canceled task {task.task_id!r}"
        )
    task.artifacts.append(artifact_id)
