// The @sap/hana-client driver will automatically
// use this to add OpenTelemetry support when @opentelemetry/api
// is already installed.

var OTel = {};
try {
    // @opentelemetry/api must be installed by the application in order for
    // the client to use it.
    OTel.API = require('@opentelemetry/api');
    OTel.Tracer = OTel.API.trace.getTracer('@sap/hana-client', '2.28.21');
    // Future: ideally use SEMATTRS_ values from OTel.semConv = require('@opentelemetry/semantic-conventions')
    // Currently do not to avoid the problem of what if @opentelemtry/api is installed but not
    // @opentelemetry/semantic-conventions?
} catch (err) {
    // If module was not found, do not do anything
    // console.log("Failed to load @opentelemetry/api: ", err); // for debugging
    if(OTel.Tracer) {
        OTel.Tracer = undefined;
    }
}

function isOpenTelemetryEnabled() {
    if(OTel.Tracer === undefined) {
        return false;
    }
    const envVar = process.env.HDB_NODEJS_SKIP_OPENTELEMETRY;
    if(envVar && envVar != '0' && envVar.toLowerCase() != 'false') {
        return false;
    }
    return true;
}

function _getSpanNameAndStatus(op, sql, conn) {
    // spanName and attributes attempt to follow:
    // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md
    // (above page was updated May 2, 2025 I think to version v1.33.0 to mark most DB attributes as stable)

    // The specs says the span name could be <db.operation.name> <db.collection.name>
    // but instead follow CAP and postgresql which roughly use "<operation> - <sql>"
    var spanName = op;
    if(sql) {
        spanName = op + " - " + sql;
        if(spanName.length > 80) {
            spanName = spanName.substring(0, 79) + '…'; // based on what CAP used
        }
    }
    // Future: consider using OTel.semConv.SEMATTRS_DB_ values instead of hardcoding attribute names
    // FYI CAP and postgresql use net.peer.name, and net.peer.port instead of server.address and server.port
    var spanOptions = {kind: OTel.API.SpanKind.CLIENT,
                        attributes: {'db.system.name' : 'sap.hana',
                                     'db.operation.name': op, // the method name, which is roughly the operation being executed
                                     'server.address': conn._destinationInfo.host,}};
    if(conn._destinationInfo.port) {
        try {
            spanOptions.attributes['server.port'] = Number(conn._destinationInfo.port);
        } catch (err) {
            // ignore conversion error
        }
    }
    if(typeof(sql) === 'string') {
        // Follow Dynatrace which limits SQL to 1000 characters
        // TODO: this should be sanitized to remove literal strings, numbers, etc.
        var sql_text = sql.length > 1000 ? sql.substring(0, 999) + '…' : sql;
        spanOptions.attributes['db.query.text'] = sql_text;
    }
    if(conn._destinationInfo.tenant) {
        spanOptions.attributes['db.namespace'] = conn._destinationInfo.tenant;
    }
    return {spanName: spanName, spanOptions: spanOptions};
}

function _setSpanStatus(span, err) {
    if(err) {
        span.setStatus(Object.assign({code: OTel.API.SpanStatusCode.ERROR }, err.message ? { message: err.message } : undefined));
        if(err.code) {
            // https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/ says this value should be a string
            span.setAttribute('db.response.status_code', err.code.toString());
        }
    } else {
        span.setStatus({code: OTel.API.SpanStatusCode.OK});
    }
}

function _openTelemetryResultCallback(span, activeCtx, cb) {
    return function (err, ...args) {
        _setSpanStatus(span, err);
        const results = args[0];
        if(results !== undefined) {
            span.setAttribute('db.response.returned_rows', (results && results.length) || results);
        }
        span.end();
        if(cb) {
            // propagate the active context for async calls
            // (otherwise spans started within cb will not know the parent span)
            return OTel.API.context.with(activeCtx, function() {
                return cb(err, ...args);
            });
        }
    };
}

function _openTelemetryResultSetCallback(span, activeCtx, cb) {
    return function (err, ...args) {
        const resultSet = args[0];
        _setSpanStatus(span, err);
        if(resultSet) {
            const rowCount = resultSet.getRowCount();
            // A negative rowCount means the number of rows is unknown.
            // This happens if the client hasn't received the last fetch chunk yet (with default server configuration,
            // this happens if the result set is larger than 32 rows)
            if(rowCount >= 0) {
                span.setAttribute('db.response.returned_rows', rowCount);
            }
            // modify resultSet for OpenTelemetry after a successful executeQuery
            // async methods that do not trace to OpenTelemetry
            _setPropagateContextWrapper(resultSet, resultSet.close, "close");
            _setPropagateContextWrapper(resultSet, resultSet.getData, "getData");
            _setPropagateContextWrapper(resultSet, resultSet.getValue, "getValue");
            _setPropagateContextWrapper(resultSet, resultSet.getValues, "getValues");
            _setPropagateContextWrapper(resultSet, resultSet.next, "next");
            _setPropagateContextWrapper(resultSet, resultSet.nextResult, "nextResult");
        }
        span.end();
        if(cb) {
            // propagate the active context for async calls
            // (otherwise spans started within cb will not know the parent span)
            return OTel.API.context.with(activeCtx, function() {
                return cb(err, ...args);
            });
        }
    };
}

