/**
 * 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 os = require('os'),
    fs = require('fs'),
    path = require('path'),
    child_process = require('child_process'),
    properLock = require('proper-lockfile'),
    crypto = require('crypto'),
    cluster = require('cluster');


const RequestLog = require('../requestlog');

var Interceptor      = require('./interceptor').Interceptor,
    HttpObserver     = require('../observers/http-observer').HttpObserver,
    GlobalObserver   = require('../observers/global-observer').GlobalObserver,
    MysqlObserver    = require('../observers/mysql-observer').MysqlObserver,
    Mysql2Observer    = require('../observers/mysql2-observer').Mysql2Observer,
    MariaObserver    = require('../observers/maria-observer').MariaObserver,
    SocketioObserver = require('../observers/socket.io-observer').SocketIOObserver,
    WebsocketObserver = require('../observers/websocket-observer').WebsocketObserver,
    // WsObserver = require('../observers/websocket-observer').WsObserver,
    ProcessObserver  = require('../observers/process-observer').ProcessObserver,
    MongoObserver    = require('../observers/mongodb-observer').MongoObserver,
    // MongooseObserver = require('../observers/mongoose-observer').MongooseObserver,
    RedisObserver    = require('../observers/redis-observer').RedisObserver,
    IORedisObserver    = require('../observers/ioredis-observer').IORedisObserver,
    MssqlObserver    = require('../observers/mssql-observer').MssqlObserver,
    PgSqlObserver    = require('../observers/pgsql-observer').PgSqlObserver,
    GRpcObserver     = require('../observers/grpc-observer').GRpcObserver,
    ApolloObserver   = require('../observers/apollo-server-observer').ApolloServerObserver,
    PrismaObserver  = require('../observers/prisma-observer').PrismaObserver,
    OracleObserver  = require('../observers/oracle-observer').OracleObserver,
    CustomMethodObserver = require('../observers/custom-method-observer').CustomMethodObserver,
    CronObserver = require('../observers/cron-observer').CronObserver,
    KafkaObserver = require('../observers/kafka-observer').KafkaObserver,
    OpenAIObserver = require('../observers/openai-observer').OpenAIObserver;


var Configuration    = require('./../conf/configure'),
    SecurityMaster   = require('./../net/security-master'),
    CounterManager   = require('./../counter/counter-manager'),
    NodeUtil         = require('./../util/nodeutil'),
    DateUtil         = require('./../util/dateutil'),
    WhatapUtil       = require('./../util'),
    TraceContextManager = require('../trace/trace-context-manager'),
    HashUtil            = require('../util/hashutil'),
    Logger           = require('../logger');

// Import UDP module
const UdpModule = require('../udp');
const UdpSession = UdpModule.UdpSession;
const AsyncSender = UdpModule.AsyncSender;
const PacketTypeEnum = UdpModule.PacketTypeEnum;

const ARCH = {
    'x64': 'amd64',
    'ia32': '386',
    'arm': 'arm',
    'arm64': 'arm64'
};

// Agent binary name
const AGENT_NAME = 'whatap_nodejs';

var NodeAgent = function(opt) {
    this._userOpt = opt;
    this._initialized = false;
    this.aop = new Interceptor(this);
    this._conf = Configuration;
    this._securityMaster = SecurityMaster;
    this._counterManager = null;
    this.setLoopTime = 5000;
    this.connectCount = 0;
    this.userProfileHash = 0;
    this.init();
};

NodeAgent.prototype.plugin=function(name,func){
    if('extra'===name){
        //test
        CounterManager.plugin[name]=func;
    }
}

NodeAgent.prototype.findRoot = function () {
    var self = this;
    var root = process.cwd();
    while(root.length > 1) {
        var dir_name = path.join(root, 'package.json');
        if(fs.existsSync(dir_name)) {
            self._conf['app.root'] = root;
            return;
        }
        root = path.join(root, '..');
    }

    root = path.dirname(require.main.filename);
    while(root.length > 1) {
        var dir_name = path.join(root, 'package.json');
        if(fs.existsSync(dir_name)) {
            self._conf['app.root'] = root;
            return;
        }
        root = path.join(root, '..');
    }
};

// Cross-platform function to check if a process is running and is whatap_nodejs
NodeAgent.prototype.isNodejsAgentProcess = function(pid) {
    try {
        pid = parseInt(pid);
        const platform = process.platform;
        let processCommand = '';

        if (platform === 'linux') {
            // On Linux, use /proc filesystem
            try {
                const cmdlineFile = path.join('/proc', pid.toString(), 'cmdline');
                if (fs.existsSync(cmdlineFile)) {
                    processCommand = fs.readFileSync(cmdlineFile, 'utf8');
                    // Log for debugging
                    // Logger.print("WHATAP-001", `Process ${pid} command line: ${processCommand}`, false);
                }
            } catch (e) {
                Logger.printError("WHATAP-101", "Error reading Linux process info", e, false);
                return false;
            }
        } else if (platform === 'darwin') {
            // On macOS, use ps command
            try {
                processCommand = child_process.execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim();
                // Logger.print("WHATAP-001", `Process ${pid} command line: ${processCommand}`, false);
            } catch (e) {
                Logger.printError("WHATAP-102", "Error reading MacOS process info", e, false);
                return false;
            }
        } else if (platform === 'win32') {
            // On Windows, use tasklist command
            try {
                const output = child_process.execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8' });
                // Windows output has quotes, e.g.: "whatap_nodejs.exe","1234","Console","1","12,345 K"
                const match = output.match(/"([^"]+)"/);
                if (match && match[1]) {
                    processCommand = match[1];
                    // Logger.print("WHATAP-001", `Process ${pid} command line: ${processCommand}`, false);
                }
            } catch (e) {
                Logger.printError("WHATAP-103", "Error reading Windows process info", e, false);
                return false;
            }
        } else {
            // On other platforms, try generic ps command
            try {
                processCommand = child_process.execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim();
                Logger.print("WHATAP-001", `Process ${pid} command line: ${processCommand}`, false);
            } catch (e) {
                Logger.printError("WHATAP-104", "Error reading other os process info", e, false);
                return false;
            }
        }

        // Check if the command contains the agent name
        const isAgentProcess = processCommand.indexOf(AGENT_NAME) >= 0;
        // Logger.print("WHATAP-002", `Process ${pid} is ${isAgentProcess ? '' : 'not '}a WhaTap agent`, false);
        return isAgentProcess;
    } catch (e) {
        Logger.printError("WHATAP-003", "Error in isNodejsAgentProcess", e, false);
        return false;
    }
};

// Function to read PID from file
NodeAgent.prototype.readPidFile = function(home, fileName) {
    try {
        const filePath = path.join(process.env[home], fileName);
        if (fs.existsSync(filePath)) {
            return fs.readFileSync(filePath, 'utf8').trim();
        }
    } catch (e) {
        Logger.printError("WHATAP-004", "Error reading PID file", e, false);
    }
    return '';
};

// Function to write to file
NodeAgent.prototype.writeToFile = function(home, fileName, value) {
    try {
        const filePath = path.join(process.env[home], fileName);
        fs.writeFileSync(filePath, value);
        return true;
    } catch (e) {
        Logger.printError("WHATAP-005", `Error writing to file ${fileName}`, e, false);
        Logger.print("WHATAP-006", `Try to execute command: \`sudo chmod -R 777 $WHATAP_HOME\``, false);
        return false;
    }
};

// Modified isGoAgentRunning to check PID +-1 range
NodeAgent.prototype.isGoAgentRunning = function(pid) {
    try {
        if (!pid) return false;

        // 원래 PID와 +1, -1된 PID 모두 확인
        const pidsToCheck = [
            parseInt(pid),
            parseInt(pid) + 1,
            parseInt(pid) - 1
        ];

        // Logger.print("WHATAP-007", `Checking PIDs: ${pidsToCheck.join(', ')} (original: ${pid})`, false);

        for (const pidToCheck of pidsToCheck) {
            try {
                // 프로세스 존재 여부 확인
                process.kill(pidToCheck, 0);

                // whatap_nodejs 프로세스인지 확인
                if (this.isNodejsAgentProcess(pidToCheck)) {
                    // Logger.print("WHATAP-008",
                    //     `Found agent process at PID ${pidToCheck} (original PID in file: ${pid})`,
                    //     false);
                    return true;
                }
            } catch (e) {
                // ESRCH error means process doesn't exist
                if (e.code !== 'ESRCH') {
                    throw e;
                }
            }
        }

        Logger.print("WHATAP-009", `No agent process found for PID ${pid} or adjacent PIDs`, false);
        return false;
    } catch (e) {
        Logger.printError("WHATAP-010", "Error checking if agent is running", e, false);
        return false;
    }
};

/**
 * Read content from a file
 * @param {string} home - Environment variable name for home directory
 * @param {string} fileName - Name of the file to read
 * @returns {string} - Content of the file, or empty string on error
 */
