# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-advertises-an-agent-card-whose-pushnotifications-capability-is-true-but-whose-webhook-receiver-cannot-complete-the-a2a-h.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-advertises-an-agent-card-whose-pushnotifications-capability-is-true-but-whose-webhook-receiver-cannot-complete-the-a2a-h.warrant.json
# Markdown SHA-256:   da04c11f5d1387e52b847dfec87e2830cf91002270a975df8a8077a249fca440
# Code AST SHA-256:   67f2141e496143c0b4084f0c1ae43e006a3f9291035176b11ae2518890ca7d8d
# Verified Classes:   BindingNotConfirmedError, TaskState, PushBinding, Task
# Defensive Guards:   5 fail-closed invariants (zero external deps)

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


class BindingNotConfirmedError(RuntimeError):
    """Raised when a task would be terminalized on a push path before
    the webhook binding has been confirmed by the remote agent."""


class TaskState(Enum):
    BINDING_PENDING = auto()
    SUBMITTED = auto()
    WORKING = auto()
    COMPLETED = auto()
    FAILED = auto()


@dataclass
class PushBinding:
    url: str
    token: str
    config_id: Optional[str] = None      # server-assigned; None until confirmed
    validation_echoed: bool = False      # inbound validation callback seen


@dataclass
class Task:
    task_id: str
    state: TaskState = TaskState.BINDING_PENDING
    binding: Optional[PushBinding] = None
    terminal_confirmed_by_get: bool = False


def advance_on_webhook_timeout(task: Task) -> TaskState:
    """
    Called when the push-delivery watchdog fires. Must NOT terminalize the
    task unless binding was confirmed AND we have a polling fallback result.
    """
    if task.binding is None:
        raise BindingNotConfirmedError(
            f"task {task.task_id}: no push binding registered; "
            "cannot advance on push path"
        )

    if task.binding.config_id is None and not task.binding.validation_echoed:
        # Binding never confirmed. The remote is (correctly) suppressing
        # deliveries to an unvalidated endpoint. This is a BINDING timeout,
        # not a task failure. Do not transition to FAILED.
        raise BindingNotConfirmedError(
            f"task {task.task_id}: webhook binding unconfirmed "
            f"(config_id=None, validation_echoed=False). "
            "Poll tasks/get and re-register; do not fail over."
        )

    if not task.terminal_confirmed_by_get:
        # Even with binding confirmed, a push timeout does not prove the
        # remote task failed. Only tasks/get may terminalize.
        raise BindingNotConfirmedError(
            f"task {task.task_id}: push timeout with confirmed binding "
            "but no tasks/get confirmation; refusing to terminalize"
        )

    task.state = TaskState.COMPLETED
    return task.state


def confirm_binding(task: Task, config_id: str) -> None:
    if not config_id:
        raise BindingNotConfirmedError(
            f"task {task.task_id}: empty config_id from pushNotificationConfig/get"
        )
    assert task.binding is not None
    task.binding.config_id = config_id
    task.state = TaskState.SUBMITTED
