"""Tool registry — decorator-based tool dispatch replacing if-elif chain."""

import json
from salmalm.security.crypto import log
from salmalm.core import audit_log

_HANDLERS = {}
_DYNAMIC_TOOLS = []  # dynamically registered tool definitions
_modules_loaded = False

# Core tool names that cannot be overwritten by dynamic registration
_CORE_TOOL_NAMES = frozenset(
    {
        "exec",
        "exec_session",
        "python_eval",
        "sandbox_exec",
        "read_file",
        "write_file",
        "edit_file",
        "list_dir",
        "delete_file",
        "web_search",
        "web_fetch",
        "http_request",
        "browser",
        "memory_read",
        "memory_write",
        "memory_search",
        "rag_search",
        "rag_index",
        "apply_patch",
        "create_dir",
        "send_email",
        "calendar_list",
        "calendar_create",
        "set_reminder",
        "get_weather",
        "search_contacts",
    }
)


def _ensure_modules():
    """Lazy-load all tools_*.py modules so @register decorators run."""
    global _modules_loaded
    if _modules_loaded:
        return
    _modules_loaded = True
    from salmalm.tools import (  # noqa: F401
        tools_file,
        tools_web,
        tools_exec,
        tools_memory,
        tools_misc,
        tools_system,
        tools_util,
        tools_agent,
        tools_browser,
        tools_google,
        tools_media,
        tools_calendar,
        tools_email,
        tools_personal,
    )

    # Optional tool modules — may not exist in all installations
    try:
        from salmalm.tools import tools_brave, tools_mesh, tools_sandbox, tools_ui  # noqa: F401
    except ImportError:
        pass

    # Register apply_patch tool
    from salmalm.tools.tools_patch import apply_patch as _apply_patch_fn

    _HANDLERS["apply_patch"] = lambda args: _apply_patch_fn(args.get("patch_text", ""), args.get("base_dir", "."))


def register(name: str):
    """Decorator to register a tool handler function."""

    def decorator(fn):
        """Decorator."""
        _HANDLERS[name] = fn
        return fn

    return decorator


def register_dynamic(name: str, handler, tool_def: dict = None) -> None:
    """Dynamically register a tool at runtime (for plugins).

    플러그인에서 런타임에 도구를 동적으로 등록합니다.
    """
    # Prevent overwriting core tools
    if name in _CORE_TOOL_NAMES:
        log.warning(f"[SECURITY] Blocked dynamic registration of core tool: {name}")
        return
    # Enforce namespace prefix for non-core tools (must contain _ or . separator)
    if "_" not in name and "." not in name:
        log.warning(f"[SECURITY] Dynamic tool name must be namespaced (contain _ or .): {name}")
        return
    _HANDLERS[name] = handler
    if tool_def:
        # Avoid duplicates
        _DYNAMIC_TOOLS[:] = [t for t in _DYNAMIC_TOOLS if t.get("name") != name]
        _DYNAMIC_TOOLS.append(tool_def)
    log.info(f"[TOOL] Dynamic tool registered: {name}")


def unregister_dynamic(name: str) -> None:
    """Remove a dynamically registered tool."""
    _HANDLERS.pop(name, None)
    _DYNAMIC_TOOLS[:] = [t for t in _DYNAMIC_TOOLS if t.get("name") != name]


def get_dynamic_tools() -> list:
    """Return all dynamically registered tool definitions."""
    return list(_DYNAMIC_TOOLS)


def get_all_tools() -> list:
    """Return all tool definitions (static + dynamic)."""
    from salmalm.tools.tools import TOOL_DEFINITIONS

    return list(TOOL_DEFINITIONS) + get_dynamic_tools()


def execute_tool(name: str, args: dict) -> str:
    """Execute a tool and return result string. Auto-dispatches to remote node if available."""
    import os as _os

    try:
        from salmalm.security.redact import scrub_secrets

        _audit_preview = scrub_secrets(json.dumps(args, ensure_ascii=False)[:200])
    except Exception as e:  # noqa: broad-except
        _audit_preview = json.dumps(args, ensure_ascii=False)[:200]
    audit_log("tool_exec", f"{name}: {_audit_preview}")

    # Defense-in-depth: tool tier re-verification at registry level
    # (supplements web middleware check — catches internal call paths)
    bind = _os.environ.get("SALMALM_BIND", "127.0.0.1")
    if bind != "127.0.0.1":
        try:
            from salmalm.web.middleware import is_tool_allowed_external

            args = dict(args)  # shallow copy to avoid mutating caller's dict
            _is_auth = bool(args.pop("_authenticated", False))
            if not is_tool_allowed_external(name, is_authenticated=_is_auth, bind_addr=bind):
                log.warning(f"[SECURITY] Tool '{name}' blocked: external bind + restricted tier")
                return f'❌ Tool "{name}" is restricted on externally-exposed instances'
        except ImportError:
            pass

    # Try remote node dispatch first (if gateway has registered nodes)
    try:
        from salmalm.features.nodes import gateway

        if gateway._nodes:
            result = gateway.dispatch_auto(name, args)
            if result and "error" not in result:
                return result.get("result", str(result))
    except Exception as e:  # noqa: broad-except
        pass  # Fall through to local execution

    _ensure_modules()

    try:
        handler = _HANDLERS.get(name)
        if handler:
            return handler(args)

        # Try legacy plugin tools as fallback
        from salmalm.core import PluginLoader

        result = PluginLoader.execute(name, args)
        if result is not None:
            return result

        # Try directory-based plugins (new plugin architecture)
        try:
            from salmalm.features.plugin_manager import plugin_manager

            result = plugin_manager._execute_plugin_tool(name, args)
            if result is not None:
                return result
        except Exception as e:  # noqa: broad-except
            log.debug(f"Suppressed: {e}")

        # Try MCP tools as last fallback
        if name.startswith("mcp_"):
            from salmalm.features.mcp import mcp_manager

            mcp_result = mcp_manager.call_tool(name, args)
            if mcp_result is not None:
                return mcp_result

        return f"❌ Unknown tool: {name}"

    except PermissionError as e:
        return f"❌ Permission denied: {e}"
    except Exception as e:
        log.error(f"Tool error ({name}): {e}")
        return f"❌ Tool error: {str(e)[:200]}"
