/**
 * 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'),
    URLPatternDetector = require('../trace/serviceurl-pattern-detector').Detector,
    conf = require('../conf/configure'),
    HashUtil = require('../util/hashutil'),
    Hexa32 = require('../util/hexa32'),
    KeyGen = require('../util/keygen'),
    TraceHelper = require('../util/trace-helper'),
    Logger = require('../logger'),
    AsyncSender = require('../udp/async_sender'),
    PacketTypeEnum = require('../udp/packet_type_enum'),
    TraceHttpc = require('../trace/trace-httpc');
const {Buffer} = require("buffer");
const shimmer = require('../core/shimmer');
const os = require('os');
const Transfer = require('../util/transfer');
const IPUtil = require('../util/iputil');
const UserIdUtil = require('../util/userid-util');
const LlmLogSinkPack = require('../llm/llm-log-sink-pack');
const LlmIdUtil = require('../llm/llm-id-util');
const llmStatCollector = require('../llm/llm-stat-collector');
const llmSender = require('../llm/llm-sender');

var _exts = new Set([".css", ".js", ".png", ".htm", ".html", ".gif", ".jpg", ".css", ".txt", ".ico"]);

var configIpHeaderKey = conf.getProperty('trace_http_client_ip_header_key', 'x-forwarded-for');
var configUserAgentKey = conf.getProperty('trace_user_agent_header_key', '');
var configRefererKey = conf.getProperty('trace_referer_header_key', '');
var ignore_http_method = conf.getProperty('ignore_http_method', 'PATCH,OPTIONS,HEAD,TRACE');
var transaction_status_error_enable = conf.getProperty('transaction_status_error_enable', true);
var status_ignore = conf.getProperty('status_ignore', '');
var httpc_status_ignore = conf.getProperty('httpc_status_ignore', '');
var status_ignore_set = conf.getProperty('status_ignore_set', '');
var httpc_status_ignore_set = conf.getProperty('httpc_status_ignore_set', '');
var profile_http_header_ignore_keys = conf.getProperty('profile_http_header_ignore_keys', 'Cookie,cookie,accept,user-agent,referer');
var profile_http_parameter_enabled = conf.getProperty('profile_http_parameter_enabled', true);
var profile_http_parameter_keys = conf.getProperty('profile_http_parameter_keys', '');
var ignore_build_file_enabled = conf.getProperty('ignore_build_file_enabled', true);
var ignore_build_file_path = conf.getProperty('ignore_build_file_path', '/_next/');

// Configuration event handlers
conf.on('trace_http_client_ip_header_key', function (newProperty) {
    configIpHeaderKey = newProperty;
});
conf.on('trace_normalize_urls', function (newProps) {
    URLPatternDetector.build(true);
});
conf.on('trace_auto_normalize_enabled', function (newProps) {
    URLPatternDetector.build(true);
});
conf.on('trace_normalize_enabled', function (newProps) {
    URLPatternDetector.build(true);
});
conf.on('trace_user_agent_header_key', function (newProps) {
    configUserAgentKey = newProps;
});
conf.on('trace_referer_header_key', function (newProps) {
    configRefererKey = newProps;
});
conf.on('ignore_http_method', function (newProps) {
    ignore_http_method = newProps;
})
conf.on('transaction_status_error_enable', function (newProps) {
    transaction_status_error_enable = newProps;
})
conf.on('status_ignore', function (newProps) {
    status_ignore = !newProps ? "" : String(newProps);
})
conf.on('httpc_status_ignore', function (newProps) {
    httpc_status_ignore = !newProps ? "" : String(newProps);
})
conf.on('status_ignore_set', function (newProps) {
    status_ignore_set = newProps;
})
conf.on('httpc_status_ignore_set', function (newProps) {
    httpc_status_ignore_set = newProps;
})
conf.on('profile_http_header_ignore_keys', function (newProps) {
    profile_http_header_ignore_keys = newProps;
})
conf.on('profile_http_parameter_enabled', function (newProps) {
    profile_http_parameter_enabled = newProps;
})
conf.on('profile_http_parameter_keys', function (newProps) {
    profile_http_parameter_keys = newProps;
})
conf.on('ignore_build_file_enabled', function (newProps) {
    ignore_build_file_enabled = newProps;
})
conf.on('ignore_build_file_path', function (newProps) {
    ignore_build_file_path = newProps;
})

var staticConents = function (newProps) {
    var x = new Set();
    var words = !newProps ? [] : newProps.split(',');
    for (var i = 0; i < words.length; i++) {
        var ex = words[i].trim();
        if (ex.length > 0) {
            if (ex.startsWith(".")) {
                x.add(ex);
            } else {
                x.add("." + ex);
            }
        }
    }
    _exts = x;
};
conf.on('web_static_content_extensions', staticConents);
staticConents(conf["web_static_content_extensions"]);

var HttpObserver = function (agent) {
    this.agent = agent;
    this.packages = ['http', 'https'];
    URLPatternDetector.build(true);
};

HttpObserver.prototype.__createTransactionObserver = function (callback, isHttps, server) {
    var self = this;
    var aop = this.agent.aop;

    return function (req, res) {
        TraceContextManager._asyncLocalStorage.run(initCtx(req, res), () => {
            var ctx = TraceContextManager._asyncLocalStorage.getStore();
            if (!ctx) {
                return callback(req, res);
            }

            ctx.service_name = req.url ? req.url : "";
            ctx.service_hash = HashUtil.hashFromString(ctx.service_name);
            ctx.isStaticContents = isStatic(ctx.service_name);

            if (ctx.isStaticContents !== true && conf._trace_ignore_url_set[ctx.service_hash]) {
                ctx.isStaticContents = true;
            }

            if (ctx.isStaticContents !== true && conf._is_trace_ignore_url_prefix === true && conf._trace_ignore_url_prefix_list.some(function(prefix) { return ctx.service_name.startsWith(prefix); })) {
                ctx.isStaticContents = true;
            }

            if(ctx.isStaticContents){
                TraceContextManager.end(ctx.id);
                return callback(req, res);
            }

            let hostname = ctx.host && ctx.host.includes(':') ? ctx.host.split(':')[0] : '';
            let datas = [
                hostname,
                ctx.service_name,
                ctx.remoteIp,
                ctx.userAgentString,
                ctx.referer,
                String(ctx.userid || ""),
                String(ctx.isStaticContents),
                req.method
            ];

            aop.after(res, 'end', function (obj, args) {
                if (ctx == null) {
                    return;
                }
                self.__endTransaction(null, ctx, req, res);
            });

            // 브라우저가 닫히면 진행 중인 외부 HTTP 요청도 정리하고 TX_HTTPC, TX_END 전송
            res.on('close', function () {
                if (ctx == null) {
                    return;
                }

                // 진행 중인 모든 외부 HTTP 요청에 대해 TX_HTTPC 전송
                // if (ctx.activeHttpcList && ctx.activeHttpcList.length > 0) {
                //     ctx.activeHttpcList.forEach(function(httpcInfo) {
                //         try {
                //             let urls = `${httpcInfo.host}:${httpcInfo.port}${httpcInfo.url}`;
                //             let httpcDatas = [urls, httpcInfo.mcallee];
                //             ctx.elapsed = Date.now() - httpcInfo.start_time;
                //             TraceHttpc.isSlowHttpc(ctx);
                //             AsyncSender.send_packet(PacketTypeEnum.TX_HTTPC, ctx, httpcDatas);
                //         } catch (e) {
                //             Logger.printError('WHATAP-243', 'Failed to send TX_HTTPC on close', e, false);
                //         }
                //     });
                //     ctx.activeHttpcList = [];
                // }

                self.__endTransaction(null, ctx, req, res);
            });

            try {
                AsyncSender.send_packet(PacketTypeEnum.TX_START, ctx, datas)

                /************************************/
                /*   Header / param Trace           */
                /************************************/
                var header_enabled = false;
                if (conf.profile_http_header_enabled === true && req.headers) {
                    header_enabled = true;
                    var prefix = conf.profile_header_url_prefix;
                    if (prefix && ctx.service_name.indexOf(prefix) < 0) {
                        header_enabled = false;
                    }
                }

                if (header_enabled) {
                    var header_ignore_key_set = new Set();
                    if (profile_http_header_ignore_keys) {
                        header_ignore_key_set = new Set(profile_http_header_ignore_keys.split(','));
                    }

                    try {
                        var headerDesc = Object.keys(req.headers).map(function (key) {
                            if (!header_ignore_key_set.has(key)) {
                                return `${key}=${req.headers[key]}`;
                            }
                        }).filter(element => {
                            if (element) return element
                        }).join('\n');

                        if (headerDesc) {
                            let headerDatas = ['HTTP-HEADERS', 'HTTP-HEADERS', headerDesc];
                            AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, headerDatas);
                        }
                    } catch (e) {
                        Logger.printError('WHATAP-236', 'Header parsing error', e, false);
                    }
                }

                return callback.apply(this, arguments);
            } catch (e) {
                Logger.printError("WHATAP-235", 'Hooking request failed..', e, false);
                self.__endTransaction(e, ctx, req, res);
                throw e;
            }
        });
    };
};

