"""ResourceType domain model for CleverAgents.

Resource types define schema-level constraints for resource categories:
CLI arguments, classification (physical/virtual), DAG constraints,
sandbox strategy, capabilities, auto-discovery, and handler references.

Built-in types use simple unnamespaced names (e.g. ``git-checkout``).
Custom types follow ``namespace/name`` (e.g. ``myorg/custom-db``).
Virtual types **must** include an ``equivalence`` configuration.

Based on ``docs/specification.md``, ``implementation_plan.md`` Task
B0.type-model, and ADR-004: Pydantic v2 Validation.

See Also:
    - ``docs/reference/resource_types_builtin.md`` -- full built-in type catalogue
    - [Resource][cleveragents.domain.models.core.resource.Resource]
"""

from __future__ import annotations

import re
from enum import StrEnum
from typing import Any, ClassVar

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

from cleveragents.domain.models.core._resource_type_validation import (
    BUILTIN_TYPE_NAMES,
)
from cleveragents.domain.models.core._resource_type_validation import (
    validate_auto_discovery as _validate_auto_discovery,
)
from cleveragents.domain.models.core._resource_type_validation import (
    validate_self_referential as _validate_self_referential,
)
from cleveragents.domain.models.core._resource_type_validation import (
    validate_virtual_type as _validate_virtual_type,
)

# ---------------------------------------------------------------------------
# Patterns
# ---------------------------------------------------------------------------

#: CLI-safe argument name: lowercase alphanumeric, hyphens, underscores.
_CLI_ARG_NAME_RE = re.compile(r"^[a-z][a-z0-9_-]*$")

#: Namespaced name: ``namespace/name`` — both components must start with a letter.
_NAMESPACED_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*/[a-zA-Z][a-zA-Z0-9_-]*$")

#: Built-in (unnamespaced) name: simple alphanumeric + hyphens/underscores.
_BUILTIN_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")

#: Valid argument types for resource type CLI arguments.
_VALID_ARG_TYPES = frozenset({"string", "integer", "float", "boolean", "path"})


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------


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

    Physical resources represent tangible assets (files, directories, repos).
    Virtual resources represent derived/computed assets (branches, tree entries).
    """

    PHYSICAL = "physical"
    VIRTUAL = "virtual"


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

    Determines how changes to resources of this type are isolated
    during plan execution.
    """

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


# ---------------------------------------------------------------------------
# ResourceTypeArgument
# ---------------------------------------------------------------------------


class ResourceTypeArgument(BaseModel):
    """Definition of a CLI argument accepted by a resource type.

    Arguments map to CLI flags ``--<name>`` when adding resources of this type.
    For example, a ``git-checkout`` type might accept ``--path`` and ``--branch``.
    """

    name: str = Field(
        ...,
        min_length=1,
        max_length=64,
        description="CLI argument name (maps to --<name>).",
    )
    type: str = Field(
        "string",
        description="Data type: string | integer | float | boolean | path.",
    )
    required: bool = Field(
        True,
        description="Whether the argument must be provided.",
    )
    description: str = Field(
        "",
        max_length=500,
        description="Human-readable description shown in help text.",
    )
    default: Any | None = Field(
        default=None,
        description="Default value when argument is not provided.",
    )
    validation_pattern: str | None = Field(
        default=None,
        description="Regex pattern for string/path argument validation.",
    )

    @field_validator("name")
    @classmethod
    def validate_cli_name(cls, v: str) -> str:
        """Validate argument name is CLI-safe (maps to ``--<name>``)."""
        if not _CLI_ARG_NAME_RE.match(v):
            raise ValueError(
                f"Argument name '{v}' is not CLI-safe. "
                "Names must start with a lowercase letter and contain only "
                "lowercase alphanumeric characters, hyphens, or underscores."
            )
        return v

    @field_validator("type")
    @classmethod
    def validate_type(cls, v: str) -> str:
        """Validate argument type is one of the allowed types."""
        v_lower = v.lower()
        if v_lower not in _VALID_ARG_TYPES:
            valid = ", ".join(sorted(_VALID_ARG_TYPES))
            raise ValueError(f"Invalid argument type '{v}'. Allowed types: {valid}.")
        return v_lower

    model_config = ConfigDict(
        str_strip_whitespace=True,
        extra="forbid",
    )


# ---------------------------------------------------------------------------
# ResourceTypeSpec
# ---------------------------------------------------------------------------


