"""Subagent loader for app.

Loads custom subagent definitions from the filesystem. Subagents are defined
as markdown files with YAML frontmatter in the agents/ directory.

Directory structure:
    .deepagents/agents/{agent_name}/AGENTS.md

Example file (researcher/AGENTS.md):
    ---
    name: researcher
    description: Research topics on the web before writing content
    model: anthropic:claude-haiku-4-5-20251001
    ---

    You are a research assistant with access to web search.

    ## Your Process
    1. Search for relevant information
    2. Summarize findings clearly
"""

from __future__ import annotations

import logging
import re
from typing import TYPE_CHECKING, TypedDict

import yaml

if TYPE_CHECKING:
    from pathlib import Path

logger = logging.getLogger(__name__)


class SubagentMetadata(TypedDict):
    """Metadata for a custom subagent loaded from filesystem."""

    name: str
    """Unique identifier for the subagent, used with the task tool."""

    description: str
    """What this subagent does. Main agent uses this to decide when to delegate."""

    system_prompt: str
    """Instructions for the subagent (body of the markdown file)."""

    model: str | None
    """Optional model override in 'provider:model-name' format."""

    source: str
    """Where this subagent was loaded from ('user' or 'project')."""

    path: str
    """Absolute path to the subagent definition file."""


def _parse_subagent_file(file_path: Path) -> SubagentMetadata | None:
    """Parse a subagent markdown file with YAML frontmatter.

    The file must have YAML frontmatter (delimited by ---) containing at minimum
    'name' and 'description' fields. The body of the file becomes the system_prompt.

    Args:
        file_path: Path to the markdown file.

    Returns:
        SubagentMetadata if parsing succeeds, None otherwise.
    """
    try:
        content = file_path.read_text(encoding="utf-8")
    except OSError as exc:
        logger.warning("Skipping subagent %s: could not read file (%s)", file_path, exc)
        return None

    # Extract YAML frontmatter (--- delimited)
    match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", content, re.DOTALL)
    if not match:
        logger.warning(
            "Skipping subagent %s: missing YAML frontmatter. The file must start "
            "with a '---' delimited block containing 'name' and 'description'.",
            file_path,
        )
        return None

    try:
        frontmatter = yaml.safe_load(match.group(1))
    except yaml.YAMLError as exc:
        logger.warning(
            "Skipping subagent %s: invalid YAML frontmatter (%s)", file_path, exc
        )
        return None

    # Validate frontmatter structure and required fields
    if not isinstance(frontmatter, dict):
        logger.warning(
            "Skipping subagent %s: frontmatter must be a mapping with 'name' and "
            "'description' fields.",
            file_path,
        )
        return None

    name = frontmatter.get("name")
    description = frontmatter.get("description")
    model = frontmatter.get("model")

    # Validate types: name and description must be non-empty strings
    # model is optional but must be string if present
    name_valid = isinstance(name, str) and name
    description_valid = isinstance(description, str) and description
    model_valid = model is None or isinstance(model, str)

    if not (name_valid and description_valid and model_valid):
        invalid_fields: list[str] = []
        if not name_valid:
            invalid_fields.append("name (non-empty string required)")
        if not description_valid:
            invalid_fields.append("description (non-empty string required)")
        if not model_valid:
            invalid_fields.append("model (string required when present)")
        logger.warning(
            "Skipping subagent %s: invalid or missing frontmatter field(s): %s",
            file_path,
            ", ".join(invalid_fields),
        )
        return None

    return {
        "name": name,
        "description": description,
        "system_prompt": match.group(2).strip(),
        "model": model,
        "source": "",  # Set by caller
        "path": str(file_path),
    }


def _load_subagents_from_dir(
    agents_dir: Path, source: str
) -> dict[str, SubagentMetadata]:
    """Load subagents from a directory.

    Expects structure: agents_dir/{subagent_name}/AGENTS.md

    Args:
        agents_dir: Directory containing subagent folders.
        source: Source identifier ('user' or 'project').

    Returns:
        Dict mapping subagent name to metadata.
    """
    subagents: dict[str, SubagentMetadata] = {}

    if not agents_dir.exists() or not agents_dir.is_dir():
        return subagents

    for entry in agents_dir.iterdir():
        if not entry.is_dir():
            # A stray file directly under agents/ is a common mistake: subagents
            # must live at agents/{name}/AGENTS.md, not agents/{name}.md.
            if entry.suffix.lower() == ".md":
                logger.warning(
                    "Ignoring %s subagent file %s: subagents must be defined at "
                    "%s/{subagent-name}/AGENTS.md, not as a file directly in the "
                    "agents directory.",
                    source,
                    entry,
                    agents_dir,
                )
            continue

        # Look for {folder_name}/AGENTS.md
        subagent_file = entry / "AGENTS.md"
        if not subagent_file.exists():
            # The folder exists but holds a differently-named markdown file
            # (e.g. agent.md or {name}.md) instead of the required AGENTS.md.
            stray_md = [p.name for p in entry.glob("*.md")]
            if stray_md:
                logger.warning(
                    "Ignoring %s subagent folder %s: expected an AGENTS.md file "
                    "but found %s. Rename the definition to AGENTS.md.",
                    source,
                    entry,
                    ", ".join(sorted(stray_md)),
                )
            continue

        subagent = _parse_subagent_file(subagent_file)
        if subagent:
            subagent["source"] = source
            # The folder name and the frontmatter `name` are independent, so two
            # folders can declare the same `name` and silently collapse to one
            # entry. Iteration order is filesystem-dependent, so warn rather than
            # let a definition vanish without explanation.
            existing = subagents.get(subagent["name"])
            if existing is not None:
                logger.warning(
                    "Subagent name collision in %s: %s and %s both declare "
                    "name=%r. Using %s; give each subagent a unique 'name' in "
                    "its frontmatter.",
                    agents_dir,
                    existing["path"],
                    subagent["path"],
                    subagent["name"],
                    subagent["path"],
                )
            subagents[subagent["name"]] = subagent

    return subagents


def list_subagents(
    *,
    user_agents_dir: Path | None = None,
    project_agents_dir: Path | None = None,
) -> list[SubagentMetadata]:
    """List subagents from user and/or project directories.

    Scans for subagent definitions in the provided directories.
    Project subagents override user subagents with the same name.

    Args:
        user_agents_dir: Path to user-level agents directory.
        project_agents_dir: Path to project-level agents directory.

    Returns:
        List of subagent metadata, with project subagents taking precedence.
    """
    all_subagents: dict[str, SubagentMetadata] = {}

    # Load user subagents first (lower priority)
    if user_agents_dir is not None:
        all_subagents.update(_load_subagents_from_dir(user_agents_dir, "user"))

    # Load project subagents second (override user)
    if project_agents_dir is not None:
        all_subagents.update(_load_subagents_from_dir(project_agents_dir, "project"))

    return list(all_subagents.values())
