/**
 * 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 MariaObserver = function (agent) {
    this.agent = agent;
    this.packages = ['mariadb'];
};

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

// 후킹된 객체 추적
var hookedInstances = new WeakSet();

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

    try {
        var errorClass = err.code || err.name || err.constructor?.name || 'MariaDBError';
        var errorMessage = err.message || err.sqlMessage || 'mariadb 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-214', 'Error handling MariaDB error', e, false);
    }
}

function setupDbcInfo(args) {
    if (dbc_hash === 0 && args.length > 0) {
        var info = args[0] || {};
        dbc = 'mariadb://';
        dbc += info.user || '';
        dbc += "@";
        dbc += info.host || '';
        dbc += '/';
        dbc += info.database || '';
        dbc_hash = HashUtil.hashFromString(dbc);
    }
}

function hookConnectionMethods(connection, agent) {
    if (connection && !hookedInstances.has(connection)) {
        hookedInstances.add(connection);
        // Logger.print('WHATAP-MARIADB-CONNECTION', 'Hooking new connection object', false);

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

// 쿼리 래핑 함수
var createQueryWrapper = function(agent, dbcUrl) {
    return function(original) {
        return function wrappedQuery() {
            var args = Array.prototype.slice.call(arguments);
            var ctx = TraceContextManager.getCurrentContext();

            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 생성
            const asyncResource = new AsyncResource('mariadb-query');

            return asyncResource.runInAsyncScope(() => {
                // 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('MariaDB Query Start');
                ctx.sql_count++;

                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 생성 (파라미터 바인딩)
                var finalSql = sql || '';
                if (typeof sql === 'string' && sql.length > 0 && params && params.length > 0) {
                    try {
                        // MariaDB는 mysql2 스타일의 파라미터 바인딩 지원
                        const result = params.map((param) => {
                            if(typeof param === 'string'){
                                return `'${param.replace(/'/g, "''")}'` // SQL injection 방지
                            }
                            return param
                        }).join(', ');

                        // 간단한 파라미터 치환 (? 기준)
                        var paramIndex = 0;
                        finalSql = sql.replace(/\?/g, function() {
                            if (paramIndex < params.length) {
                                var param = params[paramIndex++];
                                if (typeof param === 'string') {
                                    return `'${param.replace(/'/g, "''")}'`;
                                }
                                return param;
                            }
                            return '?';
                        });
                    } catch (e) {
                        Logger.printError('WHATAP-SQL-BINDING', 'Error binding parameters', e, false);
                        finalSql = sql; // 바인딩 실패 시 원본 사용
                    }
                }

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

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

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

                // Promise 기반 처리
                if (result && typeof result.then === 'function') {
                    return result.then(res => {
                        var currentCtx = TraceContextManager.getCurrentContext();
                        if (currentCtx == null) {
                            currentCtx = ctx;
                        }

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

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

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

                            // ResultSet 처리 (SELECT 쿼리)
                            if (res && 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)]);

                        currentCtx.footprint('MariaDB Query Done');
                        return res;
                    }).catch(err => {
                        var currentCtx = TraceContextManager.getCurrentContext();
                        if (currentCtx == null) {
                            currentCtx = ctx;
                        }

                        var sql_elapsed = Date.now() - sql_start_time;

                        // 에러 처리
                        handleSqlError(currentCtx, err, sqlHash);

                        currentCtx.elapsed = sql_elapsed;
                        currentCtx.active_sqlhash = false;
                        AsyncSender.send_packet(PacketTypeEnum.TX_SQL, currentCtx, [dbcUrl, finalSql, '0']);

                        currentCtx.footprint('MariaDB Query Error');
                        throw err;
                    });
                }

                return result;
            });
        };
    };
};

// 유틸리티 함수들
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', 'MariaObserver 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;
}

// 메인 inject 함수
MariaObserver.prototype.inject = function (mod, moduleName) {
    if (mod.__whatap_observe__) {
        return;
    }
    mod.__whatap_observe__ = true;
    Logger.initPrint("MariaObserver");

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

    var self = this;

    // createConnection 래핑
    if (mod.createConnection && !mod.createConnection.__whatap_wrapped__) {
        shimmer.wrap(mod, 'createConnection', function(original) {
            return function wrappedCreateConnection() {
                var args = Array.prototype.slice.call(arguments);
                var ctx = TraceContextManager.getCurrentContext();

                setupDbcInfo(args);

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

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

                // Promise 기반 처리
                if (result && typeof result.then === 'function') {
                    return result.then(connection => {
                        hookConnectionMethods(connection, self.agent);

                        if (ctx) {
                            ctx.elapsed = Date.now() - ctx.start_time;
                            ctx.footprint('MariaDB Connecting Done');
                            ctx.db_opening = false;
                        }
                        return connection;
                    }).catch(error => {
                        if (ctx) {
                            ctx.elapsed = Date.now() - ctx.start_time;
                            ctx.footprint('MariaDB Connecting Error');
                            ctx.db_opening = false;
                            handleSqlError(ctx, error, 0);
                        }
                        throw error;
                    });
                } else {
                    // 동기 처리
                    hookConnectionMethods(result, self.agent);

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

                return result;
            };
        });
        mod.createConnection.__whatap_wrapped__ = true;
    }

    // createPool 래핑
    if (mod.createPool && !mod.createPool.__whatap_wrapped__) {
        shimmer.wrap(mod, 'createPool', function(original) {
            return function wrappedCreatePool() {
                var args = Array.prototype.slice.call(arguments);
                setupDbcInfo(args);

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

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

                    // Pool의 직접 쿼리 메서드들 후킹
                    if (pool.query && !pool.query.__whatap_wrapped__) {
                        shimmer.wrap(pool, 'query', createQueryWrapper(self.agent, dbc));
                        pool.query.__whatap_wrapped__ = true;
                    }
                    if (pool.execute && !pool.execute.__whatap_wrapped__) {
                        shimmer.wrap(pool, 'execute', createQueryWrapper(self.agent, dbc));
                        pool.execute.__whatap_wrapped__ = true;
                    }

                    // getConnection 메서드 후킹
                    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);
                                }

                                const connectionResource = new AsyncResource('mariadb-getConnection');

                                return connectionResource.runInAsyncScope(() => {
                                    ctx.start_time = Date.now();

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

                                    // Promise 기반 처리
                                    if (result && typeof result.then === 'function') {
                                        return result.then(connectionResource.bind(function(connection) {
                                            TraceContextManager.resume(ctx.id);
                                            ctx.elapsed = Date.now() - ctx.start_time;

                                            if (connection && !connection.__query_hook__) {
                                                connection.__query_hook__ = true;
                                                hookConnectionMethods(connection, self.agent);
                                            }

                                            return connection;
                                        }));
                                    }

                                    // 동기 처리
                                    hookConnectionMethods(result, self.agent);
                                    ctx.elapsed = Date.now() - ctx.start_time;
                                    return result;
                                });
                            };
                        });
                        pool.getConnection.__whatap_wrapped__ = true;
                    }
                }

                return pool;
            };
        });
        mod.createPool.__whatap_wrapped__ = true;
    }
};

exports.MariaObserver = MariaObserver;