/**
 * @license
 * Copyright 2025 HopCode
 * SPDX-License-Identifier: Apache-2.0
 */
import { BaseEmitter } from './BaseEmitter.js';
import { PlanEmitter } from './PlanEmitter.js';
import { ToolNames, Kind } from '@hoptrendy/hopcode-core';
import { buildTruncatedDiffPreviewText } from '../../../utils/truncatedDiffPreview.js';
/**
 * Unified tool call event emitter.
 *
 * Handles tool_call and tool_call_update for ALL flows:
 * - Normal tool execution in runTool()
 * - History replay in HistoryReplayer
 * - SubAgent tool tracking in SubAgentTracker
 *
 * This ensures consistent behavior across all tool event sources,
 * including special handling for tools like TodoWriteTool.
 */
export class ToolCallEmitter extends BaseEmitter {
    planEmitter;
    constructor(ctx) {
        super(ctx);
        this.planEmitter = new PlanEmitter(ctx);
    }
    /**
     * Emits a tool call start event.
     *
     * @param params - Tool call start parameters
     * @returns true if event was emitted, false if skipped (e.g., TodoWriteTool)
     */
    async emitStart(params) {
        // Skip tool_call for TodoWriteTool - plan updates sent on result
        if (this.isTodoWriteTool(params.toolName)) {
            return false;
        }
        const { title, locations, kind } = this.resolveToolMetadata(params.toolName, params.args);
        await this.sendUpdate({
            sessionUpdate: 'tool_call',
            toolCallId: params.callId,
            status: params.status || 'pending',
            title,
            content: [],
            locations,
            kind,
            rawInput: params.args ?? {},
            _meta: {
                toolName: params.toolName,
                ...params.subagentMeta,
                ...(BaseEmitter.toEpochMs(params.timestamp) != null && {
                    timestamp: BaseEmitter.toEpochMs(params.timestamp),
                }),
            },
        });
        return true;
    }
    /**
     * Emits a tool call result event.
     * Handles TodoWriteTool specially by routing to plan updates.
     *
     * @param params - Tool call result parameters
     */
    async emitResult(params) {
        // Handle TodoWriteTool specially - send plan update instead
        if (this.isTodoWriteTool(params.toolName)) {
            const todos = this.planEmitter.extractTodos(params.resultDisplay, params.args);
            // Match original behavior: send plan even if empty when args['todos'] exists
            // This ensures the UI is updated even when all todos are removed
            if (todos && todos.length > 0) {
                await this.planEmitter.emitPlan(todos);
            }
            else if (params.args && Array.isArray(params.args['todos'])) {
                // Send empty plan when args had todos but result has none
                await this.planEmitter.emitPlan([]);
            }
            return; // Skip tool_call_update for TodoWriteTool
        }
        // Determine content for the update
        let contentArray = [];
        // Special case: diff result from edit tools (format from resultDisplay)
        const diffContent = this.extractDiffContent(params.resultDisplay);
        if (diffContent) {
            contentArray = [diffContent];
        }
        else if (params.error) {
            // Error case: show error message
            contentArray = [
                {
                    type: 'content',
                    content: { type: 'text', text: params.error.message },
                },
            ];
        }
        else {
            // Normal case: transform message parts to ToolCallContent[]
            contentArray = this.transformPartsToToolCallContent(params.message);
        }
        // Build the update
        const update = {
            sessionUpdate: 'tool_call_update',
            toolCallId: params.callId,
            status: params.success ? 'completed' : 'failed',
            content: contentArray,
            _meta: {
                toolName: params.toolName,
                ...params.subagentMeta,
                ...(BaseEmitter.toEpochMs(params.timestamp) != null && {
                    timestamp: BaseEmitter.toEpochMs(params.timestamp),
                }),
            },
        };
        // Add rawOutput from resultDisplay
        if (params.resultDisplay !== undefined) {
            update['rawOutput'] = params.resultDisplay;
        }
        await this.sendUpdate(update);
    }
    /**
     * Emits a tool call error event.
     * Use this for explicit error handling when not using emitResult.
     *
     * @param callId - The tool call ID
     * @param toolName - The tool name
     * @param error - The error that occurred
     * @param subagentMeta - Optional subagent metadata
     */
    async emitError(callId, toolName, error, subagentMeta) {
        await this.sendUpdate({
            sessionUpdate: 'tool_call_update',
            toolCallId: callId,
            status: 'failed',
            content: [
                { type: 'content', content: { type: 'text', text: error.message } },
            ],
            _meta: {
                toolName,
                ...subagentMeta,
            },
        });
    }
    // ==================== Public Utilities ====================
    /**
     * Checks if a tool name is the TodoWriteTool.
     * Exposed for external use in components that need to check this.
     */
    isTodoWriteTool(toolName) {
        return toolName === ToolNames.TODO_WRITE;
    }
    /**
     * Checks if a tool name is the ExitPlanModeTool.
     */
    isExitPlanModeTool(toolName) {
        return toolName === ToolNames.EXIT_PLAN_MODE;
    }
    /**
     * Resolves tool metadata from the registry.
     * Falls back to defaults if tool not found or build fails.
     *
     * @param toolName - Name of the tool
     * @param args - Tool call arguments (used to build invocation)
     */
    resolveToolMetadata(toolName, args) {
        const toolRegistry = this.config.getToolRegistry();
        const tool = toolRegistry.getTool(toolName);
        let title = tool?.displayName ?? toolName;
        let locations = [];
        let kind = 'other';
        if (tool && args) {
            try {
                const invocation = tool.build(args);
                title = `${title}: ${invocation.getDescription()}`;
                // Map locations to ensure line is null instead of undefined (for ACP consistency)
                locations = invocation.toolLocations().map((loc) => ({
                    path: loc.path,
                    line: loc.line ?? null,
                }));
                // Pass tool name to handle special cases like exit_plan_mode -> switch_mode
                kind = this.mapToolKind(tool.kind, toolName);
            }
            catch {
                // Fallback: use the description arg directly if available
                if (typeof args['description'] === 'string') {
                    title = `${title}: ${args['description']}`;
                }
                if (tool.kind) {
                    kind = this.mapToolKind(tool.kind, toolName);
                }
            }
        }
        return { title, locations, kind };
    }
    /**
     * Maps core Tool Kind enum to ACP ToolKind string literals.
     *
     * @param kind - The core Kind enum value
     * @param toolName - Optional tool name to handle special cases like exit_plan_mode
     */
    mapToolKind(kind, toolName) {
        // Special case: exit_plan_mode uses 'switch_mode' kind per ACP spec
        if (toolName && this.isExitPlanModeTool(toolName)) {
            return 'switch_mode';
        }
        const kindMap = {
            [Kind.Read]: 'read',
            [Kind.Edit]: 'edit',
            [Kind.Delete]: 'delete',
            [Kind.Move]: 'move',
            [Kind.Search]: 'search',
            [Kind.Execute]: 'execute',
            [Kind.Think]: 'think',
            [Kind.Fetch]: 'fetch',
            [Kind.Other]: 'other',
        };
        return kindMap[kind] ?? 'other';
    }
    // ==================== Private Helpers ====================
    /**
     * Extracts diff content from resultDisplay if it's a diff type (edit tool result).
     * Returns null if not a diff.
     */
    extractDiffContent(resultDisplay) {
        if (!resultDisplay || typeof resultDisplay !== 'object')
            return null;
        const obj = resultDisplay;
        // Check if this is a diff display (edit tool result)
        if ('fileName' in obj && 'newContent' in obj) {
            if (obj['truncatedForSession'] === true) {
                return {
                    type: 'content',
                    content: {
                        type: 'text',
                        text: buildTruncatedDiffPreviewText(obj),
                    },
                };
            }
            return {
                type: 'diff',
                path: obj['fileName'],
                oldText: obj['originalContent'] ?? '',
                newText: obj['newContent'],
            };
        }
        return null;
    }
    /**
     * Transforms Part[] to ToolCallContent[].
     * Extracts text from functionResponse parts and text parts.
     */
    transformPartsToToolCallContent(parts) {
        const result = [];
        for (const part of parts) {
            // Handle text parts
            if ('text' in part && part.text) {
                result.push({
                    type: 'content',
                    content: { type: 'text', text: part.text },
                });
            }
            // Handle functionResponse parts - stringify the response
            if ('functionResponse' in part && part.functionResponse) {
                try {
                    const resp = part.functionResponse.response;
                    const outputField = resp['output'];
                    const errorField = resp['error'];
                    const responseText = typeof outputField === 'string'
                        ? outputField
                        : typeof errorField === 'string'
                            ? errorField
                            : JSON.stringify(resp);
                    result.push({
                        type: 'content',
                        content: { type: 'text', text: responseText },
                    });
                }
                catch {
                    // Ignore serialization errors
                }
            }
        }
        return result;
    }
}
//# sourceMappingURL=ToolCallEmitter.js.map