"""Skill Loader - Discover and load skills from filesystem directories.

Skills are domain knowledge that inform agent behavior. They contain:
- Instructions on how to approach problems in a domain
- Pathways that accomplish tasks in this domain
- Script tools (bundled scripts that become callable tools)

Hierarchy:
    Skill (knowledge + pathway references + script tools)
       └──► Pathway (computation graph)
       │       └──► Nodes (computation units)
       │              └──► Tools (via ToolNode)
       └──► ScriptTool (bundled scripts exposed as tools)

Script-based skills (sora, speech, screenshot, etc.) can bundle executable
scripts in a scripts/ directory. These are auto-discovered or explicitly
declared in the SKILL.md frontmatter:

    ---
    name: sora
    tools:
      - id: create
        script: scripts/sora.py
        args: [create]
        description: Create a new video
    ---

Supports:
- Agent Skills format (agentskills.io): folders with SKILL.md files
- Script auto-discovery from scripts/ directory
- Explicit tool declarations in frontmatter
- Custom skill directories
- Dynamic skill discovery

Usage::

    from core.skills.loader import load_skills_from_directory

    skills = load_skills_from_directory("/path/to/skills")
    for skill in skills:
        agent.add_skill(skill)
        # skill.script_tools contains any bundled script tools
"""

import logging
from pathlib import Path
from typing import Any

import yaml

from core.skills.models import ScriptToolDeclaration, Skill, SkillManifest

logger = logging.getLogger(__name__)


# =============================================================================
# Script Detection
# =============================================================================

# Supported script extensions and their interpreters
SCRIPT_INTERPRETERS: dict[str, str] = {
    ".py": "python3",
    ".swift": "swift",
    ".sh": "bash",
    ".bash": "bash",
    ".zsh": "zsh",
    ".ps1": "powershell",
    ".js": "node",
    ".ts": "npx ts-node",
}


def _detect_interpreter(script_path: Path) -> str | None:
    """Detect the interpreter for a script based on its extension."""
    return SCRIPT_INTERPRETERS.get(script_path.suffix.lower())


def _discover_scripts(scripts_dir: Path) -> list[ScriptToolDeclaration]:
    """Auto-discover scripts in a directory and create tool declarations.

    Creates one tool declaration per script file found.
    Tool ID is derived from the script filename (without extension).

    Example:
        scripts/
            sora.py      → tool: "run_sora"
            helper.sh    → tool: "run_helper"
    """
    if not scripts_dir.exists() or not scripts_dir.is_dir():
        return []

    tools = []
    for script_path in scripts_dir.iterdir():
        if script_path.is_file() and script_path.suffix.lower() in SCRIPT_INTERPRETERS:
            # Create a generic "run" tool for this script
            tool_id = f"run_{script_path.stem}"
            rel_path = f"scripts/{script_path.name}"

            tools.append(
                ScriptToolDeclaration(
                    id=tool_id,
                    script=rel_path,
                    description=f"Run {script_path.name}",
                    interpreter=_detect_interpreter(script_path),
                ),
            )
            logger.debug(f"Auto-discovered script tool: {tool_id} -> {rel_path}")

    return tools


def _auto_register_tools(tools_dir: Path, namespace: str) -> int:
    """Load Python tool scripts from *tools_dir* and register them globally.

    Uses the same load → register pipeline as ``stdlib/bootstrap.py``.
    Idempotent: re-registering an existing tool name overwrites quietly.

    Parameters
    ----------
    tools_dir
        Absolute path to a skill's ``tools/`` directory.
    namespace
        Prefix for tool names (typically the skill ID).

    Returns
    -------
    int
        Number of tools registered.
    """
    from core.tools import get_tool_registry
    from infrastructure.tools.script_tool_loader import load_tools_from_directory

    registry = get_tool_registry()
    results = load_tools_from_directory(tools_dir)
    registered = 0

    for raw_id, schema, handler in results:
        tool_id = f"{namespace}.{raw_id}"
        try:
            registry.register(
                name=tool_id,
                handler=handler,
                description=schema.description or f"Tool: {tool_id}",
                parameters=schema.input_schema,
                skill_id=namespace,
            )
            registered += 1
        except Exception as e:
            logger.warning("Failed to register tool %s: %s", tool_id, e)

    if registered:
        logger.info(
            "Auto-registered %d tools from %s (namespace=%s)", registered, tools_dir, namespace
        )

    return registered


