/**
 * @license
 * Copyright 2026 HopCode Team Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { OutputFormat, ToolErrorType, createDebugLogger, getMCPServerStatus, } from '@hoptrendy/hopcode-core';
import { computeSessionStats } from '../ui/utils/computeStats.js';
import { getAvailableCommands } from '../nonInteractiveCliCommands.js';
const debugLogger = createDebugLogger('NON_INTERACTIVE');
/**
 * Normalizes various part list formats into a consistent Part[] array.
 *
 * @param parts - Input parts in various formats (string, Part, Part[], or null)
 * @returns Normalized array of Part objects
 */
export function normalizePartList(parts) {
    if (!parts) {
        return [];
    }
    if (typeof parts === 'string') {
        return [{ text: parts }];
    }
    if (Array.isArray(parts)) {
        return parts.map((part) => typeof part === 'string' ? { text: part } : part);
    }
    return [parts];
}
/**
 * Extracts user message parts from a CLI protocol message.
 *
 * @param message - User message sourced from the CLI protocol layer
 * @returns Extracted parts or null if the message lacks textual content
 */
export function extractPartsFromUserMessage(message) {
    if (!message) {
        return null;
    }
    const content = message.message?.content;
    if (typeof content === 'string') {
        return content;
    }
    if (Array.isArray(content)) {
        const parts = [];
        for (const block of content) {
            if (!block || typeof block !== 'object' || !('type' in block)) {
                continue;
            }
            if (block.type === 'text' && 'text' in block && block.text) {
                parts.push({ text: block.text });
            }
            else {
                parts.push({ text: JSON.stringify(block) });
            }
        }
        return parts.length > 0 ? parts : null;
    }
    return null;
}
/**
 * Extracts usage metadata from the Gemini client's debug responses.
 *
 * @param geminiClient - The Gemini client instance
 * @returns Usage information or undefined if not available
 */
export function extractUsageFromGeminiClient(geminiClient) {
    if (!geminiClient ||
        typeof geminiClient !== 'object' ||
        typeof geminiClient.getChat !== 'function') {
        return undefined;
    }
    try {
        const chat = geminiClient.getChat();
        if (!chat ||
            typeof chat !== 'object' ||
            typeof chat.getDebugResponses !==
                'function') {
            return undefined;
        }
        const responses = chat.getDebugResponses();
        for (let i = responses.length - 1; i >= 0; i--) {
            const metadata = responses[i]?.['usageMetadata'];
            if (metadata) {
                const promptTokens = metadata['promptTokenCount'];
                const completionTokens = metadata['candidatesTokenCount'];
                const totalTokens = metadata['totalTokenCount'];
                const cachedTokens = metadata['cachedContentTokenCount'];
                return {
                    input_tokens: typeof promptTokens === 'number' ? promptTokens : 0,
                    output_tokens: typeof completionTokens === 'number' ? completionTokens : 0,
                    total_tokens: typeof totalTokens === 'number' ? totalTokens : undefined,
                    cache_read_input_tokens: typeof cachedTokens === 'number' ? cachedTokens : undefined,
                };
            }
        }
    }
    catch (error) {
        debugLogger.debug('Failed to extract usage metadata:', error);
    }
    return undefined;
}
/**
 * Computes Usage information from SessionMetrics using computeSessionStats.
 * Aggregates token usage across all models in the session.
 *
 * @param metrics - Session metrics from uiTelemetryService
 * @returns Usage object with token counts
 */
export function computeUsageFromMetrics(metrics) {
    const stats = computeSessionStats(metrics);
    const { models } = metrics;
    // Sum up output tokens (candidates) and total tokens across all models
    const totalOutputTokens = Object.values(models).reduce((acc, model) => acc + model.tokens.candidates, 0);
    const totalTokens = Object.values(models).reduce((acc, model) => acc + model.tokens.total, 0);
    const usage = {
        input_tokens: stats.totalPromptTokens,
        output_tokens: totalOutputTokens,
        cache_read_input_tokens: stats.totalCachedTokens,
    };
    // Only include total_tokens if it's greater than 0
    if (totalTokens > 0) {
        usage.total_tokens = totalTokens;
    }
    return usage;
}
/**
 * Load slash command names using getAvailableCommands
 *
 * @param config - Config instance
 * @returns Promise resolving to array of slash command names
 */