NodeAgent.prototype.readFile = function(home, fileName) {
    try {
        const filePath = path.join(process.env[home], fileName);
        if (fs.existsSync(filePath)) {
            return fs.readFileSync(filePath, 'utf8').toString().trim();
        }
    } catch (e) {
        Logger.printError("WHATAP-011", `Error reading file ${fileName}`, e, false);
    }
    return '';
};

/**
 * Write content to a file
 * @param {string} home - Environment variable name for home directory
 * @param {string} fileName - Name of the file to write
 * @param {string} value - Content to write to the file
 * @returns {boolean} - True if successful, false otherwise
 */
NodeAgent.prototype.writeFile = function(home, fileName, value) {
    try {
        const filePath = path.join(process.env[home], fileName);
        fs.writeFileSync(filePath, value);
        return true;
    } catch (e) {
        Logger.printError("WHATAP-012", `Error writing to file ${fileName}`, e, false);
        Logger.print("WHATAP-013", `Try to execute command: \`sudo chmod -R 777 $WHATAP_HOME\``, false);
        return false;
    }
};

/**
 * Find whatap.conf file by searching directories
 * @returns {string|null} - Path to whatap.conf or null if not found
 */
NodeAgent.prototype.findWhatapConf = function() {
    // 1. Check current directory
    const scriptDir = path.dirname(__filename);
    const parentDir = path.dirname(scriptDir);
    let confPath = path.join(parentDir, 'whatap.conf');

    if (fs.existsSync(confPath)) {
        process.env.WHATAP_HOME = parentDir;
        return confPath;
    }

    // 2. Check parent directories
    let current = parentDir;
    while (current !== '/' && current !== path.parse(current).root) {
        confPath = path.join(current, 'whatap.conf');
        if (fs.existsSync(confPath)) {
            process.env.WHATAP_HOME = current;
            return confPath;
        }
        current = path.dirname(current);
    }

    return null;
};

/**
 * Open and lock the port file
 * @param {string} filepath - Path to the lock file
 * @returns {object|null} - File descriptor or null if failed
 */
NodeAgent.prototype.openPortFile = function(filepath) {
    filepath = filepath || process.env.WHATAP_LOCK_FILE || '/tmp/whatap-nodejs.lock';

    let fileHandle = null;
    let attempts = 0;

    // Try to open the file
    while (!fileHandle && attempts < 100) {
        try {
            // Try to open file for reading and writing
            if (fs.existsSync(filepath)) {
                fileHandle = fs.openSync(filepath, 'r+');
            } else {
                // If file doesn't exist, create the directory if needed
                const dirPath = path.dirname(filepath);
                if (!fs.existsSync(dirPath)) {
                    fs.mkdirSync(dirPath, { recursive: true });
                }
                // Create the file
                fileHandle = fs.openSync(filepath, 'w+');
            }
        } catch (e) {
            Logger.printError("WHATAP-014", `Error opening port file (attempt ${attempts})`, e, false);
        }

        // Add a delay after 50 attempts
        if (attempts > 50) {
            setTimeout(() => {}, 100); // 0.1 second delay
        }

        attempts++;
    }

    if (fileHandle) {
        try {
            // Use proper-lock with the filepath instead of file descriptor
            properLock.lockSync(filepath);
            return { fileHandle, filepath };
        } catch (e) {
            Logger.printError("WHATAP-015", "Failed to lock port file", e, false);
            try {
                fs.closeSync(fileHandle);
            } catch (closeErr) {}
            return null;
        }
    }
    return null;
};

/**
 * Get available port number for the agent
 * @param {number} defaultPort - Default port to start with
 * @param {string} home - WHATAP_HOME directory
 * @returns {number|null} - Allocated port number or null if failed
 */
NodeAgent.prototype.getPortNumber = function(port, home) {
    // Node.js cluster 모드 감지 (isPM2ClusterMode 호출 전에 실행)
    this.isClusterMode();

    port = port || 6600;
    home = home || process.env.WHATAP_HOME;

    // Return null if home is not defined
    if (!home) {
        return null;
    }

    // PM2 cluster 모드에서는 pm_id를 제외한 identifier 사용 (동일 포트 공유)
    // 일반 모드에서는 pm_id 포함 (각자 다른 포트)
    const appIdentifier = this.getApplicationIdentifier();
    const portKey = `${home}:${appIdentifier}`;
    const isPM2Cluster = this.isPM2ClusterMode();

    const lockFilePath = process.env.WHATAP_LOCK_FILE || '/tmp/whatap-nodejs.lock';

    // PM2 cluster 모드에서 whatap_nodejs가 이미 실행 중이면 lock 없이 읽기만
    if (isPM2Cluster) {
        const sharedPidFile = path.join(home, `agent-${appIdentifier}.pid`);
        if (fs.existsSync(sharedPidFile)) {
            try {
                const existingPid = fs.readFileSync(sharedPidFile, 'utf8').trim();
                if (existingPid && this.isGoAgentRunning(existingPid)) {
                    // whatap_nodejs가 실행 중이면 lock 없이 포트 파일 읽기
                    return this.readPortFromFile(lockFilePath, portKey, appIdentifier, isPM2Cluster);
                }
            } catch (e) {
                Logger.printError("WHATAP-016", "Error checking agent process", e, false);
            }
        }
    }

    // 새로운 포트 할당이 필요한 경우 (첫 실행 또는 일반 모드)
    // Open the port file with lock
    const fileInfo = this.openPortFile(lockFilePath);
    if (!fileInfo) {
        return port; // Return default port if we couldn't open the file
    }

    const { fileHandle, filepath } = fileInfo;

    try {
        // Get file size
        const stats = fs.fstatSync(fileHandle);
        let content = '';

        // Only read if file is not empty
        if (stats.size > 0) {
            const buffer = Buffer.alloc(stats.size);
            fs.readSync(fileHandle, buffer, 0, buffer.length, 0);
            content = buffer.toString().trim();
        }

        let lastPortFound = null;
        const existingPorts = new Map();

        // Process each line
        if (content && content.length > 0) {
            const lines = content.split('\n');
            for (const line of lines) {
                const trimmedLine = line.trim();
                if (!trimmedLine) continue;

                try {
                    // Parse the line "port portKey"
                    const parts = trimmedLine.split(/\t/, 2);
                    if (parts.length < 2) continue;

                    const portStr = parts[0];
                    const storedKey = parts[1];
                    const currentPort = parseInt(portStr, 10);

                    if (isNaN(currentPort)) continue;

                    // If portKey matches, return the existing port
                    if (portKey === storedKey) {
                        // Clean up before returning
                        properLock.unlockSync(filepath);
                        fs.closeSync(fileHandle);
                        Logger.print("WHATAP-017",
                            `Reusing existing port ${currentPort} for ${appIdentifier}${isPM2Cluster ? ' (PM2 cluster - shared port)' : ''}`,
                            false);
                        return currentPort;
                    }

                    // Store existing ports
                    existingPorts.set(storedKey, currentPort);

                    // Track the highest port number
                    if (!lastPortFound || lastPortFound < currentPort) {
                        lastPortFound = currentPort;
                    }
                } catch (e) {
                    continue; // Skip lines that can't be parsed
                }
            }
        }

        // Determine the new port number
        const newPort = lastPortFound ? lastPortFound + 1 : port;

        // Rebuild the file with existing entries + new entry
        let newContent = '';
        for (const [key, portNum] of existingPorts.entries()) {
            newContent += `${portNum}\t${key}\n`;
        }
        newContent += `${newPort}\t${portKey}\n`;

        // Clear and rewrite the file
        fs.ftruncateSync(fileHandle, 0);
        fs.writeSync(fileHandle, newContent);

        // Unlock and close the file
        properLock.unlockSync(filepath);
        fs.closeSync(fileHandle);

        Logger.print("WHATAP-018",
            `Allocated new port ${newPort} for ${appIdentifier}${isPM2Cluster ? ' (PM2 cluster - shared port)' : ''}`,
            false);
        return newPort;
    } catch (e) {
        Logger.printError("WHATAP-019", "Error processing port file", e, false);

        try {
            properLock.unlockSync(filepath);
            fs.closeSync(fileHandle);
        } catch (closeErr) {
        }

        return port;
    }
};

