"""Resource domain models for CleverAgents.

A **Resource** is a registered entry in the Resource Registry representing a
physical or virtual asset that plans operate on. Resources are organized in
a DAG, linked to projects, and sandboxed during plan execution.

This module implements:

- [PhysVirt][cleveragents.domain.models.core.resource.PhysVirt]
  physical / virtual classification enum
- [SandboxStrategy][cleveragents.domain.models.core.resource.SandboxStrategy]
  sandbox isolation strategy enum
- [ResourceCapabilities][cleveragents.domain.models.core.resource.ResourceCapabilities]
  capability flags (readable, writable, sandboxable, checkpointable)
- [Resource][cleveragents.domain.models.core.resource.Resource]
  the core Resource domain model (``ResourceRecord``)

Based on:

- Specification: ResourceRecord UML (lines 9932-9944)
- Specification: PhysVirt enum (lines 9961-9964)
- Specification: SandboxStrategy JSON schema (lines 17135-17138)
- Specification: Resource Capabilities (lines 9892-9903)
- Specification: resources DDL (lines 26696-26713)
- ADR-004: Pydantic v2 Validation

See Also:
    - [ResourceTypeSpec][cleveragents.domain.models.core.resource_type.ResourceTypeSpec]
      Resource type schema definitions
"""

from __future__ import annotations

import re
from datetime import UTC, datetime
from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field, field_validator

# ULID: 26 characters, Crockford's base32
ULID_PATTERN = r"^[0-9A-HJKMNP-TV-Z]{26}$"

# Namespaced name: [[server:]namespace/]name
# Built-in resource types are unnamespaced (e.g., "git-checkout").
# Custom types follow: namespace/name (e.g., "local/my-type").
NAMESPACED_NAME_PATTERN = re.compile(
    r"^(?:(?P<server>[a-zA-Z0-9_-]+):)?(?P<namespace>[a-zA-Z0-9_-]+)/(?P<name>[a-zA-Z0-9_-]+)$"
)


class PhysVirt(StrEnum):
    """Classification of a resource as physical or virtual.

    | Value      | Description                                              |
    |------------|----------------------------------------------------------|
    | `physical` | Real, tangible resource (file, directory, git repository) |
    | `virtual`  | Derived or computed resource (git branch, tree entry)     |
    """

    PHYSICAL = "physical"
    VIRTUAL = "virtual"


class SandboxStrategy(StrEnum):
    """Sandbox isolation strategy for a resource.

    | Value                  | Description                               |
    |------------------------|-------------------------------------------|
    | `git_worktree`         | Isolate via Git worktree                  |
    | `copy_on_write`        | Isolate via filesystem copy               |
    | `transaction_rollback` | Isolate via database transaction rollback |
    | `snapshot`             | Isolate via snapshot/restore              |
    | `none`                 | No isolation                              |
    """

    GIT_WORKTREE = "git_worktree"
    COPY_ON_WRITE = "copy_on_write"
    TRANSACTION_ROLLBACK = "transaction_rollback"
    SNAPSHOT = "snapshot"
    OVERLAY = "overlay"
    NONE = "none"


class ResourceCapabilities(BaseModel):
    """Capabilities declared by a resource, derived from its resource type.

    Spec reference: Resource Capabilities (lines 9892-9903).
    These are used by the tool execution flow to validate that a tool's
    resource binding is compatible with the resource.
    """

    readable: bool = Field(default=True, description="Whether the resource can be read")
    writable: bool = Field(
        default=True, description="Whether the resource can be modified"
    )
    sandboxable: bool = Field(
        default=True,
        description="Whether the resource supports sandbox isolation",
    )
    checkpointable: bool = Field(
        default=False,
        description="Whether the resource supports checkpoint/rollback",
    )

    model_config = ConfigDict(frozen=True)