async function loadSlashCommandNames(config) {
    const controller = new AbortController();
    try {
        const commands = await getAvailableCommands(config, controller.signal, 'non_interactive');
        // Extract command names and sort
        return commands.map((cmd) => cmd.name).sort();
    }
    catch (error) {
        debugLogger.error('[buildSystemMessage] Failed to load slash commands:', error);
        return [];
    }
    finally {
        controller.abort();
    }
}
/**
 * Build system message for SDK
 *
 * Constructs a system initialization message including tools, MCP servers,
 * and model configuration. System messages are independent of the control
 * system and are sent before every turn regardless of whether control
 * system is available.
 *
 * Note: Control capabilities are NOT included in system messages. They
 * are only included in the initialize control response, which is handled
 * separately by SystemController.
 *
 * @param config - Config instance
 * @param sessionId - Session identifier
 * @param permissionMode - Current permission/approval mode
 * @returns Promise resolving to CLISystemMessage
 */
export async function buildSystemMessage(config, sessionId, permissionMode) {
    const toolRegistry = config.getToolRegistry();
    const tools = toolRegistry ? toolRegistry.getAllToolNames() : [];
    const mcpServers = config.getMcpServers();
    const mcpServerList = mcpServers
        ? Object.keys(mcpServers).map((name) => ({
            name,
            status: getMCPServerStatus(name),
        }))
        : [];
    // Load slash commands available in ACP mode
    const slashCommands = await loadSlashCommandNames(config);
    // Load subagent names from config
    let agentNames = [];
    try {
        const subagentManager = config.getSubagentManager();
        const subagents = await subagentManager.listSubagents();
        agentNames = subagents.map((subagent) => subagent.name);
    }
    catch (error) {
        debugLogger.error('[buildSystemMessage] Failed to load subagents:', error);
    }
    const systemMessage = {
        type: 'system',
        subtype: 'init',
        uuid: sessionId,
        session_id: sessionId,
        cwd: config.getTargetDir(),
        tools,
        mcp_servers: mcpServerList,
        model: config.getModel(),
        permission_mode: permissionMode,
        slash_commands: slashCommands,
        HOPCODE_version: config.getCliVersion() || 'unknown',
        agents: agentNames,
    };
    return systemMessage;
}
function isMcpToolProgressData(output) {
    return (typeof output === 'object' &&
        output !== null &&
        'type' in output &&
        output.type === 'mcp_tool_progress');
}
/**
 * Creates a generic output update handler for tools with canUpdateOutput=true.
 * This handler forwards MCP progress data (McpToolProgressData) as tool_progress
 * stream events via the adapter. Progress events are only emitted when the adapter
 * supports partial messages (i.e., includePartialMessages is true).
 *
 * @param request - Tool call request info
 * @param adapter - The adapter instance for emitting messages
 * @returns An object containing the output update handler
 */
export function createToolProgressHandler(request, adapter) {
    const handler = (_callId, output) => {
        if (isMcpToolProgressData(output)) {
            adapter.emitToolProgress(request, output);
        }
    };
    return { handler };
}
/**
 * Creates an output update handler specifically for Agent tool subagent execution.
 * This handler monitors AgentResultDisplay updates and converts them to protocol messages
 * using the unified adapter's subagent APIs. All emitted messages will have parent_tool_use_id set to
 * the agent tool's callId.
 *
 * @param config - Config instance for getting output format
 * @param agentToolCallId - The agent tool's callId to use as parent_tool_use_id for all subagent messages
 * @param adapter - The unified adapter instance (JsonOutputAdapter or StreamJsonOutputAdapter)
 * @returns An object containing the output update handler
 */
