"""Middleware for loading and exposing agent skills to the system prompt.

This middleware implements Anthropic's "Agent Skills" pattern with progressive disclosure:
1. Parse YAML frontmatter from SKILL.md files at session start
2. Inject skills metadata (name + description) into system prompt
3. Agent reads full SKILL.md content when relevant to a task

Skills directory structure (per-agent + project):
User-level: ~/.deepagents/{AGENT_NAME}/skills/
Project-level: {PROJECT_ROOT}/.deepagents/skills/

Example structure:
~/.deepagents/{AGENT_NAME}/skills/
├── web-research/
│   ├── SKILL.md        # Required: YAML frontmatter + instructions
│   └── helper.py       # Optional: supporting files
├── code-review/
│   ├── SKILL.md
│   └── checklist.md

.deepagents/skills/
├── project-specific/
│   └── SKILL.md        # Project-specific skills
"""

from collections.abc import Awaitable, Callable
from pathlib import Path
from typing import NotRequired, TypedDict, cast

from langchain.agents.middleware.types import (
    AgentMiddleware,
    AgentState,
    ModelRequest,
    ModelResponse,
)
from langgraph.runtime import Runtime

from deepagents.backends.protocol import BackendFactory, BackendProtocol
from deepagents.realtimex_middleware.skills.load import SkillMetadata, list_skills


class SkillsState(AgentState):
    """State for the skills middleware."""

    skills_metadata: NotRequired[list[SkillMetadata]]
    """List of loaded skill metadata (name, description, path)."""


class SkillsStateUpdate(TypedDict):
    """State update for the skills middleware."""

    skills_metadata: list[SkillMetadata]
    """List of loaded skill metadata (name, description, path)."""


# Skills Directory Context
SKILLS_DIRECTORY_CONTEXT = """
### Skills Directory

Your skills are stored in the following locations:
{skills_locations_detail}

Skills may contain scripts or supporting files. When executing skill scripts:
- Use the absolute paths shown in the skills list below
- Example: If a skill is at `/path/to/skills/web-research/SKILL.md`, scripts are in `/path/to/skills/web-research/`
"""

# Skills System Documentation
SKILLS_SYSTEM_PROMPT = """

## Skills System

You have access to a skills library that provides specialized capabilities and domain knowledge.

{skills_locations}

**Available Skills:**

{skills_list}

**How to Use Skills (Progressive Disclosure):**

Skills follow a **progressive disclosure** pattern - you know they exist (name + description above), but you only read the full instructions when needed:

1. **Recognize when a skill applies**: Check if the user's task matches any skill's description
2. **Read the skill's full instructions**: The skill list above shows the exact path to use with read_file
3. **Follow the skill's instructions**: SKILL.md contains step-by-step workflows, best practices, and examples
4. **Access supporting files**: Skills may include Python scripts, configs, or reference docs - use absolute paths

**When to Use Skills (IMPORTANT - Check First):**
- **BEFORE starting any new task**: Review the available skills list above to see if a skill matches the task domain
- **When the user's request matches a skill's domain** (e.g., "research X" → web-research skill)
- When you need specialized knowledge or structured workflows that a skill might provide
- When a skill provides proven patterns for complex tasks

**Skills-First Task Pattern:**
1. User requests a task → Review the skills list above
2. If a skill matches the domain → Read the full SKILL.md using the path shown
3. Follow the skill's workflow and instructions
4. If no skill matches → Proceed with general capabilities

**Skills are Self-Documenting:**
- Each SKILL.md tells you exactly what the skill does and how to use it
- The skill list above shows the full path for each skill's SKILL.md file

**Executing Skill Scripts:**
Skills may contain Python scripts or other executable files. Always use absolute paths from the skill list.

**Example Workflow:**

User: "Can you research the latest developments in quantum computing?"

1. Check available skills above → See "web-research" skill with its full path
2. Read the skill using the path shown in the list
3. Follow the skill's research workflow (search → organize → synthesize)
4. Use any helper scripts with absolute paths

Remember: Skills provide specialized expertise - when a skill exists for the task, use it! When in doubt, check if a skill exists for the task domain.
"""  # noqa: E501


