# 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

"""
Familiar v1.4.0 - Tool Registry Module

Provides centralized tool management with:
- Tool schema registry with JSON Schema validation
- Tool versioning and deprecation tracking
- Runtime discovery and filtering
- Usage analytics and optimization hints
- Category-based organization
"""

import hashlib
import json
import logging
import re
import threading
import time
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

logger = logging.getLogger(__name__)


# ============================================================
# CONSTANTS
# ============================================================


def _utcnow() -> datetime:
    """Get current UTC time as timezone-aware datetime."""
    return datetime.now(timezone.utc)


# Semantic versioning pattern
SEMVER_PATTERN = re.compile(
    r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
    r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
    r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
    r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
)


# ============================================================
# ENUMS
# ============================================================


class ToolCategory(str, Enum):
    """Categories for organizing tools."""

    GENERAL = "general"
    FILE_SYSTEM = "file_system"
    NETWORK = "network"
    DATABASE = "database"
    API = "api"
    COMPUTATION = "computation"
    COMMUNICATION = "communication"
    SEARCH = "search"
    MEMORY = "memory"
    SYSTEM = "system"
    CUSTOM = "custom"


class ToolStatus(str, Enum):
    """Tool lifecycle status."""

    ACTIVE = "active"
    DEPRECATED = "deprecated"
    EXPERIMENTAL = "experimental"
    DISABLED = "disabled"


class SafetyLevel(str, Enum):
    """Tool safety classification."""

    SAFE = "safe"  # No side effects, read-only
    MODERATE = "moderate"  # Limited side effects, reversible
    DANGEROUS = "dangerous"  # Significant side effects, may be irreversible
    CRITICAL = "critical"  # System-level access, requires confirmation


class ParameterType(str, Enum):
    """Supported parameter types."""

    STRING = "string"
    INTEGER = "integer"
    NUMBER = "number"
    BOOLEAN = "boolean"
    ARRAY = "array"
    OBJECT = "object"
    NULL = "null"


# ============================================================
# VERSION HANDLING
# ============================================================


@dataclass
class Version:
    """Semantic version representation."""

    major: int
    minor: int
    patch: int
    prerelease: Optional[str] = None
    buildmetadata: Optional[str] = None

    @classmethod
    def parse(cls, version_str: str) -> "Version":
        """Parse a semantic version string."""
        match = SEMVER_PATTERN.match(version_str)
        if not match:
            raise ValueError(f"Invalid semantic version: {version_str}")

        return cls(
            major=int(match.group("major")),
            minor=int(match.group("minor")),
            patch=int(match.group("patch")),
            prerelease=match.group("prerelease"),
            buildmetadata=match.group("buildmetadata"),
        )

    def __str__(self) -> str:
        version = f"{self.major}.{self.minor}.{self.patch}"
        if self.prerelease:
            version += f"-{self.prerelease}"
        if self.buildmetadata:
            version += f"+{self.buildmetadata}"
        return version

    def __lt__(self, other: "Version") -> bool:
        if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
            return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
        # Pre-release versions have lower precedence
        if self.prerelease and not other.prerelease:
            return True
        if not self.prerelease and other.prerelease:
            return False
        if self.prerelease and other.prerelease:
            return self.prerelease < other.prerelease
        return False

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Version):
            return False
        return (self.major, self.minor, self.patch, self.prerelease) == (
            other.major,
            other.minor,
            other.patch,
            other.prerelease,
        )

    def __le__(self, other: "Version") -> bool:
        return self < other or self == other

    def __gt__(self, other: "Version") -> bool:
        return not self <= other

    def __ge__(self, other: "Version") -> bool:
        return not self < other

    def is_compatible_with(self, other: "Version") -> bool:
        """Check if versions are compatible (same major version)."""
        return self.major == other.major

    def bump_major(self) -> "Version":
        """Return new version with bumped major."""
        return Version(self.major + 1, 0, 0)

    def bump_minor(self) -> "Version":
        """Return new version with bumped minor."""
        return Version(self.major, self.minor + 1, 0)

    def bump_patch(self) -> "Version":
        """Return new version with bumped patch."""
        return Version(self.major, self.minor, self.patch + 1)


# ============================================================
# PARAMETER SCHEMA
# ============================================================