export function createAgentToolProgressHandler(config, agentToolCallId, adapter) {
    // Track previous AgentResultDisplay states per tool call to detect changes
    const previousTaskStates = new Map();
    // Track which tool call IDs have already emitted tool_use to prevent duplicates
    const emittedToolUseIds = new Set();
    // Track which tool call IDs have already emitted tool_result to prevent duplicates
    const emittedToolResultIds = new Set();
    /**
     * Builds a ToolCallRequestInfo object from a tool call.
     *
     * @param toolCall - The tool call information
     * @returns ToolCallRequestInfo object
     */
    const buildRequest = (toolCall) => ({
        callId: toolCall.callId,
        name: toolCall.name,
        args: toolCall.args || {},
        isClientInitiated: true,
        prompt_id: '',
        response_id: undefined,
    });
    /**
     * Builds a ToolCallResponseInfo object from a tool call.
     *
     * @param toolCall - The tool call information
     * @returns ToolCallResponseInfo object
     */
    const buildResponse = (toolCall) => ({
        callId: toolCall.callId,
        error: toolCall.status === 'failed'
            ? new Error(toolCall.error || 'Tool execution failed')
            : undefined,
        errorType: toolCall.status === 'failed' ? ToolErrorType.EXECUTION_FAILED : undefined,
        resultDisplay: toolCall.resultDisplay,
        responseParts: toolCall.responseParts || [],
    });
    /**
     * Checks if a tool call has result content that should be emitted.
     *
     * @param toolCall - The tool call information
     * @returns True if the tool call has result content to emit
     */
    const hasResultContent = (toolCall) => {
        // Check resultDisplay string
        if (typeof toolCall.resultDisplay === 'string' &&
            toolCall.resultDisplay.trim().length > 0) {
            return true;
        }
        // Check responseParts - only check existence, don't parse for performance
        if (toolCall.responseParts && toolCall.responseParts.length > 0) {
            return true;
        }
        // Failed status should always emit result
        return toolCall.status === 'failed';
    };
    /**
     * Emits tool_use for a tool call if it hasn't been emitted yet.
     *
     * @param toolCall - The tool call information
     * @param fallbackStatus - Optional fallback status if toolCall.status should be overridden
     */
    const emitToolUseIfNeeded = (toolCall, fallbackStatus) => {
        if (emittedToolUseIds.has(toolCall.callId)) {
            return;
        }
        const toolCallToEmit = fallbackStatus
            ? {
                ...toolCall,
                status: fallbackStatus,
            }
            : toolCall;
        if (toolCallToEmit.status === 'executing' ||
            toolCallToEmit.status === 'awaiting_approval') {
            if (adapter.processSubagentToolCall) {
                adapter.processSubagentToolCall(toolCallToEmit, agentToolCallId);
                emittedToolUseIds.add(toolCall.callId);
            }
        }
    };
    /**
     * Emits tool_result for a tool call if it hasn't been emitted yet and has content.
     *
     * @param toolCall - The tool call information
     */
    const emitToolResultIfNeeded = (toolCall) => {
        if (emittedToolResultIds.has(toolCall.callId)) {
            return;
        }
        if (!hasResultContent(toolCall)) {
            return;
        }
        // Mark as emitted even if we skip, to prevent duplicate emits
        emittedToolResultIds.add(toolCall.callId);
        const request = buildRequest(toolCall);
        const response = buildResponse(toolCall);
        // For subagent tool results, we need to pass parentToolUseId
        // The adapter implementations accept an optional parentToolUseId parameter
        if ('emitToolResult' in adapter &&
            typeof adapter.emitToolResult === 'function') {
            adapter.emitToolResult(request, response, agentToolCallId);
        }
        else {
            adapter.emitToolResult(request, response);
        }
    };
    /**
     * Processes a tool call, ensuring tool_use and tool_result are emitted exactly once.
     *
     * @param toolCall - The tool call information
     * @param previousCall - The previous state of the tool call (if any)
     */
    const processToolCall = (toolCall, previousCall) => {
        const isCompleted = toolCall.status === 'success' || toolCall.status === 'failed';
        const isExecuting = toolCall.status === 'executing' ||
            toolCall.status === 'awaiting_approval';
        const wasExecuting = previousCall &&
            (previousCall.status === 'executing' ||
                previousCall.status === 'awaiting_approval');
        // Emit tool_use if needed
        if (isExecuting) {
            // Normal case: tool call is executing or awaiting approval
            emitToolUseIfNeeded(toolCall);
        }
        else if (isCompleted && !emittedToolUseIds.has(toolCall.callId)) {
            // Edge case: tool call appeared with result already (shouldn't happen normally,
            // but handle it gracefully by emitting tool_use with 'executing' status first)
            emitToolUseIfNeeded(toolCall, 'executing');
        }
        else if (wasExecuting && isCompleted) {
            // Status changed from executing to completed - ensure tool_use was emitted
            emitToolUseIfNeeded(toolCall, 'executing');
        }
        // Emit tool_result if tool call is completed
        if (isCompleted) {
            emitToolResultIfNeeded(toolCall);
        }
    };
    const outputUpdateHandler = (callId, outputChunk) => {
        // Only process AgentResultDisplay (Task tool updates)
        if (typeof outputChunk === 'object' &&
            outputChunk !== null &&
            'type' in outputChunk &&
            outputChunk.type === 'task_execution') {
            const taskDisplay = outputChunk;
            const previous = previousTaskStates.get(callId);
            // Only process if adapter supports subagent APIs
            if (!adapter.processSubagentToolCall ||
                !adapter.emitSubagentErrorResult) {
                previousTaskStates.set(callId, taskDisplay);
                return;
            }
            if (taskDisplay.toolCalls) {
                if (!previous || !previous.toolCalls) {
                    // First time seeing tool calls - process all initial ones
                    for (const toolCall of taskDisplay.toolCalls) {
                        processToolCall(toolCall);
                    }
                }
                else {
                    // Compare with previous state to find new/changed tool calls
                    for (const toolCall of taskDisplay.toolCalls) {
                        const previousCall = previous.toolCalls.find((tc) => tc.callId === toolCall.callId);
                        processToolCall(toolCall, previousCall);
                    }
                }
            }
            // Handle task-level errors (status: 'failed', 'cancelled')
            if (taskDisplay.status === 'failed' ||
                taskDisplay.status === 'cancelled') {
                const previousStatus = previous?.status;
                // Only emit error result if status changed to failed/cancelled
                if (previousStatus !== 'failed' &&
                    previousStatus !== 'cancelled' &&
                    previousStatus !== undefined) {
                    const errorMessage = taskDisplay.terminateReason ||
                        (taskDisplay.status === 'cancelled'
                            ? 'Task was cancelled'
                            : 'Task execution failed');
                    // Use subagent adapter's emitSubagentErrorResult method
                    adapter.emitSubagentErrorResult(errorMessage, 0, agentToolCallId);
                }
            }
            // Handle subagent initial message (prompt) in non-interactive mode with json/stream-json output
            // Emit when this is the first update (previous is undefined) and task starts
            if (!previous &&
                taskDisplay.taskPrompt &&
                !config.isInteractive() &&
                (config.getOutputFormat() === OutputFormat.JSON ||
                    config.getOutputFormat() === OutputFormat.STREAM_JSON)) {
                // Emit the user message with the correct parent_tool_use_id
                adapter.emitUserMessage([{ text: taskDisplay.taskPrompt }], agentToolCallId);
            }
            // Update previous state
            previousTaskStates.set(callId, taskDisplay);
        }
    };
    // No longer need to attach adapter to handler - task.ts uses AgentResultDisplay.message instead
    return {
        handler: outputUpdateHandler,
    };
}
/**
 * Converts function response parts to a string representation.
 * Handles functionResponse parts specially by extracting their output content.
 *
 * @param parts - Array of Part objects to convert
 * @returns String representation of the parts
 */
export function functionResponsePartsToString(parts) {
    return parts
        .map((part) => {
        if ('functionResponse' in part) {
            const content = part.functionResponse?.response?.['output'] ?? '';
            return content;
        }
        return JSON.stringify(part);
    })
        .join('');
}
/**
 * Extracts content from a tool call response for inclusion in tool_result blocks.
 * Uses functionResponsePartsToString to properly handle functionResponse parts,
 * which correctly extracts output content from functionResponse objects rather
 * than simply concatenating text or JSON.stringify.
 *
 * @param response - Tool call response information
 * @returns String content for the tool_result block, or undefined if no content available
 */
export function toolResultContent(response) {
    if (typeof response.resultDisplay === 'string' &&
        response.resultDisplay.trim().length > 0) {
        return response.resultDisplay;
    }
    if (response.responseParts && response.responseParts.length > 0) {
        // Always use functionResponsePartsToString to properly handle
        // functionResponse parts that contain output content
        return functionResponsePartsToString(response.responseParts);
    }
    if (response.error) {
        return response.error.message;
    }
    return undefined;
}
//# sourceMappingURL=nonInteractiveHelpers.js.map