"""Checkpoint system for durable agent execution.

A ``Checkpoint`` is a pause-point marker emitted at agent cycle boundaries.
It captures the position (which boundary fired) and the cycle index. It does
**not** capture conversation state — pair with a ``SessionManager`` for
cross-process state continuity.

Positions per ReAct cycle:
- ``after_model``: model returned tool_use; tools have not run yet.
- ``after_tools``: tools finished; the next model call has not happened yet.

Per-tool granularity within a cycle is the ``ToolExecutor``'s responsibility.

Usage (mirrors interrupts):
- Pause: ``AgentResult`` with ``stop_reason="checkpoint"`` and ``checkpoint`` populated.
- Resume: pass back ``{"checkpointResume": {"checkpoint": ckpt.to_dict()}}``.

Precedence:
- Interrupt > checkpoint: an interrupt during a checkpointing cycle returns
  ``stop_reason="interrupt"`` and skips ``after_tools``.
- Cancel > checkpoint: a cancel signal at either boundary returns
  ``stop_reason="cancelled"``.

Notes:
- Checkpoints are only emitted on tool_use cycles. A turn with no tool calls
  emits no checkpoint; use a ``SessionManager`` for durability of every turn.
- ``EventLoopMetrics`` resets per invocation; aggregate yourself if needed.
- ``BeforeInvocationEvent`` / ``AfterInvocationEvent`` fire on every resume,
  same as interrupts.
"""

import logging
from dataclasses import asdict, dataclass, field
from typing import Any, Literal

from ...types.exceptions import CheckpointException

logger = logging.getLogger(__name__)

CHECKPOINT_SCHEMA_VERSION = "1.0"

CheckpointPosition = Literal["after_model", "after_tools"]


@dataclass(frozen=True)
class Checkpoint:
    """Pause-point marker. Treat as opaque — pass back to resume.

    Attributes:
        position: Which boundary fired (``after_model`` or ``after_tools``).
        cycle_index: ReAct loop cycle (0-based).
        snapshot: Reserved for forward extensibility (e.g. a future hook that lets
            callers attach agent state to the checkpoint). The SDK does not
            populate or read it today; it round-trips through serialization.
        app_data: Reserved for forward extensibility — caller metadata that
            round-trips through serialization. The SDK does not populate or read it.
        schema_version: Rejects incompatible checkpoints on resume.
    """

    position: CheckpointPosition
    cycle_index: int = 0
    snapshot: dict[str, Any] = field(default_factory=dict)
    app_data: dict[str, Any] = field(default_factory=dict)
    schema_version: str = field(init=False, default=CHECKPOINT_SCHEMA_VERSION)

    def to_dict(self) -> dict[str, Any]:
        """Serialize for persistence."""
        return asdict(self)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Checkpoint":
        """Reconstruct from a dict produced by to_dict().

        Args:
            data: Serialized checkpoint data.

        Raises:
            CheckpointException: If schema_version doesn't match the current version.
        """
        version = data.get("schema_version", "")
        if version != CHECKPOINT_SCHEMA_VERSION:
            raise CheckpointException(
                f"Checkpoints with schema version {version!r} are not compatible "
                f"with current version {CHECKPOINT_SCHEMA_VERSION}."
            )
        known_keys = {k for k in cls.__dataclass_fields__ if k != "schema_version"}
        unknown_keys = set(data.keys()) - known_keys - {"schema_version"}
        if unknown_keys:
            logger.warning("unknown_keys=<%s> | ignoring unknown fields in checkpoint data", unknown_keys)
        return cls(**{k: v for k, v in data.items() if k in known_keys})