def _load_skill_pathways(pathways_dir: Path, skill_id: str) -> list[str]:
    """Load pathways from a skill's ``pathways/`` directory.

    Each ``.py`` or ``.yaml`` file is loaded via the shared workspace
    pathway loader and deployed into the workspace pathway registry.

    Parameters
    ----------
    pathways_dir
        Absolute path to the skill's ``pathways/`` directory.
    skill_id
        The owning skill's ID (for logging and namespacing).

    Returns
    -------
    list[str]
        Pathway IDs that were successfully loaded.
    """
    from core.workspace import load_pathway_from_file

    loaded_ids: list[str] = []

    for f in sorted(pathways_dir.iterdir()):
        if not f.is_file() or f.name.startswith((".", "_")):
            continue
        if f.suffix not in (".py", ".yaml", ".yml"):
            continue

        pathway = load_pathway_from_file(f)
        if pathway is None:
            continue

        try:
            # Deploy into the workspace pathway registry so it's available
            # for the runner / Session to resolve.
            from core.workspace import _get_workspace_pathway_registry

            registry = _get_workspace_pathway_registry()
            registry.deploy(pathway, source=f"skill:{skill_id}")
            loaded_ids.append(pathway.id)
            logger.info("Loaded pathway %s from skill %s (%s)", pathway.id, skill_id, f.name)
        except Exception as e:
            logger.warning("Failed to deploy pathway from %s (skill %s): %s", f, skill_id, e)

    return loaded_ids


def _load_skill_agents(agents_dir: Path, skill_id: str) -> list[str]:
    """Load agent definitions from a skill's ``agents/`` directory.

    Each ``.py`` file should export an ``agent`` variable (an
    :class:`AgentDefinition`) or a ``build()`` callable that returns one.
    Loaded definitions are registered in the global
    :class:`AgentRepository`.

    Parameters
    ----------
    agents_dir
        Absolute path to the skill's ``agents/`` directory.
    skill_id
        The owning skill's ID (for logging).

    Returns
    -------
    list[str]
        Agent IDs that were successfully registered.
    """
    from core.agent import get_agent_repository
    from core.workspace import _load_agent_from_python

    repo = get_agent_repository()
    loaded_ids: list[str] = []

    for f in sorted(agents_dir.iterdir()):
        if not f.is_file() or f.name.startswith((".", "_")):
            continue
        if f.suffix != ".py":
            continue

        defn = _load_agent_from_python(f)
        if defn is None:
            continue

        try:
            import asyncio

            # register() is async; drive it synchronously.
            try:
                loop = asyncio.get_running_loop()
            except RuntimeError:
                loop = None

            if loop is not None:
                from concurrent.futures import ThreadPoolExecutor

                with ThreadPoolExecutor(max_workers=1) as pool:
                    pool.submit(asyncio.run, repo.save(defn)).result()
            else:
                asyncio.run(repo.save(defn))

            loaded_ids.append(defn.id)
            logger.info("Loaded agent %s from skill %s (%s)", defn.id, skill_id, f.name)
        except Exception as e:
            logger.warning("Failed to register agent from %s (skill %s): %s", f, skill_id, e)

    return loaded_ids


def parse_frontmatter(content: str) -> dict[str, Any]:
    """Parse YAML frontmatter from SKILL.md content.

    Expected format:
        ---
        name: Skill Name
        description: What this skill does
        tools:
          - id: create
            script: scripts/sora.py
        ---
        # Skill Instructions
        ...

    Supports nested structures for tools declarations.
    """
    frontmatter: dict[str, Any] = {}

    # Check for frontmatter delimiter
    if not content.startswith("---"):
        return frontmatter

    # Find end of frontmatter
    lines = content.split("\n")
    end_idx = None
    for i, line in enumerate(lines[1:], start=1):
        if line.strip() == "---":
            end_idx = i
            break

    if end_idx is None:
        return frontmatter

    # Parse YAML frontmatter
    yaml_content = "\n".join(lines[1:end_idx])
    try:
        parsed = yaml.safe_load(yaml_content)
        if isinstance(parsed, dict):
            return parsed
    except Exception as e:
        logger.debug(f"YAML parse failed, using simple parser: {e}")

    # Simple fallback parser for basic key: value pairs
    for line in lines[1:end_idx]:
        if ":" in line and not line.startswith(" ") and not line.startswith("\t"):
            key, value = line.split(":", 1)
            key = key.strip()
            value = value.strip().strip('"').strip("'")
            frontmatter[key] = value

    return frontmatter


