'use strict';

var conf = require('../conf/configure'),
    shimmer = require('../core/shimmer'),
    TraceContextManager = require('../trace/trace-context-manager'),
    Logger = require('../logger');

var LlmLogSinkPack = require('../llm/llm-log-sink-pack');
var llmSender = require('../llm/llm-sender');
var statCollector = require('../llm/llm-stat-collector');

var OpenAIObserver = function (agent) {
    this.agent = agent;
    this.packages = ['openai'];
};

OpenAIObserver.prototype.inject = function (mod, moduleName) {
    if (mod.__whatap_observe__) return;
    mod.__whatap_observe__ = true;

    // NOTE: llm_enabled 게이트는 conf가 비동기 로드되므로 inject 시점이 아닌
    // 런타임 (wrappedCreate)에서 체크. 패칭 자체는 무조건 걸어둠.

    Logger.print('WHATAP-LLM-010', 'OpenAIObserver injecting', false);

    try {
        _patchChatCompletions(mod);
    } catch (e) {
        Logger.printError('WHATAP-LLM-011', 'Failed to patch chat completions', e, false);
    }

    try {
        _patchEmbeddings(mod);
    } catch (e) {
        Logger.printError('WHATAP-LLM-012', 'Failed to patch embeddings', e, false);
    }

    try {
        _patchLegacyCompletions(mod);
    } catch (e) {
        Logger.printError('WHATAP-LLM-013', 'Failed to patch legacy completions', e, false);
    }

    try {
        _patchResponses();
    } catch (e) {
        Logger.printError('WHATAP-LLM-014', 'Failed to patch responses', e, false);
    }
};

// ============================================================
// Chat Completions
// ============================================================

function _patchChatCompletions(mod) {
    var CompletionsProto = _resolve(mod, 'OpenAI.Chat.Completions.prototype');
    if (!CompletionsProto || !CompletionsProto.create) return;

    shimmer.wrap(CompletionsProto, 'create', function (original) {
        return function wrappedCreate() {
            // 런타임 게이트: conf가 늦게 로드되더라도 호출 시점에 체크
            if (!conf.getProperty('llm_enabled', false)) {
                return original.apply(this, arguments);
            }
            var args = Array.prototype.slice.call(arguments);
            var body = args[0] || {};
            var ctx = TraceContextManager.getCurrentContext();

            var pack = _buildChatContext(body);
            var streamFlag = pack.stream;

            // mark for HTTP observer to detect LLM API call
            if (ctx) {
                ctx._llm_httpc_pending = true;
                ctx._llm_model = pack.model;
            }

            // active count 증가 (llm_active_stat)
            statCollector.onCallStart(pack.model, pack.operationType, ctx);

            // inject include_usage for streaming
            if (streamFlag) {
                if (!body.stream_options) body.stream_options = {};
                if (!body.stream_options.include_usage) {
                    body.stream_options.include_usage = true;
                }
            }

            pack._startTime = Date.now();

            var result;
            try {
                result = original.apply(this, args);
            } catch (err) {
                pack.setError(err, _classifyError(err));
                llmSender.dispatchLlmPack(pack, ctx);
                throw err;
            }

            // original.apply returns a Promise
            if (result && typeof result.then === 'function') {
                return result.then(function (response) {
                    if (ctx) pack.setContext(ctx);

                    if (streamFlag && response && _isAsyncIterable(response)) {
                        return _wrapStreamingResponse(response, pack, ctx);
                    }

                    // non-streaming
                    pack._endTime = Date.now();
                    _finalizeChatResponse(response, pack);
                    llmSender.dispatchLlmPack(pack, ctx);
                    return response;
                }).catch(function (err) {
                    pack.setError(err, _classifyError(err));
                    llmSender.dispatchLlmPack(pack, ctx);
                    throw err;
                });
            }

            return result;
        };
    });
}

