/**
 * 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 fs      = require('fs'),
    path    = require('path');

var conf                = require('../conf/configure'),
    Logger              = require('../logger');

var loadedPackageList   = [],
    loadedPackages      = {};

var PackageCtrHelper = function() {};

/**
 * Check if a module is ESM by checking file extension or package.json
 */
PackageCtrHelper.isESModule = function(modulePath) {
    if (modulePath.endsWith('.mjs')) {
        return true;
    }

    // Check if the module's package.json has "type": "module"
    try {
        var moduleDir = path.dirname(modulePath);
        var checkDir = moduleDir;

        // Walk up to find package.json
        for (var i = 0; i < 10; i++) {
            try {
                var pkgPath = path.join(checkDir, 'package.json');
                if (fs.existsSync(pkgPath)) {
                    var pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
                    if (pkg.type === 'module') {
                        return true;
                    }
                    break;
                }
                var parentDir = path.dirname(checkDir);
                if (parentDir === checkDir) break;
                checkDir = parentDir;
            } catch(e) {
                break;
            }
        }
    } catch(e) {
        // Ignore errors
    }

    return false;
};

/**
 * Load a package using require (CommonJS) or dynamic import (ESM)
 * Returns a Promise for ESM modules
 */
PackageCtrHelper.addPackage = function(realPath){
    try {
        if(!loadedPackages[realPath]){
            if (PackageCtrHelper.isESModule(realPath)) {
                // ESM module - return a promise
                return import(realPath).then(function(module) {
                    loadedPackages[realPath] = module;
                    return module;
                }).catch(function(e) {
                    Logger.printError("WHATAP-201", "PackageControler helper (ESM)", e, false);
                    return null;
                });
            } else {
                // CommonJS module - synchronous
                loadedPackages[realPath] = require(realPath);
                return Promise.resolve(loadedPackages[realPath]);
            }
        }
        return Promise.resolve(loadedPackages[realPath]);
    } catch(e) {
        Logger.printError("WHATAP-202", "PackageControler helper", e, false);
        return Promise.resolve(null);
    }
};
PackageCtrHelper.getLoadedPackageList = function(filter){
    return loadedPackageList;
};
PackageCtrHelper.getDependencies = function () {
    try {
        var package_json = require(path.join(conf['app.root'], 'package.json'));
        if (package_json == null) { return {} };
        return package_json.dependencies || {};
    } catch (e) {
        return {};
    }
};
PackageCtrHelper.getInstalledPackageList = function(){
    try {
        var packageFolder = path.join(conf['app.root'], 'node_modules'),
            dirs = fs.readdirSync( packageFolder );

        if(dirs && dirs.length > 0) {
            dirs = dirs.filter(function(item) {
                return item.indexOf('.') != 0;
            });
        }
        return dirs;
    } catch(e) {
        Logger.printError('WHATAP-203', 'Get Installed Package List ', e, false);
    }

    return [];
};
PackageCtrHelper.getPackageDetail = function(pkg){
    try {

        var packageFolder = path.join(conf['app.root'], 'node_modules'),
            packageFile = fs.readFileSync( path.join(packageFolder, pkg, 'package.json')),

            packageJson = JSON.parse(packageFile);
        return packageJson.dependencies;
    } catch(e) {
        Logger.printError('WHATAP-204', 'Get Package Detail ', e, false);
    }
};

/**
 * Dynamically hook a method in a user-defined module
 * @param {Object} moduleObj - The module object to hook
 * @param {string} modulePath - Path to the module
 * @param {string} methodName - Name of the method to hook
 */
