# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Tools System

Built-in tools for the agent plus an extensible architecture
for adding custom tools via skills.

Phase 3 (v2.5.0): Added metrics collection for tool execution monitoring
"""

import inspect
import json
import logging
import os
import platform
import shlex
import subprocess
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Optional

# Import metrics for tool execution monitoring
from .metrics import get_metrics_collector

# Import centralized pattern safety module
from .pattern_safety import (
    check_shell_safety,
    sanitize_tool_output,
)

logger = logging.getLogger(__name__)

# Security constants
MAX_TOOL_ITERATIONS = 15
DEFAULT_SHELL_TIMEOUT = 60
DEFAULT_HTTP_TIMEOUT = 30
MAX_FILE_READ_SIZE = 50000  # 50KB


# JSON Schema type → Python type mapping for input validation
_JSON_SCHEMA_TYPES: dict[str, tuple] = {
    "string": (str,),
    "integer": (int,),
    "number": (int, float),
    "boolean": (bool,),
    "array": (list,),
    "object": (dict,),
}


def _check_type(value: Any, expected_type: str) -> bool:
    """Check if a Python value matches a JSON Schema type."""
    py_types = _JSON_SCHEMA_TYPES.get(expected_type)
    if py_types is None:
        return True  # Unknown type — don't block
    # JSON Schema "integer" should not match bool (bool is subclass of int in Python)
    if expected_type == "integer" and isinstance(value, bool):
        return False
    if expected_type == "number" and isinstance(value, bool):
        return False
    return isinstance(value, py_types)


def _validate_tool_input(input_data: dict, schema: dict) -> Optional[str]:
    """Validate tool input against its JSON Schema. Returns error message or None."""
    props = schema.get("properties", {})
    required = schema.get("required", [])

    # Check required fields
    for field in required:
        if field not in input_data:
            return f"Required field '{field}' is missing."

    # Check types and enums for provided fields
    for field, value in input_data.items():
        if field.startswith("_"):  # Skip internal fields (_context)
            continue
        if field not in props:
            continue  # Allow extra fields (LLM may add explanation fields)
        field_schema = props[field]

        # Type check
        expected_type = field_schema.get("type")
        if expected_type and not _check_type(value, expected_type):
            return f"Field '{field}' expected type '{expected_type}', got {type(value).__name__}."

        # Enum check
        allowed = field_schema.get("enum")
        if allowed and value not in allowed:
            return f"Field '{field}' must be one of {allowed}, got '{value}'."

    return None


def safe_get(data: dict, key: str, default: Any = None) -> Any:
    """Get a value from tool input data, returning default if missing."""
    return data.get(key, default)


def require_fields(data: dict, *fields: str) -> Optional[str]:
    """Validate that required fields are present in tool input data.

    Returns an error message string if any field is missing, or None if all present.

    Usage:
        if err := require_fields(data, "url", "method"):
            return err
    """
    missing = [f for f in fields if not data.get(f)]
    if missing:
        return f"Missing required field(s): {', '.join(missing)}"
    return None


@dataclass
class Tool:
    """Represents a tool the agent can use."""

    name: str
    description: str
    input_schema: dict
    handler: Callable[[dict], str]
    category: str = "general"
    requires_confirmation: bool = False  # For dangerous operations
    input_examples: Optional[list[dict]] = None  # Example inputs for complex tools


class ToolRegistry:
    """
    Registry of available tools.

    Tools can be:
    - Built-in (defined here)
    - Loaded from skills
    - Added programmatically
    """

    def __init__(self, sandboxed_dirs: Optional[list[str]] = None):
        self.tools: dict[str, Tool] = {}
        # Default sandboxed directories - can be overridden via config
        self.sandboxed_dirs = [
            Path.home(),
            Path("/tmp"),
        ]
        if sandboxed_dirs:
            self.sandboxed_dirs = [Path(d) for d in sandboxed_dirs]

        self._register_builtins()

    # Env vars to strip from child processes (secrets, API keys)
    _SENSITIVE_ENV_PREFIXES = (
        "ANTHROPIC_",
        "OPENAI_",
        "GOOGLE_",
        "AWS_SECRET",
        "DISCORD_BOT_TOKEN",
        "TELEGRAM_BOT_TOKEN",
        "TWILIO_",
        "FAMILIAR_ENCRYPTION_KEY",
        "OWNER_PIN_HASH",
        "HA_SHARED_SECRET",
        "SSO_",
        "SMTP_PASSWORD",
        "DATABASE_URL",
        "REDIS_URL",
    )

    def _safe_env(self) -> dict:
        """Return a filtered copy of os.environ without secrets/API keys."""
        return {
            k: v
            for k, v in os.environ.items()
            if not any(k.startswith(prefix) for prefix in self._SENSITIVE_ENV_PREFIXES)
        } | {"LC_ALL": "C"}

    def _validate_shell_command(self, command: str) -> tuple[bool, str]:
        """
        Validate shell command for dangerous patterns.

        Uses the centralized pattern_safety module for timeout-protected
        pattern matching.

        Returns:
            (is_safe, error_message)
        """
        return check_shell_safety(command)

    def _validate_path(self, path: str, must_exist: bool = False) -> tuple[bool, str, Path]:
        """
        Validate file path is within sandboxed directories.
        Returns (is_valid, error_message, resolved_path).
        """
        try:
            # Expand and resolve path
            expanded = os.path.expanduser(path)
            resolved = Path(expanded).resolve()

            # Check if path is within allowed directories
            is_allowed = False
            for sandbox_dir in self.sandboxed_dirs:
                try:
                    resolved.relative_to(sandbox_dir.resolve())
                    is_allowed = True
                    break
                except ValueError:
                    continue

            if not is_allowed:
                allowed_str = ", ".join(str(d) for d in self.sandboxed_dirs)
                return False, f"Path must be within allowed directories: {allowed_str}", resolved

            # Check for path traversal attempts in the original path
            if ".." in path:
                # Verify the resolved path is still within sandbox after traversal
                pass  # Already checked above with resolve()

            # Check existence if required
            if must_exist and not resolved.exists():
                return False, f"Path does not exist: {resolved}", resolved

            return True, "", resolved

        except Exception as e:
            return False, f"Invalid path: {str(e)}", None

    def _register_builtins(self):
        """Register built-in tools."""

        # Shell command execution
        self.register(
            Tool(
                name="run_shell",
                description=(
                    "Execute a shell command on the local machine. "
                    "Use this for system administration, installing packages, or running scripts. "
                    "Do not use for reading or writing files — use read_file/write_file instead. "
                    "Returns stdout, stderr, and exit code; output is truncated at 50KB."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "command": {
                            "type": "string",
                            "description": "The shell command to execute",
                        },
                        "timeout": {
                            "type": "integer",
                            "description": "Timeout in seconds (default 60)",
                            "default": 60,
                        },
                    },
                    "required": ["command"],
                },
                handler=self._handle_run_shell,
                category="system",
                requires_confirmation=True,
            )
        )

        # File reading
        self.register(
            Tool(
                name="read_file",
                description=(
                    "Read the contents of a text file at the given path. "
                    "Use this when the user asks to see, review, or check a file. "
                    "Returns full text for files under 50KB; larger files are truncated. "
                    "Cannot read binary files — use list_directory to check file types first."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "path": {"type": "string", "description": "Path to the file to read"},
                        "max_lines": {
                            "type": "integer",
                            "description": "Maximum lines to return (default all)",
                            "default": None,
                        },
                    },
                    "required": ["path"],
                },
                handler=self._handle_read_file,
                category="filesystem",
            )
        )

        # File writing
        self.register(
            Tool(
                name="write_file",
                description=(
                    "Write content to a file, creating it if it does not exist. "
                    "Use this when the user asks to create, save, or update a file. "
                    "Overwrites existing content by default; set append=true to add to the end. "
                    "The path must be within the sandboxed directories."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "path": {"type": "string", "description": "Path to write the file"},
                        "content": {"type": "string", "description": "Content to write"},
                        "append": {
                            "type": "boolean",
                            "description": "Append instead of overwrite",
                            "default": False,
                        },
                    },
                    "required": ["path", "content"],
                },
                handler=self._handle_write_file,
                category="filesystem",
                requires_confirmation=True,
            )
        )

        # Directory listing
        self.register(
            Tool(
                name="list_directory",
                description=(
                    "List files and directories at the given path. "
                    "Use this to browse the filesystem or check what files exist. "
                    "Returns up to 100 entries with type indicators and file sizes. "
                    "Set show_hidden=true to include dotfiles."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Directory path to list",
                            "default": ".",
                        },
                        "show_hidden": {
                            "type": "boolean",
                            "description": "Include hidden files",
                            "default": False,
                        },
                    },
                },
                handler=self._handle_list_directory,
                category="filesystem",
            )
        )

        # System information
        self.register(
            Tool(
                name="get_system_info",
                description=(
                    "Get system information including hostname, OS, CPU, memory, disk, and uptime. "
                    "Use this when the user asks about the host machine or you need to check resources. "
                    "Returns plain-text fields; some values may be unavailable on non-Linux systems."
                ),
                input_schema={"type": "object", "properties": {}},
                handler=self._handle_system_info,
                category="system",
            )
        )

        # Current time
        self.register(
            Tool(
                name="get_current_time",
                description=(
                    "Get the current date and time on the host machine. "
                    "Use this when the user asks what time it is or you need a timestamp. "
                    "Returns date, time, timezone, and day of week in a single formatted string."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "timezone": {
                            "type": "string",
                            "description": "Timezone (default: local)",
                            "default": None,
                        }
                    },
                },
                handler=self._handle_current_time,
                category="utility",
            )
        )

        # Memory operations (for the agent's memory system)
        self.register(
            Tool(
                name="remember",
                description=(
                    "Store a fact in long-term memory for retrieval in future conversations. "
                    "Use this when the user shares important preferences, facts, or information to retain. "
                    "Each memory needs a unique key and a value; duplicating a key overwrites the old value. "
                    "Categorize as user_info, preference, fact, or task."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "key": {
                            "type": "string",
                            "description": "Short identifier for this memory",
                        },
                        "value": {"type": "string", "description": "The information to remember"},
                        "category": {
                            "type": "string",
                            "description": "Category: user_info, preference, fact, task",
                            "default": "fact",
                        },
                    },
                    "required": ["key", "value"],
                },
                handler=self._handle_remember,
                category="memory",
            )
        )

        self.register(
            Tool(
                name="recall",
                description=(
                    "Search long-term memory for previously stored information. "
                    "Use this when answering questions that may depend on earlier conversations. "
                    "Returns up to 10 matching memories with their keys, values, and categories."
                ),
                input_schema={
                    "type": "object",
                    "properties": {"query": {"type": "string", "description": "Search query"}},
                    "required": ["query"],
                },
                handler=self._handle_recall,
                category="memory",
            )
        )

        # Get full skill documentation on demand (progressive disclosure)
        self.register(
            Tool(
                name="get_skill_docs",
                description=(
                    "Get detailed documentation for a skill. "
                    "Use when you need full instructions for a skill before using its tools."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "skill_name": {
                            "type": "string",
                            "description": "Name of the skill to get docs for",
                        }
                    },
                    "required": ["skill_name"],
                },
                handler=self._handle_get_skill_docs,
                category="general",
            )
        )

        # HTTP request (simple)
        self.register(
            Tool(
                name="http_request",
                description=(
                    "Make an HTTP GET or POST request to a public URL. "
                    "Use this to fetch web content, call APIs, or send JSON payloads. "
                    "Requests to private/internal networks are blocked for security. "
                    "Returns the HTTP status code and response body, truncated at 10KB."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "url": {"type": "string", "description": "URL to request"},
                        "method": {
                            "type": "string",
                            "description": "HTTP method",
                            "enum": ["GET", "POST"],
                            "default": "GET",
                        },
                        "data": {
                            "type": "string",
                            "description": "POST data (JSON string)",
                            "default": None,
                        },
                    },
                    "required": ["url"],
                },
                handler=self._handle_http_request,
                category="network",
            )
        )

    def register(self, tool: Tool):
        """Register a tool."""
        self.tools[tool.name] = tool
        logger.debug(f"Registered tool: {tool.name}")

    def unregister(self, name: str):
        """Remove a tool."""
        if name in self.tools:
            del self.tools[name]

    def get(self, name: str) -> Optional[Tool]:
        """Get a tool by name."""
        return self.tools.get(name)

    def get_all(self) -> list[Tool]:
        """Get all tools."""
        return list(self.tools.values())

    def get_schemas(self) -> list[dict]:
        """Get tool schemas for LLM."""
        schemas = []
        for t in self.tools.values():
            schema = {
                "name": t.name,
                "description": t.description,
                "input_schema": t.input_schema,
            }
            if t.input_examples:
                schema["input_examples"] = t.input_examples
            schemas.append(schema)
        return schemas

    def execute(
        self, name: str, input_data: dict[str, Any], context: Optional[dict[str, Any]] = None
    ) -> str:
        """Execute a tool by name with output sanitization and metrics tracking."""
        tool = self.tools.get(name)
        if not tool:
            # Progressive disclosure: suggest enabling a skill instead of bare error
            try:
                from familiar.core.progressive import on_tool_not_found

                skill_loader = context.get("skill_loader") if context else None
                if skill_loader:
                    return on_tool_not_found(name, skill_loader)
            except (ImportError, Exception):
                pass
            return f"Error: Unknown tool '{name}'"

        start_time = time.time()
        success = True
        error_message = None
        try:
            # Strip _context before serializing — it contains non-JSON objects (SecureSession etc.)
            _serializable = (
                {k: v for k, v in input_data.items() if k != "_context"} if input_data else {}
            )
            input_size = len(json.dumps(_serializable))
        except (TypeError, ValueError):
            input_size = len(str(input_data))
        result = ""

        try:
            # RBAC permission check: if user has a role, verify tool access
            if context:
                session = context.get("session")
                if session and hasattr(session, "user_role") and session.user_role:
                    try:
                        from familiar.skills.rbac.skill import _check_perm, _get_role_permissions

                        user_perms = _get_role_permissions(session.user_role)
                        tool_category = (
                            tool.category if hasattr(tool, "category") else name.split("_")[0]
                        )
                        if user_perms and not _check_perm(user_perms, tool_category, name):
                            return f"⛔ Permission denied: your role '{session.user_role}' cannot access {name}"
                    except ImportError:
                        pass  # RBAC skill not installed, skip check

            # Validate input against schema
            if tool.input_schema:
                validation_error = _validate_tool_input(input_data, tool.input_schema)
                if validation_error:
                    return f"\u26a0\ufe0f Invalid input for {name}: {validation_error}"

            # Inject context into input_data so tools can access session, agent, etc.
            if context and isinstance(input_data, dict):
                input_data["_context"] = context

            # Pass context to handler if its signature accepts it
            if context:
                try:
                    sig = inspect.signature(tool.handler)
                    if "context" in sig.parameters:
                        result = tool.handler(input_data, context=context)
                    else:
                        result = tool.handler(input_data)
                except (ValueError, TypeError):
                    # Can't inspect (C extension, etc.) — call without context
                    result = tool.handler(input_data)
            else:
                result = tool.handler(input_data)

            # Sanitize output to prevent prompt injection
            result = sanitize_tool_output(result)

        except Exception as e:
            logger.error(f"Tool {name} failed: {e}")
            success = False
            error_message = str(e)
            result = f"Error executing {name}: {str(e)}"

        finally:
            # Record metrics
            end_time = time.time()
            duration_ms = (end_time - start_time) * 1000
            output_size = len(result) if result else 0
            user_id = context.get("user_id") if context else None

            try:
                metrics = get_metrics_collector()
                metrics.record_tool_execution(
                    tool_name=name,
                    duration_ms=duration_ms,
                    success=success,
                    error_message=error_message,
                    input_size=input_size,
                    output_size=output_size,
                    user_id=str(user_id) if user_id else None,
                )
            except Exception as metrics_error:
                logger.debug(f"Failed to record tool metrics: {metrics_error}")

        return result

    # Built-in handlers

    def _handle_run_shell(self, input_data: dict) -> str:
        """Execute shell command with security validation.

        Uses shell=False by default (splits via shlex). Falls back to
        shell=True only for commands that require it and pass validation.
        """
        command = input_data["command"]
        timeout = input_data.get("timeout", DEFAULT_SHELL_TIMEOUT)

        # Always validate against dangerous patterns first
        is_safe, error_msg = self._validate_shell_command(command)
        if not is_safe:
            logger.warning(f"Blocked potentially dangerous command: {command[:100]}...")
            return f"Error: {error_msg}"

        try:
            # Always use shell=False — reject commands that can't be safely split.
            # shell=True enables shell injection even after pattern validation.
            try:
                args = shlex.split(command)
            except ValueError as e:
                return (
                    f"Error: Command could not be parsed safely: {e}\n"
                    "Hint: Avoid unbalanced quotes or complex shell syntax. "
                    "Break the command into simpler parts."
                )

            result = subprocess.run(
                args,
                shell=False,
                capture_output=True,
                text=True,
                timeout=timeout,
                env=self._safe_env(),
            )

            output = result.stdout
            if result.stderr:
                output += f"\n[STDERR]: {result.stderr}"
            if result.returncode != 0:
                output += f"\n[Exit code: {result.returncode}]"

            # Truncate very long output
            if len(output) > MAX_FILE_READ_SIZE:
                output = output[:MAX_FILE_READ_SIZE] + "\n... (output truncated)"

            return output.strip() or "(no output)"

        except subprocess.TimeoutExpired:
            return f"Error: Command timed out after {timeout} seconds"
        except Exception as e:
            return f"Error: {str(e)}"

    def _handle_read_file(self, input_data: dict) -> str:
        """Read file with path validation."""
        raw_path = input_data["path"]
        max_lines = input_data.get("max_lines")

        # Validate path
        is_valid, error_msg, path = self._validate_path(raw_path, must_exist=True)
        if not is_valid:
            return f"Error: {error_msg}"

        try:
            with open(path, "r") as f:
                if max_lines:
                    lines = []
                    for i, line in enumerate(f):
                        if i >= max_lines:
                            lines.append(f"... ({max_lines} lines shown)")
                            break
                        lines.append(line.rstrip())
                    return "\n".join(lines)
                else:
                    content = f.read()
                    # Truncate very long files
                    if len(content) > MAX_FILE_READ_SIZE:
                        content = (
                            content[:MAX_FILE_READ_SIZE]
                            + f"\n... (truncated at {MAX_FILE_READ_SIZE // 1000}KB)"
                        )
                    return content
        except FileNotFoundError:
            return f"Error: File not found: {path}"
        except PermissionError:
            return f"Error: Permission denied: {path}"
        except UnicodeDecodeError:
            return f"Error: File is not a text file or has invalid encoding: {path}"
        except Exception as e:
            return f"Error reading file: {str(e)}"

    def _handle_write_file(self, input_data: dict) -> str:
        """Write file with path validation and atomic write."""
        raw_path = input_data["path"]
        content = input_data["content"]
        append = input_data.get("append", False)

        # Validate path (don't require existence for writes)
        is_valid, error_msg, path = self._validate_path(raw_path, must_exist=False)
        if not is_valid:
            return f"Error: {error_msg}"

        try:
            # Create directory if needed
            path.parent.mkdir(parents=True, exist_ok=True)

            if append:
                # Append mode - direct write is fine
                with open(path, "a") as f:
                    f.write(content)
                return f"✓ Appended to {path} ({len(content)} bytes)"
            else:
                # Write mode - use atomic write pattern
                tmp_path = path.with_suffix(path.suffix + ".tmp")
                try:
                    with open(tmp_path, "w") as f:
                        f.write(content)
                    tmp_path.replace(path)  # Atomic on POSIX
                    return f"✓ Wrote {path} ({len(content)} bytes)"
                except Exception:
                    # Clean up temp file on failure
                    if tmp_path.exists():
                        tmp_path.unlink()
                    raise

        except PermissionError:
            return f"Error: Permission denied: {path}"
        except Exception as e:
            return f"Error writing file: {str(e)}"

    def _handle_list_directory(self, input_data: dict) -> str:
        """List directory with path validation."""
        raw_path = input_data.get("path", ".")
        show_hidden = input_data.get("show_hidden", False)

        # Validate path
        is_valid, error_msg, path = self._validate_path(raw_path, must_exist=True)
        if not is_valid:
            return f"Error: {error_msg}"

        if not path.is_dir():
            return f"Error: Not a directory: {path}"

        try:
            items = os.listdir(path)

            if not show_hidden:
                items = [i for i in items if not i.startswith(".")]

            items.sort()

            # Add type indicators
            result = []
            for item in items[:100]:  # Limit output
                full_path = path / item
                if full_path.is_dir():
                    result.append(f"📁 {item}/")
                else:
                    try:
                        size = full_path.stat().st_size
                        result.append(f"📄 {item} ({self._format_size(size)})")
                    except OSError:
                        result.append(f"📄 {item}")

            if len(items) > 100:
                result.append(f"... and {len(items) - 100} more items")

            return "\n".join(result) or "(empty directory)"

        except FileNotFoundError:
            return f"Error: Directory not found: {path}"
        except PermissionError:
            return f"Error: Permission denied: {path}"
        except Exception as e:
            return f"Error: {str(e)}"

    def _format_size(self, size: int) -> str:
        for unit in ["B", "KB", "MB", "GB"]:
            if size < 1024:
                return f"{size:.1f}{unit}"
            size /= 1024
        return f"{size:.1f}TB"

    def _handle_system_info(self, input_data: dict) -> str:
        info = []

        # Basic info
        info.append(f"Hostname: {platform.node()}")
        info.append(f"OS: {platform.system()} {platform.release()}")
        info.append(f"Architecture: {platform.machine()}")
        info.append(f"Python: {platform.python_version()}")

        # Uptime
        try:
            uptime = subprocess.run(
                "uptime -p 2>/dev/null || uptime", shell=True, capture_output=True, text=True
            )
            info.append(f"Uptime: {uptime.stdout.strip()}")
        except (subprocess.SubprocessError, OSError):
            pass

        # Memory
        try:
            if platform.system() == "Linux":
                mem = subprocess.run(
                    "free -h | grep Mem", shell=True, capture_output=True, text=True
                )
                if mem.stdout:
                    parts = mem.stdout.split()
                    info.append(f"Memory: {parts[2]} used / {parts[1]} total")
            elif platform.system() == "Darwin":
                # macOS
                mem = subprocess.run(
                    "vm_stat | head -5", shell=True, capture_output=True, text=True
                )
                info.append(f"Memory:\n{mem.stdout}")
        except (subprocess.SubprocessError, OSError, IndexError):
            pass

        # Disk
        try:
            disk = subprocess.run("df -h / | tail -1", shell=True, capture_output=True, text=True)
            if disk.stdout:
                parts = disk.stdout.split()
                info.append(f"Disk: {parts[2]} used / {parts[1]} total ({parts[4]} full)")
        except (subprocess.SubprocessError, OSError, IndexError):
            pass

        # CPU temp (Raspberry Pi)
        try:
            temp = subprocess.run(
                "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null",
                shell=True,
                capture_output=True,
                text=True,
            )
            if temp.stdout.strip():
                celsius = int(temp.stdout.strip()) / 1000
                info.append(f"CPU Temp: {celsius:.1f}°C")
        except (subprocess.SubprocessError, OSError, ValueError):
            pass

        return "\n".join(info)

    def _handle_current_time(self, input_data: dict) -> str:
        now = datetime.now()
        return now.strftime("%Y-%m-%d %H:%M:%S %Z (%A)")

    def _handle_remember(
        self, input_data: dict[str, Any], context: Optional[dict[str, Any]] = None
    ) -> str:
        """Store in memory - requires context with memory object."""
        if not context or "memory" not in context:
            return "Error: Memory system not available"

        memory = context["memory"]
        key = input_data["key"]
        value = input_data["value"]
        category = input_data.get("category", "fact")

        memory.remember(key, value, category=category)
        return f"✓ Remembered: {key} = {value}"

    def _handle_recall(
        self, input_data: dict[str, Any], context: Optional[dict[str, Any]] = None
    ) -> str:
        """Search memory - requires context with memory object."""
        if not context or "memory" not in context:
            return "Error: Memory system not available"

        memory = context["memory"]
        query = input_data["query"]

        results = memory.search(query)
        if not results:
            return f"No memories found matching '{query}'"

        output = []
        for entry in results[:10]:
            output.append(f"• {entry.key}: {entry.value} [{entry.category}]")

        return "\n".join(output)

    def _handle_http_request(self, input_data: dict) -> str:
        url = input_data["url"]
        method = input_data.get("method", "GET")
        data = input_data.get("data")

        # SSRF protection: block requests to private/internal networks
        from .sanitization import check_ssrf

        is_safe, ssrf_msg = check_ssrf(url)
        if not is_safe:
            return f"Error: {ssrf_msg}"

        try:
            import urllib.error
            import urllib.request

            req = urllib.request.Request(url, method=method)
            req.add_header("User-Agent", "PiAgent/1.0")

            if data:
                req.data = data.encode()
                req.add_header("Content-Type", "application/json")

            with urllib.request.urlopen(req, timeout=30) as response:
                content = response.read().decode()

                # Truncate long responses
                if len(content) > 10000:
                    content = content[:10000] + "\n... (truncated)"

                return f"[{response.status}]\n{content}"

        except urllib.error.HTTPError as e:
            return f"HTTP Error {e.code}: {e.reason}"
        except urllib.error.URLError as e:
            return f"URL Error: {e.reason}"
        except Exception as e:
            return f"Error: {str(e)}"

    def _handle_get_skill_docs(
        self, input_data: dict[str, Any], context: Optional[dict[str, Any]] = None
    ) -> str:
        """Retrieve full SKILL.md documentation for a skill."""
        skill_name = input_data.get("skill_name", "")
        if not skill_name:
            return "Error: skill_name is required."

        skill_loader = context.get("skill_loader") if context else None
        if not skill_loader:
            return "Error: Skill loader not available."

        detail = skill_loader.get_skill_detail(skill_name)
        if detail:
            return detail
        return f"No documentation found for skill '{skill_name}'."


# Global registry instance
_registry: Optional[ToolRegistry] = None


def get_tool_registry(sandboxed_dirs: Optional[list[str]] = None) -> ToolRegistry:
    """
    Get or create the global tool registry.

    Args:
        sandboxed_dirs: Optional list of allowed directories for file operations.
                       Only used on first call when creating the registry.
    """
    global _registry
    if _registry is None:
        _registry = ToolRegistry(sandboxed_dirs)
    return _registry


def reset_tool_registry():
    """Reset the tool registry (for testing)."""
    global _registry
    _registry = None
