"""Skill data models - Pure data classes with no circular dependencies.

This module contains the core data models for skills. These are extracted
to avoid circular imports between core.skills.skill and core.builders.skill.

All models here are Pydantic BaseModel for:
- Type validation
- Serialization via model_dump()
- Deserialization via model_validate()

Usage:
    from core.skills.models import Skill, ScriptToolDeclaration, RuntimeContext
"""

from pathlib import Path
from typing import Any, Self

from pydantic import BaseModel, ConfigDict, Field, field_validator

# =============================================================================
# Script Tool Declarations
# =============================================================================


class ScriptToolParam(BaseModel):
    """Parameter definition for a script tool."""

    model_config = ConfigDict(extra="forbid")

    name: str
    param_type: str = "string"  # string, number, boolean, array
    description: str = ""
    required: bool = False
    default: str | int | float | bool | None = None


class RuntimeContext(BaseModel):
    """Runtime execution context for a script tool.

    Specifies how and where a script should execute:
    - local: Direct subprocess (default, fast)
    - docker: Run in a Docker container (isolated, with dependencies)
    """

    model_config = ConfigDict(extra="forbid")

    mode: str = "local"  # "local" or "docker"
    image: str | None = None  # Docker image (required if mode=docker)
    packages: list[str] = Field(default_factory=list)  # Pip packages to install
    gpu: bool = False  # Request GPU access (Docker only)
    memory_mb: int = 512  # Memory limit
    timeout_s: float = 300  # Execution timeout (seconds, can be fractional)


class ScriptToolDeclaration(BaseModel):
    """Declaration of a tool backed by a bundled script.

    Can be defined explicitly in SKILL.md frontmatter:
        ---
        tools:
          - id: create
            script: scripts/sora.py
            args: [create]
            description: Create a new video
            params:
              prompt: {type: string, required: true}
            runtime:
              mode: docker
              image: pytorch/pytorch:2.0-cuda
        ---

    Or auto-generated from scripts/ directory discovery.
    """

    model_config = ConfigDict(extra="forbid")

    id: str  # Tool ID within this skill (e.g., "create")
    script: str  # Relative path to script (e.g., "scripts/sora.py")
    description: str = ""

    # Default arguments to prepend (e.g., ["create"] for subcommands)
    default_args: list[str] = Field(default_factory=list)

    # Parameter definitions
    params: list[ScriptToolParam] = Field(default_factory=list)

    # Execution hints
    interpreter: str | None = None  # Auto-detected from extension if None
    working_dir: str | None = None  # Relative to skill dir; None = skill dir
    env: dict[str, str] = Field(default_factory=dict)  # Additional env vars

    # Runtime context (NEW: supports Docker execution)
    runtime: RuntimeContext | None = None

    @classmethod
    def from_frontmatter(cls, data: dict[str, Any]) -> Self:
        """Create from parsed frontmatter dict.

        This handles the special frontmatter format where params are a dict
        and runtime can be a simple string.
        """
        params = []
        params_data = data.get("params", {})
        if isinstance(params_data, dict):
            for name, spec in params_data.items():
                if isinstance(spec, dict):
                    params.append(
                        ScriptToolParam(
                            name=name,
                            param_type=spec.get("type", "string"),
                            description=spec.get("description", ""),
                            required=spec.get("required", False),
                            default=spec.get("default"),
                        ),
                    )
                else:
                    # Simple format: params: {prompt: string}
                    params.append(ScriptToolParam(name=name, param_type=str(spec)))

        # Parse runtime context
        runtime = None
        runtime_data = data.get("runtime")
        if runtime_data:
            if isinstance(runtime_data, str):
                runtime = RuntimeContext(mode=runtime_data)
            else:
                runtime = RuntimeContext.model_validate(runtime_data)

        return cls(
            id=data.get("id", ""),
            script=data.get("script", ""),
            description=data.get("description", ""),
            default_args=data.get("args", []),
            params=params,
            interpreter=data.get("interpreter"),
            working_dir=data.get("working_dir"),
            env=data.get("env", {}),
            runtime=runtime,
        )


