"""Resource type YAML configuration schema, validation, and loading.

Provides :class:`ResourceTypeConfigSchema`, a Pydantic model that:

* Loads raw YAML (string or file) and validates it against the resource type schema.
* Interpolates ``${ENV_VAR}`` placeholders from environment variables.
* Enforces schema version guardrails.
* Produces clear, actionable error messages for every validation failure.

Schema definition lives in ``docs/schema/resource_type.schema.yaml``.

Based on:
- docs/specification.md — Resource Types, Resource Registry
- implementation_plan.md — Task B0.type-model
"""

from __future__ import annotations

import logging
import os
import re
from pathlib import Path
from typing import Any

import yaml
from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    field_validator,
    model_validator,
)

logger = logging.getLogger(__name__)

# ────────────────────────────────────────────────────────────
# Constants
# ────────────────────────────────────────────────────────────

#: Pattern for ``${VAR}`` environment variable references.
_ENV_VAR_RE = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")

#: Supported schema versions.
_SUPPORTED_VERSIONS = frozenset({"1", "1.0"})

#: Valid resource kinds.
_VALID_KINDS = frozenset({"physical", "virtual"})

#: Valid sandbox strategies.
_VALID_STRATEGIES = frozenset(
    {
        "git_worktree",
        "copy_on_write",
        "transaction_rollback",
        "snapshot",
        "overlay",
        "none",
    }
)

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

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

#: Namespaced name pattern.
_NAMESPACED_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*/[a-zA-Z0-9][a-zA-Z0-9_-]*$")

#: Built-in (unnamespaced) name pattern.
_BUILTIN_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")


# ────────────────────────────────────────────────────────────
# Nested models
# ────────────────────────────────────────────────────────────


class ResourceTypeArgSchema(BaseModel):
    """Schema for a single CLI argument in a resource type config."""

    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.",
    )
    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_name(cls, v: str) -> str:
        """Ensure argument name is CLI-safe."""
        if not _CLI_ARG_NAME_RE.match(v):
            raise ValueError(
                f"Argument name '{v}' is not CLI-safe. "
                "Use lowercase alphanumeric, hyphens, or underscores, "
                "starting with a letter."
            )
        return v

    @field_validator("type")
    @classmethod
    def validate_type(cls, v: str) -> str:
        """Ensure argument type is valid."""
        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",
    )


# ────────────────────────────────────────────────────────────
# Main schema model
# ────────────────────────────────────────────────────────────