def _parse_tools_from_frontmatter(
    frontmatter: dict[str, Any],
    skill_dir: Path,
) -> list[ScriptToolDeclaration]:
    """Parse tool declarations from frontmatter.

    Supports format:
        tools:
          - id: create
            script: scripts/sora.py
            args: [create]
            description: Create a new video
            params:
              prompt: {type: string, required: true}
    """
    tools_data = frontmatter.get("tools", [])
    if not tools_data or not isinstance(tools_data, list):
        return []

    tools = []
    for tool_data in tools_data:
        if not isinstance(tool_data, dict):
            continue

        # Validate required fields
        tool_id = tool_data.get("id")
        script = tool_data.get("script")
        if not tool_id or not script:
            logger.warning(f"Skipping tool with missing id or script: {tool_data}")
            continue

        # Verify script exists
        script_path = skill_dir / script
        if not script_path.exists():
            logger.warning(f"Script not found: {script_path}")
            continue

        tool = ScriptToolDeclaration.from_frontmatter(tool_data)

        # Auto-detect interpreter if not specified
        if not tool.interpreter:
            tool.interpreter = _detect_interpreter(script_path)

        tools.append(tool)
        logger.debug(f"Parsed tool declaration: {tool_id} -> {script}")

    return tools


def extract_skill_instructions(content: str) -> str:
    """Extract skill instructions from SKILL.md (content after frontmatter)."""
    # Remove frontmatter if present
    if content.startswith("---"):
        lines = content.split("\n")
        end_idx = None
        for i, line in enumerate(lines[1:], start=1):
            if line.strip() == "---":
                end_idx = i
                break
        if end_idx is not None:
            content = "\n".join(lines[end_idx + 1 :])

    return content.strip()


def _parse_list_field(value: str | list | None) -> list[str]:
    """Parse a field that could be a list or comma-separated string."""
    if not value:
        return []
    if isinstance(value, list):
        return value
    # Handle empty array notation from YAML
    if value.strip() == "[]":
        return []
    # Parse comma-separated string
    return [item.strip() for item in value.split(",") if item.strip()]


def _load_skill_manifest(skill_dir: Path) -> dict[str, Any]:
    """Load optional machine-readable skill manifest.

    Supports:
    - skill.yaml
    - skill.yml

    The manifest is optional and is intended for stable, machine-consumable metadata
    (e.g., tool patterns, default pathways). SKILL.md remains the primary human-facing
    instruction source.
    """
    for name in ("skill.yaml", "skill.yml"):
        p = skill_dir / name
        if not p.exists():
            continue
        try:
            raw = p.read_text(encoding="utf-8")
            data = yaml.safe_load(raw)
            return data if isinstance(data, dict) else {}
        except Exception as e:
            logger.warning("Failed to load %s for skill %s: %s", name, skill_dir.name, e)
            return {}
    return {}


def _parse_manifest_model(raw: dict[str, Any]) -> SkillManifest:
    """Parse a raw manifest dict into a :class:`SkillManifest`.

    Returns a default (empty) manifest when *raw* is empty or invalid.
    """
    if not raw:
        return SkillManifest()
    try:
        return SkillManifest.model_validate(raw)
    except Exception as e:
        logger.warning("Invalid skill.yaml content, using defaults: %s", e)
        return SkillManifest()


_META_SKILL_DEFAULTS: dict[str, tuple[str, str]] = {
    "skill-meta": ("skill", "Skill Management"),
    "tool-meta": ("tool", "Tool Management"),
}