function _buildChatContext(body) {
    var messages = body.messages || [];
    var pack = new LlmLogSinkPack();
    pack.model = body.model;
    pack.stream = !!body.stream;
    pack.operationType = 'chat';
    if (body.temperature != null) pack.temperature = body.temperature;

    var systemParts = [];
    var promptMsgs = [];
    var toolResults = [];
    var hasVision = false;

    for (var i = 0; i < messages.length; i++) {
        var msg = messages[i];
        var role = msg.role || 'user';
        var content = msg.content;

        if (role === 'system') {
            systemParts.push(_extractContentText(content));
        } else if (role === 'tool') {
            toolResults.push({
                tool_call_id: msg.tool_call_id || '',
                content: _extractContentText(content),
            });
            var sanitizedTool = Object.assign({}, msg);
            if (Array.isArray(content)) sanitizedTool.content = _extractContentText(content);
            promptMsgs.push(sanitizedTool);
        } else {
            if (!hasVision && Array.isArray(content)) {
                for (var j = 0; j < content.length; j++) {
                    if (content[j] && content[j].type === 'image_url') {
                        hasVision = true;
                        break;
                    }
                }
            }
            var sanitized = Object.assign({}, msg);
            if (Array.isArray(content)) sanitized.content = _extractContentText(content);
            promptMsgs.push(sanitized);
        }
    }

    try {
        pack.promptText = promptMsgs.length > 0 ? JSON.stringify(promptMsgs) : '';
    } catch (e) {
        pack.promptText = '';
    }
    pack.systemTexts = systemParts;
    if (toolResults.length > 0) {
        pack.toolResultsText = JSON.stringify(toolResults);
    }

    // features
    var featureList = [];
    if (hasVision) featureList.push('vision');
    if (body.reasoning_effort) featureList.push('reasoning');
    if (body.response_format && body.response_format.type === 'json_schema') {
        featureList.push('structured_output');
    }
    pack.features = featureList.join(',');

    return pack;
}

function _finalizeChatResponse(response, pack) {
    if (!response || !response.choices) return;

    var featureList = pack.features ? pack.features.split(',').filter(Boolean) : [];
    var responseText = '';
    var reasoningText = '';
    var finishReasons = [];

    for (var i = 0; i < response.choices.length; i++) {
        var choice = response.choices[i];
        if (choice.finish_reason) finishReasons.push(choice.finish_reason);
        if (choice.message) {
            responseText += choice.message.content || '';
            reasoningText += choice.message.reasoning_content || '';

            if (choice.message.tool_calls && choice.message.tool_calls.length > 0) {
                if (featureList.indexOf('tool_use') < 0) featureList.push('tool_use');
                try {
                    pack.toolCallsText = JSON.stringify(choice.message.tool_calls);
                } catch (e) {
                    pack.toolCallsText = String(choice.message.tool_calls);
                }
            }
        }
    }
    if (finishReasons.length > 0) pack.finishReason = finishReasons.join(',');

    pack.features = featureList.join(',');
    pack.completionText = responseText;
    if (reasoningText) pack.reasoningText = reasoningText;

    if (response.usage) {
        pack.setTokens(_extractUsageDetails(response.usage));
    }

    pack.latency = Math.round(pack._endTime - pack._startTime);
    pack.success = true;
}

