"""
Tool Registry - Central registry for tool definitions and handlers.

Provides decorator and programmatic registration of tools,
schema validation, and tool lookup.

Code is the single source of truth for tools. Define tools in code,
register them with the platform, and the UI displays them (read-only).

Supports:
- Decorator-based registration (@registry.tool(...))
- Programmatic registration (registry.register_tool(...))
- Auto-discovery of handlers (registry.discover())
- Schema export for static analysis
"""
import inspect
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Any, Awaitable, Callable, Optional, Union

from memrail.tools.models import (
    ActionHandlers,
    ExecutorSchema,
    ToolDefinition,
)
from memrail.tools.signature_validation import (
    validate_handler_signature,
    SignatureValidationResult,
)



logger = logging.getLogger(__name__)


class ToolRegistry:
    """
    Central registry for tool definitions and handlers.

    Supports:
    - Decorator-based registration (@registry.tool(...))
    - Programmatic registration (registry.register_tool(...))
    - Action type handler registration
    - Schema export for static analysis
    - Tool lookup and listing

    Example:
        ```python
        registry = ToolRegistry()

        @registry.tool("send_email", "Send email", {"to": str, "subject": str})
        async def send_email(args: dict) -> dict:
            return {"sent": True}

        tool = registry.get_tool("send_email")
        ```
    """

    def __init__(
        self,
        server_name: Optional[str] = None,
        validate_signatures: bool = True,
        strict_validation: bool = False,
    ):
        """
        Initialize the tool registry.

        Args:
            server_name: Optional name for the MCP server (for Claude integration)
            validate_signatures: If True, validate handler signatures match schemas
                at registration time (default: True)
            strict_validation: If True, treat signature validation warnings as errors
                (default: False)
        """
        self._tools: dict[str, ToolDefinition] = {}
        self._action_handlers = ActionHandlers()
        self._server_name = server_name
        self._validate_signatures = validate_signatures
        self._strict_validation = strict_validation

    @property
    def server_name(self) -> Optional[str]:
        """Get the server name."""
        return self._server_name

    @server_name.setter
    def server_name(self, value: str) -> None:
        """Set the server name."""
        self._server_name = value

    # =========================================================================
    # Tool Registration
    # =========================================================================

    def tool(
        self,
        name: str,
        description: str,
        schema: dict[str, Any],
        projects: list[str],
        *,
        capabilities: Optional[list[str]] = None,
        constraints: Optional[list[str]] = None,
        examples: Optional[list[dict]] = None,
        idempotency_key: Optional[str] = None,
        rate_limit: Optional[str] = None,
        timeout_ms: Optional[int] = None,
    ) -> Callable:
        """
        Decorator for registering a tool.

        Args:
            name: Tool identifier (matches tool_id in EMU actions)
            description: Human-readable description
            schema: Input schema (Python types or JSON Schema)
            projects: List of project IDs this tool belongs to (required)
            capabilities: List of capability tags
            constraints: List of constraint descriptions
            examples: List of example inputs/outputs
            idempotency_key: Template for generating idempotency keys
            rate_limit: Rate limit declaration
            timeout_ms: Execution timeout in milliseconds

        Returns:
            Decorator function

        Raises:
            ValueError: If projects is empty

        Example:
            ```python
            @registry.tool(
                name="send_email",
                description="Send an email",
                schema={"to": str, "subject": str, "body": str},
                projects=["customer_support", "notifications"],
                capabilities=["communication"],
            )
            async def send_email(args: dict) -> dict:
                return {"sent": True}
            ```
        """
        # Validate projects is non-empty
        if not projects:
            raise ValueError(f"Tool '{name}' must specify at least one project")
        def decorator(func: Callable[[dict], Awaitable[dict[str, Any]]]) -> Callable:
            # Validate handler is async
            if not inspect.iscoroutinefunction(func):
                raise TypeError(
                    f"Tool handler '{name}' must be an async function (async def)"
                )

            # Validate handler signature matches schema
            signature_warnings: list[str] = []
            handler_pattern: str | None = None

            if self._validate_signatures:
                validation_result = validate_handler_signature(
                    handler=func,
                    schema=schema,
                    tool_name=name,
                    strict=self._strict_validation,
                )
                validation_result.raise_if_invalid(name)

                # Store validation metadata
                handler_pattern = validation_result.handler_pattern
                signature_warnings = [
                    f"{w.field}: {w.message}" for w in validation_result.warnings
                ]

                # Log warnings if not strict
                if validation_result.warnings:
                    for warning in validation_result.warnings:
                        logger.warning(
                            f"Tool '{name}' signature warning: {warning.field}: {warning.message}"
                        )

            # Create tool definition
            tool_def = ToolDefinition(
                name=name,
                description=description,
                input_schema=schema,
                projects=projects,
                handler=func,
                capabilities=capabilities or [],
                constraints=constraints or [],
                signature_warnings=signature_warnings,
                handler_pattern=handler_pattern,
                examples=examples or [],
                idempotency_key=idempotency_key,
                rate_limit=rate_limit,
                timeout_ms=timeout_ms,
            )

            # Register the tool
            self._register_tool_definition(tool_def)

            return func

        return decorator

    def register_tool(
        self,
        name: str,
        description: str,
        schema: dict[str, Any],
        projects: list[str],
        handler: Optional[Callable[[dict], Awaitable[dict[str, Any]]]] = None,
        *,
        capabilities: Optional[list[str]] = None,
        constraints: Optional[list[str]] = None,
        examples: Optional[list[dict]] = None,
        idempotency_key: Optional[str] = None,
        rate_limit: Optional[str] = None,
        timeout_ms: Optional[int] = None,
    ) -> ToolDefinition:
        """
        Programmatically register a tool.

        Args:
            name: Tool identifier
            description: Human-readable description
            schema: Input schema
            projects: List of project IDs this tool belongs to (required)
            handler: Async function to execute the tool
            capabilities: List of capability tags
            constraints: List of constraint descriptions
            examples: List of example inputs/outputs
            idempotency_key: Template for generating idempotency keys
            rate_limit: Rate limit declaration
            timeout_ms: Execution timeout in milliseconds

        Returns:
            The registered ToolDefinition

        Raises:
            ValueError: If projects is empty

        Example:
            ```python
            registry.register_tool(
                name="send_email",
                description="Send email",
                schema={"to": str, "subject": str},
                projects=["customer_support"],
                handler=my_email_handler,
            )
            ```
        """
        # Validate projects is non-empty
        if not projects:
            raise ValueError(f"Tool '{name}' must specify at least one project")
        # Validate handler is async if provided
        if handler is not None and not inspect.iscoroutinefunction(handler):
            raise TypeError(
                f"Tool handler '{name}' must be an async function (async def)"
            )

        # Validate handler signature matches schema
        signature_warnings: list[str] = []
        handler_pattern: str | None = None

        if handler is not None and self._validate_signatures:
            validation_result = validate_handler_signature(
                handler=handler,
                schema=schema,
                tool_name=name,
                strict=self._strict_validation,
            )
            validation_result.raise_if_invalid(name)

            # Store validation metadata
            handler_pattern = validation_result.handler_pattern
            signature_warnings = [
                f"{w.field}: {w.message}" for w in validation_result.warnings
            ]

            # Log warnings if not strict
            if validation_result.warnings:
                for warning in validation_result.warnings:
                    logger.warning(
                        f"Tool '{name}' signature warning: {warning.field}: {warning.message}"
                    )

        # Create tool definition
        tool_def = ToolDefinition(
            name=name,
            description=description,
            input_schema=schema,
            projects=projects,
            handler=handler,
            capabilities=capabilities or [],
            constraints=constraints or [],
            signature_warnings=signature_warnings,
            handler_pattern=handler_pattern,
            examples=examples or [],
            idempotency_key=idempotency_key,
            rate_limit=rate_limit,
            timeout_ms=timeout_ms,
        )

        self._register_tool_definition(tool_def)
        return tool_def

    def _register_tool_definition(self, tool_def: ToolDefinition) -> None:
        """Internal method to register a tool definition."""
        if tool_def.name in self._tools:
            logger.warning(f"Overwriting existing tool: {tool_def.name}")

        self._tools[tool_def.name] = tool_def
        self._action_handlers.tool_call = True  # Mark that we have tool handlers

        logger.debug(f"Registered tool: {tool_def.name}")

    # =========================================================================
    # Project Filtering
    # =========================================================================

    def get_tools_for_project(self, project_id: str) -> dict[str, ToolDefinition]:
        """
        Return tools available for a specific project.

        Args:
            project_id: The project identifier to filter by

        Returns:
            Dictionary of tool name to ToolDefinition for tools in the project
        """
        return {
            name: tool for name, tool in self._tools.items()
            if project_id in tool.projects
        }

    def get_all_projects(self) -> set[str]:
        """
        Return all unique project IDs across registered tools.

        Returns:
            Set of project identifiers
        """
        projects: set[str] = set()
        for tool in self._tools.values():
            projects.update(tool.projects)
        return projects

    def associate_tool_with_project(
        self,
        tool_name: str,
        project_id: str,
        *,
        update_capabilities: Optional[list[str]] = None,
        update_constraints: Optional[list[str]] = None,
    ) -> ToolDefinition:
        """
        Associate an existing tool with a project and update its annotations.

        When a tool is registered to a project, this method updates the tool's
        project associations and optionally updates its annotations (capabilities
        and constraints).

        Args:
            tool_name: Name of the registered tool
            project_id: Project ID to associate the tool with
            update_capabilities: Optional list of capabilities to add to the tool
            update_constraints: Optional list of constraints to add to the tool

        Returns:
            The updated ToolDefinition

        Raises:
            KeyError: If the tool is not registered

        Example:
            ```python
            # Associate tool with a new project
            registry.associate_tool_with_project("send_email", "marketing")

            # Associate and add project-specific capabilities
            registry.associate_tool_with_project(
                "send_email",
                "notifications",
                update_capabilities=["bulk_send", "templating"],
            )
            ```
        """
        if tool_name not in self._tools:
            raise KeyError(f"Tool '{tool_name}' is not registered")

        tool_def = self._tools[tool_name]

        # Add project if not already associated
        if project_id not in tool_def.projects:
            tool_def.projects.append(project_id)
            logger.debug(f"Associated tool '{tool_name}' with project '{project_id}'")

        # Update annotations if provided
        if update_capabilities:
            for cap in update_capabilities:
                if cap not in tool_def.capabilities:
                    tool_def.capabilities.append(cap)
            logger.debug(f"Updated capabilities for tool '{tool_name}': {tool_def.capabilities}")

        if update_constraints:
            for constraint in update_constraints:
                if constraint not in tool_def.constraints:
                    tool_def.constraints.append(constraint)
            logger.debug(f"Updated constraints for tool '{tool_name}': {tool_def.constraints}")

        return tool_def

    def register_native_tool(
        self,
        tool: Any,
        projects: list[str],
        *,
        capabilities: Optional[list[str]] = None,
        constraints: Optional[list[str]] = None,
        idempotency_key: Optional[str] = None,
        rate_limit: Optional[str] = None,
        timeout_ms: Optional[int] = None,
    ) -> ToolDefinition:
        """
        Register a tool created with Claude's native @tool decorator.

        Supports tools from:
        - Claude Agent SDK: @tool decorated functions (SdkMcpTool)
        - Anthropic SDK: @beta_tool decorated functions

        Args:
            tool: A tool created with @tool or @beta_tool decorator
            projects: List of project IDs this tool belongs to (required)
            capabilities: List of capability tags
            constraints: List of constraint descriptions
            idempotency_key: Template for generating idempotency keys
            rate_limit: Rate limit declaration
            timeout_ms: Execution timeout in milliseconds

        Returns:
            The registered ToolDefinition

        Example:
            ```python
            from claude_agent_sdk import tool

            @tool("greet", "Greet a user", {"name": str})
            async def greet(args: dict) -> dict:
                return {"message": f"Hello, {args['name']}!"}

            registry.register_native_tool(greet, projects=["my_project"])
            ```

            ```python
            from anthropic import beta_tool

            @beta_tool
            def get_weather(location: str) -> str:
                '''Get weather for a location'''
                return f"Weather in {location}: Sunny"

            registry.register_native_tool(get_weather, projects=["weather_app"])
            ```
        """
        name, description, schema, handler = self._extract_native_tool_info(tool)

        return self.register_tool(
            name=name,
            description=description,
            schema=schema,
            projects=projects,
            handler=handler,
            capabilities=capabilities,
            constraints=constraints,
            idempotency_key=idempotency_key,
            rate_limit=rate_limit,
            timeout_ms=timeout_ms,
        )

    def register_native_tools(
        self,
        tools: list[Any],
        projects: list[str],
        *,
        capabilities: Optional[list[str]] = None,
        constraints: Optional[list[str]] = None,
    ) -> list[ToolDefinition]:
        """
        Register multiple tools created with Claude's native @tool decorator.

        Args:
            tools: List of tools created with @tool or @beta_tool decorator
            projects: List of project IDs these tools belong to (required)
            capabilities: Default capabilities for all tools
            constraints: Default constraints for all tools

        Returns:
            List of registered ToolDefinitions

        Example:
            ```python
            registry.register_native_tools(
                [greet, get_weather, send_email],
                projects=["my_project"],
            )
            ```
        """
        return [
            self.register_native_tool(
                tool,
                projects=projects,
                capabilities=capabilities,
                constraints=constraints,
            )
            for tool in tools
        ]

    def _extract_native_tool_info(
        self,
        tool: Any,
    ) -> tuple[str, str, dict[str, Any], Callable]:
        """
        Extract tool information from a native @tool or @beta_tool decorated function.

        Returns:
            Tuple of (name, description, schema, handler)
        """
        # Check for Claude Agent SDK @tool (SdkMcpTool)
        if hasattr(tool, "name") and hasattr(tool, "description") and hasattr(tool, "input_schema"):
            name = tool.name
            description = tool.description
            schema = tool.input_schema if isinstance(tool.input_schema, dict) else {}

            # The tool itself should be callable or have a handler
            if callable(tool):
                handler = self._wrap_native_handler(tool)
            elif hasattr(tool, "handler") and callable(tool.handler):
                handler = self._wrap_native_handler(tool.handler)
            elif hasattr(tool, "__call__"):
                handler = self._wrap_native_handler(tool.__call__)
            else:
                raise TypeError(f"Native tool '{name}' is not callable")

            return name, description, schema, handler

        # Check for Anthropic SDK @beta_tool
        if hasattr(tool, "__anthropic_tool__"):
            tool_info = tool.__anthropic_tool__
            name = tool_info.get("name", tool.__name__)
            description = tool_info.get("description", tool.__doc__ or "")
            schema = tool_info.get("input_schema", {})
            handler = self._wrap_typed_handler(tool)
            return name, description, schema, handler

        # Check for function with tool metadata attributes
        if hasattr(tool, "tool_name") and hasattr(tool, "tool_description"):
            name = tool.tool_name
            description = tool.tool_description
            schema = getattr(tool, "tool_schema", getattr(tool, "input_schema", {}))
            handler = self._wrap_native_handler(tool) if self._is_args_dict_handler(tool) else self._wrap_typed_handler(tool)
            return name, description, schema, handler

        # Fallback: Try to extract from function signature and docstring
        if callable(tool):
            name = getattr(tool, "__name__", "unknown_tool")
            description = tool.__doc__ or f"Tool: {name}"
            schema = self._infer_schema_from_signature(tool)
            handler = self._wrap_typed_handler(tool)
            return name, description, schema, handler

        raise TypeError(
            f"Cannot register native tool: {tool}. "
            "Expected a tool created with @tool or @beta_tool decorator, "
            "or a callable with tool metadata attributes."
        )

    def _is_args_dict_handler(self, func: Callable) -> bool:
        """Check if function expects args: dict pattern."""
        sig = inspect.signature(func)
        params = list(sig.parameters.values())
        if len(params) == 1:
            param = params[0]
            # Check if annotated as dict
            if param.annotation is dict or str(param.annotation) == "dict[str, Any]":
                return True
            # Check if parameter name suggests dict pattern
            if param.name in ("args", "arguments", "params", "input"):
                return True
        return False

    def _wrap_native_handler(self, handler: Callable) -> Callable:
        """
        Wrap a native handler that expects args: dict.

        Ensures the handler is async and matches expected signature.
        """
        if inspect.iscoroutinefunction(handler):
            return handler

        # Check if it's a callable object with async __call__
        if hasattr(handler, "__call__") and inspect.iscoroutinefunction(handler.__call__):
            async def async_call_wrapper(args: dict) -> dict:
                result = await handler(args)
                return result if isinstance(result, dict) else {"result": result}
            return async_call_wrapper

        # Wrap sync handler in async
        async def async_wrapper(args: dict) -> dict:
            result = handler(args)
            # Handle case where result might be a coroutine
            if inspect.iscoroutine(result):
                result = await result
            return result if isinstance(result, dict) else {"result": result}

        return async_wrapper

    def _wrap_typed_handler(self, handler: Callable) -> Callable:
        """
        Wrap a typed handler (keyword arguments) to match args: dict signature.

        Converts from args: dict to individual keyword arguments.
        """
        if inspect.iscoroutinefunction(handler):
            async def async_typed_wrapper(args: dict) -> dict:
                result = await handler(**args)
                return result if isinstance(result, dict) else {"result": result}
            return async_typed_wrapper
        else:
            async def sync_typed_wrapper(args: dict) -> dict:
                result = handler(**args)
                return result if isinstance(result, dict) else {"result": result}
            return sync_typed_wrapper

    def _infer_schema_from_signature(self, func: Callable) -> dict[str, Any]:
        """
        Infer JSON schema from function signature.

        Uses type hints to generate a schema.
        """
        sig = inspect.signature(func)
        properties = {}
        required = []

        type_mapping = {
            str: {"type": "string"},
            int: {"type": "integer"},
            float: {"type": "number"},
            bool: {"type": "boolean"},
            list: {"type": "array"},
            dict: {"type": "object"},
        }

        for name, param in sig.parameters.items():
            if name in ("self", "cls"):
                continue

            # Skip if it's the args: dict pattern
            if name in ("args", "arguments") and param.annotation in (dict, inspect.Parameter.empty):
                continue

            prop_schema = {"type": "string"}  # Default

            if param.annotation != inspect.Parameter.empty:
                annotation = param.annotation
                # Handle Optional types
                origin = getattr(annotation, "__origin__", None)
                if origin is Union:
                    args = getattr(annotation, "__args__", ())
                    non_none = [a for a in args if a is not type(None)]
                    if non_none:
                        annotation = non_none[0]

                if annotation in type_mapping:
                    prop_schema = type_mapping[annotation].copy()

            properties[name] = prop_schema

            # Required if no default value
            if param.default is inspect.Parameter.empty:
                required.append(name)

        if not properties:
            return {}

        return {
            "type": "object",
            "properties": properties,
            "required": required,
        }

    # =========================================================================
    # Action Type Handler Registration
    # =========================================================================

    def on_route(self, func: Callable) -> Callable:
        """
        Decorator to register a route action handler.

        Args:
            func: Async function (destination: str, metadata: dict) -> dict

        Example:
            ```python
            @registry.on_route
            async def handle_route(destination: str, metadata: dict) -> dict:
                await queue.enqueue(destination, metadata)
                return {"queued": True}
            ```
        """
        if not inspect.iscoroutinefunction(func):
            raise TypeError("Route handler must be an async function")

        self._action_handlers.route = True
        self._action_handlers._route_handler = func
        return func

    def on_decision_prompt(self, func: Callable) -> Callable:
        """
        Decorator to register a decision prompt handler.

        Args:
            func: Async function (prompt: str, options: list[dict]) -> dict

        Example:
            ```python
            @registry.on_decision_prompt
            async def handle_prompt(prompt: str, options: list) -> dict:
                response = await llm.complete(prompt)
                return {"response": response}
            ```
        """
        if not inspect.iscoroutinefunction(func):
            raise TypeError("Decision prompt handler must be an async function")

        self._action_handlers.decision_prompt = True
        self._action_handlers._decision_prompt_handler = func
        return func

    def on_context_directive(self, decision_point: Optional[str] = None, *, project: Optional[str] = None):
        """
        Decorator to register a context directive handler.

        Args:
            decision_point: Decision point to handle (None = wildcard)
            project: Optional project scope

        Examples:
            @registry.on_context_directive("post-checkout")
            async def inject_checkout(directive, metadata): ...

            @registry.on_context_directive("pre-response", project="my-agent")
            async def inject_response(directive, metadata): ...

            @registry.on_context_directive()  # wildcard
            async def inject_any(directive, metadata): ...
        """
        def decorator(func):
            if not inspect.iscoroutinefunction(func):
                raise TypeError("Context directive handler must be an async function")
            key = "*" if decision_point is None else (
                f"{decision_point}:{project}" if project else decision_point
            )
            self._action_handlers._context_directive_handlers[key] = func
            self._action_handlers.context_directive = True
            return func
        return decorator

    # =========================================================================
    # Tool Lookup
    # =========================================================================

    def get_tool(self, name: str) -> Optional[ToolDefinition]:
        """
        Get a tool by name.

        Args:
            name: Tool identifier

        Returns:
            ToolDefinition or None if not found
        """
        return self._tools.get(name)

    def list_tools(self) -> list[ToolDefinition]:
        """
        List all registered tools.

        Returns:
            List of all ToolDefinition objects
        """
        return list(self._tools.values())

    def get_action_handlers(self) -> ActionHandlers:
        """
        Get the action handlers registry.

        Returns:
            ActionHandlers instance with handler status
        """
        return self._action_handlers

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

    # =========================================================================
    # Schema Export
    # =========================================================================

    def export_schema(self, project: Optional[str] = None) -> ExecutorSchema:
        """
        Export the registry schema for static analysis.

        Only includes tools that have handlers - tools without handlers
        are not executable and should not be uploaded.

        Args:
            project: Optional project ID to filter tools by. If provided, only
                     tools registered to this project are included.

        Returns:
            ExecutorSchema with tool definitions (handler-only)
        """
        # Get tools to export (optionally filtered by project)
        if project:
            tools_to_export = self.get_tools_for_project(project)
        else:
            tools_to_export = self._tools

        tools_dict = {}
        for name, tool_def in tools_to_export.items():
            # Skip tools without handlers - they can't be executed
            if not tool_def.has_handler:
                continue
            tools_dict[name] = {
                "name": tool_def.name,
                "description": tool_def.description,
                "input_schema": tool_def.to_json_schema(),
                "has_handler": tool_def.has_handler,
                "capabilities": tool_def.capabilities,
                "constraints": tool_def.constraints,
                "projects": tool_def.projects,
            }
            if tool_def.idempotency_key:
                tools_dict[name]["idempotency_key"] = tool_def.idempotency_key
            if tool_def.rate_limit:
                tools_dict[name]["rate_limit"] = tool_def.rate_limit
            if tool_def.timeout_ms:
                tools_dict[name]["timeout_ms"] = tool_def.timeout_ms
            if tool_def.signature_warnings:
                tools_dict[name]["signature_warnings"] = tool_def.signature_warnings
            if tool_def.handler_pattern:
                tools_dict[name]["handler_pattern"] = tool_def.handler_pattern

        # Build context_directive value: false | true | list[str]
        cd_handlers = self._action_handlers._context_directive_handlers
        if not cd_handlers:
            cd_value = False
        elif "*" in cd_handlers:
            cd_value = True
        else:
            cd_value = list(cd_handlers.keys())

        action_handlers = {
            "tool_call": self._action_handlers.tool_call,
            "context_directive": cd_value,
            "route": self._action_handlers.route,
            "decision_prompt": self._action_handlers.decision_prompt,
        }

        return ExecutorSchema(
            version="1.0.0",
            server_name=self._server_name,
            generated_at=datetime.utcnow().isoformat() + "Z",
            tools=tools_dict,
            action_handlers=action_handlers,
            lifecycle={
                "shadow_mode": "skip",  # Default, can be overridden
                "canary_decider": False,
            },
        )

    def export_schema_to_file(
        self,
        path: Union[str, Path],
        format: str = "json",
        project: Optional[str] = None,
    ) -> None:
        """
        Export the registry schema to a file.

        Args:
            path: File path to write to
            format: Output format ("json" or "yaml")
            project: Optional project ID to filter tools by

        Raises:
            ImportError: If PyYAML not installed for YAML format
        """
        schema = self.export_schema(project=project)
        schema_dict = schema.to_dict()

        path = Path(path)
        path.parent.mkdir(parents=True, exist_ok=True)

        if format == "yaml":
            try:
                import yaml
            except ImportError:
                raise ImportError("PyYAML required for YAML export: pip install pyyaml")
            with open(path, "w") as f:
                yaml.dump(schema_dict, f, default_flow_style=False, sort_keys=False)
        else:
            with open(path, "w") as f:
                json.dump(schema_dict, f, indent=2)

    # =========================================================================
    # Load from File (for static analysis without running code)
    # =========================================================================

    @classmethod
    def from_file(cls, path: Union[str, Path]) -> "ToolRegistry":
        """
        Load a registry from a schema file (for static analysis).

        Note: Loaded tools won't have handlers - only schema data.

        Args:
            path: Path to schema file (JSON or YAML)

        Returns:
            ToolRegistry with tool definitions (no handlers)
        """
        path = Path(path)

        if not path.exists():
            raise FileNotFoundError(f"Schema file not found: {path}")

        content = path.read_text()

        if path.suffix in (".yaml", ".yml"):
            try:
                import yaml
            except ImportError:
                raise ImportError("PyYAML required for YAML files: pip install pyyaml")
            data = yaml.safe_load(content)
        else:
            data = json.loads(content)

        # Create registry
        registry = cls(server_name=data.get("server_name"))

        # Load tools
        for name, tool_data in data.get("tools", {}).items():
            # Projects is required - use from schema or ["_unknown"] for legacy schemas
            projects = tool_data.get("projects", [])
            if not projects:
                logger.warning(
                    f"Tool '{name}' has no projects defined in schema. "
                    "Using placeholder project '_unknown'. Update your schema to include projects."
                )
                projects = ["_unknown"]

            # Create ToolDefinition directly to load from schema file
            # (bypasses the async handler requirement)
            tool_def = ToolDefinition(
                name=tool_data.get("name", name),
                description=tool_data.get("description", ""),
                input_schema=tool_data.get("input_schema", {}),
                projects=projects,
                handler=None,  # No handler from file
                capabilities=tool_data.get("capabilities", []),
                constraints=tool_data.get("constraints", []),
            )
            registry._register_tool_definition(tool_def)

        return registry

    # =========================================================================
    # Auto-Discovery
    # =========================================================================

    def discover(
        self,
        package: Optional[str] = None,
        path: Optional[Union[str, Path]] = None,
    ) -> "ToolRegistry":
        """
        Auto-discover and load tool handlers from a Python package.

        Scans Python files for @tool decorators using AST analysis,
        then imports the modules to trigger registration.

        Args:
            package: Package name to scan (e.g., "my_project.tools")
            path: Directory path to scan (alternative to package)

        Returns:
            Self for chaining

        Example:
            ```python
            registry = ToolRegistry().discover(package="my_app.tools")
            # Or auto-detect from current directory
            registry = ToolRegistry().discover()
            ```
        """
        from cli.tool_discovery import discover_tools

        path_obj = Path(path) if path else None
        result = discover_tools(package=package, path=path_obj)

        if result.errors:
            for error in result.errors:
                logger.error(f"Discovery error: {error}")

        if result.warnings:
            for warning in result.warnings:
                logger.warning(f"Discovery warning: {warning}")

        logger.info(
            f"Discovered {len(result.tools)} tools from "
            f"{result.modules_with_tools} modules"
        )

        return self

    # =========================================================================
    # Utility Methods
    # =========================================================================

    def clear(self) -> None:
        """Clear all registered tools and handlers."""
        self._tools.clear()
        self._action_handlers = ActionHandlers()

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

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

    def __iter__(self):
        """Iterate over tool names."""
        return iter(self._tools)

    def __repr__(self) -> str:
        return f"ToolRegistry(tools={list(self._tools.keys())})"