/**
 * Read port from file without acquiring lock (for PM2 cluster mode)
 * @param {string} filepath - Port file path
 * @param {string} portKey - Port key to search
 * @param {string} appIdentifier - Application identifier
 * @param {boolean} isPM2Cluster - Whether in PM2 cluster mode
 * @returns {number|null} - Port number or null if not found
 */
NodeAgent.prototype.readPortFromFile = function(filepath, portKey, appIdentifier, isPM2Cluster) {
    try {
        if (!fs.existsSync(filepath)) {
            Logger.printError("WHATAP-020", "Port file does not exist", null, false);
            return null;
        }

        const content = fs.readFileSync(filepath, 'utf8').trim();

        if (!content) {
            return null;
        }

        const lines = content.split('\n');
        for (const line of lines) {
            const trimmedLine = line.trim();
            if (!trimmedLine) continue;

            const parts = trimmedLine.split(/\t/, 2);
            if (parts.length < 2) continue;

            const portStr = parts[0];
            const storedKey = parts[1];
            const currentPort = parseInt(portStr, 10);

            if (isNaN(currentPort)) continue;

            if (portKey === storedKey) {
                Logger.print("WHATAP-021",
                    `Reusing existing port ${currentPort} for ${appIdentifier}${isPM2Cluster ? ' (PM2 cluster - shared port, lock-free read)' : ''}`,
                    false);
                return currentPort;
            }
        }

        return null;
    } catch (e) {
        Logger.printError("WHATAP-022", "Error reading port file without lock", e, false);
        return null;
    }
};

/**
 * Configure the port for the agent and update the configuration
 * @returns {number|null} - The configured port or null if failed
 */
NodeAgent.prototype.configurePort = function() {
    const port = this.getPortNumber();
    if (port) {
        this.updateConfig('WHATAP_HOME', 'net_udp_port', port.toString());
        return port;
    }
    return null;
};

/**
 * Update configuration file with new option value
 * @param {string} home - Environment variable name for WhaTap home
 * @param {string} optKey - Option key to update
 * @param {string} optValue - Option value to set
 */
NodeAgent.prototype.updateConfig = function(home, optKey, optValue) {
    const homePath = process.env[home];
    if (!homePath) {
        Logger.print("WHATAP-023", `${home} environment variable not set`, false);
        return;
    }

    // Use WHATAP_CONF environment variable or default to 'whatap.conf'
    const configFileName = process.env.WHATAP_CONF || 'whatap.conf';
    const configFile = path.join(homePath, configFileName);

    try {
        let content = '';
        let isUpdated = false;

        if (fs.existsSync(configFile)) {
            const lines = fs.readFileSync(configFile, 'utf8').split('\n');

            // Look for the option key and update if found
            for (let line of lines) {
                if (line.trim()) {
                    const parts = line.split('=');
                    const key = parts[0].trim();

                    if (key === optKey) {
                        line = `${key}=${optValue}`;
                        isUpdated = true;
                    }
                }
                content += line + '\n';
            }

            // If the option wasn't found, add it
            if (!isUpdated) {
                content += `\n${optKey}=${optValue}\n`;
            }
        } else {
            // If config file doesn't exist, create with just this option
            content = `${optKey}=${optValue}\n`;
        }

        // Write updated content back to file
        fs.writeFileSync(configFile, content);
        Logger.print("WHATAP-024", `Updated configuration: ${optKey}=${optValue}`, false);
    } catch (e) {
        Logger.printError("WHATAP-025", `Error updating configuration: ${optKey}=${optValue}`, e, false);
    }
};

/**
 * Detect Node.js cluster mode and set environment variables
 * This allows Node.js cluster to work the same way as PM2 cluster mode
 */
NodeAgent.prototype.isClusterMode = function() {
    // PM2 환경 변수가 이미 있으면 skip
    if (process.env.pm_id !== undefined) {
        return;
    }

    // Node.js cluster worker인 경우 PM2 cluster 형태로 환경 변수 설정
    if (cluster.worker) {
        process.env.pm_id = cluster.worker.id.toString();

        // instances 값은 실제로는 알 수 없지만, 2 이상으로 설정하여 클러스터 모드 활성화
        if (!process.env.instances) {
            process.env.instances = '2';
        }

        Logger.print("WHATAP-120", `Node.js cluster mode detected: worker ${cluster.worker.id}`, false);
    }
};

/**
 * Get cluster ID (PM2 pm_id or Node.js cluster worker id)
 * @returns {string|null} - Cluster ID or null if not in cluster mode
 */
NodeAgent.prototype.getClusterId = function() {
    if (process.env.pm_id !== undefined) {
        return process.env.pm_id;
    }
    if (cluster.worker) {
        return cluster.worker.id.toString();
    }
    return null;
};

/**
 * Check if agent_per_instance_enabled mode is active
 * @returns {boolean} - True if each cluster instance should run its own agent
 */
NodeAgent.prototype.isAgentPerInstanceMode = function() {
    const enabled = this._conf.getProperty('agent_per_instance_enabled', false);
    const clusterId = this.getClusterId();
    return enabled === true && clusterId !== null;
};

/**
 * Check if running in PM2 cluster mode
 * @returns {boolean} - True if PM2 cluster mode
 */
NodeAgent.prototype.isPM2ClusterMode = function() {
    // agent_per_instance_enabled=true면 클러스터 모드로 취급하지 않음 (각자 독립 실행)
    if (this.isAgentPerInstanceMode()) {
        return false;
    }

    return process.env.pm_id !== undefined &&
        process.env.instances !== undefined &&
        parseInt(process.env.instances) > 1;
};

