#!/usr/bin/env python3
"""
Skills Tool Module

This module provides tools for listing and viewing skill documents.
Skills are organized as directories containing a SKILL.md file (the main instructions)
and optional supporting files like references, templates, and examples.

Inspired by Anthropic's Claude Skills system with progressive disclosure architecture:
- Metadata (name ≤64 chars, description ≤1024 chars) - shown in skills_list
- Full Instructions - loaded via skill_view when needed
- Linked Files (references, templates) - loaded on demand

Directory Structure:
    skills/
    ├── my-skill/
    │   ├── SKILL.md           # Main instructions (required)
    │   ├── references/        # Supporting documentation
    │   │   ├── api.md
    │   │   └── examples.md
    │   ├── templates/         # Templates for output
    │   │   └── template.md
    │   └── assets/            # Supplementary files (agentskills.io standard)
    └── category/              # Category folder for organization
        └── another-skill/
            └── SKILL.md

SKILL.md Format (YAML Frontmatter, agentskills.io compatible):
    ---
    name: skill-name              # Required, max 64 chars
    description: Brief description # Required, max 1024 chars
    version: 1.0.0                # Optional
    license: MIT                  # Optional (agentskills.io)
    platforms: [macos]            # Optional — restrict to specific OS platforms
                                  #   Valid: macos, linux, windows
                                  #   Omit to load on all platforms (default)
    compatibility: Requires X     # Optional (agentskills.io)
    metadata:                     # Optional, arbitrary key-value (agentskills.io)
      hermes:
        tags: [fine-tuning, llm]
        related_skills: [peft, lora]
    ---
    
    # Skill Title
    
    Full instructions and content here...

Available tools:
- skills_list: List skills with metadata (progressive disclosure tier 1)
- skill_view: Load full skill content (progressive disclosure tier 2-3)

Usage:
    from tools.skills_tool import skills_list, skill_view, check_skills_requirements
    
    # List all skills (returns metadata only - token efficient)
    result = skills_list()
    
    # View a skill's main content (loads full instructions)
    content = skill_view("axolotl")
    
    # View a reference file within a skill (loads linked file)
    content = skill_view("axolotl", "references/dataset-formats.md")
"""

import json
import logging
import os
import re
import sys
from pathlib import Path
from typing import Dict, Any, List, Optional, Tuple

import yaml

logger = logging.getLogger(__name__)


# All skills live in ~/.hermes/skills/ (seeded from bundled skills/ on install).
# This is the single source of truth -- agent edits, hub installs, and bundled
# skills all coexist here without polluting the git repo.
HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
SKILLS_DIR = HERMES_HOME / "skills"

# Anthropic-recommended limits for progressive disclosure efficiency
MAX_NAME_LENGTH = 64
MAX_DESCRIPTION_LENGTH = 1024

# Platform identifiers for the 'platforms' frontmatter field.
# Maps user-friendly names to sys.platform prefixes.
_PLATFORM_MAP = {
    "macos": "darwin",
    "linux": "linux",
    "windows": "win32",
}


def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool:
    """Check if a skill is compatible with the current OS platform.

    Skills declare platform requirements via a top-level ``platforms`` list
    in their YAML frontmatter::

        platforms: [macos]          # macOS only
        platforms: [macos, linux]   # macOS and Linux

    Valid values: ``macos``, ``linux``, ``windows``.

    If the field is absent or empty the skill is compatible with **all**
    platforms (backward-compatible default).
    """
    platforms = frontmatter.get("platforms")
    if not platforms:
        return True  # No restriction → loads everywhere
    if not isinstance(platforms, list):
        platforms = [platforms]
    current = sys.platform
    for p in platforms:
        mapped = _PLATFORM_MAP.get(str(p).lower().strip(), str(p).lower().strip())
        if current.startswith(mapped):
            return True
    return False


def check_skills_requirements() -> bool:
    """Skills are always available -- the directory is created on first use if needed."""
    return True


