/**
 * Tools Manager Module
 *
 * Handles all tool registration, discovery, and execution for AI providers.
 * Extracted from BaseProvider to follow Single Responsibility Principle.
 *
 * Responsibilities:
 * - Tool registration (direct, custom, MCP, external MCP)
 * - Tool discovery and aggregation
 * - Tool creation from definitions and schemas
 * - Tool executor setup
 * - Session context management for MCP tools
 * - Event emission wrapping for tool execution
 *
 * @module core/modules/ToolsManager
 */
import { tool as createAISDKTool, jsonSchema } from "ai";
import { z } from "zod";
import { createToolEventPayload } from "../toolEvents.js";
import { tracers, ATTR, withSpan } from "../../telemetry/index.js";
import { SpanStatusCode } from "@opentelemetry/api";
import { logger } from "../../utils/logger.js";
import { getKeyCount } from "../../utils/transformationUtils.js";
import { convertJsonSchemaToZod } from "../../utils/schemaConversion.js";
import { generateToolOutputPreview } from "../../context/toolOutputLimits.js";
/**
 * ToolsManager class - Handles all tool management operations
 */
export class ToolsManager {
    providerName;
    directTools;
    neurolink;
    utilities;
    // Tool storage
    mcpTools;
    customTools;
    toolExecutor;
    // Session context
    sessionId;
    userId;
    constructor(providerName, directTools, neurolink, utilities) {
        this.providerName = providerName;
        this.directTools = directTools;
        this.neurolink = neurolink;
        this.utilities = utilities;
        this.mcpTools = {};
    }
    /**
     * BZ-666: Wrap tool execute with output truncation to prevent
     * context overflow when large results flow into the AI SDK accumulator.
     */
    wrapExecuteWithTruncation(toolName, originalExecute) {
        return async (params) => {
            const result = await originalExecute(params);
            return this.truncateToolResult(toolName, result);
        };
    }
    /**
     * BZ-666: Apply generateToolOutputPreview to tool results to prevent
     * context overflow when large results flow into the AI SDK accumulator.
     */
    truncateToolResult(toolName, result) {
        if (result === null || result === undefined) {
            return result;
        }
        // Handle string results directly
        if (typeof result === "string") {
            const { preview, truncated, originalSize } = generateToolOutputPreview(result);
            if (truncated) {
                logger.debug(`[ToolsManager] Truncated '${toolName}' string output: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
            }
            return truncated ? preview : result;
        }
        // Handle object results (e.g. readFile returns { content, ... })
        if (typeof result === "object") {
            const obj = result;
            let nextObj = null;
            // Truncate "content" if present and oversized
            if (typeof obj.content === "string") {
                const { preview, truncated, originalSize } = generateToolOutputPreview(obj.content);
                if (truncated) {
                    logger.debug(`[ToolsManager] Truncated '${toolName}' content field: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
                    nextObj = { ...(nextObj ?? obj), content: preview };
                }
            }
            // Truncate "data" if present and oversized — both fields can coexist
            if (typeof obj.data === "string") {
                const { preview, truncated, originalSize } = generateToolOutputPreview(obj.data);
                if (truncated) {
                    logger.debug(`[ToolsManager] Truncated '${toolName}' data field: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
                    nextObj = { ...(nextObj ?? obj), data: preview };
                }
            }
            if (nextObj) {
                return nextObj;
            }
            // For other objects, check if their JSON serialization is too large.
            // Use UTF-8 byte length, not string length, to match the 50KB budget.
            try {
                const jsonStr = JSON.stringify(result);
                if (Buffer.byteLength(jsonStr, "utf-8") > 51_200) {
                    const { preview, truncated, originalSize } = generateToolOutputPreview(jsonStr);
                    if (truncated) {
                        logger.debug(`[ToolsManager] Truncated '${toolName}' JSON output: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
                        // Preserve object shape so callers reading structured fields don't
                        // get a type surprise. Attach the preview under a sentinel field.
                        return {
                            _truncated: true,
                            _originalSize: originalSize,
                            _preview: preview,
                        };
                    }
                }
            }
            catch {
                // JSON serialization failed — return as-is
            }
        }
        return result;
    }
    /**
     * Set session context for MCP tools
     */
    setSessionContext(sessionId, userId) {
        this.sessionId = sessionId;
        this.userId = userId;
    }
    emitToolEvent(eventName, toolName, payload) {
        if (this.neurolink?.getEventEmitter) {
            this.neurolink
                .getEventEmitter()
                .emit(eventName, createToolEventPayload(toolName, payload));
        }
    }
    /**
     * Set up tool executor for a provider to enable actual tool execution
     * @param sdk - The NeuroLinkSDK instance for tool execution
     * @param functionTag - Function name for logging
     */
    setupToolExecutor(sdk, functionTag) {
        const span = tracers.sdk.startSpan("neurolink.tools.register", {
            attributes: {
                [ATTR.NL_PROVIDER]: this.providerName,
                "tools.custom_count": sdk.customTools.size,
            },
        });
        try {
            // Store custom tools for use in getAllTools()
            this.customTools = sdk.customTools;
            this.toolExecutor = sdk.executeTool.bind(sdk);
            logger.debug(`[${functionTag}] Setting up tool executor for provider`, {
                providerName: this.providerName,
                availableCustomTools: sdk.customTools.size,
                customToolsStored: !!this.customTools,
                toolExecutorStored: !!this.toolExecutor,
            });
            // Note: Tool execution will be handled through getAllTools() -> AI SDK tools
            // The custom tools are converted to AI SDK format in getAllTools() method
            span.setStatus({ code: SpanStatusCode.OK });
        }
        catch (error) {
            span.setStatus({
                code: SpanStatusCode.ERROR,
                message: error instanceof Error ? error.message : String(error),
            });
            if (error instanceof Error) {
                span.recordException(error);
            }
            throw error;
        }
        finally {
            span.end();
        }
    }
    /**
     * Get all available tools - direct tools are ALWAYS available
     * MCP tools are added when available (without blocking)
     */
    async getAllTools() {
        return withSpan({
            name: "neurolink.tools.getAll",
            tracer: tracers.sdk,
            attributes: {
                [ATTR.NL_PROVIDER]: this.providerName,
            },
        }, async (span) => {
            // Start with wrapped direct tools that emit events
            const tools = {};
            // Wrap direct tools with event emission
            await this.processDirectTools(tools);
            const directCount = Object.keys(tools).length;
            span.setAttribute("tools.direct_count", directCount);
            logger.debug(`[ToolsManager] getAllTools called for ${this.providerName}`, {
                directToolsCount: getKeyCount(this.directTools),
            });
            // Process all tool types using dedicated helper methods
            await this.processCustomTools(tools);
            const customCount = Object.keys(tools).length - directCount;
            span.setAttribute("tools.custom_count", customCount);
            await this.processExternalMCPTools(tools);
            const externalCount = Object.keys(tools).length - directCount - customCount;
            span.setAttribute("tools.external_mcp_count", externalCount);
            await this.processMCPTools(tools);
            const totalCount = Object.keys(tools).length;
            span.setAttribute(ATTR.NL_TOOL_COUNT, totalCount);
            // Record tool names for debugging (truncated)
            const toolNames = Object.keys(tools);
            span.setAttribute("tools.names", toolNames.slice(0, 20).join(",") +
                (toolNames.length > 20 ? `...+${toolNames.length - 20}` : ""));
            // Log a compact summary instead of full tool list
            logger.debug(`[ToolsManager] getAllTools complete: ${toolNames.length} tools available`, {
                provider: this.providerName,
                toolCount: toolNames.length,
                toolNames: toolNames.length <= 10
                    ? toolNames
                    : [
                        ...toolNames.slice(0, 10),
                        `... and ${toolNames.length - 10} more`,
                    ],
            });
            return tools;
        });
    }
    /**
     * Get direct tools (built-in agent tools)
     */
    getDirectTools() {
        return this.directTools;
    }
    /**
     * Get MCP tools
     */
    getMCPTools() {
        return this.mcpTools;
    }
    /**
     * Get custom tools
     */
    getCustomTools() {
        return this.customTools;
    }
    /**
     * Process direct tools with event emission wrapping
     */
    async processDirectTools(tools) {
        if (!this.directTools || Object.keys(this.directTools).length === 0) {
            return;
        }
        logger.debug(`[ToolsManager] Loading ${Object.keys(this.directTools).length} direct tools`);
        for (const [toolName, directTool] of Object.entries(this.directTools)) {
            // Wrap the direct tool's execute function with event emission
            if (directTool &&
                typeof directTool === "object" &&
                "execute" in directTool) {
                const originalExecute = directTool.execute;
                // Create a new tool with wrapped execute function (BZ-666/BZ-664 guards applied)
                const guardedExecute = this.wrapExecuteWithTruncation(toolName, originalExecute);
                tools[toolName] = {
                    ...directTool,
                    execute: async (params) => {
                        const startTime = Date.now();
                        this.emitToolEvent("tool:start", toolName, { input: params });
                        try {
                            const result = await guardedExecute(params);
                            this.emitToolEvent("tool:end", toolName, {
                                result,
                                success: true,
                                responseTime: Date.now() - startTime,
                            });
                            return result;
                        }
                        catch (error) {
                            const errorMsg = error instanceof Error ? error.message : String(error);
                            this.emitToolEvent("tool:end", toolName, {
                                error: errorMsg,
                                success: false,
                                responseTime: Date.now() - startTime,
                            });
                            throw error;
                        }
                    },
                };
            }
            else {
                // Fallback: include tool as-is if it doesn't have execute function
                tools[toolName] = directTool;
            }
        }
        // Direct tools processing complete — count already logged at start
    }
    /**
     * Process custom tools from setupToolExecutor
     */
    async processCustomTools(tools) {
        if (!this.customTools || this.customTools.size === 0) {
            return;
        }
        logger.debug(`[ToolsManager] Loading ${this.customTools.size} custom tools from setupToolExecutor`);
        for (const [toolName, toolDef] of this.customTools.entries()) {
            // Validate tool definition has required execute function
            const toolInfo = toolDef ||
                {};
            if (toolInfo && typeof toolInfo.execute === "function") {
                const tool = await this.createCustomToolFromDefinition(toolName, toolInfo);
                if (tool && !tools[toolName]) {
                    // BZ-666/BZ-664: Wrap custom tool execute with guards
                    const origExec = tool.execute;
                    if (origExec) {
                        const guarded = this.wrapExecuteWithTruncation(toolName, origExec);
                        tool.execute = guarded;
                    }
                    tools[toolName] = tool;
                }
            }
        }
        // Custom tools processing complete — count already logged at start
    }
    /**
     * Process MCP tools integration
     */
    async processMCPTools(tools) {
        // MCP tools loading simplified - removed functionCalling dependency
        if (!this.mcpTools) {
            // Set empty tools object - MCP tools are handled at a higher level
            this.mcpTools = {};
        }
        // Add MCP tools if available, but don't overwrite existing direct tools
        // Direct tools (Zod-based) take precedence over MCP tools (JSON Schema)
        if (this.mcpTools) {
            for (const [name, tool] of Object.entries(this.mcpTools)) {
                if (!tools[name]) {
                    tools[name] = tool;
                }
            }
        }
    }
    /**
     * Process external MCP tools
     */
    async processExternalMCPTools(tools) {
        if (!this.neurolink ||
            typeof this.neurolink.getExternalMCPTools !== "function") {
            return;
        }
        try {
            const externalTools = await this.neurolink.getExternalMCPTools();
            let addedCount = 0;
            for (const tool of externalTools) {
                const mcpTool = await this.createExternalMCPTool(tool);
                if (mcpTool && !tools[tool.name]) {
                    tools[tool.name] = mcpTool;
                    addedCount++;
                }
            }
            logger.debug(`[ToolsManager] External MCP tools loaded`, {
                found: externalTools.length,
                added: addedCount,
            });
        }
        catch (error) {
            logger.error(`[ToolsManager] Failed to load external MCP tools for ${this.providerName}:`, error);
            // Not an error - external tools are optional
        }
    }
    /**
     * Create a custom tool from tool definition
     */
    async createCustomToolFromDefinition(toolName, toolInfo) {
        try {
            let finalSchema;
            let originalInputSchema;
            // Prioritize parameters (Zod), then inputSchema (Zod or JSON Schema)
            if (toolInfo.parameters &&
                this.utilities?.isZodSchema?.(toolInfo.parameters)) {
                finalSchema = toolInfo.parameters;
            }
            else if (toolInfo.inputSchema &&
                this.utilities?.isZodSchema?.(toolInfo.inputSchema)) {
                finalSchema = toolInfo.inputSchema;
            }
            else if (toolInfo.inputSchema &&
                typeof toolInfo.inputSchema === "object") {
                // Use original JSON Schema with jsonSchema() wrapper - NO CONVERSION!
                originalInputSchema = toolInfo.inputSchema;
                finalSchema = jsonSchema(originalInputSchema);
            }
            else if (toolInfo.parameters &&
                typeof toolInfo.parameters === "object") {
                finalSchema = convertJsonSchemaToZod(toolInfo.parameters);
            }
            else {
                finalSchema = z.object({});
            }
            return createAISDKTool({
                description: toolInfo.description || `Tool ${toolName}`,
                inputSchema: finalSchema, // AI SDK v6 uses inputSchema (not parameters)
                execute: async (params) => {
                    const customToolSpan = tracers.sdk.startSpan("neurolink.tools.execute_custom", {
                        attributes: {
                            "tool.name": toolName,
                            "tool.type": "custom",
                            // Curator P1-3: pure wrapper — duplicates the AI SDK's
                            // ai.toolCall observation in Langfuse. Keep the OTel span
                            // for internal metrics; filter from Langfuse export.
                            "langfuse.internal": true,
                        },
                    });
                    const startTime = Date.now();
                    let executionId;
                    try {
                        // Route through NeuroLink.executeTool() when available for MCP enhancement support
                        // (cache, middleware, annotations, circuit breaker, routing)
                        if (this.toolExecutor) {
                            // Per-tool timeout and retries flow through the customTools map
                            // (set at registration via ToolRegistrationOptions).
                            // The execute wrapper in registerTool already enforces timeouts,
                            // but we also forward them to toolExecutor for MCP-level handling.
                            const toolTimeoutMs = toolInfo.timeoutMs;
                            const toolMaxRetries = toolInfo.maxRetries;
                            const hasRegistrationOptions = toolTimeoutMs !== undefined || toolMaxRetries !== undefined;
                            const result = await this.toolExecutor(toolName, params, hasRegistrationOptions
                                ? {
                                    ...(toolTimeoutMs !== undefined && {
                                        timeout: toolTimeoutMs,
                                    }),
                                    ...(toolMaxRetries !== undefined && {
                                        maxRetries: toolMaxRetries,
                                    }),
                                }
                                : undefined);
                            const convertedResult = this.utilities?.convertToolResult
                                ? await this.utilities.convertToolResult(result)
                                : result;
                            const endTime = Date.now();
                            customToolSpan.setAttribute("tool.duration_ms", endTime - startTime);
                            let errorResult = undefined;
                            if (convertedResult &&
                                typeof convertedResult === "object" &&
                                "isError" in convertedResult &&
                                convertedResult.isError) {
                                try {
                                    errorResult = JSON.stringify(convertedResult);
                                }
                                catch (error) {
                                    logger.error(`Failed to serialize error result for ${toolName}`, error);
                                }
                            }
                            customToolSpan.setAttribute("tool.result.status", errorResult ? "error" : "success");
                            if (errorResult) {
                                customToolSpan.setStatus({
                                    code: SpanStatusCode.ERROR,
                                    message: `Tool ${toolName} returned isError: true`,
                                });
                            }
                            else {
                                customToolSpan.setStatus({ code: SpanStatusCode.OK });
                            }
                            return convertedResult;
                        }
                        // Fallback: direct execution (standalone usage without NeuroLink SDK)
                        if (this.neurolink?.emitToolStart) {
                            executionId = this.neurolink.emitToolStart(toolName, params, startTime);
                        }
                        const result = await toolInfo.execute(params);
                        const convertedResult = this.utilities?.convertToolResult
                            ? await this.utilities.convertToolResult(result)
                            : result;
                        const endTime = Date.now();
                        let errorResult = undefined;
                        if (convertedResult &&
                            typeof convertedResult === "object" &&
                            "isError" in convertedResult &&
                            convertedResult.isError) {
                            try {
                                errorResult = JSON.stringify(convertedResult);
                            }
                            catch (error) {
                                logger.error(`Failed to serialize error result for ${toolName}`, error);
                            }
                        }
                        // Emit tool end event (success or handled error)
                        if (this.neurolink?.emitToolEnd) {
                            this.neurolink.emitToolEnd(toolName, convertedResult, errorResult, startTime, endTime, executionId);
                        }
                        customToolSpan.setAttribute("tool.duration_ms", endTime - startTime);
                        customToolSpan.setAttribute("tool.result.status", errorResult ? "error" : "success");
                        if (errorResult) {
                            customToolSpan.setStatus({
                                code: SpanStatusCode.ERROR,
                                message: `Tool ${toolName} returned isError: true`,
                            });
                        }
                        else {
                            customToolSpan.setStatus({ code: SpanStatusCode.OK });
                        }
                        return convertedResult;
                    }
                    catch (error) {
                        const endTime = Date.now();
                        const errorMsg = error instanceof Error ? error.message : String(error);
                        // Emit tool end event (error) — only for fallback path
                        // When toolExecutor is used, executeTool() handles event emission
                        if (!this.toolExecutor && this.neurolink?.emitToolEnd) {
                            this.neurolink.emitToolEnd(toolName, undefined, // no result
                            errorMsg, startTime, endTime, executionId);
                            logger.debug(`Custom tool error: ${toolName} (${endTime - startTime}ms)`, { error: errorMsg });
                        }
                        customToolSpan.setAttribute("tool.duration_ms", endTime - startTime);
                        customToolSpan.setAttribute("tool.result.status", "error");
                        customToolSpan.recordException(error instanceof Error ? error : new Error(errorMsg));
                        customToolSpan.setStatus({
                            code: SpanStatusCode.ERROR,
                            message: errorMsg,
                        });
                        throw error;
                    }
                    finally {
                        customToolSpan.end();
                    }
                },
            });
        }
        catch (toolCreationError) {
            logger.error(`Failed to create tool: ${toolName}`, toolCreationError);
            return null;
        }
    }
    /**
     * Create an external MCP tool
     */
    async createExternalMCPTool(tool) {
        try {
            // Use original JSON Schema from MCP tool if available, otherwise use permissive schema
            let finalSchema;
            if (tool.inputSchema && typeof tool.inputSchema === "object") {
                // Clone and fix the schema for OpenAI strict mode compatibility
                const originalSchema = tool.inputSchema;
                const fixedSchema = this.utilities?.fixSchemaForOpenAIStrictMode
                    ? this.utilities.fixSchemaForOpenAIStrictMode(originalSchema)
                    : originalSchema;
                finalSchema = jsonSchema(fixedSchema);
            }
            else {
                finalSchema = this.utilities?.createPermissiveZodSchema
                    ? this.utilities.createPermissiveZodSchema()
                    : z.object({});
            }
            // BZ-666/BZ-664: Wrap the raw MCP execute with guards before event wrapping
            const rawExecute = async (params) => {
                if (this.neurolink &&
                    typeof this.neurolink.executeExternalMCPTool === "function") {
                    return this.neurolink.executeExternalMCPTool(tool.serverId || "unknown", tool.name, params);
                }
                throw new Error(`Cannot execute external MCP tool: NeuroLink executeExternalMCPTool not available`);
            };
            const guardedExecute = this.wrapExecuteWithTruncation(tool.name, rawExecute);
            return createAISDKTool({
                description: tool.description || `External MCP tool ${tool.name}`,
                inputSchema: finalSchema, // AI SDK v6 uses inputSchema (not parameters)
                execute: async (params) => {
                    const startTime = Date.now();
                    this.emitToolEvent("tool:start", tool.name, { input: params });
                    try {
                        const result = await guardedExecute(params);
                        this.emitToolEvent("tool:end", tool.name, {
                            result,
                            success: true,
                            responseTime: Date.now() - startTime,
                        });
                        return result;
                    }
                    catch (mcpError) {
                        const errorMsg = mcpError instanceof Error ? mcpError.message : String(mcpError);
                        this.emitToolEvent("tool:end", tool.name, {
                            error: errorMsg,
                            success: false,
                            responseTime: Date.now() - startTime,
                        });
                        logger.error(`External MCP tool failed: ${tool.name}`, {
                            serverId: tool.serverId,
                            error: errorMsg,
                        });
                        throw mcpError;
                    }
                },
            });
        }
        catch (toolCreationError) {
            logger.error(`Failed to create external MCP tool: ${tool.name}`, toolCreationError);
            return null;
        }
    }
}
//# sourceMappingURL=ToolsManager.js.map