"""Container lifecycle state model for devcontainer resources.

Defines the :class:`ContainerLifecycleState` enum and transition
validation logic for devcontainer-instance resources.

## State Machine

::

    detected ──► building ──► running ──► stopping ──► stopped
                    │                                    │
                    ▼                                    │
                  failed ◄───────────────────────────────┘
                    │                    ▲
                    └──► building        │
                          (retry)   (rebuild)
                                        │
                         stopped ───────┘

Valid transitions:

| From      | To                  | Trigger               |
|-----------|---------------------|-----------------------|
| detected  | building            | lazy activation       |
| building  | running             | devcontainer up OK    |
| building  | failed              | devcontainer up fail  |
| running   | stopping            | stop command          |
| stopping  | stopped             | container stopped     |
| stopping  | failed              | stop failed           |
| stopped   | building            | rebuild               |
| failed    | building            | retry                 |

Based on:
    - Issue #514: Devcontainer lifecycle management
    - ADR-043: Devcontainer Integration
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime
from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field

logger = logging.getLogger(__name__)


class ContainerLifecycleState(StrEnum):
    """Lifecycle state for devcontainer-instance resources.

    State names align with the ``activation_state`` enum defined in
    ``docs/specification.md`` (devcontainer-instance resource type).
    ``stopping`` is an implementation-level intermediate state added
    for clean shutdown tracking.

    .. note:: SPEC-1 reconciliation

       Issue #514 acceptance criteria use a different vocabulary:
       ``inactive/starting/active/stopping/stopped/error``.  After
       design review these were renamed to better reflect the
       container lifecycle semantics:

       | Issue #514  | Implementation | Reason                          |
       |-------------|----------------|---------------------------------|
       | inactive    | detected       | container exists but not started |
       | starting    | building       | ``devcontainer up`` is running  |
       | active      | running        | container is healthy            |
       | stopping    | stopping       | (unchanged)                     |
       | stopped     | stopped        | (unchanged)                     |
       | error       | failed         | aligns with Docker terminology  |

       The specification (``docs/specification.md``) uses the
       implementation names.  Issue #514 should be updated to match.

    | Value      | Description                                        |
    |------------|----------------------------------------------------|
    | `detected` | Container not yet started (default for new)        |
    | `building` | Container is being activated (devcontainer up)     |
    | `running`  | Container is running and healthy                   |
    | `stopping` | Container is being stopped                         |
    | `stopped`  | Container has been stopped cleanly                 |
    | `failed`   | Container is in an error state                     |
    """

    DETECTED = "detected"
    BUILDING = "building"
    RUNNING = "running"
    STOPPING = "stopping"
    STOPPED = "stopped"
    FAILED = "failed"


#: Valid state transitions as a mapping from source to allowed targets.
VALID_TRANSITIONS: dict[ContainerLifecycleState, frozenset[ContainerLifecycleState]] = {
    ContainerLifecycleState.DETECTED: frozenset({ContainerLifecycleState.BUILDING}),
    ContainerLifecycleState.BUILDING: frozenset(
        {ContainerLifecycleState.RUNNING, ContainerLifecycleState.FAILED}
    ),
    ContainerLifecycleState.RUNNING: frozenset({ContainerLifecycleState.STOPPING}),
    ContainerLifecycleState.STOPPING: frozenset(
        {ContainerLifecycleState.STOPPED, ContainerLifecycleState.FAILED}
    ),
    ContainerLifecycleState.STOPPED: frozenset({ContainerLifecycleState.BUILDING}),
    ContainerLifecycleState.FAILED: frozenset({ContainerLifecycleState.BUILDING}),
}


class LifecycleTransition(BaseModel):
    """Record of a single lifecycle state transition.

    Captures the from/to states, timestamp, and an optional reason
    string for audit and debugging purposes.
    """

    from_state: ContainerLifecycleState
    to_state: ContainerLifecycleState
    timestamp: datetime = Field(default_factory=lambda: datetime.now(tz=UTC))
    reason: str = ""

    model_config = ConfigDict(frozen=True)


class ContainerLifecycleTracker(BaseModel):
    """Tracks lifecycle state and transition history for a container resource.

    This model is stored separately from the frozen ``Resource`` model
    and is keyed by ``resource_id`` in the resource registry.

    Attributes:
        resource_id: The ULID of the tracked resource.
        current_state: Current lifecycle state.
        container_id: Docker/devcontainer container ID (set after start).
        workspace_path: Workspace mount path inside the container.
        health_check_interval: Seconds between health probes.
        transitions: Ordered tuple of state transitions (immutable).
    """

    resource_id: str = Field(
        ...,
        min_length=1,
        description="ULID of the tracked devcontainer resource",
    )
    session_id: str | None = Field(
        default=None,
        description="Session or plan ID that owns this container (for scoped cleanup)",
    )
    current_state: ContainerLifecycleState = Field(
        default=ContainerLifecycleState.DETECTED,
        description="Current lifecycle state",
    )
    container_id: str | None = Field(
        default=None,
        description="Container ID from devcontainer up output",
    )
    workspace_path: str | None = Field(
        default=None,
        description="Workspace mount path inside the container",
    )
    host_workspace_path: str | None = Field(
        default=None,
        description=(
            "Host-side workspace folder used with devcontainer CLI "
            "(F15 fix: devcontainer exec --workspace-folder expects the host path)"
        ),
    )
    health_check_interval: float = Field(
        default=30.0,
        ge=10,
        le=3600,
        description="Seconds between health check probes (min 10, max 3600)",
    )
    # BUG-4 fix: use tuple instead of list to prevent in-place mutation
    # of the shared transition history from concurrent threads.
    transitions: tuple[LifecycleTransition, ...] = Field(
        default_factory=tuple,
        description="Ordered transition history",
    )

    model_config = ConfigDict(str_strip_whitespace=True)


def validate_transition(
    from_state: ContainerLifecycleState,
    to_state: ContainerLifecycleState,
) -> bool:
    """Check whether a state transition is valid.

    Args:
        from_state: Current state.
        to_state: Desired target state.

    Returns:
        ``True`` if the transition is allowed, ``False`` otherwise.
    """
    if not isinstance(from_state, ContainerLifecycleState):
        raise TypeError(
            f"from_state must be ContainerLifecycleState, "
            f"got {type(from_state).__name__}"
        )
    if not isinstance(to_state, ContainerLifecycleState):
        raise TypeError(
            f"to_state must be ContainerLifecycleState, got {type(to_state).__name__}"
        )
    allowed = VALID_TRANSITIONS.get(from_state, frozenset())
    return to_state in allowed


def transition_state(
    tracker: ContainerLifecycleTracker,
    to_state: ContainerLifecycleState,
    reason: str = "",
) -> ContainerLifecycleTracker:
    """Transition a tracker to a new state with validation.

    Args:
        tracker: The current lifecycle tracker.
        to_state: Desired target state.
        reason: Human-readable reason for the transition.

    Returns:
        A new tracker instance with the updated state and transition logged.

    Raises:
        ValueError: If the transition is not valid.
    """
    if not isinstance(tracker, ContainerLifecycleTracker):
        raise TypeError(
            f"tracker must be ContainerLifecycleTracker, got {type(tracker).__name__}"
        )
    if not isinstance(to_state, ContainerLifecycleState):
        raise TypeError(
            f"to_state must be ContainerLifecycleState, got {type(to_state).__name__}"
        )

    from_state = tracker.current_state
    if not validate_transition(from_state, to_state):
        raise ValueError(
            f"Invalid lifecycle transition: {from_state.value} -> {to_state.value}"
        )

    transition = LifecycleTransition(
        from_state=from_state,
        to_state=to_state,
        reason=reason,
    )

    # Cap history to prevent unbounded growth in long-running containers.
    max_history = 100
    new_transitions = [*tracker.transitions, transition]
    if len(new_transitions) > max_history:
        new_transitions = new_transitions[-max_history:]

    logger.info(
        "Container %s: %s -> %s (%s)",
        tracker.resource_id,
        from_state.value,
        to_state.value,
        reason or "no reason",
    )

    # R10 fix: clear stale container_id and workspace_path when
    # transitioning to FAILED or STOPPED so downstream code doesn't
    # get false positives from a defunct container reference.
    # BUG-2 fix: also clear on BUILDING to prevent stale references
    # from a previous lifecycle being visible during rebuild/retry.
    # R8-F4 fix: use model_copy to avoid fragile manual reconstruction.
    updates: dict[str, object] = {
        "current_state": to_state,
        "transitions": tuple(new_transitions),
    }
    if to_state in (
        ContainerLifecycleState.BUILDING,
        ContainerLifecycleState.FAILED,
        ContainerLifecycleState.STOPPED,
    ):
        updates["container_id"] = None
        updates["workspace_path"] = None

    # BUG-1 note: model_copy(update=...) intentionally bypasses Pydantic
    # validators. This is safe here because the updated fields
    # (current_state, transitions, container_id, workspace_path) have no
    # custom validators that need re-running. If a future change adds
    # validated fields to the update dict, switch to
    # ``model_validate(tracker.model_dump() | updates)`` instead.
    return tracker.model_copy(update=updates)