def _parse_frontmatter(content: str) -> Tuple[Dict[str, Any], str]:
    """
    Parse YAML frontmatter from markdown content.
    
    Uses yaml.safe_load for full YAML support (nested metadata, lists, etc.)
    with a fallback to simple key:value splitting for robustness.
    
    Args:
        content: Full markdown file content
        
    Returns:
        Tuple of (frontmatter dict, remaining content)
    """
    frontmatter = {}
    body = content
    
    if content.startswith("---"):
        end_match = re.search(r'\n---\s*\n', content[3:])
        if end_match:
            yaml_content = content[3:end_match.start() + 3]
            body = content[end_match.end() + 3:]
            
            try:
                parsed = yaml.safe_load(yaml_content)
                if isinstance(parsed, dict):
                    frontmatter = parsed
                # yaml.safe_load returns None for empty frontmatter
            except yaml.YAMLError:
                # Fallback: simple key:value parsing for malformed YAML
                for line in yaml_content.strip().split('\n'):
                    if ':' in line:
                        key, value = line.split(':', 1)
                        frontmatter[key.strip()] = value.strip()
    
    return frontmatter, body


def _get_category_from_path(skill_path: Path) -> Optional[str]:
    """
    Extract category from skill path based on directory structure.
    
    For paths like: ~/.hermes/skills/mlops/axolotl/SKILL.md -> "mlops"
    """
    try:
        rel_path = skill_path.relative_to(SKILLS_DIR)
        parts = rel_path.parts
        if len(parts) >= 3:
            return parts[0]
        return None
    except ValueError:
        return None


def _estimate_tokens(content: str) -> int:
    """
    Rough token estimate (4 chars per token average).
    
    Args:
        content: Text content
        
    Returns:
        Estimated token count
    """
    return len(content) // 4


def _parse_tags(tags_value) -> List[str]:
    """
    Parse tags from frontmatter value.
    
    Handles:
    - Already-parsed list (from yaml.safe_load): [tag1, tag2]
    - String with brackets: "[tag1, tag2]"
    - Comma-separated string: "tag1, tag2"
    
    Args:
        tags_value: Raw tags value — may be a list or string
        
    Returns:
        List of tag strings
    """
    if not tags_value:
        return []
    
    # yaml.safe_load already returns a list for [tag1, tag2]
    if isinstance(tags_value, list):
        return [str(t).strip() for t in tags_value if t]
    
    # String fallback — handle bracket-wrapped or comma-separated
    tags_value = str(tags_value).strip()
    if tags_value.startswith('[') and tags_value.endswith(']'):
        tags_value = tags_value[1:-1]
    
    return [t.strip().strip('"\'') for t in tags_value.split(',') if t.strip()]


def _find_all_skills() -> List[Dict[str, Any]]:
    """
    Recursively find all skills in ~/.hermes/skills/.
    
    Returns metadata for progressive disclosure (tier 1):
    - name, description, category
    
    Returns:
        List of skill metadata dicts
    """
    skills = []
    
    if not SKILLS_DIR.exists():
        return skills
    
    for skill_md in SKILLS_DIR.rglob("SKILL.md"):
        if any(part in ('.git', '.github', '.hub') for part in skill_md.parts):
            continue
            
        skill_dir = skill_md.parent
        
        try:
            content = skill_md.read_text(encoding='utf-8')
            frontmatter, body = _parse_frontmatter(content)

            # Skip skills incompatible with the current OS platform
            if not skill_matches_platform(frontmatter):
                continue
            
            name = frontmatter.get('name', skill_dir.name)[:MAX_NAME_LENGTH]
            
            description = frontmatter.get('description', '')
            if not description:
                for line in body.strip().split('\n'):
                    line = line.strip()
                    if line and not line.startswith('#'):
                        description = line
                        break
            
            if len(description) > MAX_DESCRIPTION_LENGTH:
                description = description[:MAX_DESCRIPTION_LENGTH - 3] + "..."
            
            category = _get_category_from_path(skill_md)
            
            skills.append({
                "name": name,
                "description": description,
                "category": category,
            })
            
        except (UnicodeDecodeError, PermissionError) as e:
            logger.warning("Failed to read skill file %s: %s", skill_md, e)
            continue
        except Exception as e:
            logger.warning("Error parsing skill %s: %s", skill_md, e, exc_info=True)
            continue
    
    return skills


