/**
 * 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'),
    conf = require('../conf/configure'),
    Logger = require('../logger');
const HashUtil = require("../util/hashutil");
const shimmer = require('../core/shimmer');
const AsyncSender = require('../udp/async_sender');
const PacketTypeEnum = require('../udp/packet_type_enum');
const os = require('os');

var grpc_profile_enabled = conf.getProperty('grpc_profile_enabled', true);
var grpc_profile_stream_client_enabled = conf.getProperty('grpc_profile_stream_client_enabled', true);
var grpc_profile_stream_server_enabled = conf.getProperty('grpc_profile_stream_server_enabled', true);
var grpc_profile_ignore_method = conf.getProperty('grpc_profile_ignore_method', '');
var ignore_method_set = null;
conf.on('grpc_profile_enabled', function(newProperty) {
    grpc_profile_enabled = newProperty;
});
conf.on('grpc_profile_stream_client_enabled', function(newProperty) {
    grpc_profile_stream_client_enabled = newProperty;
});
conf.on('grpc_profile_stream_server_enabled', function(newProperty) {
    grpc_profile_stream_server_enabled = newProperty;
});
conf.on('grpc_profile_ignore_method', function(newProperty) {
    grpc_profile_ignore_method = newProperty;
});

var GRpcObserver = function(agent) {
    this.agent = agent;
    this.packages = ['@grpc/grpc-js'];
};

const types = {
    unary: 'unary',
    client_stream: 'clientStream',
    server_stream: 'serverStream',
    bidi: 'bidi'
};

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

    if (grpc_profile_enabled) {
        shimmer.wrap(mod.Server.prototype, 'register', wrapRegister);
        shimmer.wrap(mod.Client.prototype, 'register', wrapRegister);
    }
};

function checkIgnoreMethod(ignore_method, method_name){
    try{
        if (ignore_method) {
            ignore_method_set = new Set(ignore_method.split(','));
        } else {
            ignore_method_set = null;
        }
        if (ignore_method_set && ignore_method_set.has(method_name)) {
            return true;
        }
    }catch (e) {
        Logger.printError('WHATAP-263', 'gRPC checkIgnoreMethod error: ' + e, false);
    }
    return false;
}

function wrapHandler(handler, methodName, type) {
    return function(call, callback) {
        var method_name = methodName.includes('/') ? methodName.substring(methodName.lastIndexOf('/')+1, methodName.length) : methodName;

        if (!grpc_profile_enabled || checkIgnoreMethod(conf.getProperty('grpc_profile_ignore_method', ''), method_name)) {
            return handler.call(this, call, callback);
        }

        TraceContextManager._asyncLocalStorage.run(initCtx(call, methodName), () => {
            var ctx = TraceContextManager._asyncLocalStorage.getStore();
            if (!ctx) {
                return handler.call(this, call, callback);
            }

            try {
                // TX_START: 트랜잭션 시작
                let startDatas = [
                    os.hostname(),
                    methodName,
                    ctx.remoteIp || '0.0.0.0',
                    '', // userAgent
                    '', // referer
                    String(ctx.userid || ""),
                    'false', // isStaticContents
                    method_name
                ];
                AsyncSender.send_packet(PacketTypeEnum.TX_START, ctx, startDatas);

                // TX_MSG: Type 정보
                let typeDatas = ['Type', 'Type', type];
                AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, typeDatas);

                // TX_MSG: Method 정보
                let methodDatas = ['Method', 'Method', method_name];
                AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, methodDatas);

                // TX_MSG: Parameter 정보
                if (call.request && Object.keys(call.request).length > 0) {
                    let paramDatas = ['Parameter', 'Parameter', JSON.stringify(call.request)];
                    AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, paramDatas);
                }

                function wrappedCallback(err, response, ctx) {
                    if (err) {
                        // TX_ERROR: 에러 발생
                        let errors = [
                            err.name || err.constructor?.name || 'GrpcError',
                            err.message || 'Unknown error'
                        ];
                        AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
                        ctx.status = 500;
                    } else {
                        ctx.status = 200;
                    }

                    endTransaction(ctx);

                    if (typeof callback === 'function') {
                        return callback(err, response);
                    }
                }

                return handler.call(this, call, (err, response) => wrappedCallback(err, response, ctx));
            } catch (err) {
                Logger.printError('WHATAP-261', 'gRPC wrapHandler error: ' + err, false);

                // TX_ERROR: 예외 발생
                let errors = [
                    err.name || err.constructor?.name || 'GrpcError',
                    err.message || 'Unknown error'
                ];
                AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);

                endTransaction(ctx);

                return handler.call(this, call, callback);
            }
        });
    };
}

function wrapStreamHandler(handler, methodName, type) {
    return function(call, callback) {
        var method_name = methodName.includes('/') ? methodName.substring(methodName.lastIndexOf('/')+1, methodName.length) : methodName;

        if (!grpc_profile_enabled || checkIgnoreMethod(conf.getProperty('grpc_profile_ignore_method', ''), method_name)) {
            return handler.call(this, call, callback);
        }

        switch (type) {
            case 'serverStream':
                if (!grpc_profile_stream_server_enabled) {
                    return handler.call(this, call, callback);
                }
                break;
            case 'clientStream':
                if (!grpc_profile_stream_client_enabled) {
                    return handler.call(this, call, callback);
                }
                break;
            case 'bidi':
                if (!grpc_profile_stream_server_enabled || !grpc_profile_stream_client_enabled) {
                    return handler.call(this, call, callback);
                }
                break;
        }

        TraceContextManager._asyncLocalStorage.run(initCtx(call, methodName), () => {
            var ctx = TraceContextManager._asyncLocalStorage.getStore();
            if (!ctx) {
                return handler.call(this, call, callback);
            }

            try {
                // TX_START: 트랜잭션 시작
                let startDatas = [
                    os.hostname(),
                    methodName,
                    ctx.remoteIp || '0.0.0.0',
                    '', // userAgent
                    '', // referer
                    String(ctx.userid || ""),
                    'false', // isStaticContents
                    method_name
                ];
                AsyncSender.send_packet(PacketTypeEnum.TX_START, ctx, startDatas);

                // TX_MSG: Type 정보
                let typeDatas = ['Type', 'Type', type];
                AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, typeDatas);

                // TX_MSG: Method 정보
                let methodDatas = ['Method', 'Method', method_name];
                AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, methodDatas);

                ctx.status = 200;

                call.on('end', () => {
                    endTransaction(ctx);
                });

                call.once('cancelled', () => {
                    // TX_MSG: Cancelled 정보
                    let cancelDatas = ['Cancelled', 'Cancelled', 'Request cancelled'];
                    AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, cancelDatas);
                    ctx.status = 499; // Client Closed Request
                    endTransaction(ctx);
                });

                call.on('error', (err) => {
                    // TX_ERROR: 에러 발생
                    let errors = [
                        err.name || err.constructor?.name || 'GrpcError',
                        err.message || 'Unknown error'
                    ];
                    AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
                    ctx.status = 500;
                    endTransaction(ctx);
                });

                return handler.call(this, call);
            } catch (e) {
                Logger.printError('WHATAP-262', 'gRPC wrapStreamHandler error: ' + e, false);

                // TX_ERROR: 예외 발생
                let errors = [
                    e.name || e.constructor?.name || 'GrpcError',
                    e.message || 'Unknown error'
                ];
                AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);

                endTransaction(ctx);

                return handler.call(this, call);
            }
        });
    };
}

function wrapRegister(register) {
    return function(name, handler, serialize, deserialize, type) {
        if (typeof handler === 'function') {
            switch (type) {
                case types.client_stream:
                    // Client Streaming은 callback이 필요하므로 wrapHandler 사용
                    handler = wrapHandler(handler, name, 'clientStream');
                    break;
                case types.server_stream:
                    handler = wrapStreamHandler(handler, name, 'serverStream');
                    break;
                case types.bidi:
                    handler = wrapStreamHandler(handler, name, 'bidi');
                    break;
                case types.unary:
                default:
                    handler = wrapHandler(handler, name, 'unary');
            }
        }
        return register.call(this, name, handler, serialize, deserialize, type);
    };
}

function endTransaction(ctx) {
    if (!ctx || TraceContextManager.isExist(ctx.id) === false) {
        return;
    }

    try {
        ctx.elapsed = Date.now() - ctx.start_time;

        // TX_END: 트랜잭션 종료
        let endDatas = [
            os.hostname(),
            ctx.service_name,
            ctx.mtid || 0,
            ctx.mdepth || 0,
            ctx.mcaller_txid || 0,
            ctx.mcaller_pcode || 0,
            ctx.mcaller_spec || '',
            String(ctx.mcaller_url_hash || 0),
            ctx.status || 0
        ];
        AsyncSender.send_packet(PacketTypeEnum.TX_END, ctx, endDatas);

        TraceContextManager.end(ctx.id);
    } catch (e) {
        Logger.printError('WHATAP-265', 'gRPC end transaction error: ' + e, false);
        TraceContextManager.end(ctx.id);
        ctx = null;
    }
}

function initCtx(call, methodName) {
    if (!grpc_profile_enabled) {
        return null;
    }

    const ctx = TraceContextManager.start();
    if (!ctx) {
        return null;
    }

    // gRPC 메타데이터에서 정보 추출
    try {
        if (call.metadata) {
            // Remote IP 추출 시도
            const metadata = call.metadata.getMap();
            ctx.remoteIp = metadata['x-forwarded-for'] || metadata['x-real-ip'] || '0.0.0.0';
            // WClientId를 빈 문자열로 보내 go-agent가 int64(RemoteIp) 분기를 타도록 함.
            ctx.userid = "";
        }
    } catch (e) {
        Logger.printError('WHATAP-264', 'gRPC metadata parsing error: ' + e, false);
    }

    ctx.service_name = methodName;
    ctx.service_hash = HashUtil.hashFromString(methodName);
    ctx.start_time = Date.now();

    return ctx;
}

exports.GRpcObserver = GRpcObserver;