PackageCtrHelper.dynamicHook = function(moduleObj, modulePath, methodName) {
    try {
        if (!moduleObj) {
            Logger.printError('WHATAP-205',
                'Module object is null or undefined for path: ' + modulePath,
                null, false);
            return;
        }

        // Get the target object and method
        var targetObj = moduleObj;
        var targetMethod = methodName;

        // Check if method exists as a direct property
        if (typeof targetObj[targetMethod] === 'function') {
            PackageCtrHelper.hookMethod(targetObj, modulePath, targetMethod, null);
            return;
        }

        // Check if it's a class with prototype methods
        if (targetObj.prototype && typeof targetObj.prototype[targetMethod] === 'function') {
            PackageCtrHelper.hookMethod(targetObj.prototype, modulePath, targetMethod, null);
            return;
        }

        // Check if module exports is a class instance
        if (targetObj.constructor && targetObj.constructor.prototype &&
            typeof targetObj.constructor.prototype[targetMethod] === 'function') {
            PackageCtrHelper.hookMethod(targetObj.constructor.prototype, modulePath, targetMethod, null);
            return;
        }

        Logger.printError('WHATAP-206',
            'Method "' + targetMethod + '" not found in module ' + modulePath,
            null, false);
    } catch (e) {
        Logger.printError('WHATAP-207',
            'Error hooking ' + modulePath + '/' + methodName, e, false);
    }
};

/**
 * Hook a specific method on an object
 * @param {Object} obj - The object containing the method
 * @param {string} modulePath - Path to the module (for logging)
 * @param {string} methodName - Name of the method to hook
 * @param {string} className - Optional class name for proper display
 */
PackageCtrHelper.hookMethod = function(obj, modulePath, methodName, className) {
    try {
        var TraceContextManager = require('../trace/trace-context-manager');
        // Lazy require to avoid circular dependency issues
        var AsyncSender = require('../udp/async_sender');
        var PacketTypeEnum = require('../udp/packet_type_enum');

        var originalMethod = obj[methodName];
        obj[methodName] = function() {
            var ctx = TraceContextManager.getCurrentContext();
            if (!ctx) {
                return originalMethod.apply(this, arguments);
            }

            var startTime = ctx.start_time = Date.now();
            var error = null;
            var result = null;

            // Capture original arguments once for later use
            var originalArgs = arguments;

            // Format: "ClassName.methodName" if className exists, otherwise "NULL.methodName"
            var methodFullPath = className ? className + '.' + methodName : 'NULL.' + methodName;

            // Convert arguments to string once
            var argsString = '';
            try {
                var argsArray = Array.prototype.slice.call(originalArgs);
                argsString = argsArray.map(function(arg) {
                    if (arg === null) return 'null';
                    if (arg === undefined) return 'undefined';
                    if (typeof arg === 'function') return '[Function]';
                    if (typeof arg === 'object') {
                        try {
                            return JSON.stringify(arg, null, 0);
                        } catch(e) {
                            return '[Object]';
                        }
                    }
                    return String(arg);
                }).join(', ');

                // Truncate if too long
                if (argsString.length > 1000) {
                    argsString = argsString.substring(0, 1000) + '...';
                }
            } catch(e) {
                argsString = '[args parsing error]';
            }

            var sendMetrics = function(actualElapsed) {
                // Check if AsyncSender is available before use
                if (AsyncSender && typeof AsyncSender.send_packet === 'function') {
                    var payloads = [methodFullPath, argsString];
                    ctx.elapsed = actualElapsed;

                    AsyncSender.send_packet(PacketTypeEnum.TX_METHOD, ctx, payloads);
                }
            };

            try {
                result = originalMethod.apply(this, originalArgs);

                // Check if result is a Promise
                if (result && typeof result.then === 'function') {
                    // Async function - wrap the promise to measure elapsed time
                    return result.then(function(value) {
                        var elapsed = Date.now() - startTime;
                        sendMetrics(elapsed);
                        return value;
                    }).catch(function(e) {
                        var elapsed = Date.now() - startTime;

                        if (!ctx.error) {
                            ctx.error = 1;
                            ctx.status = 500;
                        }

                        // Check if AsyncSender is available before use
                        if (AsyncSender && typeof AsyncSender.send_packet === 'function') {
                            var errors = [e.message || '', e.stack || ''];
                            AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
                        }

                        sendMetrics(elapsed);
                        throw e;
                    });
                } else {
                    // Synchronous function
                    var elapsed = Date.now() - startTime;
                    sendMetrics(elapsed);
                    return result;
                }
            } catch (e) {
                error = e;

                if (!ctx.error) {
                    ctx.error = 1;
                    ctx.status = 500;
                }

                // Check if AsyncSender is available before use
                if (AsyncSender && typeof AsyncSender.send_packet === 'function') {
                    var errors = [e.message || '', e.stack || ''];
                    AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
                } else {
                    Logger.printError('WHATAP-208', 'AsyncSender.send_packet is not available', null, false);
                }

                var elapsed = Date.now() - startTime;
                sendMetrics(elapsed);
                throw e;
            }
        };

        // Preserve original function metadata
        obj[methodName].__whatapHooked__ = true;
        obj[methodName].__whatapOriginal__ = originalMethod;
        obj[methodName].__whatapModulePath__ = modulePath;
    } catch (e) {
        Logger.printError('WHATAP-209',
            'Error in hookMethod for ' + modulePath + '/' + methodName, e, false);
    }
};

