import { OpenAIClient } from "../utils/openaiClient.js";
import { logger } from "../utils/globalLogger.js";
import { addOnceAbortListener } from "../utils/abortUtils.js";
import { transformMessagesForClaudeCache, addCacheControlToLastTool, supportsPromptCaching, extendUsageWithCacheMetrics, } from "../utils/cacheControlUtils.js";
import * as os from "os";
import * as fs from "fs";
import * as path from "path";
import { COMPRESS_MESSAGES_SYSTEM_PROMPT, WEB_CONTENT_SYSTEM_PROMPT, BTW_SYSTEM_PROMPT, } from "../prompts/index.js";
// Global rate limiter state for 1 QPS
let nextAllowedTime = 0;
const MIN_INTERVAL = 1000; // 1 second for 1 QPS
/**
 * Resets the rate limiter state. Primarily used for testing.
 */
export function resetRateLimiter() {
    nextAllowedTime = 0;
}
/**
 * Acquires a slot for an AI request, ensuring 1 QPS limit.
 * @param abortSignal Optional abort signal to cancel waiting
 */
async function acquireSlot(abortSignal) {
    if (abortSignal?.aborted) {
        throw new Error("Request was aborted");
    }
    const now = Date.now();
    const waitTime = Math.max(0, nextAllowedTime - now);
    // Reserve the slot synchronously to ensure global ordering
    nextAllowedTime = Math.max(now, nextAllowedTime) + MIN_INTERVAL;
    if (waitTime > 0) {
        await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                cleanup();
                resolve();
            }, waitTime);
            const cleanup = abortSignal
                ? addOnceAbortListener(abortSignal, () => {
                    clearTimeout(timeout);
                    reject(new Error("Request was aborted"));
                })
                : () => { };
        });
    }
}
/**
 * Get specific configuration parameters based on model name
 * @param modelName Model name
 * @param baseConfig Base configuration
 * @returns Configured model parameters
 */