@dataclass
class ParameterSchema:
    """Schema for a tool parameter."""

    name: str
    param_type: ParameterType
    description: str = ""
    required: bool = True
    default: Any = None
    enum: Optional[List[Any]] = None
    minimum: Optional[float] = None
    maximum: Optional[float] = None
    min_length: Optional[int] = None
    max_length: Optional[int] = None
    pattern: Optional[str] = None
    items: Optional["ParameterSchema"] = None  # For arrays
    properties: Optional[Dict[str, "ParameterSchema"]] = None  # For objects

    def to_json_schema(self) -> dict:
        """Convert to JSON Schema format."""
        schema = {"type": self.param_type.value, "description": self.description}

        if self.enum:
            schema["enum"] = self.enum
        if self.minimum is not None:
            schema["minimum"] = self.minimum
        if self.maximum is not None:
            schema["maximum"] = self.maximum
        if self.min_length is not None:
            schema["minLength"] = self.min_length
        if self.max_length is not None:
            schema["maxLength"] = self.max_length
        if self.pattern:
            schema["pattern"] = self.pattern
        if self.items:
            schema["items"] = self.items.to_json_schema()
        if self.properties:
            schema["properties"] = {k: v.to_json_schema() for k, v in self.properties.items()}
        if self.default is not None:
            schema["default"] = self.default

        return schema

    def validate(self, value: Any) -> Tuple[bool, Optional[str]]:
        """Validate a value against this schema."""
        # Type checking
        type_map = {
            ParameterType.STRING: str,
            ParameterType.INTEGER: int,
            ParameterType.NUMBER: (int, float),
            ParameterType.BOOLEAN: bool,
            ParameterType.ARRAY: list,
            ParameterType.OBJECT: dict,
            ParameterType.NULL: type(None),
        }

        expected_type = type_map.get(self.param_type)
        if expected_type and not isinstance(value, expected_type):
            return False, f"Expected {self.param_type.value}, got {type(value).__name__}"

        # Enum validation
        if self.enum and value not in self.enum:
            return False, f"Value must be one of: {self.enum}"

        # Numeric range
        if isinstance(value, (int, float)):
            if self.minimum is not None and value < self.minimum:
                return False, f"Value must be >= {self.minimum}"
            if self.maximum is not None and value > self.maximum:
                return False, f"Value must be <= {self.maximum}"

        # String constraints
        if isinstance(value, str):
            if self.min_length is not None and len(value) < self.min_length:
                return False, f"String length must be >= {self.min_length}"
            if self.max_length is not None and len(value) > self.max_length:
                return False, f"String length must be <= {self.max_length}"
            if self.pattern and not re.match(self.pattern, value):
                return False, f"String must match pattern: {self.pattern}"

        # Array items validation
        if isinstance(value, list) and self.items:
            for i, item in enumerate(value):
                valid, error = self.items.validate(item)
                if not valid:
                    return False, f"Array item {i}: {error}"

        return True, None


# ============================================================
# TOOL DEFINITION
# ============================================================