def load_skill_from_directory(skill_dir: Path) -> Skill | None:
    """Load a single skill from a directory.

    Supports two skill formats:

    1. Builtin skills (stdlib/skills/_builtin/):
        skill_dir/
            SKILL.md (required)
            tools/     (Python tools - loaded by bootstrap.py)
                search.py
                fetch.py

    2. External skills (agentskills.io format):
        skill_dir/
            SKILL.md (required)
            scripts/   (shell/Python scripts - loaded by ScriptToolProvider)
                sora.py
                helper.sh
            references/ (optional - supporting docs)

    For builtin skills: tools/ Python files are loaded as regular tools by bootstrap.
    For external skills: scripts/ are wrapped by ScriptToolProvider for execution.
    """
    skill_md_path = skill_dir / "SKILL.md"

    if not skill_md_path.exists():
        logger.warning(f"No SKILL.md found in {skill_dir}")
        return None

    try:
        manifest = _load_skill_manifest(skill_dir)
        content = skill_md_path.read_text(encoding="utf-8")
        frontmatter = parse_frontmatter(content)
        instructions = extract_skill_instructions(content)

        # Get skill metadata — meta skill name overrides via dispatch table
        raw_name = skill_dir.name
        if raw_name in _META_SKILL_DEFAULTS:
            default_id, default_name = _META_SKILL_DEFAULTS[raw_name]
            skill_id = default_id
            skill_name = frontmatter.get("name") or default_name
        else:
            skill_id = frontmatter.get("id") or manifest.get("id") or raw_name
            skill_name = (
                frontmatter.get("name")
                or manifest.get("name")
                or raw_name.replace("-", " ").title()
            )

        description = (
            frontmatter.get("description")
            or manifest.get("description")
            or (instructions[:100] + "..." if len(instructions) > 100 else instructions)
        )

        # Parse pathways
        pathways = _parse_list_field(frontmatter.get("pathways") or manifest.get("pathways"))

        # Parse tool patterns (machine-readable curation)
        tool_patterns = _parse_list_field(
            frontmatter.get("tool_patterns") or manifest.get("tool_patterns"),
        )

        # Parse tags
        tags_raw = frontmatter.get("tags", "") or manifest.get("tags", "")
        tags = set(_parse_list_field(tags_raw)) if tags_raw else set()

        # =================================================================
        # Script Tools - for external skills with scripts/ directory
        # =================================================================
        # Note: Builtin skills use tools/ which are loaded by bootstrap.py
        # into the global ToolRegistry. We only process scripts/ for external
        # skills that need ScriptToolProvider.
        script_tools: list[ScriptToolDeclaration] = []
        scripts_dir: Path | None = None

        # Check for scripts/ directory (external skill format)
        potential_scripts_dir = skill_dir / "scripts"
        if potential_scripts_dir.exists() and potential_scripts_dir.is_dir():
            scripts_dir = potential_scripts_dir

        # Check for tools/ directory and auto-register into the global
        # ToolRegistry.  Bootstrap already handles stdlib tools, and
        # register() is idempotent, so double-loading is harmless.
        tools_dir = skill_dir / "tools"
        has_builtin_tools = tools_dir.exists() and tools_dir.is_dir()

        if has_builtin_tools:
            try:
                _auto_register_tools(tools_dir, skill_id)
            except Exception as e:
                logger.warning("Failed to auto-register tools for skill %s: %s", skill_id, e)

        # 1. Try explicit tool declarations in frontmatter
        script_tools = _parse_tools_from_frontmatter(frontmatter, skill_dir)

        # 2. If no explicit tools but scripts/ exists, auto-discover
        # (only for external skills, not builtin)
        if not script_tools and scripts_dir and not has_builtin_tools:
            script_tools = _discover_scripts(scripts_dir)
            if script_tools:
                logger.info(f"Auto-discovered {len(script_tools)} script tools in {skill_id}")

        # Log script tool info
        if script_tools:
            tool_ids = [t.id for t in script_tools]
            logger.debug(f"Skill {skill_id} has script tools: {tool_ids}")

        # =================================================================
        # Pathways - load pathways/ directory inside the skill
        # =================================================================
        pathways_dir = skill_dir / "pathways"
        if pathways_dir.exists() and pathways_dir.is_dir():
            discovered_pathways = _load_skill_pathways(pathways_dir, skill_id)
            # Merge discovered pathway IDs with any declared in frontmatter.
            existing = set(pathways)
            for pid in discovered_pathways:
                if pid not in existing:
                    pathways.append(pid)

        # =================================================================
        # Agents - load agents/ directory inside the skill
        # =================================================================
        agents: list[str] = []
        agents_dir = skill_dir / "agents"
        if agents_dir.exists() and agents_dir.is_dir():
            agents = _load_skill_agents(agents_dir, skill_id)

        # =================================================================
        # Manifest requirements — validate env vars
        # =================================================================
        parsed_manifest = _parse_manifest_model(manifest)
        if parsed_manifest.requires.env:
            import os

            for var in parsed_manifest.requires.env:
                if var not in os.environ:
                    logger.warning("Skill %s requires env var %s but it is not set", skill_id, var)

        # Create skill with all components
        return Skill(
            id=skill_id,
            name=skill_name,
            description=description,
            instructions=instructions,
            pathways=pathways,
            agents=agents,
            tool_patterns=tool_patterns,
            scripts_dir=scripts_dir,
            script_tools=script_tools,
            source_dir=skill_dir,
            tags=tags,
            author=frontmatter.get("author") or parsed_manifest.author,
            version=frontmatter.get("version") or parsed_manifest.version,
            requires=parsed_manifest.requires
            if parsed_manifest.requires.env
            or parsed_manifest.requires.capabilities
            or parsed_manifest.requires.skills
            else None,
        )

    except Exception as e:
        logger.exception(f"Failed to load skill from {skill_dir}: {e}")
        return None