function _wrapStreamingResponse(stream, pack, ctx) {
    var collectedText = '';
    var collectedReasoning = '';
    var usageData = {};
    var hasToolUse = false;
    var collectedToolChunks = {};
    var firstTokenTime = null;
    var streamFinishReasons = [];
    var dispatched = false;

    // 정상 완료(done) / 클라이언트 abort(return) / 에러(throw) / next() reject 모두에서 호출되도록
    // idempotent finalize. dispatched flag로 중복 dispatch 방지.
    // abort/error는 flushNow=true로 LogSinkPack을 동기 송신 (TX_END race 회피).
    function finalize(success, err, abnormal) {
        if (dispatched) return;
        dispatched = true;
        try {
            var endTime = Date.now();
            if (hasToolUse) {
                var parts = pack.features ? pack.features.split(',').filter(Boolean) : [];
                if (parts.indexOf('tool_use') < 0) parts.push('tool_use');
                pack.features = parts.join(',');
            }
            if (Object.keys(collectedToolChunks).length > 0) {
                pack.toolCallsText = JSON.stringify(Object.values(collectedToolChunks));
            }
            if (streamFinishReasons.length > 0) {
                pack.finishReason = streamFinishReasons.join(',');
            }
            pack.setTokens(usageData);
            pack.completionText = collectedText;
            if (collectedReasoning) pack.reasoningText = collectedReasoning;

            pack.latency = Math.round(endTime - pack._startTime);
            if (firstTokenTime != null) {
                pack.ttft = Math.round(firstTokenTime - pack._startTime);
            }

            if (success) {
                pack.success = true;
            } else {
                pack.setError(err || new Error('stream aborted'), _classifyError(err));
            }
            // abnormal=true (abort/error)이면 동기 flush로 TX_END보다 먼저 송신 보장
            llmSender.dispatchLlmPack(pack, ctx, abnormal ? { flushNow: true } : undefined);
        } catch (e) {
            try {
                Logger.printError('WHATAP-LLM-015', 'stream finalize failed', e, false);
            } catch (e2) {}
        }
    }

    var originalIterator = stream[Symbol.asyncIterator]();

    var wrappedStream = {
        [Symbol.asyncIterator]: function () {
            return {
                next: function () {
                    return originalIterator.next().then(function (result) {
                        if (result.done) {
                            finalize(true);
                            return result;
                        }

                        var chunk = result.value;

                        // usage
                        if (chunk && chunk.usage) {
                            usageData = _extractUsageDetails(chunk.usage);
                        }

                        // choices
                        if (chunk && chunk.choices && chunk.choices.length > 0) {
                            if (chunk.choices[0].finish_reason) {
                                streamFinishReasons.push(chunk.choices[0].finish_reason);
                            }
                            var delta = chunk.choices[0].delta;
                            if (delta) {
                                var content = delta.content || '';
                                if (content && firstTokenTime == null) {
                                    firstTokenTime = Date.now();
                                }
                                collectedText += content;
                                collectedReasoning += delta.reasoning_content || '';

                                if (delta.tool_calls) {
                                    hasToolUse = true;
                                    for (var k = 0; k < delta.tool_calls.length; k++) {
                                        var tc = delta.tool_calls[k];
                                        var idx = tc.index;
                                        if (!collectedToolChunks[idx]) {
                                            collectedToolChunks[idx] = { id: '', function: '', arguments: '' };
                                        }
                                        if (tc.id) collectedToolChunks[idx].id = tc.id;
                                        if (tc.function) {
                                            if (tc.function.name) collectedToolChunks[idx].function = tc.function.name;
                                            if (tc.function.arguments) collectedToolChunks[idx].arguments += tc.function.arguments;
                                        }
                                    }
                                }
                            }
                        }

                        return result;
                    }, function (err) {
                        // upstream stream 에러 — finalize 후 throw propagate (TX_END race 회피 위해 동기 flush)
                        finalize(false, err, true);
                        throw err;
                    });
                },
                return: function (value) {
                    // 클라이언트 break/abort — 부분 데이터로 finalize (TX_END race 회피 위해 동기 flush)
                    finalize(true, null, true);
                    if (originalIterator.return) return originalIterator.return(value);
                    return Promise.resolve({ done: true, value: value });
                },
                throw: function (err) {
                    // consumer 측 throw 주입 — 에러로 finalize (TX_END race 회피 위해 동기 flush)
                    finalize(false, err, true);
                    if (originalIterator.throw) return originalIterator.throw(err);
                    return Promise.reject(err);
                },
            };
        },
    };

    // preserve non-iterator properties/methods from original stream (e.g. controller, response)
    for (var key in stream) {
        if (stream.hasOwnProperty(key) && !(key in wrappedStream)) {
            wrappedStream[key] = stream[key];
        }
    }

    return wrappedStream;
}

// ============================================================
// Embeddings
// ============================================================