function isStatic(u) {
    var x = u.lastIndexOf('.');
    if (x <= 0) return false;

    var ext = u.substring(x);
    return _exts.has(ext);
}

function initCtx(req, res) {
    /*url이 없으면 추적하지 않는다*/
    if(!req.url) { return null; }
    if(ignore_http_method && ignore_http_method.toUpperCase().split(',').includes(req.method)) { return null; }
    if (ignore_build_file_enabled && ignore_build_file_path && ignore_build_file_path.split(',').some(path => req.url.startsWith(path))) {
        return null;
    }
    if( conf.getProperty('profile_enabled', true) === false ) {
        return null;
    }

    var ctx = TraceContextManager.start();
    if (ctx == null) {
        return null;
    }

    // 진행 중인 외부 HTTP 요청 정보를 저장 (여러 개 동시 추적)
    ctx.activeHttpcList = [];

    // RemoteIP
    var remote_addr;
    try {
        remote_addr = req.headers[configIpHeaderKey] || req.connection.remoteAddress || "0.0.0.0";
        if(remote_addr.includes(',')) {
            remote_addr = remote_addr.split(',')[0].trim();
        }
        ctx.remoteIp = IPUtil.checkIp4(remote_addr);
        if(conf.getProperty('trace_user_enabled', true)){
            if(conf.getProperty('trace_user_using_ip', true)){
                // WClientId를 빈 문자열로 보내 go-agent가 int64(RemoteIp) 분기를 타도록 함.
                // Java agent와 동일한 키 공간 유지 → cross-agent merge 정상화.
                ctx.userid = "";
            }else{
                ctx.userid = UserIdUtil.getUserId(req, res, 0);
            }
        }
    } catch (e) {
    }

    // Referer
    var referer = undefined;
    if(configRefererKey && req.headers[configRefererKey]){
        ctx.referer = req.headers[configRefererKey];
    }else if(req.headers.referer){
        ctx.referer = req.headers.referer;
    }

    // UserAgent
    if(configUserAgentKey && req.headers[configUserAgentKey]){
        ctx.userAgentString = req.headers[configUserAgentKey];
    }else if(req.headers['user-agent']){
        ctx.userAgentString = req.headers['user-agent'];
    }

    // Host
    ctx.host = req.headers.host;

    /************************************/
    /*   Multi Server Transaction Trace */
    /************************************/
    if (conf.getProperty('mtrace_enabled', false)) {
        var poid = req.headers['x-wtap-po'];
        if (poid != null) {
            ctx.setCallerPOID(poid);
            try {
                var mt_caller = req.headers[conf._trace_mtrace_caller_key];
                if (mt_caller) {
                    var x = mt_caller.indexOf(',');
                    if (x > 0) {
                        ctx.mtid = Hexa32.toLong32(mt_caller.substring(0, x));
                        ctx.mtid_build_checked = true;
                        var y = mt_caller.indexOf(',', x + 1);
                        if (y > 0) {
                            ctx.mdepth = parseInt(mt_caller.substring(x + 1, y));
                            var z = mt_caller.indexOf(',', y + 1);
                            if (z < 0) {
                                ctx.mcaller_txid = Hexa32.toLong32(mt_caller.substring(y + 1));
                            } else {
                                ctx.mcaller_txid = Hexa32.toLong32(mt_caller.substring(y + 1, z));

                                var z2 = mt_caller.indexOf(',', z + 1);
                                if (z2 < 0) {
                                    ctx.mcaller_stepId = Hexa32.toLong32(mt_caller.substring(z + 1));
                                } else {
                                    ctx.mcaller_stepId = Hexa32.toLong32(mt_caller.substring(z + 1, z2));
                                }
                            }
                        }
                    }
                }
                if (conf.stat_mtrace_enabled) {
                    var inf = req.headers[conf._trace_mtrace_spec_key1];
                    if (inf != null && inf.length > 0) {
                        var px = inf.indexOf(',');
                        ctx.mcaller_spec = inf.substring(0, px);
                        ctx.mcaller_url = inf.substring(px + 1);
                        ctx.mcaller_url_hash = HashUtil.hashFromString(ctx.mcaller_url);
                    }
                }
            } catch (e) {
                Logger.printError('WHATAP-237', 'Multi Server Transaction ', e, false);
            }
        }
    }

    return ctx;
};