class SkillsMiddleware(AgentMiddleware):
    """Middleware for loading and exposing agent skills.

    This middleware implements Anthropic's agent skills pattern:
    - Loads skills metadata (name, description) from YAML frontmatter at session start
    - Injects skills list into system prompt for discoverability
    - Agent reads full SKILL.md content when a skill is relevant (progressive disclosure)

    Supports both user-level and project-level skills:
    - User skills: ~/.deepagents/{AGENT_NAME}/skills/
    - Project skills: {PROJECT_ROOT}/.deepagents/skills/
    - Project skills override user skills with the same name

    Args:
        skills_dir: Path to the user-level skills directory (per-agent).
        assistant_id: The agent identifier for path references in prompts.
        project_skills_dir: Optional path to project-level skills directory.
    """

    state_schema = SkillsState

    def __init__(  # noqa: D417
        self,
        *,
        skills_dir: str | Path | None = None,
        assistant_id: str,
        project_skills_dir: str | Path | None = None,
        backend: BackendProtocol | BackendFactory | None = None,
    ) -> None:
        """Initialize the skills middleware.

        Args:
            skills_dir: Path to the user-level (global) skills directory. Optional if project_skills_dir is provided.
            assistant_id: The agent identifier.
            project_skills_dir: Path to the project-level skills directory. Optional if skills_dir is provided.
        """
        self.skills_dir = Path(skills_dir).expanduser() if skills_dir else None
        self.assistant_id = assistant_id
        self.project_skills_dir = Path(project_skills_dir).expanduser() if project_skills_dir else None
        self.backend_or_factory = backend
        # Store display paths for prompts (adapted to provided skills root)
        self.user_skills_display = str(self.skills_dir) if self.skills_dir else None
        self.system_prompt_template = SKILLS_SYSTEM_PROMPT

    def _resolve_backend(self, runtime: Runtime) -> BackendProtocol | None:
        """Resolve backend from instance or factory."""
        if callable(self.backend_or_factory):
            try:
                return self.backend_or_factory(runtime)
            except Exception:  # noqa: BLE001
                return None
        return self.backend_or_factory

    def _format_skills_locations(self) -> str:
        """Format skills locations for display in system prompt."""
        locations = []
        if self.user_skills_display:
            locations.append(f"**Global Skills**: `{self.user_skills_display}`")
        if self.project_skills_dir:
            override_note = " (overrides global skills)" if self.user_skills_display else ""
            locations.append(f"**Workspace Skills**: `{self.project_skills_dir}`{override_note}")
        return "\n".join(locations)

    def _format_skills_locations_detail(self) -> str:
        """Format detailed skills locations for the Skills Directory section."""
        locations = []
        if self.user_skills_display:
            locations.append(f"- **Global Skills**: `{self.user_skills_display}`")
        if self.project_skills_dir:
            override_note = " (overrides global skills)" if self.user_skills_display else ""
            locations.append(f"- **Workspace Skills**: `{self.project_skills_dir}`{override_note}")

        if not locations:
            return "- No skills directories configured"
        return "\n".join(locations)

    def _format_skills_list(self, skills: list[SkillMetadata]) -> str:
        """Format skills metadata for display in system prompt."""
        if not skills:
            locations = []
            if self.user_skills_display:
                locations.append(f"{self.user_skills_display}/")
            if self.project_skills_dir:
                locations.append(f"{self.project_skills_dir}/")
            if not locations:
                return "(No skills directories configured.)"
            return f"(No skills available yet. You can create skills in {' or '.join(locations)})"

        # Group skills by source
        user_skills = [s for s in skills if s["source"] == "user"]
        project_skills = [s for s in skills if s["source"] == "project"]

        lines = []

        # Show user skills
        if user_skills:
            lines.append("**Global Skills:**")
            for skill in user_skills:
                lines.append(f"- **{skill['name']}**: {skill['description']}")
                lines.append(f"  → Read `{skill['path']}` for full instructions")
            lines.append("")

        # Show project skills
        if project_skills:
            lines.append("**Workspace Skills:**")
            for skill in project_skills:
                lines.append(f"- **{skill['name']}**: {skill['description']}")
                lines.append(f"  → Read `{skill['path']}` for full instructions")

        return "\n".join(lines)

    def before_agent(self, state: SkillsState, runtime: Runtime) -> SkillsStateUpdate | None:  # noqa: ARG002
        """Load skills metadata before agent execution.

        This runs once at session start to discover available skills from both
        user-level and project-level directories.

        Args:
            state: Current agent state.
            runtime: Runtime context.

        Returns:
            Updated state with skills_metadata populated.
        """
        # We re-load skills on every new interaction with the agent to capture
        # any changes in the skills directories.
        backend = self._resolve_backend(runtime)
        skills = list_skills(
            user_skills_dir=self.skills_dir,
            project_skills_dir=self.project_skills_dir,
            backend=backend,
        )
        return SkillsStateUpdate(skills_metadata=skills)

    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        """Inject skills documentation into the system prompt.

        This runs on every model call to ensure skills info is always available.

        Args:
            request: The model request being processed.
            handler: The handler function to call with the modified request.

        Returns:
            The model response from the handler.
        """
        # Get skills metadata from state
        skills_metadata = request.state.get("skills_metadata", [])

        # Format skills locations and list
        skills_locations = self._format_skills_locations()
        skills_locations_detail = self._format_skills_locations_detail()
        skills_list = self._format_skills_list(skills_metadata)

        # Format the skills directory context
        directory_section = SKILLS_DIRECTORY_CONTEXT.format(
            skills_locations_detail=skills_locations_detail,
        )

        # Format the skills documentation
        skills_section = self.system_prompt_template.format(
            skills_locations=skills_locations,
            skills_list=skills_list,
        )

        # Combine directory context + skills system prompt
        full_skills_prompt = directory_section + "\n" + skills_section

        if request.system_prompt:  # noqa: SIM108
            system_prompt = request.system_prompt + "\n\n" + full_skills_prompt
        else:
            system_prompt = full_skills_prompt

        return handler(request.override(system_prompt=system_prompt))

    async def awrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
    ) -> ModelResponse:
        """(async) Inject skills documentation into the system prompt.

        Args:
            request: The model request being processed.
            handler: The handler function to call with the modified request.

        Returns:
            The model response from the handler.
        """
        # The state is guaranteed to be SkillsState due to state_schema
        state = cast("SkillsState", request.state)
        skills_metadata = state.get("skills_metadata", [])

        # Format skills locations and list
        skills_locations = self._format_skills_locations()
        skills_locations_detail = self._format_skills_locations_detail()
        skills_list = self._format_skills_list(skills_metadata)

        # Format the skills directory context
        directory_section = SKILLS_DIRECTORY_CONTEXT.format(
            skills_locations_detail=skills_locations_detail,
        )

        # Format the skills documentation
        skills_section = self.system_prompt_template.format(
            skills_locations=skills_locations,
            skills_list=skills_list,
        )

        # Combine directory context + skills system prompt
        full_skills_prompt = directory_section + "\n" + skills_section

        # Inject into system prompt
        if request.system_prompt:  # noqa: SIM108
            system_prompt = request.system_prompt + "\n\n" + full_skills_prompt
        else:
            system_prompt = full_skills_prompt

        return await handler(request.override(system_prompt=system_prompt))