class ResourceTypeSpec(BaseModel):
    """Schema-level definition constraining a category of resources.

    Resource types define the structure and behavior of resources that
    conform to them. For example, a ``git-checkout`` type defines that
    resources need ``--path`` and ``--branch`` CLI arguments and use
    the ``git_worktree`` sandbox strategy.

    Built-in types can use simple names (e.g., ``git-checkout``).
    Custom types must follow the ``namespace/name`` pattern (e.g.,
    ``myorg/custom-db``).
    """

    # Built-in resource type names (unnamespaced).
    # The actual frozenset lives in _resource_type_validation to keep
    # this module under the 500-line CONTRIBUTING limit.
    # Built-in resource type names — sourced from _resource_type_validation.py
    # to keep this module under the 500-line CONTRIBUTING limit.
    BUILTIN_NAMES: ClassVar[frozenset[str]] = BUILTIN_TYPE_NAMES

    name: str = Field(
        ...,
        min_length=1,
        description="Resource type name (built-in or namespaced).",
    )
    description: str = Field(
        "",
        description="Human-readable description of this resource type.",
    )
    resource_kind: ResourceKind = Field(
        ...,
        description="Physical or virtual classification.",
    )
    sandbox_strategy: SandboxStrategy = Field(
        ...,
        description="Default sandbox strategy for resources of this type.",
    )
    user_addable: bool = Field(
        True,
        description="Whether users can manually add resources of this type.",
    )
    cli_args: list[ResourceTypeArgument] = Field(
        default_factory=list,
        description="CLI arguments accepted when adding a resource of this type.",
    )
    parent_types: list[str] = Field(
        default_factory=list,
        description="Allowed parent resource type names in the DAG.",
    )
    child_types: list[str] = Field(
        default_factory=list,
        description="Allowed child resource type names in the DAG.",
    )
    auto_discovery: dict[str, Any] | None = Field(
        default=None,
        description=(
            "Auto-discovery configuration. Two schemas coexist: "
            "(1) rules-based {enabled, scan_depth, rules: [{type, pattern}]} "
            "for git/fs types, and (2) trigger-based {trigger_types, scan_paths} "
            "for devcontainer types."
        ),
    )
    equivalence: dict[str, Any] | None = Field(
        default=None,
        description="Equivalence rules for virtual resource deduplication.",
    )
    handler: str | None = Field(
        default=None,
        description="Handler implementation reference (module:class or plugin name).",
    )
    capabilities: dict[str, bool] = Field(
        default_factory=lambda: {
            "read": True,
            "write": True,
            "sandbox": True,
            "checkpoint": False,
        },
        description="Capability flags: read, write, sandbox, checkpoint.",
    )
    built_in: bool = Field(
        False,
        description=("Whether this is a built-in type (allows unnamespaced names)."),
    )
    inherits: str | None = Field(
        default=None,
        max_length=255,
        description="Parent resource type name (ADR-042 single inheritance).",
    )

    # -- Name validation -----------------------------------------------------

    @field_validator("inherits")
    @classmethod
    def validate_inherits(cls, v: str | None) -> str | None:
        """Validate inherits field uses a legal resource type name format."""
        if v is None:
            return v
        if not _BUILTIN_NAME_RE.match(v) and not _NAMESPACED_RE.match(v):
            raise ValueError(
                f"Invalid inherits type name '{v}'. "
                "Names must start with a letter and contain only "
                "alphanumeric characters, hyphens, or underscores. "
                "Custom types must follow namespace/name format."
            )
        return v

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate resource type name format.

        Built-in types can be unnamespaced (e.g., ``git-checkout``).
        This validator only checks basic format; the cross-field check
        in ``_validate_model`` enforces namespace rules for custom types.
        """
        if not _BUILTIN_NAME_RE.match(v) and not _NAMESPACED_RE.match(v):
            raise ValueError(
                f"Invalid resource type name '{v}'. "
                "Names must start with a letter and contain only "
                "alphanumeric characters, hyphens, or underscores. "
                "Custom types must follow namespace/name format."
            )
        return v

    # -- Capabilities validation ---------------------------------------------

    @field_validator("capabilities")
    @classmethod
    def validate_capabilities(cls, v: dict[str, bool]) -> dict[str, bool]:
        """Ensure capabilities dict has the expected boolean keys."""
        expected = {"read", "write", "sandbox", "checkpoint"}
        for key in expected:
            if key not in v:
                v[key] = key != "checkpoint"
        for key in v:
            if key not in expected:
                raise ValueError(
                    f"Unknown capability '{key}'. "
                    f"Allowed capabilities: {', '.join(sorted(expected))}."
                )
            if not isinstance(v[key], bool):
                raise ValueError(
                    f"Capability '{key}' must be a boolean, "
                    f"got {type(v[key]).__name__}."
                )
        return v

    # -- Cross-field validation -----------------------------------------------

    @model_validator(mode="after")
    def _validate_model(self) -> ResourceTypeSpec:
        """Cross-field validation for resource type constraints."""
        # Custom types must be namespaced
        if not self.built_in and "/" not in self.name:
            raise ValueError(
                f"Custom resource type '{self.name}' must follow "
                "namespace/name format (e.g., 'myorg/custom-db'). "
                "Only built-in types can be unnamespaced."
            )

        # Virtual type constraints (equivalence, no sandbox, etc.)
        if self.resource_kind == ResourceKind.VIRTUAL:
            _validate_virtual_type(
                self.name,
                self.equivalence,
                self.user_addable,
                self.sandbox_strategy.value,
                self.handler,
                self.capabilities,
            )

        # Auto-discovery validation (rules-based and trigger-based schemas)
        if self.auto_discovery is not None:
            _validate_auto_discovery(self.name, self.auto_discovery, self.BUILTIN_NAMES)

        # Self-referential types must have bounded scan_depth
        _validate_self_referential(
            self.name, self.parent_types, self.child_types, self.auto_discovery
        )

        # ADR-042 rule 3: no cycles (self-inheritance)
        if self.inherits is not None and self.inherits == self.name:
            raise ValueError(f"'{self.name}' cannot inherit from itself.")
        # ADR-042 rule 4: built-in must not inherit from custom
        if self.built_in and self.inherits is not None and "/" in self.inherits:
            raise ValueError(
                f"Built-in '{self.name}' cannot inherit from custom '{self.inherits}'."
            )

        return self

    # -- Factory methods ------------------------------------------------------

    @classmethod
    def from_config(cls, config: dict[str, Any]) -> ResourceTypeSpec:
        """Create a ResourceTypeSpec from a YAML configuration dict.

        Args:
            config: Parsed YAML configuration with resource type fields.

        Returns:
            Validated ``ResourceTypeSpec`` instance.

        Raises:
            ValueError: If required fields are missing or invalid.
        """
        if "name" not in config:
            raise ValueError("Resource type config must include 'name'.")
        if "resource_kind" not in config:
            raise ValueError("Resource type config must include 'resource_kind'.")
        if "sandbox_strategy" not in config:
            raise ValueError("Resource type config must include 'sandbox_strategy'.")

        # Parse CLI arguments from list of mappings
        cli_args: list[ResourceTypeArgument] = []
        for arg_data in config.get("cli_args", []):
            if isinstance(arg_data, dict):
                cli_args.append(ResourceTypeArgument.model_validate(arg_data))
            else:
                raise ValueError(
                    f"cli_args entry must be a mapping, got {type(arg_data).__name__}."
                )

        return cls(
            name=config["name"],
            description=config.get("description", ""),
            resource_kind=ResourceKind(config["resource_kind"]),
            sandbox_strategy=SandboxStrategy(config["sandbox_strategy"]),
            user_addable=config.get("user_addable", True),
            cli_args=cli_args,
            parent_types=config.get("parent_types", []),
            child_types=config.get("child_types", []),
            auto_discovery=config.get("auto_discovery"),
            equivalence=config.get("equivalence"),
            handler=config.get("handler"),
            capabilities=config.get(
                "capabilities",
                {
                    "read": False,
                    "write": False,
                    "sandbox": False,
                    "checkpoint": False,
                }
                if config.get("resource_kind") == "virtual"
                else {
                    "read": True,
                    "write": True,
                    "sandbox": True,
                    "checkpoint": False,
                },
            ),
            built_in=config.get("built_in", False),
            inherits=config.get("inherits"),
        )

    # -- CLI rendering --------------------------------------------------------

    def as_cli_dict(self) -> dict[str, Any]:
        """Return a stable dict for CLI output rendering.

        Keys are ordered for deterministic display.
        """
        result: dict[str, Any] = {
            "name": self.name,
            "resource_kind": self.resource_kind.value,
            "sandbox_strategy": self.sandbox_strategy.value,
        }

        if self.description:
            result["description"] = self.description

        result["user_addable"] = self.user_addable
        result["built_in"] = self.built_in

        if self.inherits is not None:
            result["inherits"] = self.inherits

        if self.cli_args:
            result["cli_args"] = [
                {
                    "name": arg.name,
                    "type": arg.type,
                    "required": arg.required,
                    "description": arg.description,
                }
                for arg in self.cli_args
            ]

        if self.parent_types:
            result["parent_types"] = list(self.parent_types)
        if self.child_types:
            result["child_types"] = list(self.child_types)

        result["capabilities"] = dict(self.capabilities)

        if self.auto_discovery is not None:
            # Deep copy — two schemas coexist: rules-based (list[dict])
            # and trigger-based (list[str]).  copy.deepcopy handles both.
            import copy

            result["auto_discovery"] = copy.deepcopy(self.auto_discovery)
        if self.equivalence is not None:
            result["equivalence"] = {
                k: list(v) if isinstance(v, list) else v
                for k, v in self.equivalence.items()
            }
        if self.handler is not None:
            result["handler"] = self.handler

        return result

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