function interceptorError(statusCode, error, ctx) {
    if (!ctx) {
        return;
    }

    ctx.status = statusCode;
    let errors = [];
    let error_message = 'Request failed with status code ';
    if (statusCode >= 400 && !ctx.error) {
        ctx.error = 1;

        errors.push(error.class)
        if(error.message)
            errors.push(error.message)
        else
            errors.push(error_message + statusCode);

        AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
    }
}

HttpObserver.prototype.__endTransaction = function (error, ctx, req, res) {
    // LLM active count leak 방지: tx 종료 시 누락된 onCallEnd 강제 정리.
    // 정상 케이스(dispatchLlmPack 호출됨)에선 inflight가 비어 있어 무동작.
    if (ctx) {
        try { llmStatCollector.cleanupInflight(ctx); } catch (e) {}
    }
    var param_enabled = false;
    if (conf.profile_http_parameter_enabled === true) {
        param_enabled = true;
        var prefix = conf.profile_http_parameter_url_prefix;
        if (prefix && ctx.service_name.indexOf(prefix) < 0) {
            param_enabled = false;
        }
    }

    if (param_enabled && req.query && Object.keys(req.query).length > 0) {
        const query = req.query;
        const profileHttpParameterKeysSet = profile_http_parameter_keys ? new Set(profile_http_parameter_keys.split(',')) : null;

        try {
            const desc = Object.entries(query)
                .filter(([key]) => !profileHttpParameterKeysSet || profileHttpParameterKeysSet.has(key))
                .map(([key, value]) => `${key}=${value}`)
                .join('\n');

            if (desc) {
                let paramDatas = ['HTTP-PARAMETERS', req.method, desc];
                AsyncSender.send_packet(PacketTypeEnum.TX_SECURE_MSG, ctx, paramDatas);
            }
        } catch (e) {
            Logger.printError('WHATAP-238', 'Parameter parsing error', e, false);
        }
    }

    if (error) {
        TraceContextManager.end(ctx != null ? ctx.id : null);
        ctx = null;
        return;
    }

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

    if (ctx.isStaticContents) {
        TraceContextManager.end(ctx.id);
        ctx = null;
        return;
    }

    try {
        // TX_END 송신 전에 대기 중인 LLM pack 큐 동기 flush.
        // dispatchLlmPack은 setImmediate로 지연 처리되어 TX_END보다 늦게 송신되는
        // 순서 역전 문제를 방지. 이 flush가 없으면 input_message/output_message가
        // 트랜잭션과 매칭 실패하여 UI에서 프롬프트가 보이지 않음.
        try {
            llmSender.flushPendingPacks();
        } catch (e) {
            Logger.printError('WHATAP-LLM-019', 'flushPendingPacks failed', e, false);
        }

        ctx.start_time = Date.now();
        // golib UdpTxEndPack Node.js 분기(Ver>=60102) 순서:
        // [Host, Uri, Mtid, Mdepth, McallerTxid, McallerPcode, McallerSpec, McallerUrl, Status,
        //  McallerPoidKey, IsLlm, McallerStepId]
        // ctx._stepIdStr가 endHttpc에서 캐싱되어 있으면 재사용 (BigInt 중복 호출 방지)
        let stepIdStr = ctx._stepIdStr || LlmIdUtil.stepIdStr(ctx._llm_step_id || 0);
        let datas = [os.hostname(), ctx.service_name, ctx.mtid, ctx.mdepth, ctx.mcaller_txid,
            ctx.mcaller_pcode, ctx.mcaller_spec, String(ctx.mcaller_url_hash), ctx.status,
            ctx.mcaller_poid || '', ctx.is_llm || 0, stepIdStr];
        ctx.elapsed = Date.now() - ctx.start_time;
        AsyncSender.send_packet(PacketTypeEnum.TX_END, ctx, datas);

        // ctx._llm_tx_summary가 _accumulateTxSummary에서 채워져 있어야 함.
        try {
            llmSender.sendLlmTxStatus(ctx);
        } catch (e) {
            Logger.printError('WHATAP-LLM-020', 'sendLlmTxStatus failed at tx end', e, false);
        }

        TraceContextManager.end(ctx.id);
    } catch (e) {
        Logger.printError('WHATAP-239', 'End transaction error..', e, false);
        TraceContextManager.end(ctx.id);
        ctx = null;
    }
};

