/**
 * 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'),
    ParsedSql = require('../trace/parsed-sql'),
    conf = require('../conf/configure'),
    IntKeyMap = require('../util/intkey-map'),
    EscapeLiteralSQL = require('../util/escape-literal-sql'),
    HashUtil = require('../util/hashutil'),
    Logger = require('../logger'),
    DateUtil = require('../util/dateutil'),
    AsyncSender = require('../udp/async_sender'),
    PacketTypeEnum = require('../udp/packet_type_enum'),
    shimmer = require('../core/shimmer'),
    TraceSQL = require('../trace/trace-sql');
const { AsyncResource } = require('async_hooks');

var Mysql2Observer = function (agent) {
    this.agent = agent;
    this.packages = ['mysql2', 'mysql2/promise'];
};

var dbc_hash = 0;
var dbc = 'mysql://';

// 후킹된 객체 인스턴스들을 추적 (중복 방지)
var hookedInstances = new WeakSet();

// formatSqlWithParams 함수 개선 - mod를 직접 받아서 사용
function formatSqlWithParams(sql, params, mod) {
    try {
        if (mod && mod.format && typeof mod.format === 'function') {
            return mod.format(sql, params);
        }
        // fallback: 동적으로 require (성능상 권장하지 않음)
        var mysql = require('mysql2');
        return mysql.format(sql, params);
    } catch (e) {
        return sql;
    }
}

// 쿼리 래핑 함수 (AsyncResource.bind 적용)
var createQueryWrapper = function(isPromise = false, dbcUrl, mod) {
    return function(original) {
        return function wrappedQuery() {
            var args = Array.prototype.slice.call(arguments);
            var ctx = TraceContextManager.getCurrentContext();

            // ctx가 없으면 추적하지 않고 원본 함수만 실행
            if (ctx == null || args[0] == null) {
                return original.apply(this, arguments);
            }
            if (args[0].sql == null && typeof args[0] != 'string') {
                return original.apply(this, arguments);
            }

            // AsyncResource 생성
            var asyncResource = new AsyncResource('mysql2-query');

            // 동시 쿼리 처리를 위한 고유 ID 생성
            var queryId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);

            // 현재 처리 중인 쿼리 목록 관리
            if (!ctx._processing_queries) {
                ctx._processing_queries = new Set();
            }

            // SQL과 파라미터 추출
            var sql = args.length > 0 ? args[0] : undefined;
            var params = args.length > 1 && Array.isArray(args[1]) ? args[1] : undefined;

            if (typeof sql !== 'string') {
                sql = args[0].sql || undefined;
                if (args[0].values && Array.isArray(args[0].values)) {
                    params = args[0].values;
                }
            }

            // 최종 SQL 생성 (파라미터 바인딩) - mod 직접 전달
            var finalSql = sql;
            if (typeof sql === 'string' && sql.length > 0 && params && params.length > 0) {
                try {
                    finalSql = formatSqlWithParams(sql, params, mod);
                } catch (e) {
                    Logger.printError('WHATAP-SQL-BINDING', 'Error binding parameters', e, false);
                    finalSql = sql; // 바인딩 실패 시 원본 사용
                }
            }

            // 같은 최종 SQL이 동시에 처리되고 있는지 확인
            if (finalSql && ctx._processing_queries.has(finalSql)) {
                return original.apply(this, args);
            }

            if (finalSql) {
                ctx._processing_queries.add(finalSql);
            }

            // HTTP Observer 패턴을 따라 시작 시간 설정
            ctx.start_time = Date.now();

            // DB 연결 패킷 전송 (elapsed = 0으로 설정)
            ctx.elapsed = 0;
            AsyncSender.send_packet(PacketTypeEnum.TX_DB_CONN, ctx, [dbcUrl]);

            // SQL 시작 시간 기록
            var sql_start_time = Date.now();
            ctx.footprint('MySql2 Query Start [' + queryId + ']');
            ctx.sql_count++;

            var psql = null;
            if (typeof finalSql === 'string' && finalSql.length > 0) {
                try {
                    psql = escapeLiteral(finalSql);
                    // Logger.print('WHATAP-SQL-DEBUG', 'Processing SQL [' + queryId + ']: ' + finalSql.substring(0, 200), false);
                } catch (e) {
                    Logger.printError('WHATAP-225', 'Mysql2Observer escapeliteral error', e, false);
                }
            } else {
                finalSql = '';
                psql = escapeLiteral(finalSql);
            }

            var sqlHash = psql ? psql.sql : 0;
            ctx.active_sqlhash = true;
            ctx.active_dbc = dbc_hash;

            // AsyncResource로 바인딩된 쿼리 콜백 생성
            function createBoundQueryCallback() {
                function queryCallback(err, results, fields) {
                    var currentCtx = TraceContextManager.getCurrentContext();

                    // bind된 함수에서도 context가 없는 경우 복원
                    if (currentCtx == null) {
                        TraceContextManager.resume(ctx.id);
                        currentCtx = ctx;
                    }

                    if (currentCtx == null) {
                        return;
                    }

                    // 처리 완료된 쿼리를 목록에서 제거
                    if (finalSql && currentCtx._processing_queries) {
                        currentCtx._processing_queries.delete(finalSql);
                    }

                    var sql_elapsed = Date.now() - sql_start_time;
                    var resultCount = 0;

                    if (err) {
                        handleSqlError(currentCtx, err, sqlHash);
                    }

                    // 결과 개수 계산
                    if (!err && results) {
                        if (Array.isArray(results)) {
                            resultCount = results.length;
                        } else if (results.affectedRows !== undefined) {
                            resultCount = results.affectedRows;
                        } else if (results.changedRows !== undefined) {
                            resultCount = results.changedRows;
                        }
                    }

                    // TraceSQL 처리
                    try {
                        TraceSQL.isSlowSQL(currentCtx);

                        // ResultSet 처리 (SELECT 쿼리)
                        if (!err && results && psql && psql.type === 'S') {
                            TraceSQL.isTooManyRecords(resultCount, currentCtx);
                        }
                    } catch (e) {
                        Logger.printError('WHATAP-TRACESQL', 'Error in TraceSQL processing', e, false);
                    }

                    currentCtx.elapsed = sql_elapsed;
                    currentCtx.active_sqlhash = false;
                    AsyncSender.send_packet(PacketTypeEnum.TX_SQL, currentCtx, [dbcUrl, finalSql, String(resultCount)]);
                }

                // AsyncResource로 바인딩하여 안전한 context 보장
                return asyncResource.bind(queryCallback);
            }

            // Promise 기반 처리 (mysql2/promise)
            if (isPromise) {
                var result = original.apply(this, args);

                // Promise 체인 처리
                if (result && typeof result.then === 'function') {
                    var boundCallback = createBoundQueryCallback();

                    return result.then(function(queryResult) {
                        // MySQL2 Promise 결과는 [rows, fields] 형태
                        var rows = Array.isArray(queryResult) ? queryResult[0] : queryResult;
                        var fields = Array.isArray(queryResult) ? queryResult[1] : null;

                        // 바인딩된 콜백 호출
                        boundCallback(null, rows, fields);
                        return queryResult;
                    }).catch(function(error) {
                        // 에러 시 바인딩된 콜백 호출
                        boundCallback(error, null, null);
                        throw error;
                    });
                }
                return result;
            }

            // 콜백 기반 처리 (일반 mysql2)
            var callbackWrapped = false;
            var boundCallback = createBoundQueryCallback();

            // 마지막 인자에서 함수를 찾아서 후킹
            for (var i = args.length - 1; i >= 0; i--) {
                if (typeof args[i] === 'function') {
                    var originalCallback = args[i];
                    args[i] = function() {
                        var callbackArgs = Array.prototype.slice.call(arguments);
                        try {
                            // 바인딩된 콜백 먼저 실행
                            boundCallback(callbackArgs[0], callbackArgs[1], callbackArgs[2]);
                        } catch (e) {
                            Logger.printError('WHATAP-CALLBACK', 'Error in callback hook', e, false);
                        }
                        // 원본 콜백 실행
                        if (originalCallback && typeof originalCallback === 'function') {
                            return originalCallback.apply(this, callbackArgs);
                        }
                    };
                    callbackWrapped = true;
                    break;
                }
            }

            if (!callbackWrapped && args.length > 0 && args[0]._callback) {
                try {
                    var originalCallback = args[0]._callback;
                    args[0]._callback = function() {
                        var callbackArgs = Array.prototype.slice.call(arguments);
                        try {
                            boundCallback(callbackArgs[0], callbackArgs[1], callbackArgs[2]);
                        } catch (e) {
                            Logger.printError('WHATAP-CALLBACK', 'Error in _callback hook', e, false);
                        }
                        if (originalCallback && typeof originalCallback === 'function') {
                            return originalCallback.apply(this, callbackArgs);
                        }
                    };
                    callbackWrapped = true;
                } catch (e) {
                    Logger.printError('WHATAP-CALLBACK-HOOK', 'Error hooking _callback', e, false);
                }
            }

            try {
                return original.apply(this, args);
            } catch (queryError) {
                boundCallback(queryError, null, null);
                throw queryError;
            }
        };
    };
};

function handleSqlError(ctx, err, sqlHash) {
    if (!err) return;

    try {
        var errorClass = err.code || err.name || err.constructor?.name || 'MySQLError';
        var errorMessage = err.message || err.sqlMessage || 'mysql2 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");
            ctx.error_message = errorStack;
        }

        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;
            ctx.error_message = errorMessage;
            ctx.errClass = errorClass;
            ctx.errMessage = errorMessage;

            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);
        }
    } catch (e) {
        Logger.printError('WHATAP-226', 'Error handling MySQL2 error', e, false);
    }
}

// 연결 래핑 함수 (AsyncResource 적용)
var createConnectionWrapper = function(isPromise = false, mod) {
    return function(original) {
        return function wrappedCreateConnection() {
            var args = Array.prototype.slice.call(arguments);
            var ctx = TraceContextManager.getCurrentContext();

            // AsyncResource 생성
            var asyncResource = ctx ? new AsyncResource('mysql2-connection') : null;

            // DB 연결 정보 구성
            if (dbc_hash === 0 && args.length > 0) {
                var info = (args[0] || {});
                dbc = 'mysql://';
                dbc += info.user || '';
                dbc += "@";
                dbc += info.host || '';
                dbc += '/';
                dbc += info.database || '';
                dbc_hash = HashUtil.hashFromString(dbc);
            }

            if (ctx) {
                ctx.db_opening = true;
                ctx.footprint('MySql2 Connecting Start');
                ctx.start_time = Date.now();
            }

            var result = original.apply(this, args);

            // Connection 객체 후킹 함수
            function hookConnection(connection) {
                if (connection && !hookedInstances.has(connection)) {
                    hookedInstances.add(connection);
                    Logger.print('WHATAP-MYSQL2-CONNECTION', 'Hooking new connection object', false);

                    // Connection의 query와 execute 메서드 후킹
                    if (connection.query && !connection.query.__whatap_wrapped__) {
                        shimmer.wrap(connection, 'query', createQueryWrapper(isPromise, dbc, mod));
                        connection.query.__whatap_wrapped__ = true;
                    }
                    if (connection.execute && !connection.execute.__whatap_wrapped__) {
                        shimmer.wrap(connection, 'execute', createQueryWrapper(isPromise, dbc, mod));
                        connection.execute.__whatap_wrapped__ = true;
                    }

                    // 에러 델리게이트 후킹
                    if (connection._protocol && connection._protocol._delegateError &&
                        !connection._protocol._delegateError.__whatap_wrapped__) {
                        try {
                            shimmer.wrap(connection._protocol, '_delegateError', function(original) {
                                return function wrappedDelegateError() {
                                    var args = Array.prototype.slice.call(arguments);
                                    var ctx = TraceContextManager.getCurrentContext();

                                    if (ctx && args[0]) {
                                        handleProtocolError(ctx, args[0]);
                                    }

                                    return original.apply(this, args);
                                };
                            });
                            connection._protocol._delegateError.__whatap_wrapped__ = true;
                        } catch (e) {
                            Logger.printError('WHATAP-PROTOCOL-HOOK', 'Error hooking _delegateError', e, false);
                        }
                    }
                }
                return connection;
            }

            function handleProtocolError(ctx, error) {
                if (!error) return;

                try {
                    var errorClass = error.code || error.name || error.constructor?.name || 'MySQLError';
                    var errorMessage = error.message || 'mysql2 error';

                    var shouldAddError = false;
                    if (conf._is_trace_ignore_err_cls_contains === true && errorClass.indexOf(conf.trace_ignore_err_cls_contains) < 0) {
                        shouldAddError = true;
                    } else if (conf._is_trace_ignore_err_msg_contains === true && 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) {
                        ctx.error = 1;
                        ctx.status = 500;
                        var errors = [errorClass];
                        if (errorMessage) {
                            errors.push(errorMessage);
                        }
                        AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
                    }
                } catch (e) {
                    Logger.printError('WHATAP-PROTOCOL-ERROR-HANDLING', 'Error processing protocol error', e, false);
                }
            }

            // Promise 기반 연결 처리
            if (isPromise && result && typeof result.then === 'function') {
                if (asyncResource) {
                    return result.then(asyncResource.bind(function(connection) {
                        hookConnection(connection);

                        if (ctx) {
                            ctx.elapsed = Date.now() - ctx.start_time;
                            ctx.footprint('MySql2 Connecting Done');
                            ctx.db_opening = false;
                        }
                        return connection;
                    })).catch(asyncResource.bind(function(error) {
                        if (ctx) {
                            ctx.elapsed = Date.now() - ctx.start_time;
                            ctx.footprint('MySql2 Connecting Error');
                            ctx.db_opening = false;
                            handleSqlError(ctx, error, 0);
                        }
                        throw error;
                    }));
                } else {
                    return result.then(function(connection) {
                        hookConnection(connection);
                        return connection;
                    });
                }
            } else {
                // 동기/콜백 기반 연결 처리
                if (result && typeof result === 'object') {
                    hookConnection(result);
                }

                if (ctx) {
                    ctx.elapsed = Date.now() - ctx.start_time;
                    ctx.footprint('MySql2 Connecting Done');
                    ctx.db_opening = false;
                }
            }

            return result;
        };
    };
};

var createPoolWrapper = function(isPromise = false, mod) {
    return function(original) {
        return function wrappedCreatePool() {
            var args = Array.prototype.slice.call(arguments);

            // DB 연결 정보 구성
            if (dbc_hash === 0 && args.length > 0) {
                var info = args[0];
                dbc = 'mysql://';
                dbc += info.user || '';
                dbc += "@";
                dbc += info.host || '';
                dbc += '/';
                dbc += info.database || '';
                dbc_hash = HashUtil.hashFromString(dbc);
            }

            var pool = original.apply(this, args);

            if (pool && !hookedInstances.has(pool)) {
                hookedInstances.add(pool);

                if (pool.query && !pool.query.__whatap_wrapped__) {
                    shimmer.wrap(pool, 'query', createQueryWrapper(isPromise, dbc, mod));
                    pool.query.__whatap_wrapped__ = true;
                }
                if (pool.execute && !pool.execute.__whatap_wrapped__) {
                    shimmer.wrap(pool, 'execute', createQueryWrapper(isPromise, dbc, mod));
                    pool.execute.__whatap_wrapped__ = true;
                }

                if (pool.getConnection && !pool.getConnection.__whatap_wrapped__) {
                    shimmer.wrap(pool, 'getConnection', function(original) {
                        return function wrappedGetConnection() {
                            var args = Array.prototype.slice.call(arguments);
                            var ctx = TraceContextManager.getCurrentContext();

                            if (ctx == null) {
                                return original.apply(this, args);
                            }

                            var asyncResource = new AsyncResource('mysql2-get-connection');
                            ctx.start_time = Date.now();

                            for (var i = args.length - 1; i >= 0; i--) {
                                if (typeof args[i] === 'function') {
                                    var originalCallback = args[i];
                                    // AsyncResource로 바인딩된 콜백 생성
                                    args[i] = asyncResource.bind(function() {
                                        var callbackArgs = Array.prototype.slice.call(arguments);
                                        var err = callbackArgs[0];
                                        var conn = callbackArgs[1];

                                        var currentCtx = TraceContextManager.getCurrentContext();
                                        if (!currentCtx) {
                                            TraceContextManager.resume(ctx.id);
                                            currentCtx = ctx;
                                        }

                                        if (currentCtx) {
                                            currentCtx.elapsed = Date.now() - ctx.start_time;
                                            if (!err && conn && !conn.__query_hook__) {
                                                conn.__query_hook__ = true;

                                                // Connection 객체의 메서드들 후킹
                                                if (conn.query && !conn.query.__whatap_wrapped__) {
                                                    shimmer.wrap(conn, 'query', createQueryWrapper(isPromise, dbc, mod));
                                                    conn.query.__whatap_wrapped__ = true;
                                                }
                                                if (conn.execute && !conn.execute.__whatap_wrapped__) {
                                                    shimmer.wrap(conn, 'execute', createQueryWrapper(isPromise, dbc, mod));
                                                    conn.execute.__whatap_wrapped__ = true;
                                                }
                                            }
                                        }

                                        return originalCallback.apply(this, callbackArgs);
                                    });
                                    break;
                                }
                            }

                            var result = original.apply(this, args);

                            // Promise 기반 처리
                            if (result && typeof result.then === 'function' && args.every(arg => typeof arg !== 'function')) {
                                return result.then(asyncResource.bind(function(connection) {
                                    var currentCtx = TraceContextManager.getCurrentContext();
                                    if (!currentCtx) {
                                        TraceContextManager.resume(ctx.id);
                                        currentCtx = ctx;
                                    }

                                    if (currentCtx) {
                                        currentCtx.elapsed = Date.now() - ctx.start_time;
                                        if (connection && !connection.__query_hook__) {
                                            connection.__query_hook__ = true;

                                            if (connection.query && !connection.query.__whatap_wrapped__) {
                                                shimmer.wrap(connection, 'query', createQueryWrapper(isPromise, dbc, mod));
                                                connection.query.__whatap_wrapped__ = true;
                                            }
                                            if (connection.execute && !connection.execute.__whatap_wrapped__) {
                                                shimmer.wrap(connection, 'execute', createQueryWrapper(isPromise, dbc, mod));
                                                connection.execute.__whatap_wrapped__ = true;
                                            }
                                        }
                                    }

                                    return connection;
                                }));
                            }

                            return result;
                        };
                    });
                    pool.getConnection.__whatap_wrapped__ = true;
                }
            }

            return pool;
        };
    };
};

var checkedSql = new IntKeyMap(2000).setMax(2000);
var nonLiteSql = new IntKeyMap(5000).setMax(5000);
var date = DateUtil.yyyymmdd();

function escapeLiteral(sql) {
    if (sql == null) {
        sql = '';
    }

    if (date !== DateUtil.yyyymmdd()) {
        checkedSql.clear();
        nonLiteSql.clear();
        date = DateUtil.yyyymmdd();
        Logger.print('WHATAP-SQL-CLEAR', 'Mysql2Observer CLEAR OK!!!!!!!!!!!!!!!!', false);
    }

    var sqlHash = HashUtil.hashFromString(sql);
    var psql = nonLiteSql.get(sqlHash);

    if (psql != null) {
        return psql;
    }
    psql = checkedSql.get(sqlHash);

    if (psql != null) {
        return psql;
    }

    var els = new EscapeLiteralSQL(sql);
    els.process();

    var hash = HashUtil.hashFromString(els.getParsedSql());
    if (hash === sqlHash) {
        psql = new ParsedSql(els.sqlType, hash, null);
        nonLiteSql.put(sqlHash, psql);
    } else {
        psql = new ParsedSql(els.sqlType, hash, els.getParameter());
        checkedSql.put(sqlHash, psql);
    }
    return psql;
}

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

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

    var isPromise = moduleName && moduleName.includes('promise');

    if (mod.createConnection && !mod.createConnection.__whatap_wrapped__) {
        shimmer.wrap(mod, 'createConnection', createConnectionWrapper(isPromise, mod));
        mod.createConnection.__whatap_wrapped__ = true;
    }

    if (mod.createPool && !mod.createPool.__whatap_wrapped__) {
        shimmer.wrap(mod, 'createPool', createPoolWrapper(isPromise, mod));
        mod.createPool.__whatap_wrapped__ = true;
    }

    if (mod.createPoolCluster && !mod.createPoolCluster.__whatap_wrapped__) {
        shimmer.wrap(mod, 'createPoolCluster', createPoolWrapper(isPromise, mod));
        mod.createPoolCluster.__whatap_wrapped__ = true;
    }
};

exports.Mysql2Observer = Mysql2Observer;