import { _optionalChain } from '@sentry/utils';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { defineIntegration, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_OP, getIsolationScope, getDefaultIsolationScope, getClient, captureException } from '@sentry/core';
import { logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../../debug-build.js';
import { generateInstrumentOnce } from '../../otel/instrument.js';
import { addOriginToSpan } from '../../utils/addOriginToSpan.js';
import { ensureIsWrapped } from '../../utils/ensureIsWrapped.js';

const INTEGRATION_NAME = 'Express';

const instrumentExpress = generateInstrumentOnce(
  INTEGRATION_NAME,
  () =>
    new ExpressInstrumentation({
      requestHook(span) {
        addOriginToSpan(span, 'auto.http.otel.express');

        const attributes = spanToJSON(span).data || {};
        // this is one of: middleware, request_handler, router
        const type = attributes['express.type'];

        if (type) {
          span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.express`);
        }

        // Also update the name, we don't need to "middleware - " prefix
        const name = attributes['express.name'];
        if (typeof name === 'string') {
          span.updateName(name);
        }
      },
      spanNameHook(info, defaultName) {
        if (getIsolationScope() === getDefaultIsolationScope()) {
          DEBUG_BUILD &&
            logger.warn('Isolation scope is still default isolation scope - skipping setting transactionName');
          return defaultName;
        }
        if (info.layerType === 'request_handler') {
          // type cast b/c Otel unfortunately types info.request as any :(
          const req = info.request ;
          const method = req.method ? req.method.toUpperCase() : 'GET';
          getIsolationScope().setTransactionName(`${method} ${info.route}`);
        }
        return defaultName;
      },
    }),
);

const _expressIntegration = (() => {
  return {
    name: INTEGRATION_NAME,
    setupOnce() {
      instrumentExpress();
    },
  };
}) ;

/**
 * Express integration
 *
 * Capture tracing data for express.
 * In order to capture exceptions, you have to call `setupExpressErrorHandler(app)` before any other middleware and after all controllers.
 */
const expressIntegration = defineIntegration(_expressIntegration);

/**
 * An Express-compatible error handler.
 */
function expressErrorHandler(options) {
  return function sentryErrorMiddleware(
    error,
    _req,
    res,
    next,
  ) {
    const shouldHandleError = _optionalChain([options, 'optionalAccess', _ => _.shouldHandleError]) || defaultShouldHandleError;

    if (shouldHandleError(error)) {
      const client = getClient();
      if (client && client.getOptions().autoSessionTracking) {
        // Check if the `SessionFlusher` is instantiated on the client to go into this branch that marks the
        // `requestSession.status` as `Crashed`, and this check is necessary because the `SessionFlusher` is only
        // instantiated when the the`requestHandler` middleware is initialised, which indicates that we should be
        // running in SessionAggregates mode
        const isSessionAggregatesMode = client['_sessionFlusher'] !== undefined;
        if (isSessionAggregatesMode) {
          const requestSession = getIsolationScope().getRequestSession();
          // If an error bubbles to the `errorHandler`, then this is an unhandled error, and should be reported as a
          // Crashed session. The `_requestSession.status` is checked to ensure that this error is happening within
          // the bounds of a request, and if so the status is updated
          if (requestSession && requestSession.status !== undefined) {
            requestSession.status = 'crashed';
          }
        }
      }

      const eventId = captureException(error, { mechanism: { type: 'middleware', handled: false } });
      (res ).sentry = eventId;
      next(error);

      return;
    }

    next(error);
  };
}

/**
 * Setup an error handler for Express.
 * The error handler must be before any other middleware and after all controllers.
 */
function setupExpressErrorHandler(
  app,
  options,
) {
  app.use(expressErrorHandler(options));
  ensureIsWrapped(app.use, 'express');
}

function getStatusCodeFromResponse(error) {
  const statusCode = error.status || error.statusCode || error.status_code || (error.output && error.output.statusCode);
  return statusCode ? parseInt(statusCode , 10) : 500;
}

/** Returns true if response code is internal server error */
function defaultShouldHandleError(error) {
  const status = getStatusCodeFromResponse(error);
  return status >= 500;
}

export { expressErrorHandler, expressIntegration, instrumentExpress, setupExpressErrorHandler };