function _patchEmbeddings(mod) {
    var EmbeddingsProto = _resolve(mod, 'OpenAI.Embeddings.prototype');
    if (!EmbeddingsProto || !EmbeddingsProto.create) return;

    shimmer.wrap(EmbeddingsProto, 'create', function (original) {
        return function wrappedCreate() {
            if (!conf.getProperty('llm_enabled', false)) {
                return original.apply(this, arguments);
            }
            var args = Array.prototype.slice.call(arguments);
            var body = args[0] || {};
            var ctx = TraceContextManager.getCurrentContext();

            var pack = new LlmLogSinkPack();
            pack.model = body.model;
            pack.operationType = 'embedding';
            pack.promptText = typeof body.input === 'string' ? body.input : JSON.stringify(body.input || '');

            // mark for HTTP observer to detect LLM API call
            if (ctx) {
                ctx._llm_httpc_pending = true;
                ctx._llm_model = pack.model;
            }

            // active count 증가 (llm_active_stat)
            statCollector.onCallStart(pack.model, pack.operationType, ctx);

            pack._startTime = Date.now();

            var result;
            try {
                result = original.apply(this, args);
            } catch (err) {
                pack.setError(err, _classifyError(err));
                llmSender.dispatchLlmPack(pack, ctx);
                throw err;
            }

            if (result && typeof result.then === 'function') {
                return result.then(function (response) {
                    if (ctx) pack.setContext(ctx);
                    pack._endTime = Date.now();
                    pack.latency = Math.round(pack._endTime - pack._startTime);

                    if (response && response.usage) {
                        pack.setTokens({
                            input_tokens: response.usage.prompt_tokens || 0,
                            total_tokens_count: response.usage.total_tokens || 0,
                        });
                    }
                    if (response && response.data) {
                        pack.embedding_count = response.data.length;
                        if (response.data[0] && response.data[0].embedding) {
                            pack.dimensions = response.data[0].embedding.length;
                        }
                    }

                    pack.success = true;
                    llmSender.dispatchLlmPack(pack, ctx);
                    return response;
                }).catch(function (err) {
                    pack.setError(err, _classifyError(err));
                    llmSender.dispatchLlmPack(pack, ctx);
                    throw err;
                });
            }

            return result;
        };
    });
}

// ============================================================
// Legacy Completions (/v1/completions)
// ============================================================

function _patchLegacyCompletions(mod) {
    var LegacyProto = _resolve(mod, 'OpenAI.Completions.prototype');
    if (!LegacyProto || !LegacyProto.create) return;

    shimmer.wrap(LegacyProto, 'create', function (original) {
        return function wrappedCreate() {
            if (!conf.getProperty('llm_enabled', false)) {
                return original.apply(this, arguments);
            }
            var args = Array.prototype.slice.call(arguments);
            var body = args[0] || {};
            var ctx = TraceContextManager.getCurrentContext();

            var pack = new LlmLogSinkPack();
            pack.model = body.model;
            pack.operationType = 'completion';
            pack.stream = !!body.stream;
            if (body.temperature != null) pack.temperature = body.temperature;
            try {
                pack.promptText = Array.isArray(body.prompt) ? JSON.stringify(body.prompt) : String(body.prompt || '');
            } catch (e) { pack.promptText = ''; }

            if (ctx) {
                ctx._llm_httpc_pending = true;
                ctx._llm_model = pack.model;
            }

            statCollector.onCallStart(pack.model, pack.operationType, ctx);

            if (pack.stream) {
                if (!body.stream_options) body.stream_options = {};
                if (!body.stream_options.include_usage) {
                    body.stream_options.include_usage = true;
                }
            }

            pack._startTime = Date.now();

            var result;
            try {
                result = original.apply(this, args);
            } catch (err) {
                pack.setError(err, _classifyError(err));
                llmSender.dispatchLlmPack(pack, ctx);
                throw err;
            }

            if (result && typeof result.then === 'function') {
                return result.then(function (response) {
                    if (ctx) pack.setContext(ctx);

                    if (pack.stream && response && _isAsyncIterable(response)) {
                        return _wrapLegacyCompletionStream(response, pack, ctx);
                    }

                    pack._endTime = Date.now();
                    _finalizeLegacyCompletionResponse(response, pack);
                    llmSender.dispatchLlmPack(pack, ctx);
                    return response;
                }).catch(function (err) {
                    pack.setError(err, _classifyError(err));
                    llmSender.dispatchLlmPack(pack, ctx);
                    throw err;
                });
            }
            return result;
        };
    });
}