class Resource(BaseModel):
    """Domain model for a Resource Registry entry (ResourceRecord).

    Resources are identified by ULID (``resource_id``). They may optionally
    have a namespaced name (``None`` for auto-discovered children).

    **Constraints**:

    - ``resource_id`` must match ULID format (``^[0-9A-HJKMNP-TV-Z]{26}$``)
    - ``name``, if provided, must not be empty or whitespace-only
    - ``resource_type_name`` must not be empty
    - ``location``, if provided, must not be empty or whitespace-only

    **Convenience properties**:

    - ``is_physical`` / ``is_virtual`` -- classification check
    - ``can_sandbox`` / ``can_checkpoint`` -- capability check
    - ``is_read_only`` -- True when the resource cannot be written

    See Also:
    - [ResourceTypeSpec][cleveragents.domain.models.core.resource_type.ResourceTypeSpec]
    """

    # Identity -- resource_id is the primary key (ULID)
    resource_id: str = Field(
        ...,
        description="Unique ULID identifier (primary key)",
        pattern=ULID_PATTERN,
    )
    name: str | None = Field(
        default=None,
        description="Namespaced name (NULL for auto-discovered children)",
    )

    # Type classification
    resource_type_name: str = Field(
        ...,
        min_length=1,
        description="Namespaced type name (e.g., 'git-checkout', 'local/my-type')",
    )
    classification: PhysVirt = Field(
        ...,
        description="Physical or virtual classification",
    )

    # Descriptive
    description: str | None = Field(
        default=None,
        description="Human-readable description",
    )

    # Type-specific properties (JSON in DB)
    properties: dict[str, str | int | float | bool | None] = Field(
        default_factory=dict,
        description="Type-specific properties (stored as JSON)",
    )

    # Location and hashing
    location: str | None = Field(
        default=None,
        description="Physical location (physical resources only)",
    )
    content_hash: str | None = Field(
        default=None,
        description="Content hash for equivalence tracking",
    )

    # Sandbox strategy override (per-resource, may differ from type default)
    sandbox_strategy: SandboxStrategy | None = Field(
        default=None,
        description="Sandbox strategy override per resource",
    )

    # Capabilities (derived from resource type)
    capabilities: ResourceCapabilities = Field(
        default_factory=ResourceCapabilities,
        description=(
            "Resource capabilities (readable, writable, sandboxable, checkpointable)"
        ),
    )

    # DAG relationships (loaded lazily in practice,
    # represented here for domain completeness)
    parents: list[str] = Field(
        default_factory=list,
        description="Parent resource_id list (DAG upward references)",
    )
    children: list[str] = Field(
        default_factory=list,
        description="Child resource_id list (DAG downward references)",
    )

    # Cross-references
    linked_projects: list[str] = Field(
        default_factory=list,
        description="Project names this resource is linked to",
    )

    # Timestamps
    created_at: datetime = Field(
        default_factory=lambda: datetime.now(tz=UTC),
        description="Creation timestamp (UTC)",
    )
    updated_at: datetime = Field(
        default_factory=lambda: datetime.now(tz=UTC),
        description="Last update timestamp (UTC)",
    )

    model_config = ConfigDict(
        frozen=True,
        str_strip_whitespace=True,
        use_enum_values=True,
    )

    # -- Validators ----------------------------------------------------------

    @field_validator("name")
    @classmethod
    def validate_name(cls: type[Resource], v: str | None) -> str | None:
        """Validate namespaced name format if provided.

        Names follow the pattern: [[server:]namespace/]name
        Built-in resource types may be unnamespaced (e.g., 'git-checkout').
        """
        if v is None:
            return v
        if not v.strip():
            raise ValueError("Resource name cannot be empty (use None instead)")
        return v

    @field_validator("location")
    @classmethod
    def validate_location(cls: type[Resource], v: str | None) -> str | None:
        """Validate location is non-empty if provided."""
        if v is not None and not v.strip():
            raise ValueError("Location cannot be empty (use None instead)")
        return v

    @field_validator("resource_type_name")
    @classmethod
    def validate_resource_type_name(cls: type[Resource], v: str) -> str:
        """Validate resource type name is non-empty."""
        if not v.strip():
            raise ValueError("resource_type_name cannot be empty")
        return v

    # -- Domain Methods ------------------------------------------------------

    @property
    def is_physical(self) -> bool:
        """Check if this is a physical resource."""
        return self.classification == PhysVirt.PHYSICAL

    @property
    def is_virtual(self) -> bool:
        """Check if this is a virtual resource."""
        return self.classification == PhysVirt.VIRTUAL

    @property
    def can_sandbox(self) -> bool:
        """Check if this resource supports sandbox isolation."""
        return self.capabilities.sandboxable

    @property
    def can_checkpoint(self) -> bool:
        """Check if this resource supports checkpoint/rollback."""
        return self.capabilities.checkpointable

    @property
    def is_read_only(self) -> bool:
        """Check if this resource is read-only."""
        return not self.capabilities.writable

    def with_updated_at(self) -> Resource:
        """Return a copy with updated_at set to now (UTC)."""
        return self.model_copy(update={"updated_at": datetime.now(tz=UTC)})
