/**
 * 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 MongooseObserver = function (agent) {
    this.agent = agent;
    this.packages = ['mongoose'];
};

var dbc_hash = 0;
var dbc = '';

// Mongoose 명령어 매핑
var MONGOOSE_COMMANDS = {
    'create': 'CREATE',
    'insertMany': 'INSERTMANY',
    'find': 'FIND',
    'findById': 'FINDBYID',
    'findOne': 'FINDONE',
    'countDocuments': 'COUNTDOCUMENTS',
    'distinct': 'DISTINCT',
    'updateMany': 'UPDATEMANY',
    'updateOne': 'UPDATEONE',
    'replaceOne': 'REPLACEONE',
    'findOneAndUpdate': 'FINDONEANDUPDATE',
    'findByIdAndUpdate': 'FINDBYIDANDUPDATE',
    'deleteOne': 'DELETEONE',
    'deleteMany': 'DELETEMANY',
    'findOneAndDelete': 'FINDONEANDDELETE',
    'findByIdAndDelete': 'FINDBYIDANDDELETE',
    'aggregate': 'AGGREGATE'
};

// DBC 정보 설정
function setupDbcInfo(connectionString) {
    if (dbc_hash === 0 && connectionString) {
        dbc = connectionString;
        dbc_hash = HashUtil.hashFromString(dbc);
        // Logger.print('WHATAP-MONGOOSE-DBC', 'DBC setup: ' + dbc, false);
    }
}

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

    try {
        var errorClass = err.code || err.name || 'MongooseError';
        var errorMessage = err.message || 'mongoose 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-245', 'Error handling Mongoose error', e, false);
    }
}

// Model 메서드 래퍼 생성
function createModelMethodWrapper(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 modelName = this.modelName || this.collection?.collectionName || 'unknown';
            var commandName = MONGOOSE_COMMANDS[methodName] || methodName.toUpperCase();

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

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

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

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

                try {
                    if (methodName === 'aggregate' && Array.isArray(args[0])) {
                        // aggregate 파이프라인 처리
                        var pipelineFields = [];
                        args[0].forEach(function(stage) {
                            if (stage.$match) {
                                pipelineFields = pipelineFields.concat(Object.keys(stage.$match));
                            }
                            if (stage.$group) {
                                pipelineFields = pipelineFields.concat(Object.keys(stage.$group));
                            }
                        });
                        if (pipelineFields.length > 0) {
                            queryParts.push('field=[' + pipelineFields.join(',') + ']');
                        }
                    } else {
                        // 첫 번째 인자에서 필드명 추출
                        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(',') + ']');
                        }

                        // 두 번째 인자에서 업데이트 필드 추출 (update 계열 메서드)
                        if (args[1] && typeof args[1] === 'object' &&
                            (methodName.includes('update') || methodName.includes('Update'))) {
                            var updateFields = [];
                            if (args[1].$set) {
                                updateFields = updateFields.concat(Object.keys(args[1].$set));
                            } else {
                                updateFields = Object.keys(args[1]);
                            }
                            if (updateFields.length > 0) {
                                queryParts.push('value=[' + updateFields.join(',') + ']');
                            }
                        }
                    }
                } catch (e) {
                    Logger.printError('WHATAP-246', '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) {
                            handleMongooseError(ctx, err);
                        }

                        ctx.elapsed = command_elapsed;
                        AsyncSender.send_packet(PacketTypeEnum.TX_SQL, ctx, [dbc, commandText, '0']);
                        ctx.footprint(`Mongoose ${commandName} Done`);
                    } catch (e) {
                        Logger.printError('WHATAP-247', 'Error in Mongoose 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 createConnectWrapper = function() {
    return function(original) {
        return function wrappedConnect() {
            var args = Array.prototype.slice.call(arguments);
            var connectionString = args[0];
            var ctx = TraceContextManager.getCurrentContext();

            setupDbcInfo(connectionString);

            if (!ctx) {
                return original.apply(this, arguments);
            }

            const connectionResource = new AsyncResource('mongoose-connect');

            return connectionResource.runInAsyncScope(() => {
                ctx.start_time = Date.now();
                ctx.footprint('Mongoose Connecting Start');
                ctx.db_opening = true;

                // 콜백 래핑
                for (var i = args.length - 1; i >= 0; i--) {
                    if (typeof args[i] === 'function') {
                        var originalCallback = args[i];
                        args[i] = connectionResource.bind(function() {
                            var callbackArgs = Array.prototype.slice.call(arguments);
                            var err = callbackArgs[0];

                            if (ctx) {
                                ctx.elapsed = Date.now() - ctx.start_time;
                                ctx.db_opening = false;

                                if (err) {
                                    handleMongooseError(ctx, err);
                                    ctx.footprint('Mongoose Connecting Error');
                                } else {
                                    ctx.footprint('Mongoose Connecting Done');
                                }
                            }

                            return originalCallback.apply(this, callbackArgs);
                        });
                        break;
                    }
                }

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

                // Promise 기반 처리
                if (result && typeof result.then === 'function') {
                    return result.then(connectionResource.bind(function(connection) {
                        if (ctx) {
                            ctx.elapsed = Date.now() - ctx.start_time;
                            ctx.db_opening = false;
                            ctx.footprint('Mongoose Connecting Done');
                        }

                        // 연결 완료 후 Model 래핑
                        if (connection && connection.Model) {
                            wrapModelMethods(connection.Model);
                        }

                        return connection;
                    })).catch(connectionResource.bind(function(err) {
                        if (ctx) {
                            ctx.elapsed = Date.now() - ctx.start_time;
                            ctx.db_opening = false;
                            ctx.footprint('Mongoose Connecting Error');
                            handleMongooseError(ctx, err);
                        }
                        throw err;
                    }));
                }

                return result;
            });
        };
    };
};

// Model 메서드 래핑 함수
function wrapModelMethods(ModelConstructor) {
    if (ModelConstructor && ModelConstructor.prototype && !ModelConstructor.__whatap_wrapped__) {
        Object.keys(MONGOOSE_COMMANDS).forEach(function(methodName) {
            if (ModelConstructor.prototype[methodName] &&
                !ModelConstructor.prototype[methodName].__whatap_wrapped__) {

                shimmer.wrap(ModelConstructor.prototype, methodName,
                    createModelMethodWrapper(methodName));
                ModelConstructor.prototype[methodName].__whatap_wrapped__ = true;
            }
                         });
        ModelConstructor.__whatap_wrapped__ = true;
        // Logger.print('WHATAP-MONGOOSE-MODEL', 'Model methods wrapped', false);
    }
}

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

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

    var self = this;

    // mongoose.connect 래핑
    if (mod.connect && !mod.connect.__whatap_wrapped__) {
        shimmer.wrap(mod, 'connect', createConnectWrapper());
        mod.connect.__whatap_wrapped__ = true;
    }

    // 기본 Model이 이미 존재하는 경우 래핑
    if (mod.Model) {
        wrapModelMethods(mod.Model);
    }

    // mongoose.model() 메서드 래핑 (새로운 모델 생성 시)
    if (mod.model && !mod.model.__whatap_wrapped__) {
        var originalModel = mod.model;
        mod.model = function() {
            var model = originalModel.apply(this, arguments);

            // 새로 생성된 모델의 메서드들 래핑
            if (model && !model.__whatap_wrapped__) {
                Object.keys(MONGOOSE_COMMANDS).forEach(function(methodName) {
                    if (model[methodName] && !model[methodName].__whatap_wrapped__) {
                        shimmer.wrap(model, methodName, createModelMethodWrapper(methodName));
                        model[methodName].__whatap_wrapped__ = true;
                    }
                });
                model.__whatap_wrapped__ = true;
            }

            return model;
        };

        // 기존 속성들 복사
        Object.keys(originalModel).forEach(function(key) {
            mod.model[key] = originalModel[key];
        });

        mod.model.__whatap_wrapped__ = true;
    }
};

exports.MongooseObserver = MongooseObserver;