function _finalizeLegacyCompletionResponse(response, pack) {
    if (!response || !response.choices) return;
    var text = '';
    var finishReasons = [];
    for (var i = 0; i < response.choices.length; i++) {
        var c = response.choices[i];
        if (c.text) text += c.text;
        if (c.finish_reason) finishReasons.push(c.finish_reason);
    }
    pack.completionText = text;
    if (finishReasons.length > 0) pack.finishReason = finishReasons.join(',');
    if (response.usage) pack.setTokens(_extractUsageDetails(response.usage));
    pack.latency = Math.round(pack._endTime - pack._startTime);
    pack.success = true;
}

function _wrapLegacyCompletionStream(stream, pack, ctx) {
    var collectedText = '';
    var usageData = {};
    var finishReasons = [];
    var firstTokenTime = null;
    var dispatched = false;

    function finalize(success, err, abnormal) {
        if (dispatched) return;
        dispatched = true;
        try {
            var endTime = Date.now();
            if (finishReasons.length > 0) pack.finishReason = finishReasons.join(',');
            pack.setTokens(usageData);
            pack.completionText = collectedText;
            pack.latency = Math.round(endTime - pack._startTime);
            if (firstTokenTime != null) pack.ttft = Math.round(firstTokenTime - pack._startTime);
            if (success) {
                pack.success = true;
            } else {
                pack.setError(err || new Error('stream aborted'), _classifyError(err));
            }
            llmSender.dispatchLlmPack(pack, ctx, abnormal ? { flushNow: true } : undefined);
        } catch (e) {
            try {
                Logger.printError('WHATAP-LLM-016', 'legacy stream finalize failed', e, false);
            } catch (e2) {}
        }
    }

    var originalIterator = stream[Symbol.asyncIterator]();
    var wrapped = {
        [Symbol.asyncIterator]: function () {
            return {
                next: function () {
                    return originalIterator.next().then(function (result) {
                        if (result.done) {
                            finalize(true);
                            return result;
                        }
                        var chunk = result.value;
                        if (chunk && chunk.usage) usageData = _extractUsageDetails(chunk.usage);
                        if (chunk && chunk.choices && chunk.choices.length > 0) {
                            var ch = chunk.choices[0];
                            if (ch.finish_reason) finishReasons.push(ch.finish_reason);
                            if (ch.text) {
                                if (firstTokenTime == null) firstTokenTime = Date.now();
                                collectedText += ch.text;
                            }
                        }
                        return result;
                    }, function (err) {
                        finalize(false, err, true);
                        throw err;
                    });
                },
                return: function (v) {
                    finalize(true, null, true);
                    return originalIterator.return ? originalIterator.return(v) : Promise.resolve({ done: true, value: v });
                },
                throw: function (e) {
                    finalize(false, e, true);
                    return originalIterator.throw ? originalIterator.throw(e) : Promise.reject(e);
                },
            };
        },
    };
    for (var key in stream) {
        if (stream.hasOwnProperty(key) && !(key in wrapped)) wrapped[key] = stream[key];
    }
    return wrapped;
}

// ============================================================
// Responses API (/v1/responses)
// ============================================================