/**
 * Initialize custom method hooks based on hook_method_patterns configuration
 * This runs once when the module is first loaded
 */
(function initializeMethodHooks() {
    var hookPatterns = conf['hook_method_patterns'];

    if (!hookPatterns || typeof hookPatterns !== 'string' || hookPatterns.trim() === '') {
        return;
    }

    var methods = hookPatterns.split(',');

    methods.forEach(function (path) {
        path = path.trim();

        // Parse path: ./src/user/user.service/ClassName.methodName or ./src/user/user.service/methodName
        var parts = path.split('/').filter(function(p) { return p && p !== '.'; });

        if (parts.length < 1) {
            Logger.printError('WHATAP-210',
                'Invalid hook_method_patterns format: ' + path +
                '. Expected format: ./path/to/file/ClassName.methodName or ./path/to/file/methodName',
                null, false);
            return;
        }

        var lastPart = parts.pop();
        var methodName = null;
        var className = null;

        // Check if lastPart contains a dot (ClassName.methodName)
        if (lastPart.indexOf('.') > 0) {
            var classMethod = lastPart.split('.');
            if (classMethod.length === 2) {
                className = classMethod[0];
                methodName = classMethod[1];
            } else {
                Logger.printError('WHATAP-211',
                    'Invalid class.method format: ' + lastPart +
                    '. Expected format: ClassName.methodName',
                    null, false);
                return;
            }
        } else {
            // No dot, just a method name
            methodName = lastPart;
        }

        var relative_path = '/' + parts.join('/');

        var root = process.cwd();
        if(root.indexOf('/bin') >= 0) {
            root = root.substr(0, root.indexOf('/bin'));
        }

        // Try multiple paths for TypeScript/ESM support
        var pathsToTry = [
            root + relative_path,  // Original path
            root + relative_path.replace('/src/', '/dist/'),  // TypeScript compiled path
        ];

        // Add .js and .mjs extension variants
        var extendedPaths = [];
        pathsToTry.forEach(function(p) {
            extendedPaths.push(p);
            if (!p.endsWith('.js') && !p.endsWith('.ts') && !p.endsWith('.mjs')) {
                extendedPaths.push(p + '.js');
                extendedPaths.push(p + '.mjs');
            }
        });

        // Async function to handle module loading and hooking
        var tryLoadAndHook = function(pathIndex) {
            if (pathIndex >= extendedPaths.length) {
                Logger.printError('WHATAP-212',
                    'Could not load module for hooking. Tried paths: ' + extendedPaths.join(', '),
                    null, false);
                return;
            }

            var tryPath = extendedPaths[pathIndex];

            try {
                if (!fs.existsSync(tryPath) && !fs.existsSync(tryPath + '.js') && !fs.existsSync(tryPath + '.mjs')) {
                    // Path doesn't exist, try next
                    tryLoadAndHook(pathIndex + 1);
                    return;
                }

                // addPackage now returns a Promise
                PackageCtrHelper.addPackage(tryPath).then(function() {
                    if (!loadedPackages[tryPath]) {
                        // Failed to load, try next path
                        tryLoadAndHook(pathIndex + 1);
                        return;
                    }

                    var moduleExports = loadedPackages[tryPath];

                    // Handle TypeScript/ES6 named exports
                    if (className) {
                        // For TypeScript classes: module exports { ClassName: [Class] }
                        if (moduleExports[className]) {
                            if (moduleExports[className].prototype && typeof moduleExports[className].prototype[methodName] === 'function') {
                                PackageCtrHelper.hookMethod(moduleExports[className].prototype, tryPath, methodName, className);
                            } else {
                                PackageCtrHelper.hookMethod(moduleExports[className], tryPath, methodName, className);
                            }
                            Logger.print('WHATAP-HOOK',
                                'Hooked class method: ' + className + '.' + methodName,
                                false);
                        } else if (moduleExports.default) {
                            // ESM default export
                            if (typeof moduleExports.default === 'function' && moduleExports.default.name === className) {
                                // Default export is the class itself
                                if (moduleExports.default.prototype && typeof moduleExports.default.prototype[methodName] === 'function') {
                                    PackageCtrHelper.hookMethod(moduleExports.default.prototype, tryPath, methodName, className);
                                } else {
                                    PackageCtrHelper.hookMethod(moduleExports.default, tryPath, methodName, className);
                                }
                                Logger.print('WHATAP-HOOK',
                                    'Hooked ESM default class method: ' + className + '.' + methodName,
                                    false);
                            } else if (moduleExports.default[className]) {
                                // Default export contains the class
                                if (moduleExports.default[className].prototype && typeof moduleExports.default[className].prototype[methodName] === 'function') {
                                    PackageCtrHelper.hookMethod(moduleExports.default[className].prototype, tryPath, methodName, className);
                                } else {
                                    PackageCtrHelper.hookMethod(moduleExports.default[className], tryPath, methodName, className);
                                }
                                Logger.print('WHATAP-HOOK',
                                    'Hooked ESM class method (in default): ' + className + '.' + methodName,
                                    false);
                            } else {
                                Logger.printError('WHATAP-213',
                                    'Class "' + className + '" not found in module exports for ' + tryPath,
                                    null, false);
                            }
                        } else {
                            Logger.printError('WHATAP-214',
                                'Class "' + className + '" not found in module exports for ' + tryPath,
                                null, false);
                        }
                    } else {
                        // Direct method or try to find the method in exports
                        // For ESM, check default export first
                        if (moduleExports.default && typeof moduleExports.default === 'object') {
                            if (typeof moduleExports.default[methodName] === 'function') {
                                PackageCtrHelper.hookMethod(moduleExports.default, tryPath, methodName, null);
                            } else {
                                PackageCtrHelper.dynamicHook(moduleExports.default, tryPath, methodName);
                            }
                            Logger.print('WHATAP-HOOK',
                                'Hooked ESM default export method: ' + tryPath + '/' + methodName,
                                false);
                        } else {
                            if (typeof moduleExports[methodName] === 'function') {
                                PackageCtrHelper.hookMethod(moduleExports, tryPath, methodName, null);
                            } else {
                                PackageCtrHelper.dynamicHook(moduleExports, tryPath, methodName);
                            }
                            Logger.print('WHATAP-HOOK',
                                'Hooked method: ' + tryPath + '/' + methodName,
                                false);
                        }
                    }
                }).catch(function(e) {
                    // Error loading this path, try next
                    Logger.printError('WHATAP-215',
                        'Error loading module ' + tryPath + ': ' + e.message,
                        e, false);
                    tryLoadAndHook(pathIndex + 1);
                });
            } catch (e) {
                // Error with this path, try next
                tryLoadAndHook(pathIndex + 1);
            }
        };

        // Start trying paths from index 0
        tryLoadAndHook(0);
    });
})();

module.exports = PackageCtrHelper;
