/**
 * 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'),
    conf = require('../conf/configure'),
    IPUtil = require('../util/iputil'),
    Logger = require('../logger');
const {Detector: URLPatternDetector} = require("../trace/serviceurl-pattern-detector");
const shimmer = require('../core/shimmer');
const AsyncSender = require('../udp/async_sender');
const PacketTypeEnum = require('../udp/packet_type_enum');
const os = require('os');

// 설정을 객체로 통합하여 관리 (참조 성능 향상)
var config = {
    trace_background_socket_enabled: conf.getProperty('trace_background_socket_enabled', true),
    trace_sampling_enabled: conf.getProperty('trace_sampling_enabled', true),
    trace_sampling_tps: conf.getProperty('trace_sampling_tps', 1000),
    resource_sampling_rate: 0.1 // 리소스 프로파일링을 10%만 수행
};

// 설정 변경 감지를 단일 리스너로 통합
conf.on('trace_background_socket_enabled', function (newProps) {
    config.trace_background_socket_enabled = newProps;
});

conf.on('trace_sampling_enabled', function (newProps) {
    config.trace_sampling_enabled = newProps;
});

conf.on('trace_sampling_tps', function (newProps) {
    config.trace_sampling_tps = newProps;
});

var WebsocketObserver = function(agent){
    this.agent = agent;
    this.packages = ['websocket'];

    this.socketCounter = {
        count: 0,
        start_time: Date.now(),
        window_size: 5000,

        checkSampling: function() {
            const now = Date.now();
            if ((now - this.start_time) >= this.window_size) {
                this.start_time = now;
                this.count = 0;
                return true;
            }

            this.count++;
            return this.count <= config.trace_sampling_tps;
        }
    };

    // IP 주소 캐싱 (성능 최적화)
    this.ipCache = new Map();
};

// IP 주소 처리 최적화 함수
WebsocketObserver.prototype.getProcessedIp = function(address) {
    if (!address) return null;

    if (this.ipCache.has(address)) {
        return this.ipCache.get(address);
    }

    let host = address;
    if (address.includes(':')) {
        host = address.substring(address.lastIndexOf(':') + 1);
    }

    host = IPUtil.checkIp4(host);
    const ipInt = IPUtil.stringToInt(host);
    const ipBytes = Buffer.from(IPUtil.stringToBytes(host));

    const result = { host, ipInt, ipBytes };
    this.ipCache.set(address, result);

    // 캐시 크기 관리 (메모리 누수 방지)
    if (this.ipCache.size > 10000) {
        // 오래된 항목부터 20% 제거
        const keysToDelete = Array.from(this.ipCache.keys()).slice(0, Math.floor(this.ipCache.size * 0.2));
        keysToDelete.forEach(key => this.ipCache.delete(key));
    }

    return result;
};

// 에러 처리 함수
function handleWebSocketError(ctx, err) {
    if (!err) return null;

    try {
        var errorClass = err.code || err.name || err.constructor?.name || 'WebSocketError';
        var errorMessage = err.message || 'websocket 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.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);
            return errorClass; // 에러 정보 반환
        }
    } catch (e) {
        Logger.printError('WHATAP-218', 'Error handling WebSocket error', e, false);
    }
    return null;
}

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

    var self = this;

    // 1. WebSocketServer 래핑 - request 이벤트 후킹
    if (mod.server && mod.server.prototype) {
        const wrapWebSocketServer = function(target) {
            if (!target || typeof target.on !== 'function') {
                return false;
            }

            shimmer.wrap(target, 'on', function (original) {
                return function (event, listener) {
                    if (event === 'request') {
                        return original.call(this, event, function (request) {
                            // WebSocketConnection의 메시지 전송 메서드들 래핑
                            request.on('requestAccepted', function(connection) {
                                self.__wrapWebSocketConnection(connection, request);
                            });
                            return listener.apply(this, [request]);
                        });
                    }
                    return original.call(this, event, listener);
                };
            });

            return true;
        };

        wrapWebSocketServer(mod.server.prototype);
    }

    // 2. WebSocketConnection 직접 래핑 (구버전 호환성)
    if (mod.connection && mod.connection.prototype) {
        self.__wrapWebSocketConnection(mod.connection.prototype, null);
    }
};

WebsocketObserver.prototype.__wrapWebSocketConnection = function(connection, request) {
    var self = this;

    if (!connection) return;

    // send, sendUTF, sendBytes 메서드들 래핑
    const methods = ['send', 'sendUTF', 'sendBytes'];

    methods.forEach(function(methodName) {
        if (typeof connection[methodName] === 'function' && !connection[methodName].__whatap_wrapped__) {
            const originalMethod = connection[methodName];

            connection[methodName] = function(data) {
                if (!config.trace_background_socket_enabled) {
                    return originalMethod.apply(this, arguments);
                }

                // 빠른 샘플링 체크 (조기 반환으로 성능 향상)
                if (config.trace_sampling_enabled && !self.socketCounter.checkSampling()) {
                    return originalMethod.apply(this, arguments);
                }

                // 브로드캐스트 감지 시도
                const emitType = self.__detectBroadcastType();

                const remoteAddress = this.remoteAddress ||
                    (this.socket ? this.socket.remoteAddress : null) ||
                    (request && request.remoteAddress ? request.remoteAddress : null);

                TraceContextManager._asyncLocalStorage.run(self.__initCtx(this, data, emitType, {
                    remoteAddress: remoteAddress,
                    protocol: this.protocol,
                    request: request
                }), () => {
                    var ctx = TraceContextManager._asyncLocalStorage.getStore();
                    if (!ctx) {
                        return originalMethod.apply(this, arguments);
                    }

                    try {
                        ctx.service_name = "/websocket";
                        let hostname = '';

                        let datas = [
                            hostname,
                            ctx.service_name,
                            ctx.remoteIp || 0,
                            ctx.userAgentString || '',
                            ctx.referer || '',
                            String(ctx.userid || ""),
                            String(false), // isStaticContents
                            emitType
                        ];

                        AsyncSender.send_packet(PacketTypeEnum.TX_START, ctx, datas);

                        self.__sendWebSocketMessage(ctx, emitType, data, methodName);
                        self.__sendWebSocketEvent(ctx, this, data, emitType, remoteAddress);
                        self.__endTransaction(null, ctx);

                        return originalMethod.apply(this, arguments);
                    } catch (e) {
                        Logger.printError('WHATAP-219', 'websocket send transaction error', e, false);
                        var errorInfo = handleWebSocketError(ctx, e);
                        self.__endTransaction(errorInfo, ctx);
                        return originalMethod.apply(this, arguments);
                    }
                });
            };

            connection[methodName].__whatap_wrapped__ = true;

            // 속성 복사
            Object.defineProperties(connection[methodName], {
                length: { value: originalMethod.length },
                name: { value: originalMethod.name }
            });
        }
    });
};

WebsocketObserver.prototype.__detectBroadcastType = function() {
    try {
        // 스택 트레이스를 통해 브로드캐스트 패턴 감지
        const stack = (new Error()).stack;
        if (stack) {
            // connections.forEach, broadcast 패턴 감지
            if (stack.includes('connections.forEach') ||
                stack.includes('connections.map') ||
                stack.includes('broadcast') ||
                stack.includes('sendToAll')) {
                return 'broadcast';
            }
        }
        return 'websocket';
    } catch (e) {
        return 'websocket';
    }
};

WebsocketObserver.prototype.__sendWebSocketMessage = function(ctx, emitType, data, methodName) {
    try {
        var title, message;

        switch (emitType) {
            case 'broadcast':
                title = "Broadcast";
                message = `Broadcast to all connections using ${methodName}`;
                break;
            default:
                title = "WebSocket";
                message = `Direct WebSocket message using ${methodName}`;
        }

        AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, [title, '', message]);
    } catch (e) {
        Logger.printError('WHATAP-220', 'WebSocket send message error', e, false);
        throw e;
    }
};

WebsocketObserver.prototype.__sendWebSocketEvent = function(ctx, connection, data, emitType, remoteAddress) {
    try {
        var start_time = Date.now();
        ctx.start_time = start_time;
        ctx.elapsed = 0;
        ctx.error = 0;

        // IP 주소 및 포트 처리
        var ipString = "0.0.0.0";
        var port = 0;

        if (remoteAddress) {
            const ipInfo = this.getProcessedIp(remoteAddress);
            if (ipInfo) {
                ipString = ipInfo.host;
                ctx.remoteIp = ipInfo.ipInt;

                // 포트 정보 추출
                if (connection.socket && connection.socket.remotePort) {
                    port = Number(connection.socket.remotePort);
                } else if (remoteAddress.includes(':')) {
                    const parts = remoteAddress.split(':');
                    if (parts.length > 1) {
                        const portStr = parts[parts.length - 1];
                        const parsedPort = parseInt(portStr, 10);
                        if (!isNaN(parsedPort)) {
                            port = parsedPort;
                        }
                    }
                }
            }
        }

        var elapsed = Date.now() - start_time;
        ctx.elapsed = elapsed;

        let datas = [ipString, port, elapsed, 0];
        AsyncSender.send_packet(PacketTypeEnum.TX_WEB_SOCKET, ctx, datas);
    } catch (e) {
        Logger.printError('WHATAP-221', 'WebSocket send event error', e, false);
        throw e;
    }
};

WebsocketObserver.prototype.__endTransaction = function(error, ctx) {
    try {
        if (error) {
            TraceContextManager.end(ctx != null ? ctx.id : null);
            ctx = null;
            return;
        }

        if (ctx == null || TraceContextManager.isExist(ctx.id) === false)
            return;

        ctx.start_time = Date.now();
        let datas = [
            os.hostname(), // hostname
            ctx.service_name,
            0,  // mtid
            0,  // mdepth
            0,  // mcaller_txid
            0,  // mcaller_pcode
            '', // mcaller_spec
            String(0), // mcaller_url_hash
            200
        ];
        ctx.elapsed = Date.now() - ctx.start_time;
        AsyncSender.send_packet(PacketTypeEnum.TX_END, ctx, datas);
        TraceContextManager.end(ctx.id);
        ctx = null;
    } catch (e) {
        Logger.printError('WHATAP-222', 'WebSocket end transaction error', e, false);
        TraceContextManager.end(ctx.id);
        ctx = null;
    }
};

WebsocketObserver.prototype.__initCtx = function(connection, data, emitType, contextInfo) {
    const ctx = TraceContextManager.start();
    if (!ctx) return null;

    ctx.service_name = "/websocket";

    var remote_addr;
    var remoteAddress = contextInfo.remoteAddress;

    if (remoteAddress) {
        if(remoteAddress && remoteAddress.includes(':')){
            remote_addr = remoteAddress.substring(remoteAddress.lastIndexOf(':')+1);
        }
    }

    if(remote_addr){
        ctx.remoteIp = IPUtil.checkIp4(remote_addr);
        if(conf.getProperty('trace_user_enabled', true)){
            // WClientId를 빈 문자열로 보내 go-agent가 int64(RemoteIp) 분기를 타도록 함.
            ctx.userid = "";
        }
    }

    // 기본값 설정
    ctx.userAgentString = '';
    ctx.referer = '';
    ctx.status = 200;

    return ctx;
};

exports.WebsocketObserver = WebsocketObserver;