function getModelConfig(modelName, baseConfig = {}) {
    const config = {
        model: modelName,
        stream: false,
        ...baseConfig,
    };
    // Handle parameter exclusion: if a parameter is explicitly set to null, remove it.
    // This allows users to "unset" default parameters like temperature for models that don't support them.
    for (const key in config) {
        if (config[key] === null) {
            delete config[key];
        }
    }
    return config;
}
export async function callAgent(options) {
    const { gatewayConfig, modelConfig, messages, abortSignal, workdir, tools, model, systemPrompt, onContentUpdate, onToolUpdate, onReasoningUpdate, } = options;
    // Apply global 1 QPS rate limit
    if (process.env.NODE_ENV !== "test" ||
        modelConfig.model === "rate-limit-test") {
        await acquireSlot(abortSignal);
    }
    // Declare variables outside try block for error handling access
    let openaiMessages;
    let createParams;
    let processedTools;
    try {
        // Create OpenAI client with injected configuration
        const openai = new OpenAIClient({
            apiKey: gatewayConfig.apiKey,
            baseURL: gatewayConfig.baseURL,
            defaultHeaders: gatewayConfig.defaultHeaders,
            fetchOptions: gatewayConfig.fetchOptions,
            fetch: gatewayConfig.fetch,
        });
        // Build system prompt content
        const systemContent = systemPrompt || "";
        // Add system prompt
        const systemMessage = {
            role: "system",
            content: systemContent,
        };
        // ChatCompletionMessageParam[] is already in OpenAI format, add system prompt to the beginning
        openaiMessages = [systemMessage, ...messages];
        // Apply cache control for Claude models
        const currentModel = model || modelConfig.model;
        const resolvedMaxTokens = options.maxTokens ?? modelConfig.maxTokens;
        processedTools = tools;
        if (supportsPromptCaching(currentModel)) {
            openaiMessages = transformMessagesForClaudeCache(openaiMessages, currentModel);
            // Apply cache control to tools separately
            if (tools && tools.length > 0) {
                processedTools = addCacheControlToLastTool(tools);
            }
        }
        // Get model configuration - use injected modelConfig with optional override
        const { model: _model, fastModel: _fastModel, maxTokens: _maxTokens, permissionMode: _permissionMode, ...extraParams } = modelConfig;
        void _model;
        void _fastModel;
        void _maxTokens;
        void _permissionMode;
        const openaiModelConfig = getModelConfig(model || modelConfig.model, {
            max_tokens: resolvedMaxTokens,
            ...extraParams,
        });
        // Determine if streaming is needed
        const isStreaming = !!(onContentUpdate ||
            onToolUpdate ||
            onReasoningUpdate);
        // Prepare API call parameters
        createParams = {
            ...openaiModelConfig,
            messages: openaiMessages,
            stream: isStreaming,
        };
        // Only add tools if they exist
        if (processedTools && processedTools.length > 0) {
            createParams.tools = processedTools;
        }
        if (isStreaming) {
            // Handle streaming response
            const { data: stream, response } = await openai.chat.completions
                .create(createParams, {
                signal: abortSignal,
            })
                .withResponse();
            // Extract response headers
            const responseHeaders = {};
            response.headers.forEach((value, key) => {
                responseHeaders[key] = value;
            });
            return await processStreamingResponse(stream, onContentUpdate, onToolUpdate, onReasoningUpdate, abortSignal, responseHeaders, currentModel);
        }
        else {
            // Handle non-streaming response
            const { data: response, response: rawResponse } = await openai.chat.completions
                .create(createParams, {
                signal: abortSignal,
            })
                .withResponse();
            // Extract response headers
            const responseHeaders = {};
            rawResponse.headers.forEach((value, key) => {
                responseHeaders[key] = value;
            });
            const finalMessage = response.choices[0]?.message;
            const finishReason = response.choices[0]?.finish_reason || null;
            let totalUsage = response.usage
                ? {
                    prompt_tokens: response.usage.prompt_tokens,
                    completion_tokens: response.usage.completion_tokens,
                    total_tokens: response.usage.total_tokens,
                }
                : undefined;
            // Extend usage with cache metrics for Claude models
            if (totalUsage && supportsPromptCaching(currentModel) && response.usage) {
                totalUsage = extendUsageWithCacheMetrics(totalUsage, response.usage);
            }
            const result = {};
            if (finalMessage) {
                const { content: finalContent, tool_calls: finalToolCalls, reasoning_content: finalReasoningContent, ...otherFields } = finalMessage;
                if (typeof finalContent === "string" && finalContent.length > 0) {
                    result.content = finalContent;
                }
                if (typeof finalReasoningContent === "string" &&
                    finalReasoningContent.length > 0) {
                    result.reasoning_content = finalReasoningContent;
                }
                if (Array.isArray(finalToolCalls) && finalToolCalls.length > 0) {
                    result.tool_calls = finalToolCalls;
                }
                if (Object.keys(otherFields).length > 0) {
                    const additionalFields = {};
                    for (const [key, value] of Object.entries(otherFields)) {
                        if (value !== undefined && key !== "role") {
                            additionalFields[key] = value;
                        }
                    }
                    if (Object.keys(additionalFields).length > 0) {
                        result.additionalFields = additionalFields;
                    }
                }
            }
            if (totalUsage) {
                result.usage = totalUsage;
            }
            if (finishReason) {
                result.finish_reason = finishReason;
            }
            if (Object.keys(responseHeaders).length > 0) {
                result.response_headers = responseHeaders;
            }
            return result;
        }
    }
    catch (error) {
        if (error.name === "AbortError") {
            logger.info("OpenAI request aborted");
            throw new Error("Request was aborted");
        }
        // Check if it's a 400 error and save messages to temp directory
        if (error &&
            typeof error === "object" &&
            "status" in error &&
            error.status === 400) {
            try {
                // Create temp directory for error debugging
                const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "callAgent-400-error-"));
                const messagesFile = path.join(tempDir, "messages.json");
                const errorFile = path.join(tempDir, "error.json");
                // Save complete messages to temp file
                const debugData = {
                    originalMessages: messages,
                    timestamp: new Date().toISOString(),
                    model: model || modelConfig.model,
                    workdir,
                    sessionId: options.sessionId,
                    gatewayConfig: {
                        baseURL: gatewayConfig.baseURL,
                        // Don't include apiKey for security
                        defaultHeaders: gatewayConfig.defaultHeaders,
                    },
                };
                // Add processed messages if they exist
                if (typeof openaiMessages !== "undefined") {
                    debugData.processedMessages = openaiMessages;
                }
                // Add create params if they exist
                if (typeof createParams !== "undefined") {
                    debugData.createParams = createParams;
                }
                // Add tools if they exist
                if (processedTools) {
                    debugData.tools = processedTools;
                }
                fs.writeFileSync(messagesFile, JSON.stringify(debugData, null, 2));
                // Save error details
                const errorData = {
                    error: {
                        message: error && typeof error === "object" && "message" in error
                            ? String(error.message)
                            : undefined,
                        status: error && typeof error === "object" && "status" in error
                            ? Number(error.status)
                            : undefined,
                        type: error && typeof error === "object" && "type" in error
                            ? String(error.type)
                            : undefined,
                        code: error && typeof error === "object" && "code" in error
                            ? String(error.code)
                            : undefined,
                        body: error && typeof error === "object" && "body" in error
                            ? error.body
                            : undefined,
                        stack: error && typeof error === "object" && "stack" in error
                            ? String(error.stack)
                            : undefined,
                    },
                    timestamp: new Date().toISOString(),
                };
                fs.writeFileSync(errorFile, JSON.stringify(errorData, null, 2));
                logger.error("callAgent 400 error occurred. Debug files saved to:", tempDir);
                logger.error("Messages file:", messagesFile);
                logger.error("Error file:", errorFile);
                logger.error("Error details:", error);
            }
            catch (saveError) {
                logger.error("Failed to save 400 error debug files:", saveError);
            }
        }
        logger.error("Failed to call OpenAI:", error);
        throw error;
    }
}
/**
 * Process streaming response from OpenAI API
 * @param stream Async iterator of chat completion chunks
 * @param onContentUpdate Callback for content updates
 * @param onToolUpdate Callback for tool updates
 * @param abortSignal Optional abort signal
 * @param responseHeaders Response headers from the initial request
 * @param modelName Model name for cache control processing
 * @returns Final result with accumulated content and tool calls
 */