class ResourceTypeConfigSchema(BaseModel):
    """Pydantic model for a resource type YAML configuration file.

    Validates all fields described in ``docs/schema/resource_type.schema.yaml``.

    Create instances via the factory class methods:

    - :meth:`from_yaml` — parse a YAML string
    - :meth:`from_yaml_file` — parse a YAML file from disk
    """

    schema_version: str = Field(
        "1",
        description="Schema version for forward compatibility.",
    )
    name: str = Field(
        ...,
        description="Resource type name (built-in or namespaced).",
    )
    description: str = Field(
        "",
        description="Short description of the resource type.",
    )
    resource_kind: str = Field(
        ...,
        description="Classification: physical | virtual.",
    )
    sandbox_strategy: str = Field(
        ...,
        description="Default sandbox strategy.",
    )
    user_addable: bool = Field(
        True,
        description="Whether users can manually add resources of this type.",
    )
    cli_args: list[ResourceTypeArgSchema] = Field(
        default_factory=list,
        description="CLI arguments accepted for this resource type.",
    )
    parent_types: list[str] = Field(
        default_factory=list,
        description="Allowed parent resource type names.",
    )
    child_types: list[str] = Field(
        default_factory=list,
        description="Allowed child resource type names.",
    )
    auto_discovery: dict[str, Any] | None = Field(
        default=None,
        description="Auto-discovery configuration.",
    )
    equivalence: dict[str, Any] | None = Field(
        default=None,
        description="Equivalence rules for virtual types.",
    )
    handler: str | None = Field(
        default=None,
        description="Handler implementation reference.",
    )
    capabilities: dict[str, bool] = Field(
        default_factory=lambda: {
            "read": True,
            "write": True,
            "sandbox": True,
            "checkpoint": False,
        },
        description="Capability flags.",
    )
    built_in: bool = Field(
        False,
        description="Whether this is a built-in type.",
    )
    inherits: str | None = Field(
        default=None,
        max_length=255,
        description=(
            "Parent resource type name to inherit from (single inheritance). "
            "See ADR-042 for full inheritance semantics."
        ),
    )
    cli_args_replace: bool = Field(
        False,
        description="Replace parent cli_args entirely instead of merging.",
    )
    child_types_replace: bool = Field(
        False,
        description="Replace parent child_types entirely instead of merging.",
    )
    parent_types_replace: bool = Field(
        False,
        description="Replace parent parent_types entirely instead of merging.",
    )

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

    # ────────────────────────────────────────────────────────
    # Field validators
    # ────────────────────────────────────────────────────────

    @field_validator("schema_version")
    @classmethod
    def validate_schema_version(cls, v: str) -> str:
        """Validate schema version is supported."""
        if v not in _SUPPORTED_VERSIONS:
            supported = ", ".join(sorted(_SUPPORTED_VERSIONS))
            raise ValueError(
                f"Unsupported schema version '{v}'. Supported versions: {supported}."
            )
        return v

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate resource type name format."""
        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

    @field_validator("inherits")
    @classmethod
    def validate_inherits(cls, v: str | None) -> str | None:
        """Validate parent type name format if provided."""
        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("resource_kind")
    @classmethod
    def validate_resource_kind(cls, v: str) -> str:
        """Validate resource kind is physical or virtual."""
        v_lower = v.lower()
        if v_lower not in _VALID_KINDS:
            raise ValueError(
                f"Invalid resource_kind '{v}'. Must be 'physical' or 'virtual'."
            )
        return v_lower

    @field_validator("sandbox_strategy")
    @classmethod
    def validate_sandbox_strategy(cls, v: str) -> str:
        """Validate sandbox strategy is one of the allowed values."""
        v_lower = v.lower()
        if v_lower not in _VALID_STRATEGIES:
            valid = ", ".join(sorted(_VALID_STRATEGIES))
            raise ValueError(
                f"Invalid sandbox_strategy '{v}'. Allowed strategies: {valid}."
            )
        return v_lower

    # ────────────────────────────────────────────────────────
    # Model-level validators
    # ────────────────────────────────────────────────────────

    @model_validator(mode="after")
    def validate_cross_fields(self) -> ResourceTypeConfigSchema:
        """Cross-field validation for resource type config."""
        # 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. Only built-in types can be "
                "unnamespaced."
            )

        # Virtual types require equivalence
        if self.resource_kind == "virtual" and self.equivalence is None:
            raise ValueError(
                f"Virtual resource type '{self.name}' requires an "
                "'equivalence' configuration."
            )

        # Cannot inherit from self (ADR-042 rule 3: no cycles)
        if self.inherits is not None and self.inherits == self.name:
            raise ValueError(f"Resource type '{self.name}' cannot inherit from itself.")

        # Built-in types must not inherit from custom types (ADR-042 rule 4)
        if self.built_in and self.inherits is not None and "/" in self.inherits:
            raise ValueError(
                f"Built-in resource type '{self.name}' cannot inherit from "
                f"custom type '{self.inherits}'. Built-in types may only "
                "inherit from other built-in types."
            )

        return self

    # ────────────────────────────────────────────────────────
    # Factory class methods
    # ────────────────────────────────────────────────────────

    @classmethod
    def from_yaml(cls, yaml_string: str) -> ResourceTypeConfigSchema:
        """Parse and validate a resource type YAML string.

        Args:
            yaml_string: Raw YAML content.

        Returns:
            Validated ``ResourceTypeConfigSchema`` instance.

        Raises:
            ValueError: If the YAML is not a mapping or is empty.
            pydantic.ValidationError: If schema validation fails.
        """
        if yaml_string is None:
            raise ValueError("YAML string cannot be None. Provide a valid YAML string.")
        if not yaml_string.strip():
            raise ValueError(
                "YAML string is empty. "
                "Provide a valid resource type YAML configuration."
            )

        raw = yaml.safe_load(yaml_string)
        if not isinstance(raw, dict):
            raise ValueError(
                f"Resource type YAML must be a mapping (key: value), "
                f"got {type(raw).__name__}."
            )

        interpolated = _interpolate_env_vars(raw)
        return cls.model_validate(interpolated)

    @classmethod
    def from_yaml_file(cls, path: str | Path) -> ResourceTypeConfigSchema:
        """Load and validate a resource type YAML file from disk.

        Args:
            path: Path to the YAML file.

        Returns:
            Validated ``ResourceTypeConfigSchema`` instance.

        Raises:
            FileNotFoundError: If the file does not exist.
            ValueError: If file content is invalid.
            pydantic.ValidationError: If schema validation fails.
        """
        if path is None:
            raise ValueError(
                "File path cannot be None. "
                "Provide a valid path to a resource type YAML file."
            )

        filepath = Path(path)
        if not filepath.exists():
            raise FileNotFoundError(
                f"Resource type YAML file not found: {filepath}. "
                "Check the file path and try again."
            )
        if not filepath.is_file():
            raise ValueError(
                f"Path is not a file: {filepath}. "
                "Provide a path to a YAML file, not a directory."
            )

        content = filepath.read_text(encoding="utf-8")
        return cls.from_yaml(content)


# ────────────────────────────────────────────────────────────
# Internal helpers
# ────────────────────────────────────────────────────────────


def _interpolate_env_vars(data: dict[str, Any]) -> dict[str, Any]:
    """Replace ``${VAR}`` references with environment variable values.

    Only string values are interpolated. Missing environment variables
    are left as-is (no error) to allow deferred resolution.
    """
    result: dict[str, Any] = {}
    for key, value in data.items():
        if isinstance(value, str):
            result[key] = _ENV_VAR_RE.sub(_env_replacer, value)
        elif isinstance(value, dict):
            result[key] = _interpolate_env_vars(value)
        elif isinstance(value, list):
            result[key] = [
                (
                    _interpolate_env_vars(item)
                    if isinstance(item, dict)
                    else (
                        _ENV_VAR_RE.sub(_env_replacer, item)
                        if isinstance(item, str)
                        else item
                    )
                )
                for item in value
            ]
        else:
            result[key] = value
    return result


def _env_replacer(match: re.Match[str]) -> str:
    """Replace a single ``${VAR}`` match with its env value."""
    var_name = match.group(1)
    return os.environ.get(var_name, match.group(0))
