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

const api = require('@opentelemetry/api');
const instrumentation = require('@opentelemetry/instrumentation');
const core = require('@sentry/core');
const constants = require('./constants.js');

const PACKAGE_NAME = '@sentry/instrumentation-hono';
const PACKAGE_VERSION = '0.0.1';

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

  /**
   * Initialize the instrumentation.
   */
   init() {
    return [
      new instrumentation.InstrumentationNodeModuleDefinition('hono', ['>=4.0.0 <5'], moduleExports => this._patch(moduleExports)),
    ];
  }

  /**
   * Patches the module exports to instrument Hono.
   */
   _patch(moduleExports) {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const instrumentation = this;

    class WrappedHono extends moduleExports.Hono {
       constructor(...args) {
        super(...args);

        instrumentation._wrap(this, 'get', instrumentation._patchHandler());
        instrumentation._wrap(this, 'post', instrumentation._patchHandler());
        instrumentation._wrap(this, 'put', instrumentation._patchHandler());
        instrumentation._wrap(this, 'delete', instrumentation._patchHandler());
        instrumentation._wrap(this, 'options', instrumentation._patchHandler());
        instrumentation._wrap(this, 'patch', instrumentation._patchHandler());
        instrumentation._wrap(this, 'all', instrumentation._patchHandler());
        instrumentation._wrap(this, 'on', instrumentation._patchOnHandler());
        instrumentation._wrap(this, 'use', instrumentation._patchMiddlewareHandler());
      }
    }

    try {
      moduleExports.Hono = WrappedHono;
    } catch {
      // This is a workaround for environments where direct assignment is not allowed.
      return { ...moduleExports, Hono: WrappedHono };
    }

    return moduleExports;
  }

  /**
   * Patches the route handler to instrument it.
   */
   _patchHandler() {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const instrumentation = this;

    return function (original) {
      return function wrappedHandler( ...args) {
        if (typeof args[0] === 'string') {
          const path = args[0];
          if (args.length === 1) {
            return original.apply(this, [path]);
          }

          const handlers = args.slice(1);
          return original.apply(this, [
            path,
            ...handlers.map(handler => instrumentation._wrapHandler(handler )),
          ]);
        }

        return original.apply(
          this,
          args.map(handler => instrumentation._wrapHandler(handler )),
        );
      };
    };
  }

  /**
   * Patches the 'on' handler to instrument it.
   */
   _patchOnHandler() {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const instrumentation = this;

    return function (original) {
      return function wrappedHandler( ...args) {
        const handlers = args.slice(2);
        return original.apply(this, [
          ...args.slice(0, 2),
          ...handlers.map(handler => instrumentation._wrapHandler(handler )),
        ]);
      };
    };
  }

  /**
   * Patches the middleware handler to instrument it.
   */
   _patchMiddlewareHandler() {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const instrumentation = this;

    return function (original) {
      return function wrappedHandler( ...args) {
        if (typeof args[0] === 'string') {
          const path = args[0];
          if (args.length === 1) {
            return original.apply(this, [path]);
          }

          const handlers = args.slice(1);
          return original.apply(this, [
            path,
            ...handlers.map(handler => instrumentation._wrapHandler(handler )),
          ]);
        }

        return original.apply(
          this,
          args.map(handler => instrumentation._wrapHandler(handler )),
        );
      };
    };
  }

  /**
   * Wraps a handler or middleware handler to apply instrumentation.
   */
   _wrapHandler(handler) {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const instrumentation = this;

    return function ( c, next) {
      if (!instrumentation.isEnabled()) {
        return handler.apply(this, [c, next]);
      }

      const path = c.req.path;
      const span = instrumentation.tracer.startSpan(path);

      return api.context.with(api.trace.setSpan(api.context.active(), span), () => {
        return instrumentation._safeExecute(
          () => {
            const result = handler.apply(this, [c, next]);
            if (core.isThenable(result)) {
              return result.then(result => {
                const type = instrumentation._determineHandlerType(result);
                span.setAttributes({
                  [constants.AttributeNames.HONO_TYPE]: type,
                  [constants.AttributeNames.HONO_NAME]: type === constants.HonoTypes.REQUEST_HANDLER ? path : handler.name || 'anonymous',
                });
                instrumentation.getConfig().responseHook?.(span);
                return result;
              });
            } else {
              const type = instrumentation._determineHandlerType(result);
              span.setAttributes({
                [constants.AttributeNames.HONO_TYPE]: type,
                [constants.AttributeNames.HONO_NAME]: type === constants.HonoTypes.REQUEST_HANDLER ? path : handler.name || 'anonymous',
              });
              instrumentation.getConfig().responseHook?.(span);
              return result;
            }
          },
          () => span.end(),
          error => {
            instrumentation._handleError(span, error);
            span.end();
          },
        );
      });
    };
  }

  /**
   * Safely executes a function and handles errors.
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   _safeExecute(execute, onSuccess, onFailure) {
    try {
      const result = execute();

      if (core.isThenable(result)) {
        result.then(
          () => onSuccess(),
          (error) => onFailure(error),
        );
      } else {
        onSuccess();
      }

      return result;
    } catch (error) {
      onFailure(error);
      throw error;
    }
  }

  /**
   * Determines the handler type based on the result.
   * @param result
   * @private
   */
   _determineHandlerType(result) {
    return result === undefined ? constants.HonoTypes.MIDDLEWARE : constants.HonoTypes.REQUEST_HANDLER;
  }

  /**
   * Handles errors by setting the span status and recording the exception.
   */
   _handleError(span, error) {
    if (error instanceof Error) {
      span.setStatus({
        code: api.SpanStatusCode.ERROR,
        message: error.message,
      });
      span.recordException(error);
    }
  }
}

exports.HonoInstrumentation = HonoInstrumentation;
