"""Resume domain models for plan execution.

Defines the models supporting plan resume functionality:

- ``ResumeCheckpoint`` -- a checkpoint recorded at each completed step
- ``ResumeMetadata`` -- resume state carried on a Plan
- ``ResumeSummary`` -- output shown before resume execution
- ``ResumeEligibility`` -- result of resume eligibility validation

Checkpoints are tied to decision IDs and sandbox state to ensure
deterministic resume points.
"""

from __future__ import annotations

from datetime import datetime
from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field

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


class ResumeIneligibleReason(StrEnum):
    """Why a plan cannot be resumed."""

    TERMINAL_APPLIED = "terminal_applied"
    TERMINAL_CANCELLED = "terminal_cancelled"
    TERMINAL_CONSTRAINED = "terminal_constrained"
    NO_PROGRESS = "no_progress"
    ACTION_PHASE = "action_phase"


class ResumeCheckpoint(BaseModel):
    """A checkpoint recorded when a plan step completes.

    Ties a completed step to a decision ID and sandbox snapshot,
    enabling deterministic resume from that point.
    """

    checkpoint_id: str = Field(
        ...,
        min_length=1,
        description="Unique checkpoint ID (ULID)",
        pattern=ULID_PATTERN,
    )
    plan_id: str = Field(
        ...,
        min_length=1,
        description="Plan this checkpoint belongs to",
        pattern=ULID_PATTERN,
    )
    decision_id: str = Field(
        ...,
        min_length=1,
        description="Decision node ID this checkpoint is tied to",
    )
    step_index: int = Field(
        ...,
        ge=0,
        description="Zero-based index of the completed step",
    )
    step_text: str = Field(
        ...,
        min_length=1,
        description="Description of the completed step",
    )
    sandbox_ref: str | None = Field(
        default=None,
        description="Sandbox reference at checkpoint time",
    )
    created_at: datetime = Field(
        default_factory=datetime.now,
        description="When the checkpoint was recorded",
    )

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


class ResumeMetadata(BaseModel):
    """Resume state carried on a Plan.

    Tracks the last completed step and checkpoint to enable
    execution resume after interruption or error.
    """

    last_completed_step: int = Field(
        default=-1,
        ge=-1,
        description=(
            "Index of the last successfully completed step. "
            "-1 means no steps completed."
        ),
    )
    last_checkpoint_id: str | None = Field(
        default=None,
        description="ULID of the most recent checkpoint",
    )
    total_steps: int = Field(
        default=0,
        ge=0,
        description="Total number of steps in the plan",
    )
    checkpoints: list[ResumeCheckpoint] = Field(
        default_factory=list,
        description="All recorded checkpoints for this plan",
    )
    interrupted: bool = Field(
        default=False,
        description="Whether the plan was interrupted (e.g., shutdown)",
    )
    interrupted_at: datetime | None = Field(
        default=None,
        description="When the plan was interrupted",
    )

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

    @property
    def has_progress(self) -> bool:
        """Whether any steps have been completed."""
        return self.last_completed_step >= 0

    @property
    def next_step_index(self) -> int:
        """Index of the next step to execute."""
        return self.last_completed_step + 1

    @property
    def is_complete(self) -> bool:
        """Whether all steps have been completed."""
        return self.total_steps > 0 and self.last_completed_step >= self.total_steps - 1

    def record_checkpoint(self, checkpoint: ResumeCheckpoint) -> None:
        """Record a new checkpoint.

        Args:
            checkpoint: The checkpoint to record.

        Raises:
            ValueError: If the checkpoint step_index is invalid.
        """
        if checkpoint.step_index < 0:
            raise ValueError("Checkpoint step_index must be >= 0")
        self.checkpoints.append(checkpoint)
        self.last_completed_step = checkpoint.step_index
        self.last_checkpoint_id = checkpoint.checkpoint_id


class ResumeEligibility(BaseModel):
    """Result of resume eligibility validation."""

    eligible: bool = Field(
        ...,
        description="Whether the plan can be resumed",
    )
    reason: str = Field(
        default="",
        description="Why the plan can or cannot be resumed",
    )
    ineligible_reason: ResumeIneligibleReason | None = Field(
        default=None,
        description="Structured reason code for ineligibility",
    )

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


class ResumeSummary(BaseModel):
    """Summary output shown before resume execution.

    Displayed by ``plan resume`` and ``plan resume --dry-run``.
    """

    plan_id: str = Field(..., description="Plan ULID")
    phase: str = Field(..., description="Current plan phase")
    processing_state: str = Field(..., description="Current processing state")
    last_completed_step: int = Field(
        ...,
        description="Last completed step index (-1 if none)",
    )
    next_step_index: int = Field(
        ...,
        description="Next step to execute",
    )
    total_steps: int = Field(
        ...,
        description="Total steps in the plan",
    )
    decision_id: str | None = Field(
        default=None,
        description="Decision ID at the resume point",
    )
    last_checkpoint_id: str | None = Field(
        default=None,
        description="Last checkpoint ID",
    )
    sandbox_ref: str | None = Field(
        default=None,
        description="Sandbox reference at resume point",
    )

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

    def as_cli_dict(self) -> dict[str, object]:
        """Return a dictionary for CLI rendering."""
        result: dict[str, object] = {
            "plan_id": self.plan_id,
            "phase": self.phase,
            "processing_state": self.processing_state,
            "last_completed_step": self.last_completed_step,
            "next_step_index": self.next_step_index,
            "total_steps": self.total_steps,
        }
        if self.decision_id:
            result["decision_id"] = self.decision_id
        if self.last_checkpoint_id:
            result["last_checkpoint_id"] = self.last_checkpoint_id
        if self.sandbox_ref:
            result["sandbox_ref"] = self.sandbox_ref
        return result