@dataclass
class ToolDefinition:
    """
    Complete tool definition with schema, metadata, and versioning.

    Attributes:
        name: Unique tool identifier
        description: Human-readable description
        version: Semantic version
        parameters: List of parameter schemas
        category: Tool category
        status: Lifecycle status
        safety_level: Safety classification
        handler: Callable that executes the tool
        deprecated_message: Message shown when deprecated
        replacement_tool: Name of replacement tool if deprecated
        tags: Searchable tags
        examples: Usage examples
        metadata: Additional metadata
    """

    name: str
    description: str
    version: Version
    parameters: List[ParameterSchema] = field(default_factory=list)
    category: ToolCategory = ToolCategory.GENERAL
    status: ToolStatus = ToolStatus.ACTIVE
    safety_level: SafetyLevel = SafetyLevel.SAFE
    handler: Optional[Callable] = None
    deprecated_message: str = ""
    replacement_tool: str = ""
    tags: List[str] = field(default_factory=list)
    examples: List[Dict[str, Any]] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)

    # Internal tracking
    created_at: datetime = field(default_factory=_utcnow)
    updated_at: datetime = field(default_factory=_utcnow)

    @property
    def is_deprecated(self) -> bool:
        """Check if tool is deprecated."""
        return self.status == ToolStatus.DEPRECATED

    @property
    def is_active(self) -> bool:
        """Check if tool is active."""
        return self.status == ToolStatus.ACTIVE

    @property
    def requires_confirmation(self) -> bool:
        """Check if tool requires user confirmation."""
        return self.safety_level in (SafetyLevel.DANGEROUS, SafetyLevel.CRITICAL)

    @property
    def full_name(self) -> str:
        """Get fully qualified name with version."""
        return f"{self.name}@{self.version}"

    @property
    def schema_hash(self) -> str:
        """Get hash of the parameter schema for change detection."""
        schema_json = json.dumps(self.to_json_schema(), sort_keys=True)
        return hashlib.md5(schema_json.encode()).hexdigest()[:8]

    def to_json_schema(self) -> dict:
        """Convert to JSON Schema format for LLM tool calling."""
        required = [p.name for p in self.parameters if p.required]
        properties = {p.name: p.to_json_schema() for p in self.parameters}

        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": {"type": "object", "properties": properties, "required": required},
            },
        }

    def to_openai_schema(self) -> dict:
        """Convert to OpenAI function calling format."""
        return self.to_json_schema()

    def to_anthropic_schema(self) -> dict:
        """Convert to Anthropic tool use format."""
        required = [p.name for p in self.parameters if p.required]
        properties = {p.name: p.to_json_schema() for p in self.parameters}

        return {
            "name": self.name,
            "description": self.description,
            "input_schema": {"type": "object", "properties": properties, "required": required},
        }

    def validate_input(self, input_data: dict) -> Tuple[bool, List[str]]:
        """
        Validate input against parameter schema.

        Returns (is_valid, list_of_errors)
        """
        errors = []

        # Check required parameters
        for param in self.parameters:
            if param.required and param.name not in input_data:
                if param.default is None:
                    errors.append(f"Missing required parameter: {param.name}")

        # Validate provided parameters
        for key, value in input_data.items():
            param = next((p for p in self.parameters if p.name == key), None)
            if param is None:
                errors.append(f"Unknown parameter: {key}")
                continue

            valid, error = param.validate(value)
            if not valid:
                errors.append(f"Parameter '{key}': {error}")

        return len(errors) == 0, errors

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "name": self.name,
            "description": self.description,
            "version": str(self.version),
            "category": self.category.value,
            "status": self.status.value,
            "safety_level": self.safety_level.value,
            "tags": self.tags,
            "deprecated_message": self.deprecated_message,
            "replacement_tool": self.replacement_tool,
            "parameters": [
                {
                    "name": p.name,
                    "type": p.param_type.value,
                    "description": p.description,
                    "required": p.required,
                }
                for p in self.parameters
            ],
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
        }


# ============================================================
# USAGE ANALYTICS
# ============================================================


@dataclass
class ToolUsageStats:
    """Usage statistics for a tool."""

    tool_name: str
    call_count: int = 0
    success_count: int = 0
    error_count: int = 0
    total_latency_ms: float = 0.0
    last_called: Optional[datetime] = None
    last_error: Optional[str] = None
    error_types: Dict[str, int] = field(default_factory=dict)

    @property
    def success_rate(self) -> float:
        """Get success rate as percentage."""
        if self.call_count == 0:
            return 0.0
        return (self.success_count / self.call_count) * 100

    @property
    def average_latency_ms(self) -> float:
        """Get average latency in milliseconds."""
        if self.success_count == 0:
            return 0.0
        return self.total_latency_ms / self.success_count

    def record_call(self, success: bool, latency_ms: float, error: Optional[str] = None):
        """Record a tool call."""
        self.call_count += 1
        self.last_called = _utcnow()

        if success:
            self.success_count += 1
            self.total_latency_ms += latency_ms
        else:
            self.error_count += 1
            self.last_error = error
            if error:
                error_type = error.split(":")[0] if ":" in error else error[:50]
                self.error_types[error_type] = self.error_types.get(error_type, 0) + 1

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "tool_name": self.tool_name,
            "call_count": self.call_count,
            "success_count": self.success_count,
            "error_count": self.error_count,
            "success_rate": round(self.success_rate, 2),
            "average_latency_ms": round(self.average_latency_ms, 2),
            "last_called": self.last_called.isoformat() if self.last_called else None,
            "last_error": self.last_error,
            "top_errors": dict(
                sorted(self.error_types.items(), key=lambda x: x[1], reverse=True)[:5]
            ),
        }


