/**
 * 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 SocketIOObserver = function(agent){
    this.agent = agent;
    this.packages = ['socket.io'];

    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 주소 처리 최적화 함수
SocketIOObserver.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 handleSocketError(ctx, err) {
    if (!err) return null;

    try {
        var errorClass = err.code || err.name || err.constructor?.name || 'SocketIOError';
        var errorMessage = err.message || 'socket.io 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-227', 'Error handling Socket.IO error', e, false);
    }
    return null;
}

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

    var self = this;

    const wrapSocketIOEvents = function(target) {
        if (!target || typeof target.on !== 'function') {
            return false;
        }

        shimmer.wrap(target, 'on', function (original) {
            return function (event, listener) {
                if (event === 'connection') {
                    return original.call(this, event, function (socket) {
                        // 1. 개별 소켓 emit 래핑
                        const wrappedEmit = function (origEmit) {
                            const emitFn = function (emitEvent, ...args) {
                                if (!config.trace_background_socket_enabled) {
                                    return origEmit.apply(this, [emitEvent, ...args]);
                                }

                                self.__handleSocketEmitEvent(socket, emitEvent, args, 'socket', {
                                    socketId: socket.id,
                                    remoteAddress: socket.conn ? socket.conn.remoteAddress : null
                                });
                                return origEmit.apply(this, [emitEvent, ...args]);
                            };

                            Object.defineProperties(emitFn, {
                                length: { value: origEmit.length },
                                name: { value: origEmit.name }
                            });

                            return emitFn;
                        };

                        if (typeof socket.emit === 'function' && !socket.emit.__whatap_wrapped__) {
                            const originalEmit = socket.emit;
                            socket.emit = wrappedEmit(originalEmit);
                            socket.emit.__whatap_wrapped__ = true;
                        }

                        // 2. socket.broadcast.emit 래핑
                        if (socket.broadcast && typeof socket.broadcast.emit === 'function' && !socket.broadcast.emit.__whatap_wrapped__) {
                            const originalBroadcastEmit = socket.broadcast.emit;
                            socket.broadcast.emit = function(emitEvent, ...args) {
                                if (!config.trace_background_socket_enabled) {
                                    return originalBroadcastEmit.apply(this, [emitEvent, ...args]);
                                }

                                self.__handleSocketEmitEvent(socket, emitEvent, args, 'broadcast', {
                                    socketId: socket.id,
                                    broadcastType: 'all_except_sender',
                                    remoteAddress: socket.conn ? socket.conn.remoteAddress : null
                                });
                                return originalBroadcastEmit.apply(this, [emitEvent, ...args]);
                            };
                            socket.broadcast.emit.__whatap_wrapped__ = true;
                        }

                        // 3. socket.to() 래핑 (room broadcast)
                        if (typeof socket.to === "function" && !socket.to.__whatap_wrapped__) {
                            const originalTo = socket.to;
                            socket.to = function(room) {
                                const roomObj = originalTo.call(this, room);

                                if (roomObj && typeof roomObj.emit === 'function' && !roomObj.emit.__whatap_wrapped__) {
                                    const originalRoomEmit = roomObj.emit;
                                    roomObj.emit = function(emitEvent, ...args) {
                                        if (!config.trace_background_socket_enabled) {
                                            return originalRoomEmit.apply(this, [emitEvent, ...args]);
                                        }

                                        self.__handleSocketEmitEvent(socket, emitEvent, args, 'room_broadcast', {
                                            socketId: socket.id,
                                            broadcastType: 'room',
                                            room: room,
                                            remoteAddress: socket.conn ? socket.conn.remoteAddress : null
                                        });
                                        return originalRoomEmit.apply(this, [emitEvent, ...args]);
                                    };
                                    roomObj.emit.__whatap_wrapped__ = true;
                                }

                                return roomObj;
                            };
                            socket.to.__whatap_wrapped__ = true;
                        }

                        // 4. socket.in() 래핑 (room broadcast - 다른 방식)
                        if (typeof socket.in === "function" && !socket.in.__whatap_wrapped__) {
                            const originalIn = socket.in;
                            socket.in = function(room) {
                                const roomObj = originalIn.call(this, room);

                                if (roomObj && typeof roomObj.emit === 'function' && !roomObj.emit.__whatap_wrapped__) {
                                    const originalRoomEmit = roomObj.emit;
                                    roomObj.emit = function(emitEvent, ...args) {
                                        if (!config.trace_background_socket_enabled) {
                                            return originalRoomEmit.apply(this, [emitEvent, ...args]);
                                        }

                                        self.__handleSocketEmitEvent(socket, emitEvent, args, 'room_broadcast', {
                                            socketId: socket.id,
                                            broadcastType: 'room',
                                            room: room,
                                            remoteAddress: socket.conn ? socket.conn.remoteAddress : null
                                        });
                                        return originalRoomEmit.apply(this, [emitEvent, ...args]);
                                    };
                                    roomObj.emit.__whatap_wrapped__ = true;
                                }

                                return roomObj;
                            };
                            socket.in.__whatap_wrapped__ = true;
                        }

                        return listener.apply(this, [socket]);
                    });
                }

                return original.call(this, event, listener);
            };
        });

        // 5. io.emit 래핑 (전체 브로드캐스트)
        if (target.emit && typeof target.emit === 'function' && !target.emit.__whatap_wrapped__) {
            const originalIoEmit = target.emit;
            target.emit = function(emitEvent, ...args) {
                if (!config.trace_background_socket_enabled) {
                    return originalIoEmit.apply(this, [emitEvent, ...args]);
                }

                self.__handleSocketEmitEvent(null, emitEvent, args, 'global_broadcast', {
                    broadcastType: 'global',
                    connectedSockets: this.sockets ? (this.sockets.sockets ? this.sockets.sockets.size : 0) : 0
                });
                return originalIoEmit.apply(this, [emitEvent, ...args]);
            };
            target.emit.__whatap_wrapped__ = true;
        }

        // 6. io.to() 래핑 (서버 레벨 룸 브로드캐스트)
        if (typeof target.to === "function" && !target.to.__whatap_wrapped__) {
            const originalIoTo = target.to;
            target.to = function(room) {
                const roomObj = originalIoTo.call(this, room);
                if (roomObj && typeof roomObj.emit === 'function') {
                    const originalRoomEmit = roomObj.emit;
                    roomObj.emit = function(emitEvent, ...args) {
                        if (!config.trace_background_socket_enabled) {
                            return originalRoomEmit.apply(this, [emitEvent, ...args]);
                        }

                        // 해당 룸의 소켓 연결 개수 계산
                        let roomSocketCount = 0;
                        if (this.adapter && this.adapter.rooms) {
                            const roomSockets = this.adapter.rooms.get(room);
                            roomSocketCount = roomSockets ? roomSockets.size : 0;
                        }

                        self.__handleSocketEmitEvent(null, emitEvent, args, 'server_room_broadcast', {
                            broadcastType: 'server_room',
                            room: room,
                            connectedSockets: roomSocketCount
                        });
                        return originalRoomEmit.apply(this, [emitEvent, ...args]);
                    };
                    roomObj.emit.__whatap_wrapped__ = true;
                }

                return roomObj;
            };
            target.to.__whatap_wrapped__ = true;
        }

        // 7. io.in() 래핑 (서버 레벨 룸 브로드캐스트 - 다른 방식)
        if (typeof target.in === "function" && !target.in.__whatap_wrapped__) {
            const originalIoIn = target.in;
            target.in = function(room) {
                const roomObj = originalIoIn.call(this, room);

                if (roomObj && typeof roomObj.emit === 'function' && !roomObj.emit.__whatap_wrapped__) {
                    const originalRoomEmit = roomObj.emit;
                    roomObj.emit = function(emitEvent, ...args) {
                        if (!config.trace_background_socket_enabled) {
                            return originalRoomEmit.apply(this, [emitEvent, ...args]);
                        }

                        self.__handleSocketEmitEvent(null, emitEvent, args, 'server_room_broadcast', {
                            broadcastType: 'server_room',
                            room: room,
                            connectedSockets: this.sockets ? (this.sockets.sockets ? this.sockets.sockets.size : 0) : 0
                        });
                        return originalRoomEmit.apply(this, [emitEvent, ...args]);
                    };
                    roomObj.emit.__whatap_wrapped__ = true;
                }

                return roomObj;
            };
            target.in.__whatap_wrapped__ = true;
        }

        return true;
    };

    // 1. Socket.IO v2.x 스타일 (mod.prototype)
    if (mod.prototype) {
        wrapSocketIOEvents(mod.prototype);
    }

    // 2. Socket.IO v4.x 스타일 (mod.Server.prototype)
    if (mod.Server && mod.Server.prototype) {
        wrapSocketIOEvents(mod.Server.prototype);
    }
};

SocketIOObserver.prototype.__handleSocketEmitEvent = function(socket, event, args, emitType, contextInfo) {
    if (config.trace_sampling_enabled && !this.socketCounter.checkSampling()) {
        return;
    }

    TraceContextManager._asyncLocalStorage.run(this.__initCtx(socket, args, emitType, contextInfo), () => {
        var ctx = TraceContextManager._asyncLocalStorage.getStore();
        if (!ctx) {
            return;
        }

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

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

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

            this.__sendSocketMessage(ctx, emitType, contextInfo);
            this.__sendSocketEvent(ctx, socket, event, args, emitType, contextInfo);
            this.__endTransaction(null, ctx);
        } catch (e) {
            Logger.printError('WHATAP-228', 'socket.io emit transaction error', e, false);

            var errorInfo = handleSocketError(ctx, e);
            this.__endTransaction(errorInfo, ctx);
        }
    });
};

SocketIOObserver.prototype.__sendSocketMessage = function(ctx, emitType, contextInfo) {
    try {
        var title, message;

        switch (emitType) {
            case 'broadcast':
                title = "Broadcast";
                message = "Broadcast to all clients except sender";
                break;
            case 'room_broadcast':
                title = "Room Broadcast";
                message = `Broadcast to room: ${contextInfo.room || 'unknown'}`;
                break;
            case 'global_broadcast':
                title = "Global Broadcast";
                message = `Total clients: ${contextInfo.connectedSockets || 0}`;
                break;
            case 'server_room_broadcast':
                title = "Server Room Broadcast";
                message = `Room: ${contextInfo.room || 'unknown'}, Total clients: ${contextInfo.connectedSockets || 0}`;
                break;
            default:
                title = "Socket";
                message = "Direct socket message";
        }

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

SocketIOObserver.prototype.__sendSocketEvent = function(ctx, socket, event, args, emitType, contextInfo) {
    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;
        var remoteAddress = contextInfo.remoteAddress || (socket && socket.conn && socket.conn.remoteAddress);

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

                if (socket && socket.conn && socket.conn.remotePort) {
                    port = Number(socket.conn.remotePort);
                } else if (remoteAddress.includes(':')) {
                    // remoteAddress에서 포트 추출 시도
                    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;

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

SocketIOObserver.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-231', 'Socket.io end transaction error', e, false);
        TraceContextManager.end(ctx.id);
        ctx = null;
    }
};

SocketIOObserver.prototype.__initCtx = function(socket, args, emitType, contextInfo) {
    const ctx = TraceContextManager.start();
    if (!ctx) return null;

    ctx.service_name = "/socket.io";

    var remote_addr;
    var remoteAddress = contextInfo.remoteAddress || (socket && socket.conn && socket.conn.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.SocketIOObserver = SocketIOObserver;