"""Checkpoint domain models for CleverAgents.

Defines the checkpoint, retention policy, and rollback result models
used by the checkpoint service to create, restore, and prune sandbox
snapshots during plan execution.

## Key Classes

- ``Checkpoint`` -- Immutable record of a sandbox snapshot.
- ``CheckpointRetentionPolicy`` -- Governs max checkpoints per plan.
- ``RollbackResult`` -- Describes files restored after a rollback.

Based on issue #206 (feat: checkpointing and rollback).
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, field_validator

from cleveragents.domain.models.core.plan import ULID_PATTERN


class CheckpointMetadata(BaseModel):
    """Metadata attached to a checkpoint for auditability.

    Stores the reason, originating tool name, and execution phase so
    that operators can understand *why* a checkpoint was created.
    """

    reason: str = Field(
        default="",
        description="Human-readable reason for this checkpoint",
    )
    source_tool: str = Field(
        default="",
        description="Name of the tool that triggered the checkpoint",
    )
    phase: str = Field(
        default="",
        description="Plan phase when checkpoint was created",
    )
    extra: dict[str, Any] = Field(
        default_factory=dict,
        description="Arbitrary extra metadata",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class Checkpoint(BaseModel):
    """Immutable record of a sandbox snapshot.

    Each checkpoint captures the sandbox state at a point in time and
    stores a reference (e.g. git commit hash) that can be used to
    restore the sandbox later.  Checkpoints are aligned to Execute-phase
    decisions via the optional ``decision_id`` field, enabling
    fine-grained rollback to any individual decision point.
    """

    checkpoint_id: str = Field(
        ...,
        description="Unique ULID identifier",
        pattern=ULID_PATTERN,
    )
    plan_id: str = Field(
        ...,
        description="Plan that owns this checkpoint",
        pattern=ULID_PATTERN,
    )
    sandbox_ref: str = Field(
        ...,
        min_length=1,
        description="Sandbox reference (e.g. git commit hash or patch ID)",
    )
    decision_id: str | None = Field(
        default=None,
        description="Execute-phase decision this checkpoint is aligned to",
    )
    checkpoint_type: str = Field(
        default="manual",
        description="Checkpoint type: pre_write, post_step, or manual",
    )
    resource_id: str | None = Field(
        default=None,
        description="Resource associated with this checkpoint",
    )
    filesystem_path: str = Field(
        default="",
        description="Relative path within checkpoint directory",
    )
    size_bytes: int | None = Field(
        default=None,
        ge=0,
        description="Size of the checkpoint data in bytes",
    )
    created_at: datetime = Field(
        default_factory=lambda: datetime.now(UTC),
        description="When the checkpoint was created",
    )
    metadata: CheckpointMetadata = Field(
        default_factory=CheckpointMetadata,
        description="Checkpoint reason, source tool, and phase",
    )

    @field_validator("checkpoint_id", "plan_id")
    @classmethod
    def validate_ulid(cls: type[Checkpoint], v: str) -> str:
        """Ensure ULID values are non-empty."""
        if not v:
            raise ValueError("ULID field must not be empty")
        return v

    @field_validator("checkpoint_type")
    @classmethod
    def validate_checkpoint_type(cls: type[Checkpoint], v: str) -> str:
        """Ensure checkpoint_type is one of the allowed values."""
        allowed = {"pre_write", "post_step", "manual", "pre_decision"}
        if v not in allowed:
            raise ValueError(
                f"checkpoint_type must be one of {sorted(allowed)}, got '{v}'"
            )
        return v

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class CheckpointRetentionPolicy(BaseModel):
    """Retention policy governing how many checkpoints a plan may keep.

    When ``auto_prune`` is enabled and the number of checkpoints for a
    plan exceeds ``max_checkpoints``, the oldest checkpoints are removed
    automatically when a new checkpoint is created.  The first and most
    recent checkpoints are always preserved during pruning.
    """

    max_checkpoints: int = Field(
        default=50,
        ge=1,
        le=100,
        description="Maximum number of checkpoints to retain per plan",
    )
    auto_prune: bool = Field(
        default=True,
        description="Automatically prune oldest checkpoints on overflow",
    )

    model_config = ConfigDict(
        validate_assignment=True,
    )


class RollbackResult(BaseModel):
    """Describes the outcome of restoring sandbox state to a checkpoint.

    Returned by ``CheckpointService.rollback_to_checkpoint`` so that
    the CLI can display which files were restored.
    """

    restored_files_count: int = Field(
        default=0,
        ge=0,
        description="Number of files restored during rollback",
    )
    changed_paths: list[str] = Field(
        default_factory=list,
        description="Paths that changed during rollback",
    )
    from_checkpoint_id: str = Field(
        ...,
        description="Checkpoint ULID that was restored",
        pattern=ULID_PATTERN,
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


# Default retention policy instance
DEFAULT_RETENTION_POLICY = CheckpointRetentionPolicy()


__all__ = [
    "DEFAULT_RETENTION_POLICY",
    "Checkpoint",
    "CheckpointMetadata",
    "CheckpointRetentionPolicy",
    "RollbackResult",
]