// Wrapper for thisArg.origFn that we do NOT want to create a span for (eg stmt.getData)
// but we still want to propagate the active context on an async call.
// The method's callback first parameter must the error object.
function _propagateContextWrapperFn(thisArg, origFn) {
    // args can end with a callback
    return function (...args) {
        var cb;
        if (args.length > 0 && typeof args[args.length - 1] === 'function') {
            cb = args[args.length - 1];
        }
        if(cb) {
            const activeCtx = OTel.API.context.active();
            origFn.call(thisArg, ...args.slice(0, args.length - 1), function (...cbArgs) {
                // propagate the active context for async calls
                // (otherwise spans started within cb will not know the parent span)
                OTel.API.context.with(activeCtx, function() {
                    cb(...cbArgs);
                });
            });
        } else {
            // sync method call
            return origFn.call(thisArg, ...args);
        }
    }
}

// thisArg is the class, origFn is the method, fnName is a string (name of method)
function _setPropagateContextWrapper(thisArg, origFn, fnName) {
    Object.defineProperty(thisArg, fnName, {value: _propagateContextWrapperFn(thisArg, origFn)});
}

// Wrapper for thisArg.origFn that is not a prepare or execute* method (eg conn.commit)
// to create a span for the operation.
// The method's callback first parameter must the error object.
function _generalWrapperFn(thisArg, origFn, op, conn) {
    // args can end with a callback
    return function (...args) {
        var cb;
        var activeCtx;
        if (args.length > 0 && typeof args[args.length - 1] === 'function') {
            cb = args[args.length - 1];
            activeCtx = OTel.API.context.active();
        }
        const {spanName, spanOptions} = _getSpanNameAndStatus(op, undefined, conn);
        return OTel.Tracer.startActiveSpan(spanName, spanOptions, function(span) {
            if(cb) {
                // async method call
                return origFn.call(thisArg, ...args.slice(0, args.length - 1), function (...cbArgs) {
                    // if cbArgs is empty, cbArbs[0] is undefined, so this is safe
                    _setSpanStatus(span, cbArgs[0]);
                    span.end();
                    // propagate the active context for async calls
                    // (otherwise spans started within cb will not know the parent span)
                    OTel.API.context.with(activeCtx, function() {
                        cb(...cbArgs);
                    });
                });
            } else {
                // sync method call
                var result;
                try {
                    result = origFn.call(thisArg, ...args);
                    _setSpanStatus(span);
                    span.end();
                } catch (err) {
                    _setSpanStatus(span, err);
                    span.end();
                    throw err;
                }
                return result;
            }
        });
    }
}

// wrapper for execute, executeBatch, and executeQuery
function _executeWrapperFn(thisArg, conn, execFn, op, resultCB, sql) {
    // connection exec args = [sql, options, callback] --> options and callback is optional
    // stmt exec args = [options, callback] --> options and callback is optional
    return function (...args) {
        if(thisArg === conn && args.length > 0) {
            sql = args[0];
        }
        if(typeof(sql) !== 'string') {
            sql = ''; // execute will fail, but need sql for when the error is traced
        }
        var cb;
        var activeCtx;
        if (args.length > 0 && typeof args[args.length - 1] === 'function') {
            cb = args[args.length - 1];
            activeCtx = OTel.API.context.active();
        }
        const {spanName, spanOptions} = _getSpanNameAndStatus(op, sql, conn);
        return OTel.Tracer.startActiveSpan(spanName, spanOptions, function(span) {
            if(cb) {
                // async execute
                return execFn.call(thisArg, ...args.slice(0, args.length - 1), resultCB(span, activeCtx, cb));
            } else {
                // sync execute
                var result;
                try {
                    result = execFn.call(thisArg, ...args);
                    resultCB(span)(undefined, result);
                } catch (err) {
                    _setSpanStatus(span, err);
                    span.end();
                    throw err;
                }
                return result;
            }
        });
    }
}

