/**
 * 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');

var profile_graphql_enabled = conf.getProperty('profile_graphql_enabled', true);
var profile_graphql_variable_enabled = conf.getProperty('profile_graphql_variable_enabled', false);
var ignore_graphql_operation = conf.getProperty('ignore_graphql_operation', '');

conf.on('profile_graphql_enabled', function(newProperty) {
    profile_graphql_enabled = newProperty;
});
conf.on('profile_graphql_variable_enabled', function(newProperty) {
    profile_graphql_variable_enabled = newProperty;
});
conf.on('ignore_graphql_operation', function(newProperty) {
    ignore_graphql_operation = newProperty;
});

var ApolloServerObserver = function(agent) {
    this.agent = agent;
    this.packages = ['@apollo/server'];
};

function checkIgnoreOperation(ignore_operation, operation_name) {
    try {
        let ignore_operation_set = null;
        if (ignore_operation) {
            ignore_operation_set = new Set(ignore_operation.split(','));
        } else {
            ignore_operation_set = null;
        }
        if (ignore_operation_set && ignore_operation_set.has(operation_name)) {
            return true;
        }
    } catch (e) {
        Logger.printError('WHATAP-248', 'GraphQL checkIgnoreOperation error: ' + e, false);
    }
    return false;
}

function wrapExecuteHTTPGraphQLRequest(original) {
    return async function executeHTTPGraphQLRequest() {
        if (!profile_graphql_enabled) {
            return original.apply(this, arguments);
        }

        try {
            var ctx = TraceContextManager.getCurrentContext();
            if (!ctx) {
                return original.apply(this, arguments);
            }

            const [requestContext] = arguments;
            const { httpGraphQLRequest } = requestContext;
            const { body } = httpGraphQLRequest;

            const operationName = body.operationName;
            if (checkIgnoreOperation(ignore_graphql_operation, operationName)) {
                TraceContextManager.end(ctx._id)
                return original.apply(this, arguments);
            }

            const originalServiceName = ctx.service_name || '';
            if(operationName){
                ctx.service_name = `${originalServiceName}?operationName=${operationName}`;
                ctx.service_hash = HashUtil.hashFromString(ctx.service_name);
            }

            // Operation Type Step (query)
            if(body.query && body.query.trim()){
                const queryType = body.query.trim().startsWith('mutation') ? 'mutation' : 'query';
                let typeDatas = ['Type', 'Type', queryType];
                ctx.start_time = Date.now();
                AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, typeDatas);
            }

            // Operation Name Step
            if(operationName){
                let operationDatas = ['Operation', 'Operation', operationName || 'anonymous'];
                ctx.start_time = Date.now();
                AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, operationDatas);
            }

            // Variables Step
            if (profile_graphql_variable_enabled && body.variables && Object.keys(body.variables).length > 0) {
                const variableKeys = JSON.stringify(Object.keys(body.variables));
                let variableDatas = ['Variables', 'Variables', variableKeys];
                ctx.start_time = Date.now();
                AsyncSender.send_packet(PacketTypeEnum.TX_MSG, ctx, variableDatas);
            }

            const response = await original.apply(this, arguments);

            // 에러 처리
            if (response.body.kind === 'complete' && response.body.string.includes('"errors"')) {
                try {
                    const result = JSON.parse(response.body.string);
                    if (result.errors && result.errors.length > 0) {
                        const errorMessages = result.errors.map(error => error.message).join('\n');
                        ctx.status = response.status;

                        if (response.status >= 400 && !ctx.error) {
                            ctx.error = 1;
                            let errors = ['GraphQLError', errorMessages];
                            AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
                        }
                    }
                } catch (e) {
                    Logger.printError('WHATAP-249', 'GraphQL error parsing failed', e, false);
                }
            }

            return response;

        } catch (err) {
            Logger.printError('WHATAP-250', 'GraphQL executeHTTPGraphQLRequest error: ' + err, false);
            if (ctx) {
                ctx.status = 500;
                if (!ctx.error) {
                    ctx.error = 1;
                    let errors = [err.name || err.constructor?.name || 'UnknownError', err.message];
                    AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
                }
            }
            return original.apply(this, arguments);
        }
    };
}

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

    if (profile_graphql_enabled) {
        // Hook ApolloServer
        if (mod.ApolloServer && mod.ApolloServer.prototype) {
            shimmer.wrap(mod.ApolloServer.prototype, 'executeHTTPGraphQLRequest', wrapExecuteHTTPGraphQLRequest);
        }
    }
};

exports.ApolloServerObserver = ApolloServerObserver;