/**
 * Copyright 2016 the WHATAP project authors. All rights reserved.
 * Use of this source code is governed by a license that
 * can be found in the LICENSE file.
 */

var TraceContextManager = require('../trace/trace-context-manager'),
    HashUtil = require('../util/hashutil'),
    Logger = require('../logger'),
    conf = require('../conf/configure'),
    AsyncSender = require('../udp/async_sender'),
    PacketTypeEnum = require('../udp/packet_type_enum'),
    shimmer = require('../core/shimmer'),
    AsyncResource = require('async_hooks').AsyncResource;

var MongodbObserver = function (agent) {
    this.agent = agent;
    this.packages = ['mongodb'];
};

var dbc_hash = 0;
var dbc = '';

var hookedConnections = new WeakSet();

// MongoDB 명령어 매핑
var MONGODB_COMMANDS = {
    'insert': 'INSERT',
    'insertOne': 'INSERTONE',
    'insertMany': 'INSERTMANY',
    'find': 'FIND',
    // 'findOne': 'FINDONE',
    'count': 'COUNT',
    'countDocuments': 'COUNTDOCUMENTS',
    'update': 'UPDATE',
    'updateOne': 'UPDATEONE',
    'updateMany': 'UPDATEMANY',
    'delete': 'DELETE',
    'deleteOne': 'DELETEONE',
    'deleteMany': 'DELETEMANY',
    'remove': 'REMOVE',
    'removeOne': 'REMOVEONE',
    'removeMany': 'REMOVEMANY',
    'replaceOne': 'REPLACEONE',
    'findAndReplace': 'FINDANDREPLACE',
    'findAndUpdate': 'FINDANDUPDATE',
    'findAndDelete': 'FINDANDDELETE',
    'findOneAndUpdate': 'FINDONEANDUPDATE',
    'findOneAndReplace': 'FINDONEANDREPLACE',
    'findOneAndDelete': 'FINDONEANDDELETE',
    'findAndModify': 'FINDANDMODIFY'
};

// MongoDB 에러 처리
function handleMongoError(ctx, err) {
    if (!err) return;

    try {
        var errorClass = err.code || err.name || 'MongoError';
        var errorMessage = err.message || 'mongodb error';
        var errorStack = '';

        if (conf.trace_sql_error_stack && conf.trace_sql_error_depth && err.stack) {
            var traceDepth = conf.trace_sql_error_depth;
            var stackLines = err.stack.split("\n");
            if (stackLines.length > traceDepth) {
                stackLines = stackLines.slice(0, traceDepth + 1);
            }
            errorStack = stackLines.join("\n");
        }

        var shouldAddError = false;
        if (conf._is_trace_ignore_err_cls_contains === true && errorClass &&
            errorClass.toString().indexOf(conf.trace_ignore_err_cls_contains) < 0) {
            shouldAddError = true;
        } else if (conf._is_trace_ignore_err_msg_contains === true && errorMessage &&
            errorMessage.indexOf(conf.trace_ignore_err_msg_contains) < 0) {
            shouldAddError = true;
        } else if (conf._is_trace_ignore_err_cls_contains === false &&
            conf._is_trace_ignore_err_msg_contains === false) {
            shouldAddError = true;
        }

        if (shouldAddError) {
            if (!ctx.error) ctx.error = 1;
            ctx.status = 500;
            var errors = [errorClass];
            if (errorMessage) {
                errors.push(errorMessage);
            }
            if (errorStack || err.stack) {
                errors.push(errorStack || err.stack);
            }

            AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
        }
    } catch (e) {
        Logger.printError('WHATAP-242', 'Error handling MongoDB error', e, false);
    }
}