class SkillRequirements(BaseModel):
    """Requirements a skill declares for correct operation.

    These are validated at load time (warnings, not errors).
    """

    env: list[str] = Field(default_factory=list, description="Required environment variables.")
    capabilities: list[str] = Field(
        default_factory=list,
        description="Model capabilities the skill needs (e.g. 'fast', 'reasoning').",
    )
    skills: list[str] = Field(
        default_factory=list, description="Other skill IDs this skill depends on."
    )


class SkillManifest(BaseModel):
    """Machine-readable manifest loaded from ``skill.yaml``.

    The manifest complements ``SKILL.md`` with structured metadata that
    ``load_skill()`` uses for directory scanning, dependency tracking,
    and environment validation.
    """

    name: str | None = None
    version: str | None = None
    description: str | None = None

    # Directory overrides (relative to skill root).
    tools: str | None = Field(default=None, description="Path to tools directory.")
    pathways: str | None = Field(default=None, description="Path to pathways directory.")
    agents: str | None = Field(default=None, description="Path to agents directory.")

    requires: SkillRequirements = Field(default_factory=SkillRequirements)
    tags: list[str] = Field(default_factory=list)

    # Standard metadata forwarded to the Skill model.
    id: str | None = None
    author: str | None = None
    tool_patterns: list[str] = Field(default_factory=list)


class Skill(BaseModel):
    """Domain knowledge, pathway references, and script tools for an agent.

    A Skill provides:
    - Domain expertise (what area this covers)
    - Instructions (how to approach problems in this domain)
    - Pathways (composable graphs that accomplish tasks in this domain)
    - Script tools (bundled scripts exposed as callable tools)

    Skills inform behavior, point to pathways, and can register script-backed
    tools. The ScriptToolProvider registers these tools when the skill loads.

    Example:
        research_skill = Skill(
            id="research",
            name="Research",
            description="Deep research on any topic",
            instructions='''
            When researching a topic:
            - For comprehensive research, use the deep_research pathway
            - For quick lookups, use the quick_search pathway
            ''',
            pathways=["deep_research", "quick_search"],
        )

        # Script-based skill example
        sora_skill = Skill(
            id="sora",
            name="Sora Video Generation",
            description="Generate videos with Sora",
            instructions="...",
            scripts_dir=Path("/path/to/sora/scripts"),
            script_tools=[
                ScriptToolDeclaration(
                    id="create",
                    script="scripts/sora.py",
                    default_args=["create"],
                    description="Create a new Sora video",
                ),
            ],
        )
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    id: str
    name: str
    description: str = ""
    instructions: str = ""

    # Pathways that accomplish tasks in this domain
    # These are pathway IDs that can be invoked via pathway.invoke
    pathways: list[str] = Field(default_factory=list)

    # Agents loaded from this skill's agents/ directory.
    agents: list[str] = Field(default_factory=list)

    # Tool patterns this skill implies/recommends (machine-readable curation).
    # Example: ["web.*", "workspace.*", "docs.extract_pdf"].
    # This is guidance/policy metadata; it does not execute anything.
    tool_patterns: list[str] = Field(default_factory=list)

    # Script tools - bundled scripts exposed as callable tools
    # When present, ScriptToolProvider registers these as tools
    scripts_dir: Path | None = None  # Absolute path to skill's scripts/ directory
    script_tools: list[ScriptToolDeclaration] = Field(default_factory=list)

    # Metadata
    tags: set[str] = Field(default_factory=set)
    author: str | None = None
    version: str | None = None

    # Structured requirements (from skill.yaml ``requires:`` block).
    requires: SkillRequirements | None = None

    # Auto-activation triggers.
    # When non-empty, the skill auto-activates if the user query matches
    # any of these conditions (substring/keyword matching).
    use_when: list[str] = Field(
        default_factory=list,
        description="Conditions under which this skill should auto-activate",
    )
    auto_activate: bool = Field(
        default=False,
        description="Always activate this skill (no trigger needed)",
    )

    # Source location (for resolving relative script paths)
    source_dir: Path | None = None

    # Input/output documentation (for understanding, not for execution)
    inputs_schema: dict[str, str] = Field(default_factory=dict)
    outputs_schema: dict[str, str] = Field(default_factory=dict)

    @field_validator("tags", mode="before")
    @classmethod
    def convert_tags_to_set(cls, v: Any) -> set[str]:
        """Convert list to set for tags field."""
        if isinstance(v, list):
            return set(v)
        return v if isinstance(v, set) else set()

    @field_validator("scripts_dir", "source_dir", mode="before")
    @classmethod
    def convert_str_to_path(cls, v: Any) -> Path | None:
        """Convert string to Path."""
        if v is None:
            return None
        if isinstance(v, str):
            return Path(v)
        return v

    @property
    def has_script_tools(self) -> bool:
        """Check if this skill has bundled script tools."""
        return bool(self.script_tools) or bool(self.scripts_dir)

    def get_tool_ids(self) -> list[str]:
        """Get fully-qualified tool IDs for this skill's script tools.

        Returns tool IDs in format: {skill_id}.{tool_id}
        Example: ["sora.create", "sora.poll", "sora.download"]
        """
        return [f"{self.id}.{tool.id}" for tool in self.script_tools]

    def to_prompt_section(self) -> str:
        """Format this skill as a section for an agent's system prompt."""
        parts = [f"### {self.name}"]

        if self.description:
            parts.append(self.description)

        if self.instructions:
            parts.append("")
            parts.append(self.instructions)

        if self.pathways:
            parts.append("")
            parts.append(f"Available pathways: {', '.join(self.pathways)}")
            parts.append('Use: pathway.invoke("<pathway_id>", {...inputs...})')

        if self.tool_patterns:
            parts.append("")
            parts.append(f"Recommended tools: {', '.join(self.tool_patterns)}")

        if self.script_tools:
            parts.append("")
            tool_ids = self.get_tool_ids()
            parts.append(f"Available tools: {', '.join(tool_ids)}")

        return "\n".join(parts)

    @classmethod
    def from_frontmatter(cls, data: dict[str, Any]) -> Self:
        """Create skill from parsed frontmatter dict.

        This handles special frontmatter formats where script_tools need
        to be parsed via from_frontmatter.
        """
        # Parse script tools using their from_frontmatter
        script_tools = [
            ScriptToolDeclaration.from_frontmatter(tool_data)
            for tool_data in data.get("script_tools", [])
        ]

        # Build data dict with parsed script tools
        parsed_data = {
            "id": data["id"],
            "name": data["name"],
            "description": data.get("description", ""),
            "instructions": data.get("instructions", ""),
            "pathways": data.get("pathways", []),
            "tool_patterns": data.get("tool_patterns", []),
            "scripts_dir": data.get("scripts_dir"),
            "script_tools": script_tools,
            "tags": set(data.get("tags", [])),
            "author": data.get("author"),
            "version": data.get("version"),
            "source_dir": data.get("source_dir"),
            "inputs_schema": data.get("inputs_schema", {}),
            "outputs_schema": data.get("outputs_schema", {}),
            "use_when": data.get("use_when", []),
            "auto_activate": data.get("auto_activate", False),
        }
        return cls.model_validate(parsed_data)