NodeAgent.prototype.getApplicationIdentifier = function() {
    // 1. 명시적 그룹 지정 (환경 변수)
    if (process.env.WHATAP_APP_GROUP) {
        return process.env.WHATAP_APP_GROUP;
    }

    // 2. 설정 파일에서 그룹 지정
    if (this._conf['app_group']) {
        return this._conf['app_group'];
    }

    // 3. 자동 생성
    const appRoot = this._conf['app.root'] || process.cwd();
    const appName = this.getApplicationName();
    const isPM2Cluster = this.isPM2ClusterMode();

    // PM2 cluster 모드에서는 pm_id를 제외하여 모든 인스턴스가 동일한 identifier를 공유
    // agent_per_instance_enabled=true이거나 일반 PM2 모드에서는 pm_id 포함 (각자 독립)
    let identifierBase = `${appRoot}:${appName}`;
    if (process.env.pm_id !== undefined && !isPM2Cluster) {
        identifierBase += `:${process.env.pm_id}`;
    }

    const identifier = crypto
        .createHash('md5')
        .update(identifierBase)
        .digest('hex')
        .substr(0, 8);

    // Logger.print("WHATAP-026",
    //     `Application identifier: ${appName} at ${appRoot}${isPM2Cluster ? ' (PM2 cluster mode)' : (process.env.pm_id !== undefined ? ' (PM2 instance: ' + process.env.pm_id + ')' : '')} => ${identifier}`,
    //     false);

    return identifier;
};

NodeAgent.prototype.getApplicationName = function() {
    // 1. PM2 실행 시 (PM2의 공식 환경 변수 사용)
    if (process.env.pm_id !== undefined) {
        // PM2 환경에서는 name 환경 변수 사용
        if (process.env.name) {
            return process.env.name;
        }
    }

    // 2. 설정 파일에서
    if (this._conf['app_name']) {
        return this._conf['app_name'];
    }

    // 3. 명령행 인수에서 --name 옵션 찾기
    const nameArgIndex = process.argv.indexOf('--name');
    if (nameArgIndex !== -1 && nameArgIndex < process.argv.length - 1) {
        return process.argv[nameArgIndex + 1];
    }

    // 4. 환경 변수로 직접 설정
    if (process.env.APP_NAME) {
        return process.env.APP_NAME;
    }

    // 5. 기본값: 실행 파일명
    return path.basename(process.argv[1], '.js');
};

// Restart tracking variables
NodeAgent.prototype._restartCount = 0;
NodeAgent.prototype._lastRestartTime = 0;
NodeAgent.prototype._maxRestarts = 5;
NodeAgent.prototype._restartInterval = 10000; // 10초
NodeAgent.prototype._healthCheckInterval = null;

/**
 * Start health check for whatap_nodejs process
 * Periodically checks if the agent is running and restarts if needed
 */
NodeAgent.prototype.startHealthCheck = function() {
    const self = this;
    const whatapHome = process.env.WHATAP_HOME;

    if (!whatapHome) {
        Logger.print("WHATAP-206", "Cannot start health check: WHATAP_HOME not set", false);
        return;
    }

    // Clear existing health check if any
    if (self._healthCheckInterval) {
        clearInterval(self._healthCheckInterval);
    }

    // PM2 cluster 모드에서는 첫 번째 인스턴스만 health check 수행
    const isPM2Cluster = self.isPM2ClusterMode();
    if (isPM2Cluster && process.env.pm_id !== '0') {
        // Logger.print("WHATAP-207",
        //     `PM2 cluster mode: Instance ${process.env.pm_id} will not run health check (only instance 0)`,
        //     false);
        return;
    }

    Logger.print("WHATAP-208", "Starting agent health check (interval: 30s)", false);

    self._healthCheckInterval = setInterval(() => {
        try {
            const appIdentifier = self.getApplicationIdentifier();
            const sharedPidFile = path.join(whatapHome, `agent-${appIdentifier}.pid`);

            // PID 파일이 없으면 에이전트가 시작되지 않은 것
            if (!fs.existsSync(sharedPidFile)) {
                Logger.print("WHATAP-209", "Agent PID file not found, attempting to start...", false);
                self.startGoAgent();
                return;
            }

            // PID 읽기
            const pid = fs.readFileSync(sharedPidFile, 'utf8').trim();
            if (!pid) {
                Logger.print("WHATAP-210", "Agent PID file is empty, attempting to start...", false);
                self.startGoAgent();
                return;
            }

            // 프로세스가 실행 중인지 확인
            if (!self.isGoAgentRunning(pid)) {
                Logger.printError("WHATAP-211",
                    `Agent process (PID: ${pid}) is not running. Attempting restart...`,
                    null, false);
                self.restartGoAgent();
            }
        } catch (e) {
            Logger.printError("WHATAP-212", "Error during health check", e, false);
        }
    }, 30000); // 30초마다 체크

    // Node.js 프로세스 종료 시 health check도 정리
    process.on('exit', () => {
        if (self._healthCheckInterval) {
            clearInterval(self._healthCheckInterval);
        }
    });
};

/**
 * Stop health check
 */
NodeAgent.prototype.stopHealthCheck = function() {
    if (this._healthCheckInterval) {
        clearInterval(this._healthCheckInterval);
        this._healthCheckInterval = null;
        Logger.print("WHATAP-213", "Stopped agent health check", false);
    }
};

/**
 * Restart the Go agent
 * @param {object} opts - Options for agent
 * @returns {boolean} - True if restart attempted, false if blocked
 */
NodeAgent.prototype.restartGoAgent = function(opts = {}) {
    const self = this;
    const now = Date.now();
    const timeSinceLastRestart = now - self._lastRestartTime;

    // Reset restart count if enough time has passed (5 minutes)
    if (timeSinceLastRestart > 300000) {
        self._restartCount = 0;
    }

    // Check if we've exceeded max restarts
    if (self._restartCount >= self._maxRestarts) {
        Logger.printError("WHATAP-200",
            `Max restart attempts (${self._maxRestarts}) reached. Agent will not restart automatically.`,
            null, false);
        return false;
    }

    // Check if restart interval has passed
    if (timeSinceLastRestart < self._restartInterval) {
        Logger.print("WHATAP-201",
            `Restart interval not reached. Waiting ${(self._restartInterval - timeSinceLastRestart) / 1000}s`,
            false);
        return false;
    }

    self._restartCount++;
    self._lastRestartTime = now;

    Logger.print("WHATAP-202",
        `Attempting to restart agent (attempt ${self._restartCount}/${self._maxRestarts})`,
        false);

    // Clean up lock and PID files before restart
    const whatapHome = process.env.WHATAP_HOME;
    if (whatapHome) {
        const appIdentifier = self.getApplicationIdentifier();
        const sharedLockFile = path.join(whatapHome, `agent-${appIdentifier}.lock`);
        const sharedPidFile = path.join(whatapHome, `agent-${appIdentifier}.pid`);

        try {
            if (fs.existsSync(sharedLockFile)) fs.unlinkSync(sharedLockFile);
            if (fs.existsSync(sharedPidFile)) fs.unlinkSync(sharedPidFile);
            Logger.print("WHATAP-203", "Cleaned up lock and PID files", false);
        } catch (e) {
            Logger.printError("WHATAP-204", "Error cleaning up files before restart", e, false);
        }
    }

    // Start the agent again
    self.startGoAgent(opts);

    // UDP 재초기화 (새 포트로 연결)
    // whatap_nodejs가 daemon으로 전환되는 시간을 고려하여 지연 후 재초기화
    setTimeout(() => {
        try {
            Logger.print("WHATAP-214", "Reinitializing UDP connection after agent restart...", false);
            self.initUdp();
        } catch (e) {
            Logger.printError("WHATAP-215", "Error reinitializing UDP after restart", e, false);
        }
    }, 2000); // 2초 대기 (daemon 전환 시간 고려)

    return true;
};