def _load_category_description(category_dir: Path) -> Optional[str]:
    """
    Load category description from DESCRIPTION.md if it exists.
    
    Args:
        category_dir: Path to the category directory
        
    Returns:
        Description string or None if not found
    """
    desc_file = category_dir / "DESCRIPTION.md"
    if not desc_file.exists():
        return None
    
    try:
        content = desc_file.read_text(encoding='utf-8')
        # Parse frontmatter if present
        frontmatter, body = _parse_frontmatter(content)
        
        # Prefer frontmatter description, fall back to first non-header line
        description = frontmatter.get('description', '')
        if not description:
            for line in body.strip().split('\n'):
                line = line.strip()
                if line and not line.startswith('#'):
                    description = line
                    break
        
        # Truncate to reasonable length
        if len(description) > MAX_DESCRIPTION_LENGTH:
            description = description[:MAX_DESCRIPTION_LENGTH - 3] + "..."
        
        return description if description else None
    except (UnicodeDecodeError, PermissionError) as e:
        logger.debug("Failed to read category description %s: %s", desc_file, e)
        return None
    except Exception as e:
        logger.warning("Error parsing category description %s: %s", desc_file, e, exc_info=True)
        return None


def skills_categories(verbose: bool = False, task_id: str = None) -> str:
    """
    List available skill categories with descriptions (progressive disclosure tier 0).
    
    Returns category names and descriptions for efficient discovery before drilling down.
    Categories can have a DESCRIPTION.md file with a description frontmatter field
    or first paragraph to explain what skills are in that category.
    
    Args:
        verbose: If True, include skill counts per category (default: False, but currently always included)
        task_id: Optional task identifier (unused, for API consistency)
        
    Returns:
        JSON string with list of categories and their descriptions
    """
    try:
        if not SKILLS_DIR.exists():
            return json.dumps({
                "success": True,
                "categories": [],
                "message": "No skills directory found."
            }, ensure_ascii=False)
        
        category_dirs = {}
        for skill_md in SKILLS_DIR.rglob("SKILL.md"):
            category = _get_category_from_path(skill_md)
            if category:
                category_dir = SKILLS_DIR / category
                if category not in category_dirs:
                    category_dirs[category] = category_dir
        
        categories = []
        for name in sorted(category_dirs.keys()):
            category_dir = category_dirs[name]
            description = _load_category_description(category_dir)
            skill_count = sum(1 for _ in category_dir.rglob("SKILL.md"))
            
            cat_entry = {"name": name, "skill_count": skill_count}
            if description:
                cat_entry["description"] = description
            categories.append(cat_entry)
        
        return json.dumps({
            "success": True,
            "categories": categories,
            "hint": "If a category is relevant to your task, use skills_list with that category to see available skills"
        }, ensure_ascii=False)
        
    except Exception as e:
        return json.dumps({
            "success": False,
            "error": str(e)
        }, ensure_ascii=False)


def skills_list(category: str = None, task_id: str = None) -> str:
    """
    List all available skills (progressive disclosure tier 1 - minimal metadata).
    
    Returns only name + description to minimize token usage. Use skill_view() to 
    load full content, tags, related files, etc.
    
    Args:
        category: Optional category filter (e.g., "mlops")
        task_id: Optional task identifier (unused, for API consistency)
        
    Returns:
        JSON string with minimal skill info: name, description, category
    """
    try:
        if not SKILLS_DIR.exists():
            SKILLS_DIR.mkdir(parents=True, exist_ok=True)
            return json.dumps({
                "success": True,
                "skills": [],
                "categories": [],
                "message": "No skills found. Skills directory created at ~/.hermes/skills/"
            }, ensure_ascii=False)
        
        # Find all skills
        all_skills = _find_all_skills()
        
        if not all_skills:
            return json.dumps({
                "success": True,
                "skills": [],
                "categories": [],
                "message": "No skills found in skills/ directory."
            }, ensure_ascii=False)
        
        # Filter by category if specified
        if category:
            all_skills = [s for s in all_skills if s.get("category") == category]
        
        # Sort by category then name
        all_skills.sort(key=lambda s: (s.get("category") or "", s["name"]))
        
        # Extract unique categories
        categories = sorted(set(s.get("category") for s in all_skills if s.get("category")))
        
        return json.dumps({
            "success": True,
            "skills": all_skills,
            "categories": categories,
            "count": len(all_skills),
            "hint": "Use skill_view(name) to see full content, tags, and linked files"
        }, ensure_ascii=False)
        
    except Exception as e:
        return json.dumps({
            "success": False,
            "error": str(e)
        }, ensure_ascii=False)


