from __future__ import annotations

import typing
import time
import inspect
from typing import TYPE_CHECKING

import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
from langbot_plugin.api.entities.events import pipeline_query

from . import loader as tool_loader
from .errors import ToolNotFoundError
from ...pipeline.pool import get_query_execution_context
from ...api.http.service.tenant import TenantContext

if TYPE_CHECKING:
    from ...core import app
    from langbot.pkg.provider.tools.loaders import (
        mcp as mcp_loader,
        native as native_loader,
        plugin as plugin_loader,
        skill_authoring as skill_authoring_loader,
    )


class ToolManager:
    """LLM工具管理器"""

    ap: app.Application

    native_tool_loader: native_loader.NativeToolLoader
    plugin_tool_loader: plugin_loader.PluginToolLoader
    mcp_tool_loader: mcp_loader.MCPLoader
    skill_tool_loader: skill_authoring_loader.SkillToolLoader

    def __init__(self, ap: app.Application):
        self.ap = ap

    async def _bind_plugin_workspace(self, context: TenantContext) -> None:
        """Select the tenant before any plugin catalog lookup.

        Tool discovery happens before invocation, so relying on ``call_tool``
        to bind the Workspace is too late and can expose another task's
        catalog in a shared Runtime.
        """

        connector = getattr(self.ap, 'plugin_connector', None)
        require_context = getattr(connector, 'require_workspace_context', None)
        if require_context is None:
            return
        result = require_context(context)
        if inspect.isawaitable(result):
            await result

    async def _workspace_sandbox_available(self, context: TenantContext) -> bool:
        """Resolve the Workspace capability before exposing sandbox tools."""

        box_service = getattr(self.ap, 'box_service', None)
        checker = getattr(box_service, 'is_workspace_sandbox_available', None)
        if not callable(checker):
            # Compatibility for OSS embedders and isolated manager tests. The
            # BoxService execution path remains the final authority.
            return True
        try:
            return bool(await checker(context))
        except Exception:
            return False

    async def initialize(self):
        from langbot.pkg.utils import importutil
        from langbot.pkg.provider.tools import loaders
        from langbot.pkg.provider.tools.loaders import (
            mcp as mcp_loader,
            native as native_loader,
            plugin as plugin_loader,
            skill_authoring as skill_authoring_loader,
        )

        importutil.import_modules_in_pkg(loaders)

        self.native_tool_loader = native_loader.NativeToolLoader(self.ap)
        await self.native_tool_loader.initialize()

        self.plugin_tool_loader = plugin_loader.PluginToolLoader(self.ap)
        await self.plugin_tool_loader.initialize()
        self.mcp_tool_loader = mcp_loader.MCPLoader(self.ap)
        await self.mcp_tool_loader.initialize()
        self.skill_tool_loader = skill_authoring_loader.SkillToolLoader(self.ap)
        await self.skill_tool_loader.initialize()

    async def get_all_tools(
        self,
        context: TenantContext,
        bound_plugins: list[str] | None = None,
        bound_mcp_servers: list[str] | None = None,
        include_skill_authoring: bool = False,
        include_mcp_resource_tools: bool = True,
    ) -> list[resource_tool.LLMTool]:
        await self._bind_plugin_workspace(context)
        all_functions: list[resource_tool.LLMTool] = []

        sandbox_available = await self._workspace_sandbox_available(context)
        if sandbox_available:
            all_functions.extend(await self.native_tool_loader.get_tools())
        if include_skill_authoring and sandbox_available:
            all_functions.extend(await self.skill_tool_loader.get_tools())
        all_functions.extend(await self.plugin_tool_loader.get_tools(bound_plugins))
        all_functions.extend(
            await self.mcp_tool_loader.get_tools(
                context,
                bound_mcp_servers,
                include_resource_tools=include_mcp_resource_tools,
            )
        )

        return all_functions

    async def get_tool_catalog(
        self,
        context: TenantContext,
        bound_plugins: list[str] | None = None,
        bound_mcp_servers: list[str] | None = None,
        include_skill_authoring: bool = False,
        include_mcp_resource_tools: bool = False,
    ) -> list[dict[str, typing.Any]]:
        await self._bind_plugin_workspace(context)
        catalog: list[dict[str, typing.Any]] = []

        def append_tools(source: str, source_name: str, tools: list[resource_tool.LLMTool]) -> None:
            for tool in tools:
                catalog.append(
                    {
                        'name': tool.name,
                        'description': tool.description,
                        'human_desc': tool.human_desc,
                        'parameters': tool.parameters,
                        'source': source,
                        'source_name': source_name,
                    }
                )

        sandbox_available = await self._workspace_sandbox_available(context)
        if sandbox_available:
            append_tools('builtin', 'LangBot', await self.native_tool_loader.get_tools())
        if include_skill_authoring and sandbox_available:
            append_tools('skill', 'LangBot', await self.skill_tool_loader.get_tools())
        catalog.extend(await self.plugin_tool_loader.get_tool_catalog(bound_plugins))

        if self.mcp_tool_loader:
            for item in await self.mcp_tool_loader.get_tool_catalog(
                context,
                bound_mcp_servers,
                include_resource_tools=include_mcp_resource_tools,
            ):
                catalog.append(item)

        return catalog

    async def get_tool_by_name(self, context: TenantContext, name: str) -> tool_loader.ToolLookupResult | None:
        """Get tool by name from any active loader."""
        await self._bind_plugin_workspace(context)
        sandbox_available = await self._workspace_sandbox_available(context)
        if sandbox_available:
            tool = await self.native_tool_loader.get_tool(name)
            if tool:
                return tool
        for active_loader in (self.plugin_tool_loader,):
            tool = await active_loader.get_tool(name)
            if tool:
                return tool
        if sandbox_available:
            tool = await self.skill_tool_loader.get_tool(name)
            if tool:
                return tool

        return await self.mcp_tool_loader.get_tool(context, name)

    async def generate_tools_for_openai(self, use_funcs: list[resource_tool.LLMTool]) -> list:
        tools = []

        for function in use_funcs:
            function_schema = {
                'type': 'function',
                'function': {
                    'name': function.name,
                    'description': function.description,
                    'parameters': function.parameters,
                },
            }
            tools.append(function_schema)

        return tools

    def _get_query_session_id(self, query: pipeline_query.Query) -> str | None:
        launcher_type = getattr(query, 'launcher_type', None)
        launcher_id = getattr(query, 'launcher_id', None)
        if launcher_type is None or launcher_id is None:
            return None

        launcher_type_value = launcher_type.value if hasattr(launcher_type, 'value') else launcher_type
        return f'{launcher_type_value}_{launcher_id}'

    async def _record_tool_call(
        self,
        *,
        name: str,
        source: str,
        parameters: dict,
        query: pipeline_query.Query,
        duration_ms: int,
        status: str,
        result: typing.Any = None,
        error_message: str | None = None,
    ) -> None:
        monitoring_service = getattr(self.ap, 'monitoring_service', None)
        if not monitoring_service:
            return

        variables = getattr(query, 'variables', {}) or {}
        message_id = variables.get('_monitoring_message_id') if isinstance(variables, dict) else None
        bot_name = variables.get('_monitoring_bot_name') if isinstance(variables, dict) else None
        pipeline_name = variables.get('_monitoring_pipeline_name') if isinstance(variables, dict) else None

        try:
            await monitoring_service.record_tool_call(
                get_query_execution_context(query),
                tool_name=name,
                tool_source=source,
                duration=duration_ms,
                status=status,
                bot_id=getattr(query, 'bot_uuid', None),
                bot_name=bot_name,
                pipeline_name=pipeline_name,
                session_id=self._get_query_session_id(query),
                message_id=message_id,
                arguments=parameters,
                result=result,
                error_message=error_message,
            )
        except Exception as e:
            self.ap.logger.warning(f'Failed to record tool call: {e}')

    async def _invoke_tool_with_monitoring(
        self,
        *,
        source: str,
        name: str,
        parameters: dict,
        query: pipeline_query.Query,
        invoke: typing.Callable[[], typing.Awaitable[typing.Any]],
    ) -> typing.Any:
        start_time = time.perf_counter()
        try:
            result = await invoke()
        except Exception as e:
            duration_ms = int((time.perf_counter() - start_time) * 1000)
            await self._record_tool_call(
                name=name,
                source=source,
                parameters=parameters,
                query=query,
                duration_ms=duration_ms,
                status='error',
                error_message=str(e),
            )
            raise

        duration_ms = int((time.perf_counter() - start_time) * 1000)
        await self._record_tool_call(
            name=name,
            source=source,
            parameters=parameters,
            query=query,
            duration_ms=duration_ms,
            status='success',
            result=result,
        )
        return result

    async def execute_func_call(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
        from langbot.pkg.telemetry import features as telemetry_features

        execution_context = get_query_execution_context(query)
        await self._bind_plugin_workspace(execution_context)
        sandbox_available = await self._workspace_sandbox_available(execution_context)
        if sandbox_available and await self.native_tool_loader.has_tool(name):
            telemetry_features.increment(query, 'tool_calls', 'native')
            return await self._invoke_tool_with_monitoring(
                source='native',
                name=name,
                parameters=parameters,
                query=query,
                invoke=lambda: self.native_tool_loader.invoke_tool(name, parameters, query),
            )
        if await self.plugin_tool_loader.has_tool(name):
            telemetry_features.increment(query, 'tool_calls', 'plugin')
            return await self._invoke_tool_with_monitoring(
                source='plugin',
                name=name,
                parameters=parameters,
                query=query,
                invoke=lambda: self.plugin_tool_loader.invoke_tool(name, parameters, query),
            )
        if await self.mcp_tool_loader.has_tool(execution_context, name):
            telemetry_features.increment(query, 'tool_calls', 'mcp')
            return await self._invoke_tool_with_monitoring(
                source='mcp',
                name=name,
                parameters=parameters,
                query=query,
                invoke=lambda: self.mcp_tool_loader.invoke_tool(name, parameters, query),
            )
        if sandbox_available and await self.skill_tool_loader.has_tool(name):
            telemetry_features.increment(query, 'tool_calls', 'skill')
            return await self._invoke_tool_with_monitoring(
                source='skill',
                name=name,
                parameters=parameters,
                query=query,
                invoke=lambda: self.skill_tool_loader.invoke_tool(name, parameters, query),
            )
        raise ToolNotFoundError(name)

    async def shutdown(self):
        await self.native_tool_loader.shutdown()
        await self.plugin_tool_loader.shutdown()
        await self.mcp_tool_loader.shutdown()
        await self.skill_tool_loader.shutdown()