// startGoAgent with improved PID handling
NodeAgent.prototype.startGoAgent = function(opts = {}) {
    const self = this;
    const home = 'WHATAP_HOME';
    const pidFileName = `${AGENT_NAME}.pid`;
    const whatapHome = process.env[home];

    if (!whatapHome) {
        Logger.printError("WHATAP-105", "WHATAP_HOME environment variable is not set", true);
        return;
    }

    // 1. 애플리케이션 식별자 생성
    const appIdentifier = this.getApplicationIdentifier();
    const isPM2Cluster = this.isPM2ClusterMode();
    const sharedLockFile = path.join(whatapHome, `agent-${appIdentifier}.lock`);
    const sharedPidFile = path.join(whatapHome, `agent-${appIdentifier}.pid`);

    // 2. 공유 잠금 파일을 사용하여 중복 실행 방지
    try {
        if (fs.existsSync(sharedPidFile)) {
            // PID 파일에서 기존 PID 읽기
            const existingPid = fs.readFileSync(sharedPidFile, 'utf8').trim();
            if (existingPid && this.isGoAgentRunning(existingPid)) {
                const logMsg = isPM2Cluster
                    ? `Go agent already running for application ${appIdentifier} (PID: ${existingPid}) - PM2 instance ${process.env.pm_id} will connect to shared agent`
                    : `Go agent already running for application ${appIdentifier} (PID: ${existingPid})`;
                Logger.print("WHATAP-027", logMsg, false);
                return;
            } else {
                // 프로세스가 죽었다면 파일 제거
                Logger.print("WHATAP-028", `Previous agent process ${existingPid} not found, cleaning up`, false);
                try { fs.unlinkSync(sharedLockFile); } catch (e) {}
                try { fs.unlinkSync(sharedPidFile); } catch (e) {}
            }
        }
    } catch (e) {
        Logger.printError("WHATAP-029", "Error checking shared lock file", e, false);
    }

    // 3. PM2 cluster 모드에서는 첫 번째 인스턴스만 에이전트 시작
    if (isPM2Cluster) {
        // 잠금 파일로 동시 실행 방지
        let lockAcquired = false;
        try {
            if (!fs.existsSync(sharedLockFile)) {
                fs.writeFileSync(sharedLockFile, process.pid.toString());
                lockAcquired = true;
                Logger.print("WHATAP-030",
                    `PM2 cluster mode: Instance ${process.env.pm_id} will start shared agent`,
                    false);
            } else {
                // 다른 인스턴스가 이미 에이전트를 시작하는 중
                Logger.print("WHATAP-031",
                    `PM2 cluster mode: Instance ${process.env.pm_id} detected another instance starting the agent, will use shared agent`,
                    false);
                return;
            }
        } catch (e) {
            Logger.printError("WHATAP-032", "Error acquiring lock in PM2 cluster mode", e, false);
            return;
        }

        if (!lockAcquired) {
            return;
        }
    } else {
        // 일반 모드에서는 기존 로직 사용
        let lockAcquired = false;
        try {
            if (!fs.existsSync(sharedLockFile)) {
                fs.writeFileSync(sharedLockFile, process.pid.toString());
                lockAcquired = true;
            }
        } catch (e) {
            Logger.printError("WHATAP-033", "Error acquiring lock", e, false);
        }

        if (!lockAcquired) {
            Logger.print("WHATAP-034", "Another process is starting the agent, waiting...", false);
            return;
        }
    }

    // 기존 whatap_nodejs.pid 파일 처리 (호환성 유지)
    const pid = this.readPidFile(home, pidFileName);
    if (pid) {
        try {
            if (this.isGoAgentRunning(pid)) {
                // Windows: 기존 프로세스가 실행 중이면 스킵
                // Unix/Linux: 기존 프로세스 종료 후 새로 시작
                if (process.platform === 'win32') {
                    Logger.print("WHATAP-106", `Agent already running (PID: ${pid}). Skipping duplicate execution.`, false);
                    return;
                } else {
                    Logger.print("WHATAP-106", `Found existing agent with PID ${pid}, terminating...`, false);
                    try {
                        process.kill(parseInt(pid), 'SIGKILL');
                        Logger.print("WHATAP-107", `Successfully terminated existing agent with PID ${pid}`, false);
                    } catch (killError) {
                        Logger.printError("WHATAP-108", `Error terminating process with PID ${pid}`, killError, false);
                    }
                }
            }
        } catch (e) {
            Logger.printError("WHATAP-109", `Error checking process with PID ${pid}`, e, false);
        }
    }

    // 포트 설정은 initUdp()에서 처리됨
    Logger.print("WHATAP-035", "Port configuration will be handled during UDP initialization", false);

    try {
        // 바이너리 경로 설정
        const platform = process.platform;
        const architecture = ARCH[process.arch] || process.arch;

        const binaryName = platform === 'win32' ? AGENT_NAME + '.exe' : AGENT_NAME;
        const agentPath = path.join(whatapHome, binaryName);

        // Windows는 architecture 하위 디렉토리 없이 단일 바이너리 사용
        const sourcePath = platform === 'win32'
            ? path.join(__dirname, '..', '..', 'agent', 'windows', binaryName)
            : path.join(__dirname, '..', '..', 'agent', platform, architecture, AGENT_NAME);

        // sourcePath 존재 여부 확인
        if (!fs.existsSync(sourcePath)) {
            throw new Error(`Agent binary not found at ${sourcePath}. Platform: ${platform}, Architecture: ${architecture}`);
        }

        // 심볼릭 링크 또는 복사
        if (!fs.existsSync(agentPath)) {
            try {
                fs.symlinkSync(sourcePath, agentPath);
                Logger.print("WHATAP-036", `Created symbolic link for ${AGENT_NAME}`, false);
            } catch (e) {
                if (e.code !== 'EEXIST') {
                    // Symlink 실패 시 모든 플랫폼에서 복사 시도 (컨테이너 환경 대응)
                    Logger.print("WHATAP-037", `Symlink failed (${e.code}), copying binary instead`, false);
                    try {
                        fs.copyFileSync(sourcePath, agentPath);
                        // 실행 권한 부여 (Windows는 불필요)
                        if (platform !== 'win32') {
                            fs.chmodSync(agentPath, 0o755);
                        }
                        Logger.print("WHATAP-221", `Binary copied and made executable at ${agentPath}`, false);
                    } catch (copyErr) {
                        throw new Error(`Failed to copy agent binary: ${copyErr.message}`);
                    }
                }
            }
        }

        // agentPath 실행 권한 확인 (Windows는 스킵)
        if (platform !== 'win32') {
            try {
                fs.accessSync(agentPath, fs.constants.X_OK);
            } catch (e) {
                Logger.print("WHATAP-222", `Agent binary not executable, adding execute permission`, false);
                fs.chmodSync(agentPath, 0o755);
            }
        }

        // run 디렉토리 생성
        const sockfilePath = path.join(whatapHome, 'run');
        if (!fs.existsSync(sockfilePath)) {
            fs.mkdirSync(sockfilePath, { recursive: true });
        }

        // 환경 변수 설정
        const newEnv = Object.assign({}, process.env);
        newEnv['WHATAP_VERSION'] = NodeUtil.getVersion();
        newEnv['whatap.start'] = Date.now().toString();
        newEnv['node.uptime'] = new Date().toString();
        newEnv['node.version'] = process.version;
        newEnv['node.tzname'] = new Date().toLocaleString('en', {timeZoneName: 'short'}).split(' ').pop();
        newEnv['os.release'] = os.release();
        newEnv['whatap.enabled'] = 'true';
        newEnv['WHATAP_PID_FILE'] = sharedPidFile; // 공유 PID 파일 사용
        newEnv['APP_IDENTIFIER'] = appIdentifier;
        newEnv['APP_NAME'] = this.getApplicationName();

        // PM2 정보 추가
        const isPM2 = process.env.pm_id !== undefined;

        if (isPM2) {
            newEnv['PM2_APP_NAME'] = process.env.name || '';
            newEnv['PM2_INSTANCES'] = process.env.instances || '1';

            // PM2 cluster 모드
            if (isPM2Cluster) {
                // cluster 모드에서는 첫 번째 인스턴스의 ID만 전달
                newEnv['PM2_CLUSTER_MODE'] = 'true';
                newEnv['PM2_STARTER_ID'] = process.env.pm_id || '0';
                Logger.print("WHATAP-038",
                    `PM2 cluster mode: Starting shared agent from instance ${process.env.pm_id}`,
                    false);
            } else {
                // 단일 인스턴스 모드
                newEnv['PM2_ID'] = process.env.pm_id || '';
                newEnv['NODEJS_PARENT_APP_PID'] = process.pid.toString();
            }
        } else {
            // PM2가 아닌 경우
            newEnv['NODEJS_PARENT_APP_PID'] = process.pid.toString();
        }

        Object.assign(newEnv, opts);

        // 에이전트 프로세스 시작
        // Windows: -t 2 foreground (detached 모드)
        // Unix/Linux: -t 2 -d 1 (daemon 모드)
        const spawnArgs = platform === 'win32'
            ? ['-t', '2', 'foreground']
            : ['-t', '2', '-d', '1'];

        const spawnOptions = {
            cwd: whatapHome,
            env: newEnv,
            stdio: ['pipe', 'pipe', 'pipe'],
            detached: platform === 'win32' // Windows에서는 detached 모드
        };

        // Windows에서는 DETACHED_PROCESS 플래그 추가
        if (platform === 'win32') {
            spawnOptions.detached = true;
            // Windows 전용: 새로운 콘솔 윈도우 생성 방지
            spawnOptions.windowsHide = true;
        }

        const agentProcess = child_process.spawn(agentPath, spawnArgs, spawnOptions);

        const MAX_BUFFER_SIZE = 50000; // 50KB - prevent memory leak
        let stdout = '';
        let stderr = '';

        agentProcess.stdout.on('data', (data) => {
            stdout += data.toString();
            if (stdout.length > MAX_BUFFER_SIZE) {
                stdout = ''; // Clear buffer when limit exceeded
            }
        });

        agentProcess.stderr.on('data', (data) => {
            stderr += data.toString();
            if (stderr.length > MAX_BUFFER_SIZE) {
                stderr = ''; // Clear buffer when limit exceeded
            }
        });

        // PID 추적 (detached: false이므로 agentProcess.pid가 실제 PID)
        agentProcess.on('spawn', () => {
            const actualPid = agentProcess.pid;
            Logger.print("WHATAP-039", `Agent process spawned with PID: ${actualPid}`, false);

            // PID 파일에 기록
            try {
                fs.writeFileSync(sharedPidFile, actualPid.toString());
                this.writeToFile(home, pidFileName, actualPid.toString());
                Logger.print("WHATAP-040", `PID ${actualPid} written to PID files`, false);

                // Lock 파일 해제 (spawn 완료 후 안전하게 삭제)
                try {
                    properLock.unlockSync(sharedLockFile);
                    fs.unlinkSync(sharedLockFile);
                    Logger.print("WHATAP-041", "Lock file released after spawn", false);
                } catch (e) {}

                // AGENT UP 메시지 (spawn 성공 후)
                const agentMsg = isPM2Cluster
                    ? `AGENT UP! (process name: ${AGENT_NAME}, app: ${appIdentifier}, PM2 cluster - shared by ${process.env.instances} instances)`
                    : `AGENT UP! (process name: ${AGENT_NAME}, app: ${appIdentifier})`;
                Logger.print("WHATAP-047", agentMsg, false);
            } catch (e) {
                Logger.printError("WHATAP-042", "Error writing PID to files", e, false);
            }
        });

        // spawn 에러 핸들러 (파일 없음, 권한 없음 등)
        agentProcess.on('error', (err) => {
            Logger.printError("WHATAP-216", `Failed to spawn agent process: ${err.message}`, err, false);

            // 파일 정리
            try { fs.unlinkSync(sharedLockFile); } catch (e) {}
            try { fs.unlinkSync(sharedPidFile); } catch (e) {}

            // PM2 cluster 모드에서는 instance 0만 재시작 시도
            if (isPM2Cluster && process.env.pm_id !== '0') {
                Logger.print("WHATAP-217",
                    `Spawn failed but instance ${process.env.pm_id} will not restart (only instance 0)`,
                    false);
                return;
            }

            // 자동 재시작 시도
            Logger.print("WHATAP-218", "Attempting to restart agent after spawn failure...", false);
            setTimeout(() => {
                self.restartGoAgent(opts);
            }, 5000); // 5초 후 재시작 (spawn 실패는 더 긴 대기)
        });

        agentProcess.on('close', (code) => {
            if (code !== 0) {
                Logger.printError("WHATAP-043", `Agent process exited with code ${code}`, null, false);
                Logger.print("WHATAP-044", `STDOUT: ${stdout}`, false);
                Logger.print("WHATAP-045", `STDERR: ${stderr}`, false);
                // 에이전트가 비정상 종료되면 파일 제거
                try { fs.unlinkSync(sharedLockFile); } catch (e) {}
                try { fs.unlinkSync(sharedPidFile); } catch (e) {}

                // PM2 cluster 모드에서는 instance 0만 재시작 시도
                if (isPM2Cluster && process.env.pm_id !== '0') {
                    Logger.print("WHATAP-219",
                        `Agent crashed but instance ${process.env.pm_id} will not restart (only instance 0)`,
                        false);
                    return;
                }

                // 자동 재시작 시도
                Logger.print("WHATAP-220", "Attempting to restart agent after abnormal exit...", false);
                setTimeout(() => {
                    self.restartGoAgent(opts);
                }, 2000); // 2초 후 재시작
            } else {
                Logger.print("WHATAP-046", "Agent started successfully", false);
            }
        });

        // detached: false이므로 unref() 불필요 (부모 프로세스와 함께 종료됨)
        // "AGENT UP!" 메시지는 spawn 이벤트 핸들러에서 출력됨 (line 1118-1122)

    } catch (e) {
        Logger.printError("WHATAP-048", "Error starting agent", e, false);
        // 에러 발생 시 파일 정리
        try {
            properLock.unlockSync(sharedLockFile);
            fs.unlinkSync(sharedLockFile);
        } catch (unlockErr) {}
        try { fs.unlinkSync(sharedPidFile); } catch (pidErr) {}
    }
};