async function processStreamingResponse(stream, onContentUpdate, onToolUpdate, onReasoningUpdate, abortSignal, responseHeaders, modelName) {
    let accumulatedContent = "";
    let accumulatedReasoningContent = "";
    const toolCalls = [];
    const additionalDeltaFields = {};
    let usage = undefined;
    let finishReason = null;
    try {
        for await (const chunk of stream) {
            // Check for abort signal
            if (abortSignal?.aborted) {
                throw new Error("Request was aborted");
            }
            // Check for usage information in any chunk
            if (chunk.usage) {
                let chunkUsage = {
                    prompt_tokens: chunk.usage.prompt_tokens,
                    completion_tokens: chunk.usage.completion_tokens,
                    total_tokens: chunk.usage.total_tokens,
                };
                // Extend usage with cache metrics for Claude models
                if (modelName && supportsPromptCaching(modelName)) {
                    chunkUsage = extendUsageWithCacheMetrics(chunkUsage, chunk.usage);
                }
                usage = chunkUsage;
            }
            // Check for finish_reason in the choice
            const choice = chunk.choices?.[0];
            if (choice?.finish_reason) {
                finishReason = choice.finish_reason;
            }
            const delta = choice?.delta;
            if (!delta) {
                continue;
            }
            const { content, tool_calls: toolCallUpdates, reasoning_content, ...deltaMetadata } = delta;
            if (Object.keys(deltaMetadata).length > 0) {
                Object.assign(additionalDeltaFields, deltaMetadata);
            }
            if (typeof content === "string" && content.length > 0) {
                // Note: OpenAI API already handles UTF-8 character boundaries correctly in streaming,
                // ensuring that delta.content always contains complete UTF-8 strings
                accumulatedContent += content;
                if (onContentUpdate) {
                    onContentUpdate(accumulatedContent);
                }
            }
            if (typeof reasoning_content === "string" &&
                reasoning_content.length > 0) {
                accumulatedReasoningContent += reasoning_content;
                if (onReasoningUpdate) {
                    onReasoningUpdate(accumulatedReasoningContent);
                }
            }
            if (Array.isArray(toolCallUpdates)) {
                for (const rawToolCall of toolCallUpdates) {
                    const toolCallDelta = rawToolCall;
                    if (!toolCallDelta.function) {
                        continue;
                    }
                    const functionDelta = toolCallDelta.function;
                    let existingCall;
                    let isNew = false;
                    if (toolCallDelta.id) {
                        existingCall = toolCalls.find((t) => t.id === toolCallDelta.id);
                        if (!existingCall) {
                            existingCall = {
                                id: toolCallDelta.id,
                                type: "function",
                                function: {
                                    name: functionDelta.name || "",
                                    arguments: "",
                                },
                            };
                            toolCalls.push(existingCall);
                            isNew = true;
                        }
                    }
                    else {
                        existingCall = toolCalls[toolCalls.length - 1];
                    }
                    if (!existingCall) {
                        continue;
                    }
                    if (functionDelta.name) {
                        existingCall.function.name = functionDelta.name;
                    }
                    // Emit start stage when a new tool call is created and we have the tool name
                    if (onToolUpdate && isNew && existingCall.function.name) {
                        onToolUpdate({
                            id: existingCall.id,
                            name: existingCall.function.name,
                            parameters: "", // Empty parameters for start stage
                            parametersChunk: "", // Empty chunk for start stage
                            stage: "start", // New tool call triggers start stage
                        });
                        isNew = false; // Prevent duplicate start emissions
                    }
                    if (functionDelta.arguments) {
                        existingCall.function.arguments += functionDelta.arguments;
                    }
                    // Emit streaming updates for all chunks with actual content (including first chunk)
                    if (onToolUpdate &&
                        existingCall.function.name &&
                        functionDelta.arguments &&
                        functionDelta.arguments.length > 0 // Only emit streaming for chunks with actual content
                    ) {
                        onToolUpdate({
                            id: existingCall.id,
                            name: existingCall.function.name,
                            parameters: existingCall.function.arguments,
                            parametersChunk: functionDelta.arguments,
                            stage: "streaming",
                        });
                    }
                }
            }
        }
    }
    catch (error) {
        if (error.message === "Request was aborted") {
            throw error;
        }
        throw error;
    }
    // Prepare final result
    const result = {};
    if (accumulatedContent) {
        result.content = accumulatedContent.trim();
    }
    if (accumulatedReasoningContent) {
        result.reasoning_content = accumulatedReasoningContent.trim();
    }
    if (toolCalls.length > 0) {
        result.tool_calls = toolCalls;
    }
    if (usage) {
        result.usage = usage;
    }
    if (finishReason) {
        result.finish_reason = finishReason;
    }
    if (responseHeaders && Object.keys(responseHeaders).length > 0) {
        result.response_headers = responseHeaders;
    }
    if (Object.keys(additionalDeltaFields).length > 0) {
        result.additionalFields = {};
        for (const [key, value] of Object.entries(additionalDeltaFields)) {
            if (value !== undefined && key !== "role") {
                result.additionalFields[key] = value;
            }
        }
        if (Object.keys(result.additionalFields).length === 0) {
            delete result.additionalFields;
        }
    }
    return result;
}
export async function compressMessages(options) {
    const { gatewayConfig, modelConfig, messages, abortSignal } = options;
    // Apply global 1 QPS rate limit
    if (process.env.NODE_ENV !== "test" ||
        modelConfig.model === "rate-limit-test") {
        await acquireSlot(abortSignal);
    }
    // Create OpenAI client with injected configuration
    const openai = new OpenAIClient({
        apiKey: gatewayConfig.apiKey,
        baseURL: gatewayConfig.baseURL,
        defaultHeaders: gatewayConfig.defaultHeaders,
        fetchOptions: gatewayConfig.fetchOptions,
        fetch: gatewayConfig.fetch,
    });
    // Get model configuration - use injected agent model
    const { model: _model, fastModel: _fastModel, maxTokens: _maxTokens, permissionMode: _permissionMode, ...extraParams } = modelConfig;
    void _model;
    void _fastModel;
    void _maxTokens;
    void _permissionMode;
    const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
        temperature: 0.1,
        max_tokens: 8192,
        ...extraParams,
    });
    try {
        const response = await openai.chat.completions.create({
            ...openaiModelConfig,
            messages: [
                {
                    role: "system",
                    content: COMPRESS_MESSAGES_SYSTEM_PROMPT,
                },
                ...messages,
                {
                    role: "user",
                    content: `Please create a detailed summary of the conversation so far.`,
                },
            ],
        }, {
            signal: abortSignal,
        });
        const content = response.choices[0]?.message?.content?.trim();
        if (!content) {
            throw new Error("Failed to compress conversation history: Empty response from AI");
        }
        const usage = response.usage
            ? {
                prompt_tokens: response.usage.prompt_tokens,
                completion_tokens: response.usage.completion_tokens,
                total_tokens: response.usage.total_tokens,
            }
            : undefined;
        return {
            content,
            usage,
        };
    }
    catch (error) {
        if (error.name === "AbortError") {
            logger.info("Compression request was aborted");
            throw new Error("Compression request was aborted");
        }
        logger.error("Failed to compress messages:", error);
        throw error;
    }
}
export async function processWebContent(options) {
    const { gatewayConfig, modelConfig, content, prompt, abortSignal } = options;
    // Apply global 1 QPS rate limit
    if (process.env.NODE_ENV !== "test" ||
        modelConfig.model === "rate-limit-test") {
        await acquireSlot(abortSignal);
    }
    // Create OpenAI client with injected configuration
    const openai = new OpenAIClient({
        apiKey: gatewayConfig.apiKey,
        baseURL: gatewayConfig.baseURL,
        defaultHeaders: gatewayConfig.defaultHeaders,
        fetchOptions: gatewayConfig.fetchOptions,
        fetch: gatewayConfig.fetch,
    });
    // Get model configuration - use injected agent model
    const { model: _model, fastModel: _fastModel, maxTokens: _maxTokens, permissionMode: _permissionMode, ...extraParams } = modelConfig;
    void _model;
    void _fastModel;
    void _maxTokens;
    void _permissionMode;
    const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
        temperature: 0.1,
        max_tokens: 4096,
        ...extraParams,
    });
    try {
        const response = await openai.chat.completions.create({
            ...openaiModelConfig,
            messages: [
                {
                    role: "system",
                    content: WEB_CONTENT_SYSTEM_PROMPT,
                },
                {
                    role: "user",
                    content: `Web Content:\n\n${content}\n\nUser Prompt: ${prompt}`,
                },
            ],
        }, {
            signal: abortSignal,
        });
        const result = response.choices[0]?.message?.content?.trim();
        if (!result) {
            throw new Error("Failed to process web content: Empty response from AI");
        }
        const usage = response.usage
            ? {
                prompt_tokens: response.usage.prompt_tokens,
                completion_tokens: response.usage.completion_tokens,
                total_tokens: response.usage.total_tokens,
            }
            : undefined;
        return {
            content: result,
            usage,
        };
    }
    catch (error) {
        if (error.name === "AbortError") {
            logger.info("Web content processing request was aborted");
            throw new Error("Web content processing request was aborted");
        }
        logger.error("Failed to process web content:", error);
        throw error;
    }
}
export async function btw(options) {
    const { gatewayConfig, modelConfig, messages, question, abortSignal } = options;
    // Apply global 1 QPS rate limit
    if (process.env.NODE_ENV !== "test" ||
        modelConfig.model === "rate-limit-test") {
        await acquireSlot(abortSignal);
    }
    // Create OpenAI client with injected configuration
    const openai = new OpenAIClient({
        apiKey: gatewayConfig.apiKey,
        baseURL: gatewayConfig.baseURL,
        defaultHeaders: gatewayConfig.defaultHeaders,
        fetchOptions: gatewayConfig.fetchOptions,
        fetch: gatewayConfig.fetch,
    });
    // Get model configuration - use injected agent model
    const { model: _model, fastModel: _fastModel, maxTokens: _maxTokens, permissionMode: _permissionMode, ...extraParams } = modelConfig;
    void _model;
    void _fastModel;
    void _maxTokens;
    void _permissionMode;
    const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
        temperature: 0.1,
        max_tokens: 4096,
        ...extraParams,
    });
    try {
        const response = await openai.chat.completions.create({
            ...openaiModelConfig,
            messages: [
                {
                    role: "system",
                    content: BTW_SYSTEM_PROMPT,
                },
                ...messages,
                {
                    role: "user",
                    content: question,
                },
            ],
        }, {
            signal: abortSignal,
        });
        const result = response.choices[0]?.message?.content?.trim();
        if (!result) {
            throw new Error("Failed to process side question: Empty response from AI");
        }
        const usage = response.usage
            ? {
                prompt_tokens: response.usage.prompt_tokens,
                completion_tokens: response.usage.completion_tokens,
                total_tokens: response.usage.total_tokens,
            }
            : undefined;
        return {
            content: result,
            usage,
        };
    }
    catch (error) {
        if (error.name === "AbortError") {
            logger.info("Side question request was aborted");
            throw new Error("Side question request was aborted");
        }
        logger.error("Failed to process side question:", error);
        throw error;
    }
}
