Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });

const api = require('@opentelemetry/api');
const core = require('@opentelemetry/core');
const instrumentation = require('@opentelemetry/instrumentation');
const semanticConventions = require('@opentelemetry/semantic-conventions');
const core$1 = require('@sentry/core');
const AttributeNames = require('./enums/AttributeNames.js');
const utils = require('./utils.js');

// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable jsdoc/require-jsdoc */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */

/** @knipignore */

const PACKAGE_VERSION = '0.1.0';

const PACKAGE_NAME = '@sentry/instrumentation-fastify-v3';
const ANONYMOUS_NAME = 'anonymous';

// The instrumentation creates a span for invocations of lifecycle hook handlers
// that take `(request, reply, ...[, done])` arguments. Currently this is all
// lifecycle hooks except `onRequestAbort`.
// https://fastify.dev/docs/latest/Reference/Hooks
const hooksNamesToWrap = new Set([
  'onTimeout',
  'onRequest',
  'preParsing',
  'preValidation',
  'preSerialization',
  'preHandler',
  'onSend',
  'onResponse',
  'onError',
]);

/**
 * Fastify instrumentation for OpenTelemetry
 */
class FastifyInstrumentationV3 extends instrumentation.InstrumentationBase {
   constructor(config = {}) {
    super(PACKAGE_NAME, PACKAGE_VERSION, config);
  }

   init() {
    return [
      new instrumentation.InstrumentationNodeModuleDefinition('fastify', ['>=3.0.0 <4'], moduleExports => {
        return this._patchConstructor(moduleExports);
      }),
    ];
  }

   _hookOnRequest() {
    const instrumentation = this;

    return function onRequest(request, reply, done) {
      if (!instrumentation.isEnabled()) {
        return done();
      }
      instrumentation._wrap(reply, 'send', instrumentation._patchSend());

      const anyRequest = request ;

      const rpcMetadata = core.getRPCMetadata(api.context.active());
      const routeName = anyRequest.routeOptions
        ? anyRequest.routeOptions.url // since fastify@4.10.0
        : request.routerPath;
      if (routeName && rpcMetadata?.type === core.RPCType.HTTP) {
        rpcMetadata.route = routeName;
      }

      const method = request.method || 'GET';

      core$1.getIsolationScope().setTransactionName(`${method} ${routeName}`);
      done();
    };
  }

   _wrapHandler(
    pluginName,
    hookName,
    original,
    syncFunctionWithDone,
  ) {
    const instrumentation = this;
    this._diag.debug('Patching fastify route.handler function');

    return function ( ...args) {
      if (!instrumentation.isEnabled()) {
        return original.apply(this, args);
      }

      const name = original.name || pluginName || ANONYMOUS_NAME;
      const spanName = `${AttributeNames.FastifyNames.MIDDLEWARE} - ${name}`;

      const reply = args[1] ;

      const span = utils.startSpan(reply, instrumentation.tracer, spanName, {
        [AttributeNames.AttributeNames.FASTIFY_TYPE]: AttributeNames.FastifyTypes.MIDDLEWARE,
        [AttributeNames.AttributeNames.PLUGIN_NAME]: pluginName,
        [AttributeNames.AttributeNames.HOOK_NAME]: hookName,
      });

      const origDone = syncFunctionWithDone && (args[args.length - 1] );
      if (origDone) {
        args[args.length - 1] = function (...doneArgs) {
          utils.endSpan(reply);
          origDone.apply(this, doneArgs);
        };
      }

      return api.context.with(api.trace.setSpan(api.context.active(), span), () => {
        return utils.safeExecuteInTheMiddleMaybePromise(
          () => {
            return original.apply(this, args);
          },
          err => {
            if (err instanceof Error) {
              span.setStatus({
                code: api.SpanStatusCode.ERROR,
                message: err.message,
              });
              span.recordException(err);
            }
            // async hooks should end the span as soon as the promise is resolved
            if (!syncFunctionWithDone) {
              utils.endSpan(reply);
            }
          },
        );
      });
    };
  }

   _wrapAddHook() {
    const instrumentation = this;
    this._diag.debug('Patching fastify server.addHook function');

    // biome-ignore lint/complexity/useArrowFunction: <explanation>
    return function (original) {
      return function wrappedAddHook( ...args) {
        const name = args[0] ;
        const handler = args[1] ;
        const pluginName = this.pluginName;
        if (!hooksNamesToWrap.has(name)) {
          return original.apply(this, args);
        }

        const syncFunctionWithDone =
          typeof args[args.length - 1] === 'function' && handler.constructor.name !== 'AsyncFunction';

        return original.apply(this, [
          name,
          instrumentation._wrapHandler(pluginName, name, handler, syncFunctionWithDone),
        ] );
      };
    };
  }