function _patchResponses() {
    var ResponsesCls, AsyncResponsesCls;
    try {
        ResponsesCls = require('openai/resources/responses/responses').Responses;
    } catch (e) {
        try { ResponsesCls = require('openai').OpenAI.Responses; } catch (e2) {}
    }
    try {
        AsyncResponsesCls = require('openai/resources/responses/responses').AsyncResponses;
    } catch (e) {}

    var wrapper = function (proto) {
        if (!proto || !proto.create) return;
        shimmer.wrap(proto, 'create', function (original) {
            return function wrappedCreate() {
                if (!conf.getProperty('llm_enabled', false)) {
                    return original.apply(this, arguments);
                }
                var args = Array.prototype.slice.call(arguments);
                var body = args[0] || {};
                var ctx = TraceContextManager.getCurrentContext();

                var pack = new LlmLogSinkPack();
                pack.model = body.model;
                pack.operationType = 'response';
                pack.stream = !!body.stream;
                if (body.temperature != null) pack.temperature = body.temperature;
                try {
                    pack.promptText = typeof body.input === 'string' ? body.input : JSON.stringify(body.input || body.messages || '');
                } catch (e) { pack.promptText = ''; }

                if (ctx) {
                    ctx._llm_httpc_pending = true;
                    ctx._llm_model = pack.model;
                }
                statCollector.onCallStart(pack.model, pack.operationType, ctx);
                pack._startTime = Date.now();

                var result;
                try {
                    result = original.apply(this, args);
                } catch (err) {
                    pack.setError(err, _classifyError(err));
                    llmSender.dispatchLlmPack(pack, ctx);
                    throw err;
                }
                if (result && typeof result.then === 'function') {
                    return result.then(function (response) {
                        if (ctx) pack.setContext(ctx);
                        pack._endTime = Date.now();
                        _finalizeResponsesResponse(response, pack);
                        llmSender.dispatchLlmPack(pack, ctx);
                        return response;
                    }).catch(function (err) {
                        pack.setError(err, _classifyError(err));
                        llmSender.dispatchLlmPack(pack, ctx);
                        throw err;
                    });
                }
                return result;
            };
        });
    };
    if (ResponsesCls) wrapper(ResponsesCls.prototype);
    if (AsyncResponsesCls) wrapper(AsyncResponsesCls.prototype);
}

function _finalizeResponsesResponse(response, pack) {
    if (!response) return;
    try {
        if (typeof response.output_text === 'string') {
            pack.completionText = response.output_text;
        } else if (Array.isArray(response.output)) {
            var text = '';
            for (var i = 0; i < response.output.length; i++) {
                var item = response.output[i];
                if (item && item.content) {
                    for (var j = 0; j < item.content.length; j++) {
                        var block = item.content[j];
                        if (block && block.type === 'output_text' && block.text) text += block.text;
                    }
                }
            }
            pack.completionText = text;
        }
    } catch (e) {}
    if (response.status) pack.finishReason = response.status;
    if (response.usage) {
        pack.setTokens({
            input_tokens: response.usage.input_tokens || 0,
            output_tokens: response.usage.output_tokens || 0,
            total_tokens_count: response.usage.total_tokens || 0,
        });
    }
    pack.latency = Math.round(pack._endTime - pack._startTime);
    pack.success = true;
}

// ============================================================
// Utilities
// ============================================================

function _extractUsageDetails(usage) {
    if (!usage) return {};
    var inputTokens = usage.prompt_tokens || 0;
    var outputTokens = usage.completion_tokens || 0;

    var promptDetails = usage.prompt_tokens_details || {};
    var completionDetails = usage.completion_tokens_details || {};

    return {
        input_tokens: inputTokens,
        output_tokens: outputTokens,
        total_tokens_count: inputTokens + outputTokens,
        cached_tokens: promptDetails.cached_tokens || 0,
        audio_input_tokens: promptDetails.audio_tokens || 0,
        reasoning_tokens: completionDetails.reasoning_tokens || 0,
        audio_output_tokens: completionDetails.audio_tokens || 0,
        accepted_prediction_tokens: completionDetails.accepted_prediction_tokens || 0,
        rejected_prediction_tokens: completionDetails.rejected_prediction_tokens || 0,
    };
}

function _extractContentText(content) {
    if (typeof content === 'string') return content;
    if (!Array.isArray(content)) return String(content || '');
    var texts = [];
    for (var i = 0; i < content.length; i++) {
        var block = content[i];
        if (typeof block === 'string') {
            texts.push(block);
        } else if (block && block.type === 'text') {
            texts.push(block.text || '');
        }
    }
    return texts.join('\n');
}

function _classifyError(err) {
    if (err && err.constructor && err.constructor.name &&
        (err.constructor.name.indexOf('API') >= 0 || err.constructor.name.indexOf('OpenAI') >= 0 || err.status)) {
        return 'api_error';
    }
    return 'program_error';
}

function _isAsyncIterable(obj) {
    return obj != null && typeof obj[Symbol.asyncIterator] === 'function';
}

function _resolve(mod, path) {
    var parts = path.split('.');
    var obj = mod;
    for (var i = 0; i < parts.length; i++) {
        if (!obj) return null;
        obj = obj[parts[i]];
    }
    return obj;
}

exports.OpenAIObserver = OpenAIObserver;
