"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAIService = exports.OpenAIServiceError = void 0;
const openai_1 = __importDefault(require("openai"));
const ai_config_1 = require("../../config/ai.config");
/**
 * Error with retry information
 */
class OpenAIServiceError extends Error {
    constructor(message, code, statusCode, retryable = false, retryAfterMs) {
        super(message);
        this.code = code;
        this.statusCode = statusCode;
        this.retryable = retryable;
        this.retryAfterMs = retryAfterMs;
        this.name = 'OpenAIServiceError';
    }
}
exports.OpenAIServiceError = OpenAIServiceError;
/**
 * Service for interacting with OpenAI API
 * Implements GPT-4-turbo client with retry logic, token counting, and error handling
 */
class OpenAIService {
    constructor(config) {
        this.requestCount = 0;
        this.totalTokensUsed = 0;
        this.config = config || (0, ai_config_1.getAIConfig)();
        this.client = new openai_1.default({
            apiKey: this.config.openai.apiKey,
            timeout: this.config.openai.timeout,
            maxRetries: 0, // We handle retries manually for better control
        });
    }
    /**
     * Send a completion request to OpenAI with retry logic
     * @param messages Chat messages to send
     * @param options Additional options for the completion
     * @returns Completion result with token usage
     */
    async complete(messages, options = {}) {
        const startTime = Date.now();
        // Build the request parameters
        const params = {
            model: this.config.openai.model,
            messages: this.buildMessages(messages, options.systemPrompt),
            temperature: options.temperature ?? this.config.openai.temperature,
            max_tokens: options.maxTokens ?? this.config.openai.maxTokens,
            top_p: options.topP,
            frequency_penalty: options.frequencyPenalty,
            presence_penalty: options.presencePenalty,
            stop: options.stop,
            response_format: options.responseFormat === 'json'
                ? { type: 'json_object' }
                : undefined,
        };
        // Execute with retry logic
        const response = await this.executeWithRetry(() => this.client.chat.completions.create(params), this.config.openai.retryAttempts);
        const executionTimeMs = Date.now() - startTime;
        // Extract result
        const choice = response.choices[0];
        const usage = response.usage;
        if (!choice || !usage) {
            throw new OpenAIServiceError('Invalid response from OpenAI API');
        }
        // Track token usage
        this.requestCount++;
        this.totalTokensUsed += usage.total_tokens;
        return {
            content: choice.message.content || '',
            promptTokens: usage.prompt_tokens,
            completionTokens: usage.completion_tokens,
            totalTokens: usage.total_tokens,
            model: response.model,
            finishReason: choice.finish_reason,
            executionTimeMs,
        };
    }
    /**
     * Stream a completion from OpenAI
     * @param messages Chat messages to send
     * @param options Additional options for the completion
     * @param onChunk Callback for each streamed chunk
     * @returns Final completion result
     */
    async streamComplete(messages, options = {}, onChunk) {
        const startTime = Date.now();
        const params = {
            model: this.config.openai.model,
            messages: this.buildMessages(messages, options.systemPrompt),
            temperature: options.temperature ?? this.config.openai.temperature,
            max_tokens: options.maxTokens ?? this.config.openai.maxTokens,
            stream: true,
        };
        const stream = await this.executeWithRetry(() => this.client.chat.completions.create(params), this.config.openai.retryAttempts);
        let content = '';
        let finishReason = null;
        for await (const chunk of stream) {
            const delta = chunk.choices[0]?.delta;
            if (delta?.content) {
                content += delta.content;
                onChunk(delta.content);
            }
            if (chunk.choices[0]?.finish_reason) {
                finishReason = chunk.choices[0].finish_reason;
            }
        }
        const executionTimeMs = Date.now() - startTime;
        // Estimate token usage for streamed responses
        const estimatedTokens = this.estimateTokens(content);
        this.requestCount++;
        this.totalTokensUsed += estimatedTokens;
        return {
            content,
            promptTokens: 0, // Not available in streaming
            completionTokens: estimatedTokens,
            totalTokens: estimatedTokens,
            model: this.config.openai.model,
            finishReason,
            executionTimeMs,
        };
    }
    /**
     * Execute a function with exponential backoff retry logic
     * @param fn Function to execute
     * @param maxRetries Maximum number of retry attempts
     * @returns Result of the function
     */
    async executeWithRetry(fn, maxRetries) {
        let lastError;
        let baseDelay = this.config.openai.retryDelayMs;
        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                return await fn();
            }
            catch (error) {
                lastError = error;
                // Check if error is retryable
                if (!this.isRetryableError(error)) {
                    throw this.wrapError(error);
                }
                // Check if we've exhausted retries
                if (attempt === maxRetries) {
                    throw this.wrapError(error, attempt + 1);
                }
                // Calculate delay with exponential backoff
                const delay = this.calculateBackoffDelay(baseDelay, attempt, error);
                console.warn(`OpenAI request failed (attempt ${attempt + 1}/${maxRetries + 1}), ` +
                    `retrying in ${delay}ms: ${error.message}`);
                await this.sleep(delay);
            }
        }
        throw lastError || new OpenAIServiceError('Unknown error in retry logic');
    }
    /**
     * Check if an error is retryable
     * @param error The error to check
     * @returns True if the error is retryable
     */
    isRetryableError(error) {
        // Rate limit errors
        if (error.status === 429)
            return true;
        // Server errors
        if (error.status >= 500)
            return true;
        // Network errors
        if (error.code === 'ECONNRESET' ||
            error.code === 'ETIMEDOUT' ||
            error.code === 'ENOTFOUND')
            return true;
        // OpenAI specific retryable errors
        if (error.error?.type === 'server_error')
            return true;
        return false;
    }
    /**
     * Calculate backoff delay with jitter
     * @param baseDelay Base delay in milliseconds
     * @param attempt Current attempt number
     * @param error The error that occurred
     * @returns Delay in milliseconds
     */
    calculateBackoffDelay(baseDelay, attempt, error) {
        // Check for Retry-After header
        if (error.headers?.['retry-after']) {
            const retryAfter = parseInt(error.headers['retry-after'], 10);
            if (!isNaN(retryAfter)) {
                return retryAfter * 1000;
            }
        }
        // Exponential backoff with jitter
        const exponentialDelay = baseDelay * Math.pow(2, attempt);
        const jitter = Math.random() * 1000; // Up to 1 second of jitter
        const maxDelay = 60000; // Cap at 60 seconds
        return Math.min(exponentialDelay + jitter, maxDelay);
    }
    /**
     * Wrap an error with additional context
     * @param error The original error
     * @param retryCount Number of retries attempted
     * @returns Wrapped error
     */
    wrapError(error, retryCount) {
        const message = retryCount
            ? `OpenAI API error after ${retryCount} retries: ${error.message}`
            : `OpenAI API error: ${error.message}`;
        return new OpenAIServiceError(message, error.code || error.error?.code, error.status, this.isRetryableError(error), error.headers?.['retry-after'] ? parseInt(error.headers['retry-after'], 10) * 1000 : undefined);
    }
    /**
     * Build messages array with optional system prompt
     * @param messages User messages
     * @param systemPrompt Optional system prompt
     * @returns Complete messages array
     */
    buildMessages(messages, systemPrompt) {
        if (systemPrompt) {
            return [
                { role: 'system', content: systemPrompt },
                ...messages,
            ];
        }
        return messages;
    }
    /**
     * Estimate token count for a string (rough approximation)
     * @param text Text to estimate tokens for
     * @returns Estimated token count
     */
    estimateTokens(text) {
        // Rough estimation: ~4 characters per token for English text
        // This is a simplified heuristic; consider using tiktoken for accuracy
        return Math.ceil(text.length / 4);
    }
    /**
     * Sleep for a specified duration
     * @param ms Duration in milliseconds
     */
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    /**
     * Get token usage statistics
     * @returns Token usage stats
     */
    getUsageStats() {
        // Rough cost estimation (GPT-4-turbo pricing as of 2024)
        const costPerToken = 0.00002; // $0.02 per 1K tokens average
        const estimatedCost = this.totalTokensUsed * costPerToken;
        return {
            requestCount: this.requestCount,
            totalTokensUsed: this.totalTokensUsed,
            estimatedCost,
        };
    }
    /**
     * Reset usage statistics
     */
    resetUsageStats() {
        this.requestCount = 0;
        this.totalTokensUsed = 0;
    }
    /**
     * Test the connection to OpenAI API
     * @returns True if connection is successful
     */
    async testConnection() {
        try {
            const result = await this.complete([
                { role: 'user', content: 'Say "OK" if you can hear me.' },
            ], {
                maxTokens: 10,
                temperature: 0,
            });
            return result.content.toLowerCase().includes('ok');
        }
        catch (error) {
            console.error('OpenAI connection test failed:', error);
            return false;
        }
    }
}
exports.OpenAIService = OpenAIService;
//# sourceMappingURL=openai.service.js.map