class UsageAnalytics:
    """Tracks tool usage across the registry."""

    def __init__(self):
        self._stats: Dict[str, ToolUsageStats] = {}
        self._lock = threading.RLock()

    def record(self, tool_name: str, success: bool, latency_ms: float, error: Optional[str] = None):
        """Record a tool execution."""
        with self._lock:
            if tool_name not in self._stats:
                self._stats[tool_name] = ToolUsageStats(tool_name=tool_name)
            self._stats[tool_name].record_call(success, latency_ms, error)

    def get_stats(self, tool_name: str) -> Optional[ToolUsageStats]:
        """Get stats for a specific tool."""
        return self._stats.get(tool_name)

    def get_all_stats(self) -> Dict[str, ToolUsageStats]:
        """Get all tool stats."""
        return dict(self._stats)

    def get_top_tools(self, n: int = 10) -> List[ToolUsageStats]:
        """Get most frequently used tools."""
        return sorted(self._stats.values(), key=lambda s: s.call_count, reverse=True)[:n]

    def get_slowest_tools(self, n: int = 10) -> List[ToolUsageStats]:
        """Get slowest tools by average latency."""
        return sorted(
            [s for s in self._stats.values() if s.success_count > 0],
            key=lambda s: s.average_latency_ms,
            reverse=True,
        )[:n]

    def get_error_prone_tools(self, n: int = 10) -> List[ToolUsageStats]:
        """Get tools with highest error rates."""
        return sorted(
            [s for s in self._stats.values() if s.call_count >= 10],
            key=lambda s: s.error_count / max(s.call_count, 1),
            reverse=True,
        )[:n]

    def get_optimization_hints(self) -> List[dict]:
        """Get optimization suggestions based on analytics."""
        hints = []

        for stats in self._stats.values():
            # High error rate
            if stats.call_count >= 10 and stats.success_rate < 90:
                hints.append(
                    {
                        "tool": stats.tool_name,
                        "type": "high_error_rate",
                        "message": f"Tool has {100 - stats.success_rate:.1f}% error rate",
                        "severity": "warning" if stats.success_rate >= 70 else "error",
                    }
                )

            # High latency
            if stats.success_count >= 10 and stats.average_latency_ms > 5000:
                hints.append(
                    {
                        "tool": stats.tool_name,
                        "type": "high_latency",
                        "message": f"Average latency is {stats.average_latency_ms:.0f}ms",
                        "severity": "warning",
                    }
                )

            # Unused tools (called but rarely)
            if stats.call_count > 0 and stats.call_count < 5:
                hints.append(
                    {
                        "tool": stats.tool_name,
                        "type": "low_usage",
                        "message": f"Tool only called {stats.call_count} times",
                        "severity": "info",
                    }
                )

        return sorted(hints, key=lambda h: {"error": 0, "warning": 1, "info": 2}[h["severity"]])

    def reset(self, tool_name: Optional[str] = None):
        """Reset stats for a tool or all tools."""
        with self._lock:
            if tool_name:
                self._stats.pop(tool_name, None)
            else:
                self._stats.clear()

    def to_dict(self) -> dict:
        """Export all analytics as dictionary."""
        return {
            "tools": {name: stats.to_dict() for name, stats in self._stats.items()},
            "summary": {
                "total_tools_used": len(self._stats),
                "total_calls": sum(s.call_count for s in self._stats.values()),
                "total_errors": sum(s.error_count for s in self._stats.values()),
                "overall_success_rate": self._calculate_overall_success_rate(),
            },
            "hints": self.get_optimization_hints(),
        }

    def _calculate_overall_success_rate(self) -> float:
        total_calls = sum(s.call_count for s in self._stats.values())
        total_success = sum(s.success_count for s in self._stats.values())
        if total_calls == 0:
            return 0.0
        return (total_success / total_calls) * 100


# ============================================================
# TOOL REGISTRY
# ============================================================