/**
 * Start a separate Go Agent for LLM monitoring (with --llm flag)
 */
NodeAgent.prototype.startLlmGoAgent = function () {
    var self = this;
    var whatapHome = process.env.WHATAP_HOME;
    if (!whatapHome) return;

    try {
        var platform = process.platform;
        var agentBinaryName = platform === 'win32' ? AGENT_NAME + '.exe' : AGENT_NAME;
        var agentPath = path.join(whatapHome, agentBinaryName);

        if (!fs.existsSync(agentPath)) {
            Logger.print("WHATAP-LLM-020", "Agent binary not found for LLM agent: " + agentPath, false);
            return;
        }

        // LLM UDP port
        var basePort = parseInt(self._conf.getProperty('net_udp_port', 6600)) || 6600;
        var llmPort = parseInt(self._conf.getProperty('llm_net_udp_port', 0)) || (basePort + 100);
        self._conf['llm_net_udp_port'] = llmPort;

        // Write to whatap.conf so Go Agent can read it
        try {
            var confFile = path.join(whatapHome, 'whatap.conf');
            if (fs.existsSync(confFile)) {
                var confContent = fs.readFileSync(confFile, 'utf8');
                if (confContent.indexOf('llm_net_udp_port') < 0) {
                    fs.appendFileSync(confFile, '\nllm_net_udp_port=' + llmPort + '\n');
                }
            }
        } catch (e) {
            Logger.printError("WHATAP-LLM-021", "Error writing llm_net_udp_port to whatap.conf", e, false);
        }

        // Check existing LLM agent
        var llmPidFile = path.join(whatapHome, AGENT_NAME + '.pid.llm');
        if (fs.existsSync(llmPidFile)) {
            try {
                var existingPid = parseInt(fs.readFileSync(llmPidFile, 'utf8').trim());
                if (existingPid && self.isGoAgentRunning(existingPid)) {
                    Logger.print("WHATAP-LLM-022", "LLM Go Agent already running (PID: " + existingPid + ")", false);
                    return;
                }
            } catch (e) {}
        }

        // Build environment
        var newEnv = Object.assign({}, process.env);
        newEnv['WHATAP_NET_UDP_PORT'] = String(llmPort);
        newEnv['WHATAP_PID_FILE'] = AGENT_NAME + '.pid.llm';
        newEnv['NODEJS_PARENT_APP_PID'] = process.pid.toString();
        newEnv['whatap.enabled'] = 'true';

        // Spawn: whatap_nodejs -t 2 -d 1 --llm  (normal agent와 동일 -t/-d, --llm만 추가)
        var spawnArgs = platform === 'win32'
            ? ['-t', '2', '--llm', 'foreground']
            : ['-t', '2', '-d', '1', '--llm'];

        var spawnOptions = {
            cwd: whatapHome,
            env: newEnv,
            stdio: ['pipe', 'pipe', 'pipe'],
            detached: true, //
        };

        if (platform === 'win32') {
            spawnOptions.windowsHide = true;
        }

        var llmProcess = child_process.spawn(agentPath, spawnArgs, spawnOptions);

        llmProcess.on('spawn', function () {
            Logger.print("WHATAP-LLM-023", "executed LLM golang module in background (PID: " + llmProcess.pid + ")", false);
            try {
                fs.writeFileSync(llmPidFile, String(llmProcess.pid));
            } catch (e) {}
            Logger.print("WHATAP-LLM-028", "WHATAP: AGENT UP! (process name: " + AGENT_NAME + " --llm)", false);
        });

        llmProcess.on('error', function (err) {
            Logger.printError("WHATAP-LLM-024", "Failed to spawn LLM Go Agent", err, false);
        });

        llmProcess.on('close', function (code) {
            Logger.print("WHATAP-LLM-025", "LLM Go Agent exited with code: " + code, false);
        });

        // Capture stderr for logging
        llmProcess.stderr.on('data', function (data) {
            var msg = data.toString().trim();
            if (msg) Logger.print("WHATAP-LLM-026", "[LLM Agent] " + msg, false);
        });

    } catch (e) {
        Logger.printError("WHATAP-LLM-027", "Error starting LLM Go Agent", e, false);
    }
};

