# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-silent-task-orphaning-in-a2a-v1-0-agent-card-discovery-task-lifecycle-state-machines-and-push-notification-webhook-binding.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-silent-task-orphaning-in-a2a-v1-0-agent-card-discovery-task-lifecycle-state-machines-and-push-notification-webhook-binding.warrant.json
# Markdown SHA-256:   cf7e6244d039b559c47ea6c18317d19d82b6191454b26ec44e29ebd65a8cda38
# Code AST SHA-256:   20076e94a0ac07e94019c8382388749cb289983bad5b86bd9bbc1314e2d80c91
# Verified Classes:   TaskOrphanedError, BindingState, TaskBinding
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from __future__ import annotations

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


class TaskOrphanedError(RuntimeError):
    """Raised when a task reaches a terminal state with no live delivery channel bound."""


class BindingState(Enum):
    UNBOUND = "unbound"
    REGISTERED = "registered"
    ACKNOWLEDGED = "acknowledged"
    DEAD = "dead"  # receiver returned 4xx, binding invalid


@dataclass
class TaskBinding:
    task_id: str
    card_advertises_push: bool
    binding: BindingState = BindingState.UNBOUND
    last_ack_at: Optional[float] = None
    last_poll_at: Optional[float] = None
    reconciliation_interval_s: float = 60.0
    expected_transition_window_s: float = 120.0


def assert_delivery_channel_live(
    tb: TaskBinding,
    now: Optional[float] = None,
    terminal_state_reached: bool = False,
) -> None:
    """
    Enforce the invariant: a terminal state is durable only if a live delivery
    channel is bound at the moment of transition.

    Called before treating any task as safely monitored, and again when a
    terminal state is observed server-side.
    """
    now = time.time() if now is None else now

    # Card flag is permission, not obligation. Absence of binding + absence of
    # polling means the task has no observer at all.
    has_webhook = tb.binding in (BindingState.REGISTERED, BindingState.ACKNOWLEDGED)
    has_polling = tb.last_poll_at is not None and (
        now - tb.last_poll_at <= tb.reconciliation_interval_s
    )

    if not has_webhook and not has_polling:
        raise TaskOrphanedError(
            f"task {tb.task_id!r} has no live delivery channel "
            f"(binding={tb.binding.value}, polling_stale={tb.last_poll_at is None}); "
            f"card_advertises_push={tb.card_advertises_push} does not create a binding"
        )

    if tb.binding is BindingState.DEAD:
        # Receiver rejected delivery; terminal states may already be unobserved.
        raise TaskOrphanedError(
            f"task {tb.task_id!r} webhook binding is dead (receiver 4xx); "
            f"terminal state may be unobserved — re-enable polling and re-register"
        )

    if terminal_state_reached and has_webhook:
        if tb.last_ack_at is None:
            raise TaskOrphanedError(
                f"task {tb.task_id!r} reached terminal state but webhook never "
                f"acknowledged an event; binding is suspect"
            )
        if now - tb.last_ack_at > tb.expected_transition_window_s:
            raise TaskOrphanedError(
                f"task {tb.task_id!r} terminal state not acknowledged within "
                f"{tb.expected_transition_window_s}s of last ack; treat as orphaned"
            )


def on_receiver_response(tb: TaskBinding, status_code: int, now: Optional[float] = None) -> None:
    now = time.time() if now is None else now
    if 200 <= status_code < 300:
        tb.binding = BindingState.ACKNOWLEDGED
        tb.last_ack_at = now
    elif status_code in (404, 410):
        tb.binding = BindingState.DEAD
        # Force caller to re-register before next transition.
        raise TaskOrphanedError(
            f"task {tb.task_id!r} receiver returned {status_code}; "
            f"binding invalidated, re-register before next transition"
        )
