# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-re-negotiates-a2a-task-state-after-an-artifact-validator-rejects-a-mid-stream-artifact-schema.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-re-negotiates-a2a-task-state-after-an-artifact-validator-rejects-a-mid-stream-artifact-schema.warrant.json
# Markdown SHA-256:   046864786bab3796a9bab9c447b17d1c176e1ad56edb0bb1681a5de2199018bc
# Code AST SHA-256:   751a9bc1190a49a6c7a2b12d9bcd44ee14c6565944a79b03120362d5505e2183
# Verified Classes:   TaskState, ArtifactLifecycleDivergence, StatusEvent, TaskContext
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from __future__ import annotations

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


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


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


class ArtifactLifecycleDivergence(RuntimeError):
    """Raised when a peer closes a task context without a final:true event,
    or when a non-final rejection is treated as authoritative for task state."""
    pass


@dataclass
class StatusEvent:
    state: TaskState
    final: bool
    reason: Optional[str] = None
    rejected_artifact_id: Optional[str] = None


@dataclass
class TaskContext:
    task_id: str
    state: TaskState = TaskState.WORKING
    negotiated_schema: str = "1.0.1"
    buffered_artifacts: list = field(default_factory=list)
    closed_without_final: bool = False


def apply_status_event(ctx: TaskContext, event: StatusEvent) -> TaskContext:
    """Apply a status event under the A2A v1.0 invariant.

    Non-final rejections are advisory only; they must not transition the
    task to a terminal state. Only final:true events close the lifecycle.
    """
    if event.state in TERMINAL_STATES and not event.final:
        raise ArtifactLifecycleDivergence(
            f"task {ctx.task_id}: terminal state {event.state.value} "
            f"emitted without final:true; refusing to close context"
        )

    if event.final and event.state in TERMINAL_STATES:
        if event.rejected_artifact_id is not None and event.reason is None:
            raise ArtifactLifecycleDivergence(
                f"task {ctx.task_id}: final rejection for artifact "
                f"{event.rejected_artifact_id} lacks a reason field"
            )
        ctx.state = event.state
        # discard buffered artifacts that were never acknowledged
        ctx.buffered_artifacts.clear()
        return ctx

    # non-final event: advisory, task stays in working
    if event.rejected_artifact_id is not None:
        # do NOT transition state; record for diagnostics
        ctx.buffered_artifacts = [
            a for a in ctx.buffered_artifacts
            if a.get("id") != event.rejected_artifact_id
        ]
    return ctx


def reconcile_after_get(ctx: TaskContext, server_task_exists: bool) -> TaskContext:
    """Called when tasks/get returns TaskNotFoundError for a context the
    client still holds as working. This is the divergence-detection point."""
    if not server_task_exists and ctx.state not in TERMINAL_STATES:
        ctx.closed_without_final = True
        ctx.buffered_artifacts.clear()
        raise ArtifactLifecycleDivergence(
            f"task {ctx.task_id}: server released context while client "
            f"held it in {ctx.state.value}; {len(ctx.buffered_artifacts)} "
            f"artifact(s) orphaned — server violated final:true invariant"
        )
    return ctx