class ToolRegistry:
    """
    Centralized tool registry with versioning, discovery, and analytics.

    Features:
    - Register tools with schemas and handlers
    - Version tracking with deprecation support
    - Discovery and filtering by category, tags, status
    - Usage analytics and optimization hints
    - Schema validation
    """

    def __init__(self, name: str = "default"):
        self.name = name
        self._tools: Dict[str, Dict[str, ToolDefinition]] = {}  # name -> version -> tool
        self._latest: Dict[str, str] = {}  # name -> latest version
        self._lock = threading.RLock()
        self.analytics = UsageAnalytics()

        logger.info(f"ToolRegistry '{name}' initialized")

    # ============================================================
    # REGISTRATION
    # ============================================================

    def register(
        self,
        name: str,
        description: str,
        version: Union[str, Version] = "1.0.0",
        parameters: Optional[List[ParameterSchema]] = None,
        handler: Optional[Callable] = None,
        category: ToolCategory = ToolCategory.GENERAL,
        safety_level: SafetyLevel = SafetyLevel.SAFE,
        tags: Optional[List[str]] = None,
        examples: Optional[List[Dict[str, Any]]] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> ToolDefinition:
        """
        Register a new tool or new version.

        Args:
            name: Unique tool name
            description: Human-readable description
            version: Semantic version string or Version object
            parameters: List of parameter schemas
            handler: Callable that executes the tool
            category: Tool category
            safety_level: Safety classification
            tags: Searchable tags
            examples: Usage examples
            metadata: Additional metadata

        Returns:
            The registered ToolDefinition
        """
        if isinstance(version, str):
            version = Version.parse(version)

        tool = ToolDefinition(
            name=name,
            description=description,
            version=version,
            parameters=parameters or [],
            handler=handler,
            category=category,
            safety_level=safety_level,
            tags=tags or [],
            examples=examples or [],
            metadata=metadata or {},
        )

        with self._lock:
            if name not in self._tools:
                self._tools[name] = {}

            version_str = str(version)
            if version_str in self._tools[name]:
                raise ValueError(f"Tool '{name}' version '{version_str}' already registered")

            self._tools[name][version_str] = tool

            # Update latest if this is newer
            if name not in self._latest or version > Version.parse(self._latest[name]):
                self._latest[name] = version_str

        logger.info(f"Registered tool: {name}@{version}")
        return tool

    def register_from_function(
        self,
        func: Callable,
        name: Optional[str] = None,
        version: str = "1.0.0",
        category: ToolCategory = ToolCategory.GENERAL,
        safety_level: SafetyLevel = SafetyLevel.SAFE,
        tags: Optional[List[str]] = None,
    ) -> ToolDefinition:
        """
        Register a tool from a function using its docstring and type hints.

        Args:
            func: Function to register as a tool
            name: Override function name
            version: Version string
            category: Tool category
            safety_level: Safety level
            tags: Tags for discovery

        Returns:
            The registered ToolDefinition
        """
        import inspect

        tool_name = name or func.__name__
        description = func.__doc__ or f"Tool: {tool_name}"

        # Extract parameters from type hints
        sig = inspect.signature(func)
        hints = func.__annotations__ if hasattr(func, "__annotations__") else {}

        parameters = []
        for param_name, param in sig.parameters.items():
            if param_name in ("self", "cls"):
                continue

            # Determine type from hints
            hint = hints.get(param_name)
            param_type = ParameterType.STRING  # Default

            if hint is str:
                param_type = ParameterType.STRING
            elif hint is int:
                param_type = ParameterType.INTEGER
            elif hint in (float, int, "number"):
                param_type = ParameterType.NUMBER
            elif hint is bool:
                param_type = ParameterType.BOOLEAN
            elif hint is list or (hasattr(hint, "__origin__") and hint.__origin__ is list):
                param_type = ParameterType.ARRAY
            elif hint is dict or (hasattr(hint, "__origin__") and hint.__origin__ is dict):
                param_type = ParameterType.OBJECT

            # Check if required (no default)
            required = param.default is inspect.Parameter.empty
            default = None if required else param.default

            parameters.append(
                ParameterSchema(
                    name=param_name,
                    param_type=param_type,
                    description=f"Parameter: {param_name}",
                    required=required,
                    default=default,
                )
            )

        return self.register(
            name=tool_name,
            description=description.strip(),
            version=version,
            parameters=parameters,
            handler=func,
            category=category,
            safety_level=safety_level,
            tags=tags,
        )

    def tool(
        self,
        name: Optional[str] = None,
        version: str = "1.0.0",
        category: ToolCategory = ToolCategory.GENERAL,
        safety_level: SafetyLevel = SafetyLevel.SAFE,
        tags: Optional[List[str]] = None,
    ):
        """
        Decorator to register a function as a tool.

        Usage:
            @registry.tool()
            def my_tool(query: str) -> str:
                '''Search for something.'''
                return search(query)
        """

        def decorator(func: Callable) -> Callable:
            self.register_from_function(
                func=func,
                name=name,
                version=version,
                category=category,
                safety_level=safety_level,
                tags=tags,
            )
            return func

        return decorator

    def unregister(self, name: str, version: Optional[str] = None) -> bool:
        """
        Unregister a tool or specific version.

        Args:
            name: Tool name
            version: Specific version to unregister (None = all versions)

        Returns:
            True if something was unregistered
        """
        with self._lock:
            if name not in self._tools:
                return False

            if version:
                if version in self._tools[name]:
                    del self._tools[name][version]
                    # Update latest if we removed it
                    if self._latest.get(name) == version:
                        remaining = sorted(
                            self._tools[name].keys(), key=lambda v: Version.parse(v), reverse=True
                        )
                        if remaining:
                            self._latest[name] = remaining[0]
                        else:
                            del self._latest[name]
                    logger.info(f"Unregistered tool: {name}@{version}")
                    return True
            else:
                del self._tools[name]
                self._latest.pop(name, None)
                logger.info(f"Unregistered all versions of tool: {name}")
                return True

            return False

    # ============================================================
    # DEPRECATION
    # ============================================================

    def deprecate(
        self, name: str, version: Optional[str] = None, message: str = "", replacement: str = ""
    ):
        """
        Mark a tool or version as deprecated.

        Args:
            name: Tool name
            version: Specific version (None = latest)
            message: Deprecation message
            replacement: Name of replacement tool
        """
        tool = self.get(name, version)
        if tool:
            tool.status = ToolStatus.DEPRECATED
            tool.deprecated_message = message
            tool.replacement_tool = replacement
            tool.updated_at = _utcnow()
            logger.warning(f"Deprecated tool: {name}@{tool.version}")

    def disable(self, name: str, version: Optional[str] = None):
        """Disable a tool (cannot be used)."""
        tool = self.get(name, version)
        if tool:
            tool.status = ToolStatus.DISABLED
            tool.updated_at = _utcnow()
            logger.warning(f"Disabled tool: {name}@{tool.version}")

    def enable(self, name: str, version: Optional[str] = None):
        """Re-enable a disabled tool."""
        tool = self.get(name, version)
        if tool:
            tool.status = ToolStatus.ACTIVE
            tool.updated_at = _utcnow()
            logger.info(f"Enabled tool: {name}@{tool.version}")

    # ============================================================
    # RETRIEVAL
    # ============================================================

    def get(self, name: str, version: Optional[str] = None) -> Optional[ToolDefinition]:
        """
        Get a tool by name and optional version.

        Args:
            name: Tool name
            version: Specific version (None = latest)

        Returns:
            ToolDefinition or None
        """
        with self._lock:
            if name not in self._tools:
                return None

            if version:
                return self._tools[name].get(version)

            # Return latest
            latest_version = self._latest.get(name)
            if latest_version:
                return self._tools[name].get(latest_version)

            return None

    def get_all_versions(self, name: str) -> List[ToolDefinition]:
        """Get all versions of a tool, sorted newest first."""
        with self._lock:
            if name not in self._tools:
                return []
            return sorted(self._tools[name].values(), key=lambda t: t.version, reverse=True)

    def exists(self, name: str, version: Optional[str] = None) -> bool:
        """Check if a tool exists."""
        return self.get(name, version) is not None

    # ============================================================
    # DISCOVERY
    # ============================================================

    def list_tools(
        self,
        category: Optional[ToolCategory] = None,
        status: Optional[ToolStatus] = None,
        safety_level: Optional[SafetyLevel] = None,
        tags: Optional[List[str]] = None,
        include_deprecated: bool = False,
        latest_only: bool = True,
    ) -> List[ToolDefinition]:
        """
        List tools with optional filtering.

        Args:
            category: Filter by category
            status: Filter by status
            safety_level: Filter by safety level
            tags: Filter by tags (any match)
            include_deprecated: Include deprecated tools
            latest_only: Only return latest version of each tool

        Returns:
            List of matching ToolDefinitions
        """
        results = []

        with self._lock:
            for name, versions in self._tools.items():
                if latest_only:
                    latest_ver = self._latest.get(name)
                    tools = [versions[latest_ver]] if latest_ver and latest_ver in versions else []
                else:
                    tools = list(versions.values())

                for tool in tools:
                    # Apply filters
                    if not include_deprecated and tool.is_deprecated:
                        continue
                    if category and tool.category != category:
                        continue
                    if status and tool.status != status:
                        continue
                    if safety_level and tool.safety_level != safety_level:
                        continue
                    if tags and not any(t in tool.tags for t in tags):
                        continue

                    results.append(tool)

        return sorted(results, key=lambda t: t.name)

    def search(self, query: str, limit: int = 20) -> List[ToolDefinition]:
        """
        Search tools by name, description, or tags.

        Args:
            query: Search query
            limit: Maximum results

        Returns:
            List of matching tools, ranked by relevance
        """
        query_lower = query.lower()
        scored = []

        with self._lock:
            for name, versions in self._tools.items():
                latest_ver = self._latest.get(name)
                if not latest_ver or latest_ver not in versions:
                    continue

                tool = versions[latest_ver]
                score = 0

                # Name match (highest weight)
                if query_lower in tool.name.lower():
                    score += 10
                    if tool.name.lower().startswith(query_lower):
                        score += 5

                # Description match
                if query_lower in tool.description.lower():
                    score += 3

                # Tag match
                for tag in tool.tags:
                    if query_lower in tag.lower():
                        score += 2

                if score > 0:
                    scored.append((score, tool))

        # Sort by score descending
        scored.sort(key=lambda x: x[0], reverse=True)
        return [tool for _, tool in scored[:limit]]

    def get_categories(self) -> Dict[ToolCategory, int]:
        """Get all categories with tool counts."""
        counts: Dict[ToolCategory, int] = defaultdict(int)

        for tool in self.list_tools(include_deprecated=True):
            counts[tool.category] += 1

        return dict(counts)

    def get_tags(self) -> Dict[str, int]:
        """Get all tags with usage counts."""
        counts: Dict[str, int] = defaultdict(int)

        for tool in self.list_tools(include_deprecated=True):
            for tag in tool.tags:
                counts[tag] += 1

        return dict(sorted(counts.items(), key=lambda x: x[1], reverse=True))

    # ============================================================
    # EXECUTION
    # ============================================================

    def execute(
        self, name: str, input_data: dict, version: Optional[str] = None, validate: bool = True
    ) -> Any:
        """
        Execute a tool with the given input.

        Args:
            name: Tool name
            input_data: Input parameters
            version: Specific version (None = latest)
            validate: Validate input against schema

        Returns:
            Tool execution result
        """
        tool = self.get(name, version)
        if not tool:
            raise ValueError(f"Tool not found: {name}@{version or 'latest'}")

        if tool.status == ToolStatus.DISABLED:
            raise ValueError(f"Tool is disabled: {name}")

        if tool.is_deprecated:
            logger.warning(
                f"Using deprecated tool: {name}. {tool.deprecated_message}"
                f"{f' Use {tool.replacement_tool} instead.' if tool.replacement_tool else ''}"
            )

        if not tool.handler:
            raise ValueError(f"Tool has no handler: {name}")

        # Validate input
        if validate:
            is_valid, errors = tool.validate_input(input_data)
            if not is_valid:
                raise ValueError(f"Invalid input for {name}: {'; '.join(errors)}")

        # Execute with analytics
        start_time = time.time()
        error_msg = None

        try:
            result = tool.handler(**input_data)
            success = True
            return result
        except Exception as e:
            success = False
            error_msg = str(e)
            raise
        finally:
            latency_ms = (time.time() - start_time) * 1000
            self.analytics.record(name, success, latency_ms, error_msg)

    async def execute_async(
        self, name: str, input_data: dict, version: Optional[str] = None, validate: bool = True
    ) -> Any:
        """Execute a tool asynchronously."""
        import asyncio

        tool = self.get(name, version)
        if not tool:
            raise ValueError(f"Tool not found: {name}@{version or 'latest'}")

        if tool.status == ToolStatus.DISABLED:
            raise ValueError(f"Tool is disabled: {name}")

        if not tool.handler:
            raise ValueError(f"Tool has no handler: {name}")

        if validate:
            is_valid, errors = tool.validate_input(input_data)
            if not is_valid:
                raise ValueError(f"Invalid input for {name}: {'; '.join(errors)}")

        start_time = time.time()
        error_msg = None

        try:
            if asyncio.iscoroutinefunction(tool.handler):
                result = await tool.handler(**input_data)
            else:
                result = tool.handler(**input_data)
            success = True
            return result
        except Exception as e:
            success = False
            error_msg = str(e)
            raise
        finally:
            latency_ms = (time.time() - start_time) * 1000
            self.analytics.record(name, success, latency_ms, error_msg)

    # ============================================================
    # EXPORT
    # ============================================================

    def get_schemas(
        self,
        format: str = "openai",
        category: Optional[ToolCategory] = None,
        include_deprecated: bool = False,
    ) -> List[dict]:
        """
        Get tool schemas for LLM tool calling.

        Args:
            format: 'openai' or 'anthropic'
            category: Filter by category
            include_deprecated: Include deprecated tools

        Returns:
            List of tool schemas
        """
        tools = self.list_tools(category=category, include_deprecated=include_deprecated)

        schemas = []
        for tool in tools:
            if tool.status == ToolStatus.DISABLED:
                continue

            if format == "anthropic":
                schemas.append(tool.to_anthropic_schema())
            else:
                schemas.append(tool.to_json_schema())

        return schemas

    def export_registry(self) -> dict:
        """Export full registry state."""
        tools_export = {}

        with self._lock:
            for name, versions in self._tools.items():
                tools_export[name] = {
                    "versions": {v: t.to_dict() for v, t in versions.items()},
                    "latest": self._latest.get(name),
                }

        return {
            "name": self.name,
            "tools": tools_export,
            "analytics": self.analytics.to_dict(),
            "summary": {
                "total_tools": len(self._tools),
                "total_versions": sum(len(v) for v in self._tools.values()),
                "categories": {k.value: v for k, v in self.get_categories().items()},
                "by_status": self._count_by_status(),
                "by_safety": self._count_by_safety(),
            },
        }

    def _count_by_status(self) -> Dict[str, int]:
        counts: Dict[str, int] = defaultdict(int)
        for tool in self.list_tools(include_deprecated=True):
            counts[tool.status.value] += 1
        return dict(counts)

    def _count_by_safety(self) -> Dict[str, int]:
        counts: Dict[str, int] = defaultdict(int)
        for tool in self.list_tools(include_deprecated=True):
            counts[tool.safety_level.value] += 1
        return dict(counts)

    def __len__(self) -> int:
        """Get number of registered tools."""
        return len(self._tools)

    def __contains__(self, name: str) -> bool:
        """Check if tool is registered."""
        return name in self._tools


# ============================================================
# GLOBAL REGISTRY
# ============================================================

_default_registry: Optional[ToolRegistry] = None
_registries: Dict[str, ToolRegistry] = {}


def get_tool_registry(name: str = "default") -> ToolRegistry:
    """Get or create a tool registry by name."""
    global _default_registry, _registries  # noqa: F841 - _registries mutated via dict write

    if name == "default":
        if _default_registry is None:
            _default_registry = ToolRegistry(name="default")
        return _default_registry

    if name not in _registries:
        _registries[name] = ToolRegistry(name=name)

    return _registries[name]


def set_tool_registry(registry: ToolRegistry, name: str = "default"):
    """Set a registry as the named registry."""
    global _default_registry, _registries

    if name == "default":
        _default_registry = registry
    else:
        _registries[name] = registry


def reset_tool_registry(name: str = "default"):
    """Reset a registry to empty state."""
    global _default_registry, _registries  # noqa: F841 - _registries mutated via dict pop

    if name == "default":
        _default_registry = ToolRegistry(name="default")
    elif name in _registries:
        _registries[name] = ToolRegistry(name=name)


# ============================================================
# CONVENIENCE FUNCTIONS
# ============================================================


def register_tool(
    name: str,
    description: str,
    version: str = "1.0.0",
    parameters: Optional[List[ParameterSchema]] = None,
    handler: Optional[Callable] = None,
    category: ToolCategory = ToolCategory.GENERAL,
    safety_level: SafetyLevel = SafetyLevel.SAFE,
    tags: Optional[List[str]] = None,
) -> ToolDefinition:
    """Register a tool in the default registry."""
    return get_tool_registry().register(
        name=name,
        description=description,
        version=version,
        parameters=parameters,
        handler=handler,
        category=category,
        safety_level=safety_level,
        tags=tags,
    )


def tool(
    name: Optional[str] = None,
    version: str = "1.0.0",
    category: ToolCategory = ToolCategory.GENERAL,
    safety_level: SafetyLevel = SafetyLevel.SAFE,
    tags: Optional[List[str]] = None,
):
    """Decorator to register a function as a tool in the default registry."""
    return get_tool_registry().tool(
        name=name, version=version, category=category, safety_level=safety_level, tags=tags
    )


def get_tool(name: str, version: Optional[str] = None) -> Optional[ToolDefinition]:
    """Get a tool from the default registry."""
    return get_tool_registry().get(name, version)


def list_tools(**kwargs) -> List[ToolDefinition]:
    """List tools from the default registry."""
    return get_tool_registry().list_tools(**kwargs)


def search_tools(query: str, limit: int = 20) -> List[ToolDefinition]:
    """Search tools in the default registry."""
    return get_tool_registry().search(query, limit)


def execute_tool(name: str, input_data: dict, **kwargs) -> Any:
    """Execute a tool from the default registry."""
    return get_tool_registry().execute(name, input_data, **kwargs)


# ============================================================
# EXPORTS
# ============================================================

__all__ = [
    # Enums
    "ToolCategory",
    "ToolStatus",
    "SafetyLevel",
    "ParameterType",
    # Version
    "Version",
    # Schema
    "ParameterSchema",
    # Tool Definition
    "ToolDefinition",
    # Analytics
    "ToolUsageStats",
    "UsageAnalytics",
    # Registry
    "ToolRegistry",
    # Global functions
    "get_tool_registry",
    "set_tool_registry",
    "reset_tool_registry",
    # Convenience functions
    "register_tool",
    "tool",
    "get_tool",
    "list_tools",
    "search_tools",
    "execute_tool",
]