// Collection 메서드 래퍼 생성
function createCollectionMethodWrapper(methodName) {
    return function(original) {
        return function wrappedMethod() {
            var ctx = TraceContextManager.getCurrentContext();
            if (!ctx) {
                return original.apply(this, arguments);
            }

            var args = Array.prototype.slice.call(arguments);
            var collectionName = this.collectionName || this.s?.name || 'unknown';
            var commandName = MONGODB_COMMANDS[methodName] || methodName.toUpperCase();
            var _dbc = this.dbName && dbc.indexOf(this.dbName) === -1 ? dbc + '/' + this.dbName : dbc;

            const asyncResource = new AsyncResource('mongodb-command');

            return asyncResource.runInAsyncScope(() => {
                // DB 연결 패킷 전송
                ctx.start_time = Date.now();
                ctx.elapsed = 0;
                ctx.active_sqlhash = true;
                AsyncSender.send_packet(PacketTypeEnum.TX_DB_CONN, ctx, [_dbc]);

                var command_start_time = Date.now();
                ctx.footprint(`MongoDB ${commandName} Start`);

                // 쿼리 텍스트 구성
                var queryParts = [commandName, collectionName];
                var fieldNames = [];

                try {
                    // 첫 번째 인자에서 필드명 추출 (filter)
                    if (args[0] && typeof args[0] === 'object' && !Array.isArray(args[0])) {
                        fieldNames = Object.keys(args[0]);
                    }
                    if (fieldNames.length > 0) {
                        queryParts.push('field=[' + fieldNames.join(',') + ']');
                    }
                } catch (e) {
                    Logger.printError('WHATAP-243', 'Error extracting field names', e, false);
                }

                var commandText = 'MongoDB ' + queryParts.join(' ');

                function executeCallback(err, result) {
                    try {
                        var command_elapsed = Date.now() - command_start_time;

                        if (err) {
                            handleMongoError(ctx, err);
                        }

                        ctx.elapsed = command_elapsed;
                        ctx.active_sqlhash = false;
                        AsyncSender.send_packet(PacketTypeEnum.TX_SQL, ctx, [dbc, commandText, '0']);
                        ctx.footprint(`MongoDB ${commandName} Done`);
                    } catch (e) {
                        Logger.printError('WHATAP-244', 'Error in MongoDB callback', e, false);
                    }
                }

                // 콜백 처리
                var hasCallback = false;
                for (var i = args.length - 1; i >= 0; i--) {
                    if (typeof args[i] === 'function') {
                        hasCallback = true;
                        var originalCallback = args[i];

                        args[i] = asyncResource.bind(function() {
                            var callbackArgs = Array.prototype.slice.call(arguments);
                            executeCallback(callbackArgs[0], callbackArgs[1]);

                            if (originalCallback && typeof originalCallback === 'function') {
                                return originalCallback.apply(this, callbackArgs);
                            }
                        });
                        break;
                    }
                }

                try {
                    var result = original.apply(this, args);

                    // Promise 기반 처리
                    if (!hasCallback && result && typeof result.then === 'function') {
                        return result.then(function(res) {
                            executeCallback(null, res);
                            return res;
                        }).catch(function(err) {
                            executeCallback(err, null);
                            throw err;
                        });
                    }

                    // 동기적 결과 처리
                    if (!hasCallback) {
                        executeCallback(null, result);
                    }

                    return result;
                } catch (executeError) {
                    executeCallback(executeError, null);
                    throw executeError;
                }
            });
        };
    };
}

// Connection wrapper 함수
var createConnectionWrapper = function() {
    return function(original) {
        return function wrappedConnect() {
            dbc = this.s && this.s.url ? this.s.url : 'mongodb://localhost:27017';
            dbc_hash = HashUtil.hashFromString(dbc);
            return original.apply(this, arguments);
        };
    };
};

MongodbObserver.prototype.inject = function(mod, moduleName) {
    if (mod.__whatap_observe__) {
        return;
    }
    mod.__whatap_observe__ = true;
    Logger.initPrint("MongodbObserver");

    if (conf.sql_enabled === false) {
        return;
    }

    var self = this;

    if (mod.MongoClient && !mod.MongoClient.__whatap_wrapped__) {
        // 정적 connect 메서드 래핑
        if (mod.MongoClient.prototype.connect && !mod.MongoClient.prototype.connect.__whatap_wrapped__) {
            shimmer.wrap(mod.MongoClient.prototype, 'connect', createConnectionWrapper());
            mod.MongoClient.connect.__whatap_wrapped__ = true;
        }

        mod.MongoClient.__whatap_wrapped__ = true;
    }

    // Collection 메서드들 래핑
    if (mod.Collection && mod.Collection.prototype && !mod.Collection.__whatap_wrapped__) {
        Object.keys(MONGODB_COMMANDS).forEach(function(methodName) {
            if (mod.Collection.prototype[methodName] && !mod.Collection.prototype[methodName].__whatap_wrapped__) {
                shimmer.wrap(mod.Collection.prototype, methodName, createCollectionMethodWrapper(methodName));
                mod.Collection.prototype[methodName].__whatap_wrapped__ = true;
            }
        });
        mod.Collection.__whatap_wrapped__ = true;
    }
};

exports.MongoObserver = MongodbObserver