import { trace, context } from '@opentelemetry/api';
import { InstrumentationBase, InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation';
import { ATTR_DB_RESPONSE_STATUS_CODE, ATTR_ERROR_TYPE, ATTR_DB_OPERATION_NAME, ATTR_DB_SYSTEM_NAME, ATTR_DB_NAMESPACE, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_DB_QUERY_TEXT } from '@opentelemetry/semantic-conventions';
import { SDK_VERSION, SPAN_STATUS_ERROR, startSpanManual, getCurrentScope, debug, defineIntegration } from '@sentry/core';
import { generateInstrumentOnce, addOriginToSpan } from '@sentry/node-core';

// Instrumentation for https://github.com/porsager/postgres

const INTEGRATION_NAME = 'PostgresJs';
const SUPPORTED_VERSIONS = ['>=3.0.0 <4'];

const instrumentPostgresJs = generateInstrumentOnce(
  INTEGRATION_NAME,
  (options) =>
    new PostgresJsInstrumentation({
      requireParentSpan: options?.requireParentSpan ?? true,
      requestHook: options?.requestHook,
    }),
);

/**
 * Instrumentation for the [postgres](https://www.npmjs.com/package/postgres) library.
 * This instrumentation captures postgresjs queries and their attributes,
 */
class PostgresJsInstrumentation extends InstrumentationBase {
   constructor(config) {
    super('sentry-postgres-js', SDK_VERSION, config);
  }

  /**
   * Initializes the instrumentation.
   */
   init() {
    const instrumentationModule = new InstrumentationNodeModuleDefinition('postgres', SUPPORTED_VERSIONS);

    ['src', 'cf/src', 'cjs/src'].forEach(path => {
      instrumentationModule.files.push(
        new InstrumentationNodeModuleFile(
          `postgres/${path}/connection.js`,
          ['*'],
          this._patchConnection.bind(this),
          this._unwrap.bind(this),
        ),
      );

      instrumentationModule.files.push(
        new InstrumentationNodeModuleFile(
          `postgres/${path}/query.js`,
          SUPPORTED_VERSIONS,
          this._patchQuery.bind(this),
          this._unwrap.bind(this),
        ),
      );
    });

    return [instrumentationModule];
  }

  /**
   * Determines whether a span should be created based on the current context.
   * If `requireParentSpan` is set to true in the configuration, a span will
   * only be created if there is a parent span available.
   */
   _shouldCreateSpans() {
    const config = this.getConfig();
    const hasParentSpan = trace.getSpan(context.active()) !== undefined;
    return hasParentSpan || !config.requireParentSpan;
  }

  /**
   * Patches the reject method of the Query class to set the span status and end it
   */
   _patchReject(rejectTarget, span) {
    return new Proxy(rejectTarget, {
      apply: (
        rejectTarget,
        rejectThisArg,
        rejectArgs

,
      ) => {
        span.setStatus({
          code: SPAN_STATUS_ERROR,
          // This message is the error message from the rejectArgs, when available
          // e.g "relation 'User' does not exist"
          message: rejectArgs?.[0]?.message || 'internal_error',
        });

        const result = Reflect.apply(rejectTarget, rejectThisArg, rejectArgs);

        // This status code is PG error code, e.g. '42P01' for "relation does not exist"
        // https://www.postgresql.org/docs/current/errcodes-appendix.html
        span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'Unknown error');
        // This is the error type, e.g. 'PostgresError' for a Postgres error
        span.setAttribute(ATTR_ERROR_TYPE, rejectArgs?.[0]?.name || 'Unknown error');

        span.end();
        return result;
      },
    });
  }

  /**
   * Patches the resolve method of the Query class to end the span when the query is resolved.
   */
   _patchResolve(resolveTarget, span) {
    return new Proxy(resolveTarget, {
      apply: (resolveTarget, resolveThisArg, resolveArgs) => {
        const result = Reflect.apply(resolveTarget, resolveThisArg, resolveArgs);
        const sqlCommand = resolveArgs?.[0]?.command;

        if (sqlCommand) {
          // SQL command is only available when the query is resolved successfully
          span.setAttribute(ATTR_DB_OPERATION_NAME, sqlCommand);
        }
        span.end();
        return result;
      },
    });
  }

  /**
   * Patches the Query class to instrument the handle method.
   */
   _patchQuery(moduleExports

) {
    moduleExports.Query.prototype.handle = new Proxy(moduleExports.Query.prototype.handle, {
      apply: async (
        handleTarget,
        handleThisArg

,
        handleArgs,
      ) => {
        if (!this._shouldCreateSpans()) {
          // If we don't need to create spans, just call the original method
          return Reflect.apply(handleTarget, handleThisArg, handleArgs);
        }

        const sanitizedSqlQuery = this._sanitizeSqlQuery(handleThisArg.strings?.[0]);

        return startSpanManual(
          {
            name: sanitizedSqlQuery || 'postgresjs.query',
            op: 'db',
          },
          (span) => {
            const scope = getCurrentScope();
            const postgresConnectionContext = scope.getScopeData().contexts['postgresjsConnection']

;

            addOriginToSpan(span, 'auto.db.otel.postgres');

            const { requestHook } = this.getConfig();

            if (requestHook) {
              safeExecuteInTheMiddle(
                () => requestHook(span, sanitizedSqlQuery, postgresConnectionContext),
                error => {
                  if (error) {
                    debug.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, error);
                  }
                },
              );
            }

            // ATTR_DB_NAMESPACE is used to indicate the database name and the schema name
            // It's only the database name as we don't have the schema information
            const databaseName = postgresConnectionContext?.ATTR_DB_NAMESPACE || '<unknown database>';
            const databaseHost = postgresConnectionContext?.ATTR_SERVER_ADDRESS || '<unknown host>';
            const databasePort = postgresConnectionContext?.ATTR_SERVER_PORT || '<unknown port>';

            span.setAttribute(ATTR_DB_SYSTEM_NAME, 'postgres');
            span.setAttribute(ATTR_DB_NAMESPACE, databaseName);
            span.setAttribute(ATTR_SERVER_ADDRESS, databaseHost);
            span.setAttribute(ATTR_SERVER_PORT, databasePort);
            span.setAttribute(ATTR_DB_QUERY_TEXT, sanitizedSqlQuery);

            handleThisArg.resolve = this._patchResolve(handleThisArg.resolve, span);
            handleThisArg.reject = this._patchReject(handleThisArg.reject, span);

            try {
              return Reflect.apply(handleTarget, handleThisArg, handleArgs);
            } catch (error) {
              span.setStatus({
                code: SPAN_STATUS_ERROR,
              });
              span.end();
              throw error; // Re-throw the error to propagate it
            }
          },
        );
      },
    });

    return moduleExports;
  }

  /**
   * Patches the Connection class to set the database, host, and port attributes
   * when a new connection is created.
   */
   _patchConnection(Connection) {
    return new Proxy(Connection, {
      apply: (connectionTarget, thisArg, connectionArgs) => {
        const databaseName = connectionArgs[0]?.database || '<unknown database>';
        const databaseHost = connectionArgs[0]?.host?.[0] || '<unknown host>';
        const databasePort = connectionArgs[0]?.port?.[0] || '<unknown port>';

        const scope = getCurrentScope();
        scope.setContext('postgresjsConnection', {
          ATTR_DB_NAMESPACE: databaseName,
          ATTR_SERVER_ADDRESS: databaseHost,
          ATTR_SERVER_PORT: databasePort,
        });

        return Reflect.apply(connectionTarget, thisArg, connectionArgs);
      },
    });
  }

  /**
   * Sanitize SQL query as per the OTEL semantic conventions
   * https://opentelemetry.io/docs/specs/semconv/database/database-spans/#sanitization-of-dbquerytext
   */
   _sanitizeSqlQuery(sqlQuery) {
    if (!sqlQuery) {
      return 'Unknown SQL Query';
    }

    return (
      sqlQuery
        .replace(/\s+/g, ' ')
        .trim() // Remove extra spaces including newlines and trim
        .substring(0, 1024) // Truncate to 1024 characters
        .replace(/--.*?(\r?\n|$)/g, '') // Single line comments
        .replace(/\/\*[\s\S]*?\*\//g, '') // Multi-line comments
        .replace(/;\s*$/, '') // Remove trailing semicolons
        .replace(/\b\d+\b/g, '?') // Replace standalone numbers
        // Collapse whitespace to a single space
        .replace(/\s+/g, ' ')
        // Collapse IN and in clauses
        // eg. IN (?, ?, ?, ?) to IN (?)
        .replace(/\bIN\b\s*\(\s*\?(?:\s*,\s*\?)*\s*\)/g, 'IN (?)')
    );
  }
}

const _postgresJsIntegration = (() => {
  return {
    name: INTEGRATION_NAME,
    setupOnce() {
      instrumentPostgresJs();
    },
  };
}) ;

/**
 * Adds Sentry tracing instrumentation for the [postgres](https://www.npmjs.com/package/postgres) library.
 *
 * For more information, see the [`postgresIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/postgres/).
 *
 * @example
 * ```javascript
 * const Sentry = require('@sentry/node');
 *
 * Sentry.init({
 *  integrations: [Sentry.postgresJsIntegration()],
 * });
 * ```
 */

const postgresJsIntegration = defineIntegration(_postgresJsIntegration);

export { PostgresJsInstrumentation, instrumentPostgresJs, postgresJsIntegration };
