# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-refuse-to-treat-a-terminal-a2a-task-as-immutable-push-notification-callbacks-racing-final-state-transitions-in-a2a-v1-0.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-refuse-to-treat-a-terminal-a2a-task-as-immutable-push-notification-callbacks-racing-final-state-transitions-in-a2a-v1-0.warrant.json
# Markdown SHA-256:   f970e013f4d04d99382e118958515fc04931e8cddfa541d943bb7a7f6f06af20
# Code AST SHA-256:   27170892cef4160eccbc991829c5bfd49361b04d110f12bd1120d58b2731bd5d
# Verified Classes:   StaleTerminalPayloadError, TerminalStateConflictError, TaskSnapshot
# Defensive Guards:   5 fail-closed invariants (zero external deps)

"""Reconcile an A2A v1.0 push-notification payload against locally stored task state.

Encodes the decision boundary from the record: terminal state is not content
finality; a terminal webhook is a snapshot and may be stale.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal

TaskState = Literal["submitted", "working", "input-required", "completed", "canceled", "failed"]
TERMINAL: frozenset[str] = frozenset({"completed", "canceled", "failed"})


class StaleTerminalPayloadError(RuntimeError):
    """Raised when a terminal webhook payload cannot be trusted as authoritative."""


class TerminalStateConflictError(RuntimeError):
    """Raised when a webhook terminal state disagrees with a stored terminal state."""


@dataclass(frozen=True)
class TaskSnapshot:
    task_id: str
    state: TaskState
    revision: int
    artifacts: tuple[str, ...] = field(default_factory=tuple)


def reconcile_webhook(
    stored: TaskSnapshot | None,
    webhook: TaskSnapshot,
    *,
    server_serializes_terminal_writes: bool,
) -> TaskSnapshot:
    """Return the snapshot the client should persist.

    Raises StaleTerminalPayloadError when the client must re-fetch via tasks/get
    instead of trusting the webhook.
    """
    if webhook.task_id != (stored.task_id if stored else webhook.task_id):
        raise StaleTerminalPayloadError(
            f"task_id mismatch: webhook={webhook.task_id!r} stored={stored.task_id!r}"
        )

    if stored is None:
        # No prior state: a non-terminal webhook is safe to seed from.
        if webhook.state in TERMINAL:
            raise StaleTerminalPayloadError(
                f"terminal webhook for unseen task {webhook.task_id!r}; "
                "must confirm via tasks/get before persisting"
            )
        return webhook

    # Conflict: two different terminal states. The webhook is a stale snapshot;
    # only tasks/get may arbitrate. Never overwrite.
    if stored.state in TERMINAL and webhook.state in TERMINAL and stored.state != webhook.state:
        raise TerminalStateConflictError(
            f"task {webhook.task_id!r}: stored={stored.state!r} "
            f"webhook={webhook.state!r}; re-fetch via tasks/get"
        )

    # Stale payload: same or older revision than what we already hold.
    if webhook.revision < stored.revision:
        raise StaleTerminalPayloadError(
            f"task {webhook.task_id!r}: webhook revision {webhook.revision} "
            f"< stored revision {stored.revision}; payload is superseded"
        )

    # Terminal webhook without serialized terminal writes is only a hint.
    if webhook.state in TERMINAL and not server_serializes_terminal_writes:
        raise StaleTerminalPayloadError(
            f"task {webhook.task_id!r}: terminal webhook is a snapshot and the "
            "server does not serialize terminal writes; confirm via tasks/get"
        )

    return webhook