// modify stmt for OpenTelemetry after a successful prepare
function _modifyStmt(stmt, conn, sql) {
    const originalExecFn = stmt.exec;
    stmt.exec = _executeWrapperFn(stmt, conn, originalExecFn, "execute", _openTelemetryResultCallback, sql);
    stmt.execute = stmt.exec;

    const originalExecBatchFn = stmt.execBatch;
    stmt.execBatch = _executeWrapperFn(stmt, conn, originalExecBatchFn, "executeBatch", _openTelemetryResultCallback, sql);
    stmt.executeBatch = stmt.execBatch;

    const originalExecQueryFn = stmt.execQuery;
    stmt.execQuery = _executeWrapperFn(stmt, conn, originalExecQueryFn, "executeQuery", _openTelemetryResultSetCallback, sql);
    stmt.executeQuery = stmt.execQuery;

    // async methods that do not trace to OpenTelemetry
    _setPropagateContextWrapper(stmt, stmt.drop, "drop");
    _setPropagateContextWrapper(stmt, stmt.getData, "getData");
    _setPropagateContextWrapper(stmt, stmt.getParameterValue, "getParameterValue");
    _setPropagateContextWrapper(stmt, stmt.sendParameterData, "sendParameterData");
}

function _prepareWrapperFn(conn, prepareFn) {
    // args = [sql, options, callback] --> options is optional
    return function (...args) {
        var cb;
        var activeCtx;
        if(args.length > 0 && typeof args[args.length - 1] === 'function') {
            cb = args[args.length - 1];
            activeCtx = OTel.API.context.active();
        }
        var sql = args[0];
        if(typeof(sql) !== 'string') {
            sql = ''; // prepare will fail, but need sql for when the error is traced
        }
        const {spanName, spanOptions} = _getSpanNameAndStatus("prepare", sql, conn);
        return OTel.Tracer.startActiveSpan(spanName, spanOptions, function(span) {
            if(cb) {
                // async prepare
                prepareFn.call(conn, ...args.slice(0, args.length - 1), function prepare_handler(err, stmt) {
                    _setSpanStatus(span, err);
                    span.end();
                    // propagate the active context for async calls
                    // (otherwise spans started within cb will not know the parent span)
                    OTel.API.context.with(activeCtx, function() {
                        if (err) {
                            cb(err);
                        } else {
                            _modifyStmt(stmt, conn, sql);
                            cb(err, stmt);
                        }
                    });
                });
            } else {
                // sync prepare
                try {
                    var stmt = prepareFn.call(conn, ...args);
                    _setSpanStatus(span);
                    span.end();
                    _modifyStmt(stmt, conn, sql);
                    return stmt;
                } catch (err) {
                    _setSpanStatus(span, err);
                    span.end();
                    throw err;
                }
            }
        });
    }
}

// destinationInfo is an object with host, port and optionally tenant keys
function openTelemetryConnection(conn, destinationInfo) {
    if(OTel.Tracer === undefined) {
        return conn;
    }
    if(conn._destinationInfo) {
    // openTelemetryConnection has already been called on conn, use new destinationInfo
    // in case it changed, but don't wrap conn again
    conn._destinationInfo = destinationInfo;
        return conn;
    }
    conn._destinationInfo = destinationInfo;
    // hana-client does not like decorating.
    // because of that, we need to override the fn and pass the original fn for execution
    const originalExecFn = conn.exec;
    conn.exec = _executeWrapperFn(conn, conn, originalExecFn, "execute", _openTelemetryResultCallback);
    conn.execute = conn.exec;
    const originalPrepareFn = conn.prepare;
    // Must use defineProperty and not just conn.<method> = ... for N-API methods.
    // (The execute methods are set to non-N-API JavaScript methods by
    // index.js dynamicallyAddConnExecMethod)
    Object.defineProperty(conn, 'prepare', {value: _prepareWrapperFn(conn, originalPrepareFn)});

    const originalCommitFn = conn.commit;
    Object.defineProperty(conn, 'commit', {value: _generalWrapperFn(conn, originalCommitFn, "commit", conn)});
    const originalRollbackFn = conn.rollback;
    Object.defineProperty(conn, 'rollback', {value: _generalWrapperFn(conn, originalRollbackFn, "rollback", conn)});

    // async methods that do not trace to OpenTelemetry
    _setPropagateContextWrapper(conn, conn.abort, "abort");
    _setPropagateContextWrapper(conn, conn.cancel, "cancel");
    _setPropagateContextWrapper(conn, conn.clean, "clean");
    _setPropagateContextWrapper(conn, conn.clearPool, "clearPool");
    _setPropagateContextWrapper(conn, conn.disconnect, "disconnect");
    _setPropagateContextWrapper(conn, conn.close, "close");
    _setPropagateContextWrapper(conn, conn.end, "end");
    _setPropagateContextWrapper(conn, conn.switchUser, "switchUser");
    _setPropagateContextWrapper(conn, conn.isValid, "isValid");

    return conn;
}

module.exports = { openTelemetryConnection, isOpenTelemetryEnabled };