def skill_view(name: str, file_path: str = None, task_id: str = None) -> str:
    """
    View the content of a skill or a specific file within a skill directory.
    
    Args:
        name: Name or path of the skill (e.g., "axolotl" or "03-fine-tuning/axolotl")
        file_path: Optional path to a specific file within the skill (e.g., "references/api.md")
        task_id: Optional task identifier (unused, for API consistency)
        
    Returns:
        JSON string with skill content or error message
    """
    try:
        if not SKILLS_DIR.exists():
            return json.dumps({
                "success": False,
                "error": "Skills directory does not exist yet. It will be created on first install."
            }, ensure_ascii=False)
        
        skill_dir = None
        skill_md = None
        
        # Try direct path first (e.g., "mlops/axolotl")
        direct_path = SKILLS_DIR / name
        if direct_path.is_dir() and (direct_path / "SKILL.md").exists():
            skill_dir = direct_path
            skill_md = direct_path / "SKILL.md"
        elif direct_path.with_suffix('.md').exists():
            skill_md = direct_path.with_suffix('.md')
        
        # Search by directory name
        if not skill_md:
            for found_skill_md in SKILLS_DIR.rglob("SKILL.md"):
                if found_skill_md.parent.name == name:
                    skill_dir = found_skill_md.parent
                    skill_md = found_skill_md
                    break
        
        # Legacy: flat .md files
        if not skill_md:
            for found_md in SKILLS_DIR.rglob(f"{name}.md"):
                if found_md.name != "SKILL.md":
                    skill_md = found_md
                    break
        
        if not skill_md or not skill_md.exists():
            # List available skills in error message
            all_skills = _find_all_skills()
            available = [s["name"] for s in all_skills[:20]]  # Limit to 20
            return json.dumps({
                "success": False,
                "error": f"Skill '{name}' not found.",
                "available_skills": available,
                "hint": "Use skills_list to see all available skills"
            }, ensure_ascii=False)
        
        # If a specific file path is requested, read that instead
        if file_path and skill_dir:
            # Security: Prevent path traversal attacks
            normalized_path = Path(file_path)
            if ".." in normalized_path.parts:
                return json.dumps({
                    "success": False,
                    "error": "Path traversal ('..') is not allowed.",
                    "hint": "Use a relative path within the skill directory"
                }, ensure_ascii=False)
            
            target_file = skill_dir / file_path
            
            # Security: Verify resolved path is still within skill directory
            try:
                resolved = target_file.resolve()
                skill_dir_resolved = skill_dir.resolve()
                if not resolved.is_relative_to(skill_dir_resolved):
                    return json.dumps({
                        "success": False,
                        "error": "Path escapes skill directory boundary.",
                        "hint": "Use a relative path within the skill directory"
                    }, ensure_ascii=False)
            except (OSError, ValueError):
                return json.dumps({
                    "success": False,
                    "error": f"Invalid file path: '{file_path}'",
                    "hint": "Use a valid relative path within the skill directory"
                }, ensure_ascii=False)
            if not target_file.exists():
                # List available files in the skill directory, organized by type
                available_files = {
                    "references": [],
                    "templates": [],
                    "assets": [],
                    "scripts": [],
                    "other": []
                }
                
                # Scan for all readable files
                for f in skill_dir.rglob("*"):
                    if f.is_file() and f.name != "SKILL.md":
                        rel = str(f.relative_to(skill_dir))
                        if rel.startswith("references/"):
                            available_files["references"].append(rel)
                        elif rel.startswith("templates/"):
                            available_files["templates"].append(rel)
                        elif rel.startswith("assets/"):
                            available_files["assets"].append(rel)
                        elif rel.startswith("scripts/"):
                            available_files["scripts"].append(rel)
                        elif f.suffix in ['.md', '.py', '.yaml', '.yml', '.json', '.tex', '.sh']:
                            available_files["other"].append(rel)
                
                # Remove empty categories
                available_files = {k: v for k, v in available_files.items() if v}
                
                return json.dumps({
                    "success": False,
                    "error": f"File '{file_path}' not found in skill '{name}'.",
                    "available_files": available_files,
                    "hint": "Use one of the available file paths listed above"
                }, ensure_ascii=False)
            
            # Read the file content
            try:
                content = target_file.read_text(encoding='utf-8')
            except UnicodeDecodeError:
                # Binary file - return info about it instead
                return json.dumps({
                    "success": True,
                    "name": name,
                    "file": file_path,
                    "content": f"[Binary file: {target_file.name}, size: {target_file.stat().st_size} bytes]",
                    "is_binary": True
                }, ensure_ascii=False)
            
            return json.dumps({
                "success": True,
                "name": name,
                "file": file_path,
                "content": content,
                "file_type": target_file.suffix
            }, ensure_ascii=False)
        
        # Read the main skill content
        content = skill_md.read_text(encoding='utf-8')
        frontmatter, body = _parse_frontmatter(content)
        
        # Get reference, template, asset, and script files if this is a directory-based skill
        reference_files = []
        template_files = []
        asset_files = []
        script_files = []
        
        if skill_dir:
            references_dir = skill_dir / "references"
            if references_dir.exists():
                reference_files = [str(f.relative_to(skill_dir)) for f in references_dir.glob("*.md")]
            
            templates_dir = skill_dir / "templates"
            if templates_dir.exists():
                for ext in ['*.md', '*.py', '*.yaml', '*.yml', '*.json', '*.tex', '*.sh']:
                    template_files.extend([str(f.relative_to(skill_dir)) for f in templates_dir.rglob(ext)])
            
            # assets/ — agentskills.io standard directory for supplementary files
            assets_dir = skill_dir / "assets"
            if assets_dir.exists():
                for f in assets_dir.rglob("*"):
                    if f.is_file():
                        asset_files.append(str(f.relative_to(skill_dir)))
            
            scripts_dir = skill_dir / "scripts"
            if scripts_dir.exists():
                for ext in ['*.py', '*.sh', '*.bash', '*.js', '*.ts', '*.rb']:
                    script_files.extend([str(f.relative_to(skill_dir)) for f in scripts_dir.glob(ext)])
        
        # Read tags/related_skills with backward compat:
        # Check metadata.hermes.* first (agentskills.io convention), fall back to top-level
        hermes_meta = {}
        metadata = frontmatter.get('metadata')
        if isinstance(metadata, dict):
            hermes_meta = metadata.get('hermes', {}) or {}
        
        tags = _parse_tags(hermes_meta.get('tags') or frontmatter.get('tags', ''))
        related_skills = _parse_tags(hermes_meta.get('related_skills') or frontmatter.get('related_skills', ''))
        
        # Build linked files structure for clear discovery
        linked_files = {}
        if reference_files:
            linked_files["references"] = reference_files
        if template_files:
            linked_files["templates"] = template_files
        if asset_files:
            linked_files["assets"] = asset_files
        if script_files:
            linked_files["scripts"] = script_files
        
        rel_path = str(skill_md.relative_to(SKILLS_DIR))
        
        result = {
            "success": True,
            "name": frontmatter.get('name', skill_md.stem if not skill_dir else skill_dir.name),
            "description": frontmatter.get('description', ''),
            "tags": tags,
            "related_skills": related_skills,
            "content": content,
            "path": rel_path,
            "linked_files": linked_files if linked_files else None,
            "usage_hint": "To view linked files, call skill_view(name, file_path) where file_path is e.g. 'references/api.md' or 'assets/config.yaml'" if linked_files else None
        }
        
        # Surface agentskills.io optional fields when present
        if frontmatter.get('compatibility'):
            result["compatibility"] = frontmatter['compatibility']
        if isinstance(metadata, dict):
            result["metadata"] = metadata
        
        return json.dumps(result, ensure_ascii=False)
        
    except Exception as e:
        return json.dumps({
            "success": False,
            "error": str(e)
        }, ensure_ascii=False)