// 프로세스 종료 시 정리 작업 개선
process.on('exit', () => {
    if (!process.env.WHATAP_HOME || !NodeAgent.prototype.getApplicationIdentifier || !NodeAgent.prototype.isPM2ClusterMode) {
        return;
    }

    try {
        const isPM2Cluster = NodeAgent.prototype.isPM2ClusterMode.call(NodeAgent);

        // PM2 cluster 모드에서는 인스턴스가 종료되어도 공유 에이전트를 종료하지 않음
        // 다른 인스턴스가 계속 사용 중일 수 있음
        if (isPM2Cluster) {
            Logger.print("WHATAP-049",
                `PM2 cluster instance ${process.env.pm_id} exiting - shared agent will remain running`,
                false);
            return;
        }

        // 일반 모드에서는 기존 로직 사용
        const appIdentifier = NodeAgent.prototype.getApplicationIdentifier.call(NodeAgent);
        const sharedPidFile = path.join(process.env.WHATAP_HOME, `agent-${appIdentifier}.pid`);

        if (fs.existsSync(sharedPidFile)) {
            const pid = fs.readFileSync(sharedPidFile, 'utf8').trim();
            // 현재 프로세스가 기록된 PID의 부모 프로세스인 경우에만 제거
            if (pid === process.env.NODEJS_PARENT_APP_PID) {
                fs.unlinkSync(sharedPidFile);
            }
        }
    } catch (e) {
        // 에러 무시
    }
});

// graceful shutdown 처리 추가
['SIGTERM', 'SIGINT'].forEach((signal) => {
    process.on(signal, () => {
        Logger.print("WHATAP-050", `Received ${signal}, cleaning up...`, false);
        process.exit(0);
    });
});

NodeAgent.prototype.init = function(cb) {
    var self = this;
    if (self._initialized) {
        return self;
    }
    self._initialized = true;
    self.starttime = Date.now();

    self.findRoot();

    // Set WHATAP_HOME if not exists - use application root directory
    if (!process.env.WHATAP_HOME) {
        process.env.WHATAP_HOME = process.cwd() || self._conf['app.root'];
        Logger.print('WHATAP-114', 'WHATAP_HOME not set, using application root: ' + process.env.WHATAP_HOME, false);
    }

    // Fallback: app.root가 없으면 WHATAP_HOME을 사용
    if (self._conf['app.root'] == null || self._conf['app.root'].length == 0) {
        if (process.env.WHATAP_HOME) {
            self._conf['app.root'] = process.env.WHATAP_HOME;
            Logger.print('WHATAP-115', 'app.root not found (no package.json), using WHATAP_HOME: ' + process.env.WHATAP_HOME, false);
        } else {
            self._conf['app.root'] = process.cwd();
            Logger.print('WHATAP-115', 'app.root not found (no package.json), using cwd: ' + process.cwd(), false);
        }
    }

    Logger.initializer.process();
    Logger.print('WHATAP-110', 'Start initialize WhaTap Agent... Root[' + self._conf['app.root'] + ']', true);

    // Initialize configuration with proper home directory
    if (!self.initConfig('WHATAP_HOME')) {
        Logger.printError("WHATAP-051", "Failed to initialize configuration", null, false);
        return self;
    }

    NodeUtil.getPackageJson();

    self._conf.init(this._userOpt, function(e) {
        if (e) {
            Logger.printError("WHATAP-112", "Configuration initialize error. " + e.message,
                e, false);
            return;
        }
        Logger.print('WHATAP-113', 'Finish initialize configuration... ' + Boolean(self._conf['reqlog_enabled']), false);
        if (Boolean(self._conf['reqlog_enabled']) == true) {
            Logger.print("WHATAP-052", "ReqLog Init Start !!!!", false);
            RequestLog.initializer.process();
        }

        // Cluster mode detection (sets pm_id for Node.js cluster)
        self.isClusterMode();

        // agent_per_instance_enabled 모드: 각 클러스터 인스턴스별 독립 실행
        // WHATAP_HOME, whatap.conf는 공통 사용, WHATAP_NAME만 cluster_id 추가
        const agentPerInstance = self._conf.getProperty('agent_per_instance_enabled', false);
        const clusterId = self.getClusterId();
        if (agentPerInstance === true && clusterId !== null) {
            // WHATAP_NAME 또는 WHATAP_ONAME에 cluster_id 추가 (ONAME 구분용)
            const originalName = process.env.WHATAP_NAME || process.env.WHATAP_ONAME;
            if (originalName) {
                const instanceName = `${originalName}-${clusterId}`;
                if (process.env.WHATAP_NAME) {
                    process.env.WHATAP_NAME = instanceName;
                }
                if (process.env.WHATAP_ONAME) {
                    process.env.WHATAP_ONAME = instanceName;
                }
                Logger.print('WHATAP-121', `agent_per_instance_enabled: Using instance-specific WHATAP_NAME: ${instanceName} (cluster_id: ${clusterId})`, false);
            }
        }

        if(!self._counterManager){
            self._counterManager = new CounterManager();
            self._counterManager.run();
        }

        // Set Oname,Oid
        self._securityMaster.decideAgentOnameOid();

        // Start Node.js agent with proper port configuration
        WhatapUtil.printWhatap();
        self.startGoAgent();

        // Start LLM Go Agent if llm_enabled
        if (self._conf.getProperty('llm_enabled', false)) {
            self.startLlmGoAgent();
        }

        TraceContextManager.initialized = true;

        self.initUdp();

        // Start health check to monitor agent process
        self.startHealthCheck();
    });

    self.loadObserves();
    return self;
};

