/**
 * 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'),
    AsyncResource = require('async_hooks').AsyncResource;

var MssqlObserver = function (agent) {
    this.agent = agent;
    this.packages = ['mssql'];
};

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

var hookedInstances = new WeakSet();

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

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

    try {
        var errorClass = err.code || err.name || err.constructor?.name || 'MSSQLError';
        var errorMessage = err.message || 'mssql 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);
        }
    } catch (e) {
        Logger.printError('WHATAP-209', 'Error handling MSSQL error', e, false);
    }
}

function setupDbcInfo(config) {
    if (dbc_hash === 0 && config) {
        dbc = 'mssql://';
        dbc += (config.server || '') + ':';
        dbc += (config.port || '') + '/';
        dbc += config.database || '';
        dbc_hash = HashUtil.hashFromString(dbc);
    }
}

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

    if (date !== DateUtil.yyyymmdd()) {
        checkedSql.clear();
        nonLiteSql.clear();
        date = DateUtil.yyyymmdd();
    }

    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;
}

function createRequestWrapper(agent, dbcUrl, command) {
    return function(original) {
        return function wrappedRequest() {
            var args = Array.prototype.slice.call(arguments);
            var ctx = TraceContextManager.getCurrentContext();

            if (ctx == null || args[0] == null) {
                return original.apply(this, arguments);
            }

            if (dbcUrl === 'mssql://' || !dbc_hash) {
                return original.apply(this, arguments);
            }

            const asyncResource = new AsyncResource('mssql-request');
            const callbackResource = new AsyncResource('mssql-callback');

            return asyncResource.runInAsyncScope(() => {
                ctx.start_time = Date.now();
                ctx.elapsed = 0;
                AsyncSender.send_packet(PacketTypeEnum.TX_DB_CONN, ctx, [dbcUrl]);

                var sql_start_time = Date.now();
                ctx.footprint('MsSQL ' + command + ' Start');
                ctx.sql_count++;

                var sql = args.length > 0 ? args[0] : undefined;
                var finalSql = '';

                try {
                    if (typeof sql === 'string') {
                        finalSql = sql;
                    } else if (sql !== null && typeof sql === 'object') {
                        finalSql = JSON.stringify(sql);
                    }
                } catch (formatError) {
                    Logger.printError('WHATAP-210', 'Error formatting MSSQL query', formatError, false);
                    finalSql = '';
                }

                if (typeof finalSql !== 'string' || finalSql.length === 0) {
                    finalSql = '';
                }

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

                ctx.active_sqlhash = true;
                ctx.active_dbc = dbc_hash;

                function requestCallback(err, results) {
                    var currentCtx = TraceContextManager.getCurrentContext();
                    if (currentCtx == null) {
                        return;
                    }

                    TraceContextManager.resume(currentCtx.id);

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

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

                    if (!err && results) {
                        if (Array.isArray(results) && results.length > 0) {
                            if (results[0] && Array.isArray(results[0])) {
                                resultCount = results[0].length;
                            } else if (results[0] && results[0].recordset && Array.isArray(results[0].recordset)) {
                                resultCount = results[0].recordset.length;
                            }
                        } else if (results.recordset && Array.isArray(results.recordset)) {
                            resultCount = results.recordset.length;
                        } else if (results.rowsAffected && Array.isArray(results.rowsAffected)) {
                            resultCount = results.rowsAffected.reduce((sum, count) => sum + count, 0);
                        }
                    }

                    try {
                        TraceSQL.isSlowSQL(currentCtx);
                        if (!err && psql != null && psql.type === 'S') {
                            TraceSQL.isTooManyRecords(resultCount, currentCtx);
                        }
                    } catch (e) {
                        Logger.printError('WHATAP-212', '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('MsSQL ' + command + ' Done');
                }

                var callbackWrapped = false;
                var hasCallback = false;

                // 콜백 찾기 및 래핑
                for (var i = args.length - 1; i >= 0; i--) {
                    if (typeof args[i] === 'function') {
                        hasCallback = true;
                        var originalCallback = args[i];
                        args[i] = callbackResource.bind(function() {
                            var callbackArgs = Array.prototype.slice.call(arguments);
                            try {
                                requestCallback(callbackArgs[0], callbackArgs[1]);
                            } catch (e) {
                                Logger.printError('WHATAP-213', 'Error in callback hook', e, false);
                            }
                            if (originalCallback && typeof originalCallback === 'function') {
                                return originalCallback.apply(this, callbackArgs);
                            }
                        });
                        callbackWrapped = true;
                        break;
                    }
                }

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

                    // Promise 기반 처리
                    if (!hasCallback && result && typeof result.then === 'function') {
                        return result.then(function(res) {
                            requestCallback(null, res);
                            return res;
                        }).catch(function(err) {
                            requestCallback(err, null);
                            throw err;
                        });
                    }

                    return result;
                } catch (requestError) {
                    requestCallback(requestError, null);
                    throw requestError;
                }
            });
        };
    };
}

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

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

    var self = this;

    // Request 클래스의 메서드들 후킹
    if (mod.Request && mod.Request.prototype) {
        var requestCommands = ['_bulk', '_query', '_execute'];

        requestCommands.forEach(function(command) {
            if (mod.Request.prototype[command] && !mod.Request.prototype[command].__whatap_wrapped__) {
                shimmer.wrap(mod.Request.prototype, command, function(original) {
                    return function wrappedRequestCommand() {
                        var args = Array.prototype.slice.call(arguments);
                        var ctx = TraceContextManager.getCurrentContext();

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

                        // DBC 정보 설정
                        if (dbc_hash === 0 && this.parent && this.parent.config) {
                            setupDbcInfo(this.parent.config);
                        }

                        return createRequestWrapper(self.agent, dbc, command)(original).apply(this, args);
                    };
                });
                mod.Request.prototype[command].__whatap_wrapped__ = true;
            }
        });
    }

    // connect 함수 후킹
    if (mod.connect && !mod.connect.__whatap_wrapped__) {
        shimmer.wrap(mod, 'connect', function(original) {
            return function wrappedConnect() {
                var args = Array.prototype.slice.call(arguments);
                var ctx = TraceContextManager.getCurrentContext();

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

                // 연결 정보 설정
                if (args[0]) {
                    setupDbcInfo(args[0]);
                }

                const connectionResource = new AsyncResource('mssql-connect');

                return connectionResource.runInAsyncScope(() => {
                    ctx.start_time = Date.now();
                    ctx.db_opening = true;
                    ctx.footprint('MsSQL Connecting Start');

                    // 콜백 래핑
                    for (var i = args.length - 1; i >= 0; i--) {
                        if (typeof args[i] === 'function') {
                            var originalCallback = args[i];
                            args[i] = connectionResource.bind(function() {
                                var callbackArgs = Array.prototype.slice.call(arguments);
                                var err = callbackArgs[0];

                                if (ctx) {
                                    TraceContextManager.resume(ctx.id);
                                    ctx.elapsed = Date.now() - ctx.start_time;

                                    if (err) {
                                        handleSqlError(ctx, err);
                                        ctx.footprint('MsSQL Connecting Error');
                                    } else {
                                        ctx.footprint('MsSQL Connecting Done');
                                    }
                                }

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

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

                    // Promise 기반 처리
                    if (result && typeof result.then === 'function') {
                        return result.then(connectionResource.bind(function(pool) {
                            if (ctx) {
                                TraceContextManager.resume(ctx.id);
                                ctx.elapsed = Date.now() - ctx.start_time;
                                ctx.db_opening = false;
                                ctx.footprint('MsSQL Connecting Done');
                            }
                            return pool;
                        })).catch(connectionResource.bind(function(err) {
                            if (ctx) {
                                TraceContextManager.resume(ctx.id);
                                ctx.elapsed = Date.now() - ctx.start_time;
                                ctx.db_opening = false;
                                ctx.footprint('MsSQL Connecting Error');
                                handleSqlError(ctx, err);
                            }
                            throw err;
                        }));
                    }

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

    // ConnectionPool 클래스 후킹 (있는 경우)
    if (mod.ConnectionPool && !mod.ConnectionPool.__whatap_wrapped__) {
        shimmer.wrap(mod, 'ConnectionPool', function(original) {
            return function wrappedConnectionPool() {
                var args = Array.prototype.slice.call(arguments);

                // 연결 정보 설정
                if (args[0]) {
                    setupDbcInfo(args[0]);
                }

                var pool = new (Function.prototype.bind.apply(original, [null].concat(args)));

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

                    // connect 메서드 후킹
                    if (pool.connect && !pool.connect.__whatap_wrapped__) {
                        shimmer.wrap(pool, 'connect', function(original) {
                            return function wrappedPoolConnect() {
                                var args = Array.prototype.slice.call(arguments);
                                var ctx = TraceContextManager.getCurrentContext();

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

                                const connectionResource = new AsyncResource('mssql-pool-connect');

                                return connectionResource.runInAsyncScope(() => {
                                    ctx.start_time = Date.now();
                                    ctx.footprint('MsSQL Pool Connecting Start');

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

                                    // Promise 기반 처리
                                    if (result && typeof result.then === 'function') {
                                        return result.then(connectionResource.bind(function(res) {
                                            if (ctx) {
                                                TraceContextManager.resume(ctx.id);
                                                ctx.elapsed = Date.now() - ctx.start_time;
                                                ctx.footprint('MsSQL Pool Connecting Done');
                                            }
                                            return res;
                                        })).catch(connectionResource.bind(function(err) {
                                            if (ctx) {
                                                TraceContextManager.resume(ctx.id);
                                                ctx.elapsed = Date.now() - ctx.start_time;
                                                ctx.footprint('MsSQL Pool Connecting Error');
                                                handleSqlError(ctx, err);
                                            }
                                            throw err;
                                        }));
                                    }

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

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

exports.MssqlObserver = MssqlObserver;