import { isBaseMessage, ToolMessage, isAIMessage } from '@langchain/core/messages';
import { isCommand, isGraphInterrupt, Command, Send, END } from '@langchain/langgraph';
import { Constants, GraphEvents } from '../common/enum.mjs';
import 'nanoid';
import '../messages/core.mjs';
import { RunnableCallable } from '../utils/run.mjs';
import 'ai-tokenizer';
import 'zod-to-json-schema';
import { safeDispatchCustomEvent } from '../utils/events.mjs';

/**
 * Helper to check if a value is a Send object
 */
function isSend(value) {
    return value instanceof Send;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
class ToolNode extends RunnableCallable {
    toolMap;
    loadRuntimeTools;
    handleToolErrors = true;
    trace = false;
    toolCallStepIds;
    errorHandler;
    toolUsageCount;
    /** Maps toolCallId → turn captured in runTool, used by handleRunToolCompletions */
    toolCallTurns = new Map();
    /** Tool registry for filtering (lazy computation of programmatic maps) */
    toolRegistry;
    /** Cached programmatic tools (computed once on first PTC call) */
    programmaticCache;
    /** Reference to Graph's sessions map for automatic session injection */
    sessions;
    /** When true, dispatches ON_TOOL_EXECUTE events instead of invoking tools directly */
    eventDrivenMode = false;
    /** Agent ID for event-driven mode */
    agentId;
    /** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
    directToolNames;
    constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, agentId, directToolNames, }) {
        super({ name, tags, func: (input, config) => this.run(input, config) });
        this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
        this.toolCallStepIds = toolCallStepIds;
        this.handleToolErrors = handleToolErrors ?? this.handleToolErrors;
        this.loadRuntimeTools = loadRuntimeTools;
        this.errorHandler = errorHandler;
        this.toolUsageCount = new Map();
        this.toolRegistry = toolRegistry;
        this.sessions = sessions;
        this.eventDrivenMode = eventDrivenMode ?? false;
        this.agentId = agentId;
        this.directToolNames = directToolNames;
    }
    /**
     * Returns cached programmatic tools, computing once on first access.
     * Single iteration builds both toolMap and toolDefs simultaneously.
     */
    getProgrammaticTools() {
        if (this.programmaticCache)
            return this.programmaticCache;
        const toolMap = new Map();
        const toolDefs = [];
        if (this.toolRegistry) {
            for (const [name, toolDef] of this.toolRegistry) {
                if ((toolDef.allowed_callers ?? ['direct']).includes('code_execution')) {
                    toolDefs.push(toolDef);
                    const tool = this.toolMap.get(name);
                    if (tool)
                        toolMap.set(name, tool);
                }
            }
        }
        this.programmaticCache = { toolMap, toolDefs };
        return this.programmaticCache;
    }
    /**
     * Returns a snapshot of the current tool usage counts.
     * @returns A ReadonlyMap where keys are tool names and values are their usage counts.
     */
    getToolUsageCounts() {
        return new Map(this.toolUsageCount); // Return a copy
    }
    /**
     * Runs a single tool call with error handling
     */
    async runTool(call, config) {
        const tool = this.toolMap.get(call.name);
        try {
            if (tool === undefined) {
                throw new Error(`Tool "${call.name}" not found.`);
            }
            const turn = this.toolUsageCount.get(call.name) ?? 0;
            this.toolUsageCount.set(call.name, turn + 1);
            if (call.id != null && call.id !== '') {
                this.toolCallTurns.set(call.id, turn);
            }
            const args = call.args;
            const stepId = this.toolCallStepIds?.get(call.id);
            // Build invoke params - LangChain extracts non-schema fields to config.toolCall
            let invokeParams = {
                ...call,
                args,
                type: 'tool_call',
                stepId,
                turn,
            };
            // Inject runtime data for special tools (becomes available at config.toolCall)
            if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
                const { toolMap, toolDefs } = this.getProgrammaticTools();
                invokeParams = {
                    ...invokeParams,
                    toolMap,
                    toolDefs,
                };
            }
            else if (call.name === Constants.TOOL_SEARCH) {
                invokeParams = {
                    ...invokeParams,
                    toolRegistry: this.toolRegistry,
                };
            }
            /**
             * Inject session context for code execution tools when available.
             * Each file uses its own session_id (supporting multi-session file tracking).
             * Both session_id and _injected_files are injected directly to invokeParams
             * (not inside args) so they bypass Zod schema validation and reach config.toolCall.
             *
             * session_id is always injected when available (even without tracked files)
             * so the CodeExecutor can fall back to the /files endpoint for session continuity.
             */
            if (call.name === Constants.EXECUTE_CODE ||
                call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
                const codeSession = this.sessions?.get(Constants.EXECUTE_CODE);
                if (codeSession?.session_id != null && codeSession.session_id !== '') {
                    invokeParams = {
                        ...invokeParams,
                        session_id: codeSession.session_id,
                    };
                    if (codeSession.files != null && codeSession.files.length > 0) {
                        const fileRefs = codeSession.files.map((file) => ({
                            session_id: file.session_id ?? codeSession.session_id,
                            id: file.id,
                            name: file.name,
                        }));
                        invokeParams._injected_files = fileRefs;
                    }
                }
            }
            const output = await tool.invoke(invokeParams, config);
            if ((isBaseMessage(output) && output._getType() === 'tool') ||
                isCommand(output)) {
                return output;
            }
            else {
                return new ToolMessage({
                    status: 'success',
                    name: tool.name,
                    content: typeof output === 'string' ? output : JSON.stringify(output),
                    tool_call_id: call.id,
                });
            }
        }
        catch (_e) {
            const e = _e;
            if (!this.handleToolErrors) {
                throw e;
            }
            if (isGraphInterrupt(e)) {
                throw e;
            }
            if (this.errorHandler) {
                try {
                    await this.errorHandler({
                        error: e,
                        id: call.id,
                        name: call.name,
                        input: call.args,
                    }, config.metadata);
                }
                catch (handlerError) {
                    // eslint-disable-next-line no-console
                    console.error('Error in errorHandler:', {
                        toolName: call.name,
                        toolCallId: call.id,
                        toolArgs: call.args,
                        stepId: this.toolCallStepIds?.get(call.id),
                        turn: this.toolUsageCount.get(call.name),
                        originalError: {
                            message: e.message,
                            stack: e.stack ?? undefined,
                        },
                        handlerError: handlerError instanceof Error
                            ? {
                                message: handlerError.message,
                                stack: handlerError.stack ?? undefined,
                            }
                            : {
                                message: String(handlerError),
                                stack: undefined,
                            },
                    });
                }
            }
            return new ToolMessage({
                status: 'error',
                content: `Error: ${e.message}\n Please fix your mistakes.`,
                name: call.name,
                tool_call_id: call.id ?? '',
            });
        }
    }
    /**
     * Builds code session context for injection into event-driven tool calls.
     * Mirrors the session injection logic in runTool() for direct execution.
     */
    getCodeSessionContext() {
        if (!this.sessions) {
            return undefined;
        }
        const codeSession = this.sessions.get(Constants.EXECUTE_CODE);
        if (!codeSession) {
            return undefined;
        }
        const context = {
            session_id: codeSession.session_id,
        };
        if (codeSession.files && codeSession.files.length > 0) {
            context.files = codeSession.files.map((file) => ({
                session_id: file.session_id ?? codeSession.session_id,
                id: file.id,
                name: file.name,
            }));
        }
        return context;
    }
    /**
     * Extracts code execution session context from tool results and stores in Graph.sessions.
     * Mirrors the session storage logic in handleRunToolCompletions for direct execution.
     */
    storeCodeSessionFromResults(results, requests) {
        if (!this.sessions) {
            return;
        }
        for (let i = 0; i < results.length; i++) {
            const result = results[i];
            if (result.status !== 'success' || result.artifact == null) {
                continue;
            }
            const request = requests.find((r) => r.id === result.toolCallId);
            if (request?.name !== Constants.EXECUTE_CODE &&
                request?.name !== Constants.PROGRAMMATIC_TOOL_CALLING) {
                continue;
            }
            const artifact = result.artifact;
            if (artifact?.session_id == null || artifact.session_id === '') {
                continue;
            }
            const newFiles = artifact.files ?? [];
            const existingSession = this.sessions.get(Constants.EXECUTE_CODE);
            const existingFiles = existingSession?.files ?? [];
            if (newFiles.length > 0) {
                const filesWithSession = newFiles.map((file) => ({
                    ...file,
                    session_id: artifact.session_id,
                }));
                const newFileNames = new Set(filesWithSession.map((f) => f.name));
                const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
                this.sessions.set(Constants.EXECUTE_CODE, {
                    session_id: artifact.session_id,
                    files: [...filteredExisting, ...filesWithSession],
                    lastUpdated: Date.now(),
                });
            }
            else {
                this.sessions.set(Constants.EXECUTE_CODE, {
                    session_id: artifact.session_id,
                    files: existingFiles,
                    lastUpdated: Date.now(),
                });
            }
        }
    }
    /**
     * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
     * and stores code session context. Mirrors the completion handling in
     * dispatchToolEvents for the event-driven path.
     *
     * By handling completions here in graph context (rather than in the
     * stream consumer via ToolEndHandler), the race between the stream
     * consumer and graph execution is eliminated.
     */
    handleRunToolCompletions(calls, outputs, config) {
        for (let i = 0; i < calls.length; i++) {
            const call = calls[i];
            const output = outputs[i];
            const turn = this.toolCallTurns.get(call.id) ?? 0;
            if (isCommand(output)) {
                continue;
            }
            const toolMessage = output;
            const toolCallId = call.id ?? '';
            // Skip error ToolMessages when errorHandler already dispatched ON_RUN_STEP_COMPLETED
            // via handleToolCallErrorStatic. Without this check, errors would be double-dispatched.
            if (toolMessage.status === 'error' && this.errorHandler != null) {
                continue;
            }
            // Store code session context from tool results
            if (this.sessions &&
                (call.name === Constants.EXECUTE_CODE ||
                    call.name === Constants.PROGRAMMATIC_TOOL_CALLING)) {
                const artifact = toolMessage.artifact;
                if (artifact?.session_id != null && artifact.session_id !== '') {
                    const newFiles = artifact.files ?? [];
                    const existingSession = this.sessions.get(Constants.EXECUTE_CODE);
                    const existingFiles = existingSession?.files ?? [];
                    if (newFiles.length > 0) {
                        const filesWithSession = newFiles.map((file) => ({
                            ...file,
                            session_id: artifact.session_id,
                        }));
                        const newFileNames = new Set(filesWithSession.map((f) => f.name));
                        const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
                        this.sessions.set(Constants.EXECUTE_CODE, {
                            session_id: artifact.session_id,
                            files: [...filteredExisting, ...filesWithSession],
                            lastUpdated: Date.now(),
                        });
                    }
                    else {
                        this.sessions.set(Constants.EXECUTE_CODE, {
                            session_id: artifact.session_id,
                            files: existingFiles,
                            lastUpdated: Date.now(),
                        });
                    }
                }
            }
            // Dispatch ON_RUN_STEP_COMPLETED via custom event (same path as dispatchToolEvents)
            const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
            if (!stepId) {
                continue;
            }
            const contentString = typeof toolMessage.content === 'string'
                ? toolMessage.content
                : JSON.stringify(toolMessage.content);
            const tool_call = {
                args: typeof call.args === 'string'
                    ? call.args
                    : JSON.stringify(call.args ?? {}),
                name: call.name,
                id: toolCallId,
                output: contentString,
                progress: 1,
            };
            safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_COMPLETED, {
                result: {
                    id: stepId,
                    index: turn,
                    type: 'tool_call',
                    tool_call,
                },
            }, config);
        }
    }
    /**
     * Dispatches tool calls to the host via ON_TOOL_EXECUTE event and returns raw ToolMessages.
     * Core logic for event-driven execution, separated from output shaping.
     */
    async dispatchToolEvents(toolCalls, config) {
        const requests = toolCalls.map((call) => {
            const turn = this.toolUsageCount.get(call.name) ?? 0;
            this.toolUsageCount.set(call.name, turn + 1);
            const request = {
                id: call.id,
                name: call.name,
                args: call.args,
                stepId: this.toolCallStepIds?.get(call.id),
                turn,
            };
            if (call.name === Constants.EXECUTE_CODE ||
                call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
                request.codeSessionContext = this.getCodeSessionContext();
            }
            return request;
        });
        const results = await new Promise((resolve, reject) => {
            const request = {
                toolCalls: requests,
                userId: config.configurable?.user_id,
                agentId: this.agentId,
                configurable: config.configurable,
                metadata: config.metadata,
                resolve,
                reject,
            };
            safeDispatchCustomEvent(GraphEvents.ON_TOOL_EXECUTE, request, config);
        });
        this.storeCodeSessionFromResults(results, requests);
        return results.map((result) => {
            const request = requests.find((r) => r.id === result.toolCallId);
            const toolName = request?.name ?? 'unknown';
            const stepId = this.toolCallStepIds?.get(result.toolCallId) ?? '';
            if (!stepId) {
                // eslint-disable-next-line no-console
                console.warn(`[ToolNode] toolCallStepIds missing entry for toolCallId=${result.toolCallId} (tool=${toolName}). ` +
                    'This indicates a race between the stream consumer and graph execution. ' +
                    `Map size: ${this.toolCallStepIds?.size ?? 0}`);
            }
            let toolMessage;
            let contentString;
            if (result.status === 'error') {
                contentString = `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`;
                toolMessage = new ToolMessage({
                    status: 'error',
                    content: contentString,
                    name: toolName,
                    tool_call_id: result.toolCallId,
                });
            }
            else {
                contentString =
                    typeof result.content === 'string'
                        ? result.content
                        : JSON.stringify(result.content);
                toolMessage = new ToolMessage({
                    status: 'success',
                    name: toolName,
                    content: contentString,
                    artifact: result.artifact,
                    tool_call_id: result.toolCallId,
                });
            }
            const tool_call = {
                args: typeof request?.args === 'string'
                    ? request.args
                    : JSON.stringify(request?.args ?? {}),
                name: toolName,
                id: result.toolCallId,
                output: contentString,
                progress: 1,
            };
            const runStepCompletedData = {
                result: {
                    id: stepId,
                    index: request?.turn ?? 0,
                    type: 'tool_call',
                    tool_call,
                },
            };
            safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_COMPLETED, runStepCompletedData, config);
            return toolMessage;
        });
    }
    /**
     * Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
     * Used in event-driven mode where the host handles actual tool execution.
     */
    async executeViaEvent(toolCalls, config, 
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    input) {
        const outputs = await this.dispatchToolEvents(toolCalls, config);
        return (Array.isArray(input) ? outputs : { messages: outputs });
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    async run(input, config) {
        this.toolCallTurns.clear();
        let outputs;
        if (this.isSendInput(input)) {
            const isDirectTool = this.directToolNames?.has(input.lg_tool_call.name);
            if (this.eventDrivenMode && isDirectTool !== true) {
                return this.executeViaEvent([input.lg_tool_call], config, input);
            }
            outputs = [await this.runTool(input.lg_tool_call, config)];
            this.handleRunToolCompletions([input.lg_tool_call], outputs, config);
        }
        else {
            let messages;
            if (Array.isArray(input)) {
                messages = input;
            }
            else if (this.isMessagesState(input)) {
                messages = input.messages;
            }
            else {
                throw new Error('ToolNode only accepts BaseMessage[] or { messages: BaseMessage[] } as input.');
            }
            const toolMessageIds = new Set(messages
                .filter((msg) => msg._getType() === 'tool')
                .map((msg) => msg.tool_call_id));
            let aiMessage;
            for (let i = messages.length - 1; i >= 0; i--) {
                const message = messages[i];
                if (isAIMessage(message)) {
                    aiMessage = message;
                    break;
                }
            }
            if (aiMessage == null || !isAIMessage(aiMessage)) {
                throw new Error('ToolNode only accepts AIMessages as input.');
            }
            if (this.loadRuntimeTools) {
                const { tools, toolMap } = this.loadRuntimeTools(aiMessage.tool_calls ?? []);
                this.toolMap =
                    toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
                this.programmaticCache = undefined; // Invalidate cache on toolMap change
            }
            const filteredCalls = aiMessage.tool_calls?.filter((call) => {
                /**
                 * Filter out:
                 * 1. Already processed tool calls (present in toolMessageIds)
                 * 2. Server tool calls (e.g., web_search with IDs starting with 'srvtoolu_')
                 *    which are executed by the provider's API and don't require invocation
                 */
                return ((call.id == null || !toolMessageIds.has(call.id)) &&
                    !(call.id?.startsWith('srvtoolu_') ?? false));
            }) ?? [];
            if (this.eventDrivenMode && filteredCalls.length > 0) {
                if (!this.directToolNames || this.directToolNames.size === 0) {
                    return this.executeViaEvent(filteredCalls, config, input);
                }
                const directCalls = filteredCalls.filter((c) => this.directToolNames.has(c.name));
                const eventCalls = filteredCalls.filter((c) => !this.directToolNames.has(c.name));
                const directOutputs = directCalls.length > 0
                    ? await Promise.all(directCalls.map((call) => this.runTool(call, config)))
                    : [];
                if (directCalls.length > 0 && directOutputs.length > 0) {
                    this.handleRunToolCompletions(directCalls, directOutputs, config);
                }
                const eventOutputs = eventCalls.length > 0
                    ? await this.dispatchToolEvents(eventCalls, config)
                    : [];
                outputs = [...directOutputs, ...eventOutputs];
            }
            else {
                outputs = await Promise.all(filteredCalls.map((call) => this.runTool(call, config)));
                this.handleRunToolCompletions(filteredCalls, outputs, config);
            }
        }
        if (!outputs.some(isCommand)) {
            return (Array.isArray(input) ? outputs : { messages: outputs });
        }
        const combinedOutputs = [];
        let parentCommand = null;
        /**
         * Collect handoff commands (Commands with string goto and Command.PARENT)
         * for potential parallel handoff aggregation
         */
        const handoffCommands = [];
        for (const output of outputs) {
            if (isCommand(output)) {
                if (output.graph === Command.PARENT &&
                    Array.isArray(output.goto) &&
                    output.goto.every((send) => isSend(send))) {
                    /** Aggregate Send-based commands */
                    if (parentCommand) {
                        parentCommand.goto.push(...output.goto);
                    }
                    else {
                        parentCommand = new Command({
                            graph: Command.PARENT,
                            goto: output.goto,
                        });
                    }
                }
                else if (output.graph === Command.PARENT) {
                    /**
                     * Handoff Command with destination.
                     * Handle both string ('agent') and array (['agent']) formats.
                     * Collect for potential parallel aggregation.
                     */
                    const goto = output.goto;
                    const isSingleStringDest = typeof goto === 'string';
                    const isSingleArrayDest = Array.isArray(goto) &&
                        goto.length === 1 &&
                        typeof goto[0] === 'string';
                    if (isSingleStringDest || isSingleArrayDest) {
                        handoffCommands.push(output);
                    }
                    else {
                        /** Multi-destination or other command - pass through */
                        combinedOutputs.push(output);
                    }
                }
                else {
                    /** Other commands - pass through */
                    combinedOutputs.push(output);
                }
            }
            else {
                combinedOutputs.push(Array.isArray(input) ? [output] : { messages: [output] });
            }
        }
        /**
         * Handle handoff commands - convert to Send objects for parallel execution
         * when multiple handoffs are requested
         */
        if (handoffCommands.length > 1) {
            /**
             * Multiple parallel handoffs - convert to Send objects.
             * Each Send carries its own state with the appropriate messages.
             * This enables LLM-initiated parallel execution when calling multiple
             * transfer tools simultaneously.
             */
            /** Collect all destinations for sibling tracking */
            const allDestinations = handoffCommands.map((cmd) => {
                const goto = cmd.goto;
                return typeof goto === 'string' ? goto : goto[0];
            });
            const sends = handoffCommands.map((cmd, idx) => {
                const destination = allDestinations[idx];
                /** Get siblings (other destinations, not this one) */
                const siblings = allDestinations.filter((d) => d !== destination);
                /** Add siblings to ToolMessage additional_kwargs */
                const update = cmd.update;
                if (update && update.messages) {
                    for (const msg of update.messages) {
                        if (msg.getType() === 'tool') {
                            msg.additional_kwargs.handoff_parallel_siblings =
                                siblings;
                        }
                    }
                }
                return new Send(destination, cmd.update);
            });
            const parallelCommand = new Command({
                graph: Command.PARENT,
                goto: sends,
            });
            combinedOutputs.push(parallelCommand);
        }
        else if (handoffCommands.length === 1) {
            /** Single handoff - pass through as-is */
            combinedOutputs.push(handoffCommands[0]);
        }
        if (parentCommand) {
            combinedOutputs.push(parentCommand);
        }
        return combinedOutputs;
    }
    isSendInput(input) {
        return (typeof input === 'object' && input != null && 'lg_tool_call' in input);
    }
    isMessagesState(input) {
        return (typeof input === 'object' &&
            input != null &&
            'messages' in input &&
            Array.isArray(input.messages) &&
            input.messages.every(isBaseMessage));
    }
}
function areToolCallsInvoked(message, invokedToolIds) {
    if (!invokedToolIds || invokedToolIds.size === 0)
        return false;
    return (message.tool_calls?.every((toolCall) => toolCall.id != null && invokedToolIds.has(toolCall.id)) ?? false);
}
function toolsCondition(state, toolNode, invokedToolIds) {
    const messages = Array.isArray(state) ? state : state.messages;
    const message = messages[messages.length - 1];
    if (message &&
        'tool_calls' in message &&
        (message.tool_calls?.length ?? 0) > 0 &&
        !areToolCallsInvoked(message, invokedToolIds)) {
        return toolNode;
    }
    return END;
}

export { ToolNode, toolsCondition };
//# sourceMappingURL=ToolNode.mjs.map