// interTxTraceAutoOn function moved to util/trace-helper.js

HttpObserver.prototype.inject = function (mod, moduleName) {
    var self = this;
    var aop = self.agent.aop;

    if (mod.__whatap_observe__) {
        return;
    }
    mod.__whatap_observe__ = true;
    Logger.initPrint("HttpObserver");
    if (conf.getProperty('profile_enabled', true) === false) {
        return;
    }

    aop.after(mod, 'createServer', function (obj, args, ret) {
        aop.before(ret, 'listen', function (obj, args) {
            conf['whatap.port'] = (args[0] || 80);
        });
    });

    shimmer.wrap(mod.Server.prototype, 'emit', function wrapEmit(originalEmit) {
            return function wrappedEmit(eventName, ...args) {
                if (eventName !== 'request') {
                    return originalEmit.apply(this, [eventName, ...args]);
                }
                let [req, res] = args;
                return self.__createTransactionObserver(
                    (req, res) => originalEmit.call(this, eventName, req, res),
                    moduleName === 'https',
                    this
                )(req, res);
            };
        }
    );

    if (conf.getProperty('httpc_enabled', true) === false) {
        return;
    }

    shimmer.wrap(mod, 'request', function (original) {
        return function wrappedRequest() {
            // 인자 정규화: http.request(url[, options][, callback]) 모든 시그니처 지원
            var args = Array.prototype.slice.call(arguments);
            var options, callback;

            if (args[0] instanceof URL || typeof args[0] === 'string') {
                // 첫 번째 인자가 URL인 경우
                var url;
                try {
                    url = typeof args[0] === 'string' ? new URL(args[0]) : args[0];
                } catch (e) {
                    // URL 파싱 실패 시 원본 호출
                    return original.apply(this, arguments);
                }

                var optionsFromUrl = {
                    hostname: url.hostname,
                    port: url.port || (url.protocol === 'https:' ? 443 : 80),
                    path: url.pathname + url.search,
                    protocol: url.protocol,
                };

                if (typeof args[1] === 'object' && args[1] !== null && typeof args[1] !== 'function') {
                    // http.request(URL, options, callback)
                    // args[1]의 method, headers 등을 유지하고, URL에서 추출한 hostname, port, path를 덮어씀
                    options = Object.assign({}, args[1]);
                    options.hostname = optionsFromUrl.hostname;
                    options.port = optionsFromUrl.port;
                    options.path = optionsFromUrl.path;
                    options.protocol = optionsFromUrl.protocol;
                    // headers 복사 (참조 문제 방지)
                    if (args[1].headers) {
                        options.headers = Object.assign({}, args[1].headers);
                    }
                    callback = args[2];
                } else if (typeof args[1] === 'function') {
                    // http.request(URL, callback)
                    options = optionsFromUrl;
                    callback = args[1];
                } else {
                    // http.request(URL)
                    options = optionsFromUrl;
                    callback = undefined;
                }
            } else {
                // 기존: http.request(options, callback)
                options = args[0] || {};
                callback = args[1];
            }

            var ctx = TraceContextManager.getCurrentContext();
            if (!ctx || (!options.host && !options.hostname)) {
                return original.apply(this, arguments);
            }

            var isHttpRepeat = false;
            if (moduleName === 'https') {
                options.__isHttps = true;
            }
            if (moduleName === 'http' && options.__isHttps) {
                isHttpRepeat = true;
            }
            ctx.start_time = Date.now();

            if (!isHttpRepeat) {
                if (options.method === 'OPTION') {
                    return original.apply(this, arguments);
                }
                try {
                    TraceHelper.interTxTraceAutoOn(ctx);
                    if (conf.getProperty('mtrace_enabled', false)) {
                        if (options.headers) {
                            options.headers['x-wtap-po'] = Transfer.POID();
                        } else {
                            options.headers = {'x-wtap-po': Transfer.POID()};
                        }
                        if (conf.stat_mtrace_enabled) {
                            options.headers[conf._trace_mtrace_spec_key1] = Transfer.SPEC_URL(ctx);
                        }
                        if (!ctx.mtid.isZero()) {
                            options.headers[conf._trace_mtrace_caller_key] = Transfer.MTID_CALLERTX(ctx);
                        }

                        ctx.mcallee = KeyGen.next();
                        options.headers[conf._trace_mtrace_callee_key] = Hexa32.toString32(ctx.mcallee);
                    }

                    ctx.httpc_url = options.path || '/';
                    ctx.httpc_host = options.host || options.hostname || '';
                    ctx.httpc_port = options.port || -1;
                    ctx.active_httpc_hash = true;

                    if (ctx.httpc_port < 0) {
                        ctx.httpc_port = 80
                    }

                    // LLM API detection
                    try {
                        // collector key 폭발 방지: query string 제거된 path만 _llm_httpc_url에 저장
                        var rawUrl = ctx.httpc_url || '';
                        var qIdx = rawUrl.indexOf('?');
                        var pathOnly = qIdx >= 0 ? rawUrl.substring(0, qIdx) : rawUrl;
                        var fullHttpcUrl = ctx.httpc_host + pathOnly;
                        var llmOpType = LlmLogSinkPack.classifyOperation(fullHttpcUrl);
                        if (ctx._llm_httpc_pending || llmOpType !== 'unknown') {
                            ctx.driver = 'LLM API';
                            ctx.is_llm = 1;
                            ctx._llm_httpc_pending = false;
                            ctx._llm_operation_type = llmOpType;
                            ctx._llm_httpc_url = fullHttpcUrl;
                            ctx._llm_provider = ctx.httpc_host;
                        }
                    } catch (e) {
                        // ignore LLM detection errors
                    }

                    // if not ctx.step_id: ctx.step_id = random.getrandbits(63)
                    // 모든 httpc 호출에 step_id 생성
                    if (!ctx.step_id) {
                        ctx.step_id = KeyGen.next();
                    }

                    // Send TX_HTTPC at start if enabled (for lost connection cases)
                    if (conf.getProperty('profile_httpc_start_step_enabled', false)) {
                        ctx.active_httpc_hash = false;
                        let urls = `${ctx.httpc_host}:${ctx.httpc_port}${ctx.httpc_url}`;
                        let httpcDatas = [urls, ctx.step_id || 0, ctx.driver || ''];
                        ctx.elapsed = 0;
                        TraceHttpc.isSlowHttpc(ctx);
                        AsyncSender.send_packet(PacketTypeEnum.TX_HTTPC, ctx, httpcDatas);
                    }
                } catch (e) {
                    Logger.printError('WHATAP-240', 'Http Setup Error', e, false);
                    return original.apply(this, arguments);
                }
            } else {
                return original.apply(this, arguments);
            }

            // 진행 중인 요청 정보 저장 (여러 개 추적)
            const httpcInfo = {
                host: ctx.httpc_host,
                port: ctx.httpc_port,
                url: ctx.httpc_url,
                mcallee: ctx.mcallee,
                start_time: ctx.start_time
            };

            // 배열에 추가
            if (ctx.activeHttpcList) {
                ctx.activeHttpcList.push(httpcInfo);
            }

            // 배열에서 제거하는 헬퍼 함수
            const removeHttpcInfo = function() {
                if (ctx.activeHttpcList) {
                    const index = ctx.activeHttpcList.indexOf(httpcInfo);
                    if (index > -1) {
                        ctx.activeHttpcList.splice(index, 1);
                    }
                }
            };

            var wrappedCallback;
            if (typeof callback === 'function') {
                wrappedCallback = function (response) {
                    if (TraceContextManager.resume(ctx.id) === null) {
                        return callback.apply(this, arguments);
                    }

                    response.on('end', function () {
                        // 배열에서 제거
                        removeHttpcInfo();

                        // HTTP 클라이언트 에러 처리
                        if (response.statusCode >= 400 && transaction_status_error_enable) {
                            let error = {
                                class: response.statusMessage,
                                message: ''
                            }
                            interceptorError(response.statusCode, error, ctx);
                        }

                        endHttpc(ctx);
                    });

                    return callback.apply(this, arguments);
                };
            }

            var req = original.apply(this, [options, wrappedCallback]);

            req.on('error', function (err) {
                if (TraceContextManager.resume(ctx.id) === null) {
                    return;
                }

                // 배열에서 제거
                removeHttpcInfo();

                const isIgnoreHttpc = shouldIgnoreError(err.code, ctx.httpc_url, httpc_status_ignore, httpc_status_ignore_set);

                const networkErrorToStatusCode = {
                    'ECONNREFUSED': 503,
                    'ETIMEDOUT': 504,
                    'ENOTFOUND': 502,
                    'ECONNRESET': 503,
                    'EPIPE': 503,
                    'EHOSTUNREACH': 503,
                };
                const statusCode = networkErrorToStatusCode[err.code] || 500;

                if (!isIgnoreHttpc && transaction_status_error_enable) {
                    let error = {
                        class: err.code,
                        message: err.message || ''
                    }
                    interceptorError(statusCode, error, ctx);
                }
                endHttpc(ctx);
            });

            shimmer.wrap(req, 'write', function (original) {
                return function wrappedWrite() {
                    try {
                        if (conf.getProperty('profile_httpc_parameter_enabled', true)) {
                            if (arguments && arguments[0]) {
                                var bodyData;

                                if (typeof arguments[0] === 'object' && !(arguments[0] instanceof Buffer)) {
                                    bodyData = JSON.stringify(arguments[0]);
                                }
                                if (arguments[0] instanceof Buffer) {
                                    bodyData = arguments[0].toString('utf8');
                                }
                                if (bodyData) {
                                    let paramDatas = ['HTTP-PARAMETERS', options.method, bodyData];
                                    AsyncSender.send_packet(PacketTypeEnum.TX_SECURE_MSG, ctx, paramDatas);
                                }
                            }
                        }
                    } catch (e) {
                        Logger.printError('WHATAP-241', 'HTTP Write hook failed', e, false);
                    }
                    return original.apply(this, arguments);
                }
            });

            return req;
        }
    });

    function endHttpc(ctx) {
        if (ctx == null) {
            return;
        }

        // If profile_httpc_start_step_enabled is true, TX_HTTPC was already sent at start
        if (conf.getProperty('profile_httpc_start_step_enabled', false)) {
            return;
        }

        ctx.active_httpc_hash = false;
        let urls = `${ctx.httpc_host}:${ctx.httpc_port}${ctx.httpc_url}`;
        let stepIdRaw = ctx.step_id || 0;
        let stepIdStr = LlmIdUtil.stepIdStr(stepIdRaw);
        ctx._stepIdStr = stepIdStr;
        let httpcDatas = [urls, stepIdStr, ctx.driver || ''];
        ctx.elapsed = Date.now() - ctx.start_time;
        TraceHttpc.isSlowHttpc(ctx);
        AsyncSender.send_packet(PacketTypeEnum.TX_HTTPC, ctx, httpcDatas);

        if (ctx.is_llm) {
            ctx._llm_step_id = ctx.step_id;
        }
        ctx.driver = '';
        ctx.httpc_url = '';
        ctx.step_id = 0;
    }
};

function shouldIgnoreError(statusCode, url, statusCodeIgnore, statusIgnoreSet) {
    if (statusCodeIgnore) {
        const ignoreCodes = statusCodeIgnore.split(',').map(code => code.trim());
        if (ignoreCodes.includes(String(statusCode))) {
            return true;
        }
    }

    if (statusIgnoreSet) {
        const ignoreSet = statusIgnoreSet.split(',').map(item => item.trim());
        const urlStatusCombo = `${url}:${statusCode}`;
        if (ignoreSet.includes(urlStatusCombo)) {
            return true;
        }
    }

    return false;
}

exports.HttpObserver = HttpObserver;