NodeAgent.prototype.context = function(cb) {
    return TraceContextManager.getCurrentContext();
}

NodeAgent.prototype.loadObserves = function() {
    var agent = this;
    var observes = [];

    observes.push(HttpObserver);
    observes.push(MysqlObserver);
    observes.push(Mysql2Observer);
    observes.push(MariaObserver);
    observes.push(SocketioObserver);
    observes.push(WebsocketObserver);
    // observes.push(WsObserver);
    observes.push(MongoObserver);
    // observes.push(MongooseObserver);
    observes.push(RedisObserver);
    observes.push(IORedisObserver);
    observes.push(MssqlObserver);
    observes.push(PgSqlObserver);
    observes.push(GRpcObserver);
    observes.push(ApolloObserver);
    observes.push(PrismaObserver);
    observes.push(OracleObserver);
    observes.push(CronObserver);
    observes.push(KafkaObserver);
    observes.push(OpenAIObserver);

    var packageToObserve = {};
    observes.forEach(function(observeObj) {
        var observe = new observeObj(agent);
        observe.packages.forEach(function(pkg){
            if(!packageToObserve[pkg]){
                packageToObserve[pkg] = [];
            }
            packageToObserve[pkg].push( observe );
        });
    });

    agent.aop.after(module.__proto__, 'require', function(obj, args, result) {
        var observes = packageToObserve[args[0]];

        if(observes) {
            observes.forEach(function(observe){
                return observe.inject(result, args[0]);
            });
        }
    });

    for (var name in packageToObserve) {
        try {
            require(name);
        } catch (err) {
            Logger.print("WHATAP-053", 'unable to load ' + name + ' module', false);
        }
    }
    new ProcessObserver(agent).inject(process, 'process');
    new GlobalObserver(agent).inject(global, 'global');
    new CustomMethodObserver(agent).inject(null, 'custom-method');
};

NodeAgent.prototype.setServicePort = function (port) {
    Configuration['whatap.port'] = port || 0;
}

NodeAgent.prototype.initUdp = function() {
    var self = this;
    Logger.print('WHATAP-054', 'Initializing UDP connection...', false);

    if (self.isAgentPerInstanceMode()) {
        // agent_per_instance_enabled 모드: whatap_port_{pid} 파일에서 포트 읽기
        const portFromFile = self.readPortFromPidFile();
        if (portFromFile) {
            self._conf['net_udp_port'] = portFromFile;
            Logger.print('WHATAP-055', `Using port from whatap_port_${process.pid}: ${portFromFile}`, false);
        }
    } else {
        // 기존 로직: lock 파일 기반 포트 설정
        const configuredPort = self.configurePort();
        if (configuredPort) {
            self._conf['net_udp_port'] = configuredPort;
            Logger.print('WHATAP-055', `Using configured port: ${configuredPort}`, false);
        }
    }

    // Initialize the UDP session with updated config
    UdpSession.udp(self._conf);

    // Initialize the async sender
    AsyncSender.startWhatapThread();

    Logger.print('WHATAP-056', 'UDP connection initialized', false);
};

/**
 * Read UDP port from whatap_port_{pid} file
 * Used in agent_per_instance_enabled mode
 * @returns {number|null} - Port number or null if not found
 */
NodeAgent.prototype.readPortFromPidFile = function() {
    const whatapHome = process.env.WHATAP_HOME;
    if (!whatapHome) {
        return null;
    }

    const portFilePath = path.join(whatapHome, `whatap_port_${process.pid}`);
    const maxRetries = 10;
    const retryInterval = 1000; // 1초

    for (let i = 0; i < maxRetries; i++) {
        try {
            if (fs.existsSync(portFilePath)) {
                const content = fs.readFileSync(portFilePath, 'utf8').trim();
                const match = content.match(/net_udp_port=(\d+)/);
                if (match) {
                    return parseInt(match[1]);
                }
            }
        } catch (e) {
            Logger.printError('WHATAP-125', `Error reading port file: ${portFilePath}`, e, false);
        }

        // whatap_nodejs가 파일을 생성할 때까지 대기 (동기 대기)
        if (i < maxRetries - 1) {
            Logger.print('WHATAP-126', `Waiting for port file: ${portFilePath} (attempt ${i + 1}/${maxRetries})`, false);
            var waitUntil = Date.now() + retryInterval;
            while (Date.now() < waitUntil) {
                // busy wait
            }
        }
    }

    Logger.printError('WHATAP-127', `Port file not found after ${maxRetries} retries: ${portFilePath}`, null, false);
    return null;
};

/**
 * Initialize configuration with default values
 * @param {string} home - Environment variable name for home directory
 * @returns {boolean} - True if successful, false otherwise
 */
NodeAgent.prototype.initConfig = function(home) {
    let whatapHome = process.env[home];
    if (!whatapHome) {
        whatapHome = this.findWhatapConf();
    }
    if (!whatapHome) {
        whatapHome = process.cwd();
        process.env[home] = whatapHome;
        Logger.print("WHATAP-058", "WHATAP_HOME is empty", true);
    }

    process.env[home] = whatapHome;

    // Use WHATAP_CONF environment variable or default to 'whatap.conf'
    const configFileName = process.env.WHATAP_CONF || 'whatap.conf';
    const configFile = path.join(process.env[home], configFileName);

    if (!fs.existsSync(configFile)) {
        try {
            // Copy default config from module directory
            const defaultConfigPath = path.join(path.dirname(__dirname), 'whatap.conf');
            if (fs.existsSync(defaultConfigPath)) {
                const content = fs.readFileSync(defaultConfigPath, 'utf8');
                fs.writeFileSync(configFile, content);
            } else {
                // Create empty config file
                fs.writeFileSync(configFile, '# WhaTap Node.js Agent Configuration\n');
            }
        } catch (e) {
            Logger.printError("WHATAP-059", "Permission error creating config file. Try to execute command: `sudo chmod -R 777 $WHATAP_HOME`", e, false);
            return false;
        }
    }

    return true;
};

exports.NodeAgent = new NodeAgent();