var TraceContextManager = require('../trace/trace-context-manager'),
    HashUtil = require('../util/hashutil'),
    Logger = require('../logger'),
    conf = require('../conf/configure'),
    AsyncSender = require('../udp/async_sender'),
    PacketTypeEnum = require('../udp/packet_type_enum');

const { AsyncResource } = require('async_hooks');

var IORedisObserver = function (agent) {
    this.agent = agent;
    this.aop = agent.aop;
    this.packages = ['ioredis'];
};

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

    if (conf.sql_enabled === false) {
        return;
    }

    var self = this;
    var dbc_hash = 0;
    var dbc = 'redis://';

    // Redis 명령어 매핑 (Redis observer와 동일)
    var REDIS_COMMANDS = {
        // 기본 명령어들
        'ping': 'PING', 'get': 'GET', 'set': 'SET', 'del': 'DEL', 'exists': 'EXISTS', 'keys': 'KEYS',
        'setex': 'SETEX', 'setEx': 'SETEX', 'expire': 'EXPIRE', 'expireAt': 'EXPIREAT', 'expireat': 'EXPIREAT',
        'ttl': 'TTL', 'incr': 'INCR', 'decr': 'DECR', 'incrBy': 'INCRBY', 'incrby': 'INCRBY',
        'decrBy': 'DECRBY', 'decrby': 'DECRBY', 'mGet': 'MGET', 'mget': 'MGET', 'mSet': 'MSET', 'mset': 'MSET',

        // Hash 명령어들
        'hSet': 'HSET', 'hset': 'HSET', 'hGet': 'HGET', 'hget': 'HGET', 'hGetAll': 'HGETALL', 'hgetall': 'HGETALL',
        'hMGet': 'HMGET', 'hmget': 'HMGET', 'hMSet': 'HMSET', 'hmset': 'HMSET', 'hDel': 'HDEL', 'hdel': 'HDEL',

        // List 명령어들
        'lPush': 'LPUSH', 'lpush': 'LPUSH', 'rPush': 'RPUSH', 'rpush': 'RPUSH', 'lPop': 'LPOP', 'lpop': 'LPOP',
        'rPop': 'RPOP', 'rpop': 'RPOP', 'lRange': 'LRANGE', 'lrange': 'LRANGE', 'lLen': 'LLEN', 'llen': 'LLEN',
        'lSet': 'LSET', 'lset': 'LSET', 'lRem': 'LREM', 'lrem': 'LREM',

        // Set 명령어들
        'sAdd': 'SADD', 'sadd': 'SADD', 'sMembers': 'SMEMBERS', 'smembers': 'SMEMBERS', 'sRem': 'SREM', 'srem': 'SREM',
        'sCard': 'SCARD', 'scard': 'SCARD',

        // Sorted Set 명령어들
        'zAdd': 'ZADD', 'zadd': 'ZADD', 'zRange': 'ZRANGE', 'zrange': 'ZRANGE', 'zRem': 'ZREM', 'zrem': 'ZREM'
    };

    // DBC 정보 설정
    function setupDbcInfo(connectionOptions) {
        if (dbc_hash === 0 && connectionOptions) {
            dbc = 'redis://';
            dbc += connectionOptions.host || 'localhost';
            dbc += ':';
            dbc += connectionOptions.port || 6379;
            dbc += '/';
            dbc += connectionOptions.db || 0;
            dbc_hash = HashUtil.hashFromString(dbc);
        }
    }

    function handleRedisError(ctx, err) {
        if (!err) return;

        try {
            var errorClass = err.code || err.name || 'IORedisError';
            var errorMessage = err.message || 'ioredis error';

            if (!ctx.error) ctx.error = 1;
            ctx.status = 500;
            var errors = [errorClass, errorMessage];

            AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
        } catch (e) {
            Logger.printError('WHATAP-234', 'Error handling IORedis error', e, false);
        }
    }

    // IORedis 클래스의 prototype.sendCommand 메서드를 직접 후킹하는 방식
    if (mod && mod.prototype && !mod.__whatap_wrapped__) {
        // IORedis의 핵심 메서드인 sendCommand를 후킹
        if (mod.prototype.sendCommand && !mod.prototype.sendCommand.__whatap_wrapped__) {
            var originalSendCommand = mod.prototype.sendCommand;

            mod.prototype.sendCommand = function(command, stream) {
                var ctx = TraceContextManager.getCurrentContext();
                if (!ctx || !command || !command.name) {
                    return originalSendCommand.apply(this, arguments);
                }

                var commandName = command.name.toLowerCase();
                var normalizedCommand = REDIS_COMMANDS[commandName] || command.name.toUpperCase();

                // 연결 정보 설정 (한 번만)
                if (dbc_hash === 0 && this.options) {
                    setupDbcInfo(this.options);
                }

                var self = this;
                var commandArgs = command.args || [];

                const asyncResource = new AsyncResource('ioredis-command');

                return asyncResource.runInAsyncScope(() => {
                    // DB 연결 패킷 전송
                    ctx.start_time = Date.now();
                    ctx.elapsed = 0;
                    ctx.active_sqlhash = true;
                    AsyncSender.send_packet(PacketTypeEnum.TX_DB_CONN, ctx, [dbc]);

                    // IORedis 명령 시작
                    var command_start_time = Date.now();
                    ctx.footprint(`IORedis ${normalizedCommand} Start`);

                    // IORedis에서는 command.args에서 쿼리 조합
                    var queryParts = [normalizedCommand];
                    if (commandArgs && commandArgs.length > 0) {
                        // 각 인자를 문자열로 변환하고 20자로 제한
                        var argStrings = commandArgs.map(function(arg) {
                            var argStr = String(arg);
                            return argStr.length > 20 ? argStr.substring(0, 20) + '...' : argStr;
                        });
                        queryParts = queryParts.concat(argStrings);
                    }
                    var commandText = `Redis ` + queryParts.join(' ');

                    try {
                        var result = originalSendCommand.apply(self, arguments);

                        // IORedis는 항상 Promise를 반환
                        if (result && typeof result.then === 'function') {
                            return result.then(
                                function(res) {
                                    var command_elapsed = Date.now() - command_start_time;
                                    var resultCount = 0;

                                    if (res !== undefined && res !== null) {
                                        if (Array.isArray(res)) {
                                            resultCount = res.length;
                                        } else if (typeof res === 'string' || typeof res === 'number') {
                                            resultCount = 1;
                                        }
                                    }

                                    ctx.elapsed = command_elapsed;
                                    ctx.active_sqlhash = false;
                                    AsyncSender.send_packet(PacketTypeEnum.TX_SQL, ctx, [dbc, commandText, String(resultCount)]);
                                    ctx.footprint(`IORedis ${normalizedCommand} Done`);
                                    return res;
                                },
                                function(err) {
                                    handleRedisError(ctx, err);
                                    var command_elapsed = Date.now() - command_start_time;
                                    ctx.elapsed = command_elapsed;
                                    ctx.active_sqlhash = false;
                                    AsyncSender.send_packet(PacketTypeEnum.TX_SQL, ctx, [dbc, commandText, '0']);
                                    ctx.footprint(`IORedis ${normalizedCommand} Done`);
                                    throw err;
                                }
                            );
                        } else {
                            // 예외적으로 Promise가 아닌 경우 처리
                            var command_elapsed = Date.now() - command_start_time;
                            var resultCount = 0;

                            if (result !== undefined && result !== null) {
                                if (Array.isArray(result)) {
                                    resultCount = result.length;
                                } else if (typeof result === 'string' || typeof result === 'number') {
                                    resultCount = 1;
                                }
                            }

                            ctx.elapsed = command_elapsed;
                            ctx.active_sqlhash = false;
                            AsyncSender.send_packet(PacketTypeEnum.TX_SQL, ctx, [dbc, commandText, String(resultCount)]);
                            ctx.footprint(`IORedis ${normalizedCommand} Done`);
                            return result;
                        }
                    } catch (commandError) {
                        handleRedisError(ctx, commandError);
                        var command_elapsed = Date.now() - command_start_time;
                        ctx.elapsed = command_elapsed;
                        ctx.active_sqlhash = false;
                        AsyncSender.send_packet(PacketTypeEnum.TX_SQL, ctx, [dbc, commandText, '0']);
                        ctx.footprint(`IORedis ${normalizedCommand} Done`);
                        throw commandError;
                    }
                });
            };

            mod.prototype.sendCommand.__whatap_wrapped__ = true;
        }

        // IORedis 생성자도 후킹하여 연결 정보 수집
        var originalConstructor = mod;
        function WrappedIORedis() {
            var args = Array.prototype.slice.call(arguments);
            var ctx = TraceContextManager.getCurrentContext();

            if (ctx) {
                ctx.start_time = Date.now();
                ctx.footprint('IORedis Client Creating');
                ctx.db_opening = true;
            }

            // 원본 생성자 호출
            var client = Object.create(originalConstructor.prototype);
            originalConstructor.apply(client, args);

            // 연결 옵션 파싱 및 DBC 정보 설정
            var options = {};
            if (args.length > 0) {
                if (typeof args[0] === 'string') {
                    try {
                        var url = new URL(args[0]);
                        options = {
                            host: url.hostname,
                            port: parseInt(url.port) || 6379,
                            db: parseInt(url.pathname.slice(1)) || 0
                        };
                    } catch (e) {
                        options = { host: 'localhost', port: 6379, db: 0 };
                    }
                } else if (typeof args[0] === 'number') {
                    options = {
                        host: args[1] || 'localhost',
                        port: args[0],
                        db: 0
                    };
                } else if (typeof args[0] === 'object') {
                    options = args[0] || {};
                    if (!options.host) options.host = 'localhost';
                    if (!options.port) options.port = 6379;
                    if (options.db === undefined) options.db = 0;
                }
            } else {
                options = { host: 'localhost', port: 6379, db: 0 };
            }

            setupDbcInfo(options);

            if (ctx) {
                ctx.elapsed = Date.now() - (ctx.start_time || Date.now());
                ctx.footprint('IORedis Client Created');
                if (ctx.db_opening !== undefined) {
                    ctx.db_opening = false;
                }
            }

            return client;
        }

        // 프로토타입과 정적 속성 복사
        WrappedIORedis.prototype = originalConstructor.prototype;
        Object.setPrototypeOf(WrappedIORedis, originalConstructor);

        Object.getOwnPropertyNames(originalConstructor).forEach(function(prop) {
            if (prop !== 'prototype' && prop !== 'name' && prop !== 'length') {
                try {
                    var descriptor = Object.getOwnPropertyDescriptor(originalConstructor, prop);
                    if (descriptor && descriptor.configurable !== false) {
                        Object.defineProperty(WrappedIORedis, prop, descriptor);
                    }
                } catch (e) {
                    // 복사할 수 없는 속성은 건너뛰기
                }
            }
        });

        // 모듈 교체
        Object.getOwnPropertyNames(WrappedIORedis).forEach(function(prop) {
            if (prop !== 'prototype') {
                try {
                    mod[prop] = WrappedIORedis[prop];
                } catch (e) {
                    // 설정할 수 없는 속성은 건너뛰기
                }
            }
        });

        mod.__whatap_wrapped__ = true;
        Logger.initPrint("IORedis sendCommand method successfully wrapped");
    }
};

exports.IORedisObserver = IORedisObserver;