def skill(
    id: str,
    name: str,
    description: str = "",
    instructions: str = "",
    pathways: list[str] | None = None,
    script_tools: list[ScriptToolDeclaration] | None = None,
    scripts_dir: Path | str | None = None,
    source_dir: Path | str | None = None,
    tags: set[str] | None = None,
) -> Self:
    """Create a skill (factory function).

    Args:
        id: Unique identifier for the skill
        name: Human-readable name
        description: What this skill covers
        instructions: How to approach problems in this domain
        pathways: List of pathway IDs that accomplish tasks in this domain
        script_tools: Bundled scripts exposed as tools
        scripts_dir: Path to scripts/ directory (for auto-discovery)
        source_dir: Path to skill source directory (for resolving relative paths)
        tags: Optional tags for categorization
    """
    return Skill(
        id=id,
        name=name,
        description=description,
        instructions=instructions,
        pathways=pathways or [],
        script_tools=script_tools or [],
        scripts_dir=Path(scripts_dir) if scripts_dir else None,
        source_dir=Path(source_dir) if source_dir else None,
        tags=tags or set(),
    )


__all__ = [
    # Runtime context
    "RuntimeContext",
    # Script tool declarations
    "ScriptToolDeclaration",
    "ScriptToolParam",
    # Skill
    "Skill",
    "SkillManifest",
    "SkillRequirements",
    # Factory function
    "skill",
]