# Tool description for model_tools.py
SKILLS_TOOL_DESCRIPTION = """Access skill documents providing specialized instructions, guidelines, and executable knowledge.

Progressive disclosure workflow:
1. skills_list() - Returns metadata (name, description, tags, linked_file_count) for all skills
2. skill_view(name) - Loads full SKILL.md content + shows available linked_files
3. skill_view(name, file_path) - Loads specific linked file (e.g., 'references/api.md', 'scripts/train.py')

Skills may include:
- references/: Additional documentation, API specs, examples
- templates/: Output formats, config files, boilerplate code
- assets/: Supplementary files (agentskills.io standard)
- scripts/: Executable helpers (Python, shell scripts)"""


if __name__ == "__main__":
    """Test the skills tool"""
    print("🎯 Skills Tool Test")
    print("=" * 60)
    
    # Test listing skills
    print("\n📋 Listing all skills:")
    result = json.loads(skills_list())
    if result["success"]:
        print(f"Found {result['count']} skills in {len(result.get('categories', []))} categories")
        print(f"Categories: {result.get('categories', [])}")
        print("\nFirst 10 skills:")
        for skill in result["skills"][:10]:
            cat = f"[{skill['category']}] " if skill.get('category') else ""
            refs = f" (+{len(skill['reference_files'])} refs)" if skill.get('reference_files') else ""
            print(f"  • {cat}{skill['name']}: {skill['description'][:60]}...{refs}")
    else:
        print(f"Error: {result['error']}")
    
    # Test viewing a skill
    print("\n📖 Viewing skill 'axolotl':")
    result = json.loads(skill_view("axolotl"))
    if result["success"]:
        print(f"Name: {result['name']}")
        print(f"Description: {result.get('description', 'N/A')[:100]}...")
        print(f"Content length: {len(result['content'])} chars")
        if result.get('reference_files'):
            print(f"Reference files: {result['reference_files']}")
    else:
        print(f"Error: {result['error']}")
    
    # Test viewing a reference file
    print("\n📄 Viewing reference file 'axolotl/references/dataset-formats.md':")
    result = json.loads(skill_view("axolotl", "references/dataset-formats.md"))
    if result["success"]:
        print(f"File: {result['file']}")
        print(f"Content length: {len(result['content'])} chars")
        print(f"Preview: {result['content'][:150]}...")
    else:
        print(f"Error: {result['error']}")


# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------
from tools.registry import registry

SKILLS_LIST_SCHEMA = {
    "name": "skills_list",
    "description": "List available skills (name + description). Use skill_view(name) to load full content.",
    "parameters": {
        "type": "object",
        "properties": {
            "category": {
                "type": "string",
                "description": "Optional category filter to narrow results"
            }
        },
        "required": []
    }
}

SKILL_VIEW_SCHEMA = {
    "name": "skill_view",
    "description": "Skills allow for loading information about specific tasks and workflows, as well as scripts and templates. Load a skill's full content or access its linked files (references, templates, scripts). First call returns SKILL.md content plus a 'linked_files' dict showing available references/templates/scripts. To access those, call again with file_path parameter.",
    "parameters": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "The skill name (use skills_list to see available skills)"
            },
            "file_path": {
                "type": "string",
                "description": "OPTIONAL: Path to a linked file within the skill (e.g., 'references/api.md', 'templates/config.yaml', 'scripts/validate.py'). Omit to get the main SKILL.md content."
            }
        },
        "required": ["name"]
    }
}

registry.register(
    name="skills_list",
    toolset="skills",
    schema=SKILLS_LIST_SCHEMA,
    handler=lambda args, **kw: skills_list(category=args.get("category")),
    check_fn=check_skills_requirements,
)
registry.register(
    name="skill_view",
    toolset="skills",
    schema=SKILL_VIEW_SCHEMA,
    handler=lambda args, **kw: skill_view(args.get("name", ""), file_path=args.get("file_path")),
    check_fn=check_skills_requirements,
)