def load_skills_from_directory(
    skills_dir: str | Path,
    *,
    recursive: bool = True,
) -> list[Skill]:
    """Discover and load all skills from a directory.

    Parameters
    ----------
    skills_dir
        Path to directory containing skill folders.
    recursive
        If True, search subdirectories (default: True).

    Returns
    -------
    list[Skill]
        Loaded Skill objects.
    """
    skills_dir = Path(skills_dir)

    if not skills_dir.exists():
        logger.warning(f"Skills directory does not exist: {skills_dir}")
        return []

    if not skills_dir.is_dir():
        logger.warning(f"Not a directory: {skills_dir}")
        return []

    loaded_skills: list[Skill] = []

    # Ensure absolute path for consistent path handling
    skills_dir = skills_dir.resolve()

    # Find all directories that might contain skills
    if recursive:
        # Look for any directory with SKILL.md
        for skill_md in skills_dir.rglob("SKILL.md"):
            skill_dir = skill_md.parent.resolve()  # Ensure absolute path
            loaded_skill = load_skill_from_directory(skill_dir)
            if loaded_skill:
                loaded_skills.append(loaded_skill)
    else:
        # Only check immediate subdirectories
        for item in skills_dir.iterdir():
            if item.is_dir() and not item.name.startswith("__"):
                loaded_skill = load_skill_from_directory(item)
                if loaded_skill:
                    loaded_skills.append(loaded_skill)

    logger.info(f"Loaded {len(loaded_skills)} skills from {skills_dir}")
    return loaded_skills


def load_agent_skills_from_directories(
    *skill_directories: str | Path,
) -> list[Skill]:
    """Load skills from multiple directories.

    Usage::

        skills = load_agent_skills_from_directories(
            "/path/to/agentskills/skills",
            "/path/to/custom/skills",
        )
    """
    all_skills: list[Skill] = []

    for skill_dir in skill_directories:
        skills = load_skills_from_directory(skill_dir)
        all_skills.extend(skills)

    return all_skills


__all__ = [
    # Script detection
    "SCRIPT_INTERPRETERS",
    "extract_skill_instructions",
    "load_agent_skills_from_directories",
    # Main loading functions
    "load_skill_from_directory",
    "load_skills_from_directory",
    # Parsing utilities
    "parse_frontmatter",
]