   _patchConstructor(moduleExports

) {
    const instrumentation = this;

    function fastify( ...args) {
      const app = moduleExports.fastify.apply(this, args);
      app.addHook('onRequest', instrumentation._hookOnRequest());
      app.addHook('preHandler', instrumentation._hookPreHandler());

      instrumentClient();

      instrumentation._wrap(app, 'addHook', instrumentation._wrapAddHook());

      return app;
    }

    if (moduleExports.errorCodes !== undefined) {
      fastify.errorCodes = moduleExports.errorCodes;
    }
    fastify.fastify = fastify;
    fastify.default = fastify;
    return fastify;
  }

   _patchSend() {
    const instrumentation$1 = this;
    this._diag.debug('Patching fastify reply.send function');

    return function patchSend(original) {
      return function send( ...args) {
        const maybeError = args[0];

        if (!instrumentation$1.isEnabled()) {
          return original.apply(this, args);
        }

        return instrumentation.safeExecuteInTheMiddle(
          () => {
            return original.apply(this, args);
          },
          err => {
            if (!err && maybeError instanceof Error) {
              // eslint-disable-next-line no-param-reassign
              err = maybeError;
            }
            utils.endSpan(this, err);
          },
        );
      };
    };
  }

   _hookPreHandler() {
    const instrumentation$1 = this;
    this._diag.debug('Patching fastify preHandler function');

    return function preHandler( request, reply, done) {
      if (!instrumentation$1.isEnabled()) {
        return done();
      }
      const anyRequest = request ;

      const handler = anyRequest.routeOptions?.handler || anyRequest.context?.handler;
      const handlerName = handler?.name.startsWith('bound ') ? handler.name.substring(6) : handler?.name;
      const spanName = `${AttributeNames.FastifyNames.REQUEST_HANDLER} - ${handlerName || this.pluginName || ANONYMOUS_NAME}`;

      const spanAttributes = {
        [AttributeNames.AttributeNames.PLUGIN_NAME]: this.pluginName,
        [AttributeNames.AttributeNames.FASTIFY_TYPE]: AttributeNames.FastifyTypes.REQUEST_HANDLER,
        // eslint-disable-next-line deprecation/deprecation
        [semanticConventions.SEMATTRS_HTTP_ROUTE]: anyRequest.routeOptions
          ? anyRequest.routeOptions.url // since fastify@4.10.0
          : request.routerPath,
      };
      if (handlerName) {
        spanAttributes[AttributeNames.AttributeNames.FASTIFY_NAME] = handlerName;
      }
      const span = utils.startSpan(reply, instrumentation$1.tracer, spanName, spanAttributes);

      addFastifyV3SpanAttributes(span);

      const { requestHook } = instrumentation$1.getConfig();
      if (requestHook) {
        instrumentation.safeExecuteInTheMiddle(
          () => requestHook(span, { request }),
          e => {
            if (e) {
              instrumentation$1._diag.error('request hook failed', e);
            }
          },
          true,
        );
      }

      return api.context.with(api.trace.setSpan(api.context.active(), span), () => {
        done();
      });
    };
  }
}

function instrumentClient() {
  const client = core$1.getClient();
  if (client) {
    client.on('spanStart', (span) => {
      addFastifyV3SpanAttributes(span);
    });
  }
}

function addFastifyV3SpanAttributes(span) {
  const attributes = core$1.spanToJSON(span).data;

  // this is one of: middleware, request_handler
  const type = attributes['fastify.type'];

  // If this is already set, or we have no fastify span, no need to process again...
  if (attributes[core$1.SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
    return;
  }

  span.setAttributes({
    [core$1.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.fastify',
    [core$1.SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.fastify`,
  });

  // Also update the name, we don't need to "middleware - " prefix
  const name = attributes['fastify.name'] || attributes['plugin.name'] || attributes['hook.name'];
  if (typeof name === 'string') {
    // Try removing `fastify -> ` and `@fastify/otel -> ` prefixes
    // This is a bit of a hack, and not always working for all spans
    // But it's the best we can do without a proper API
    const updatedName = name.replace(/^fastify -> /, '').replace(/^@fastify\/otel -> /, '');

    span.updateName(updatedName);
  }
}

exports.FastifyInstrumentationV3 = FastifyInstrumentationV3;
