# frozen_string_literal: true

module Gitlab
  module ErrorTracking
    class << self
      def configure
        Raven.configure do |config|
          config.dsn = sentry_dsn
          config.release = Gitlab.revision
          config.current_environment = Gitlab.config.sentry.environment

          # Sanitize fields based on those sanitized from Rails.
          config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
          # Sanitize authentication headers
          config.sanitize_http_headers = %w[Authorization Private-Token]
          config.tags = { program: Gitlab.process_name }
          # Debugging for https://gitlab.com/gitlab-org/gitlab-foss/issues/57727
          config.before_send = method(:add_context_from_exception_type)
        end
      end

      def with_context(current_user = nil)
        last_user_context = Raven.context.user

        user_context = {
          id: current_user&.id,
          email: current_user&.email,
          username: current_user&.username
        }.compact

        Raven.tags_context(default_tags)
        Raven.user_context(user_context)

        yield
      ensure
        Raven.user_context(last_user_context)
      end

      # This should be used when you want to passthrough exception handling:
      # rescue and raise to be catched in upper layers of the application.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      def track_and_raise_exception(exception, extra = {})
        process_exception(exception, sentry: true, extra: extra)

        raise exception
      end

      # This can be used for investigating exceptions that can be recovered from in
      # code. The exception will still be raised in development and test
      # environments.
      #
      # That way we can track down these exceptions with as much information as we
      # need to resolve them.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      #
      # Provide an issue URL for follow up.
      # as `issue_url: 'http://gitlab.com/gitlab-org/gitlab/issues/111'`
      def track_and_raise_for_dev_exception(exception, extra = {})
        process_exception(exception, sentry: true, extra: extra)

        raise exception if should_raise_for_dev?
      end

      # This should be used when you only want to track the exception.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      def track_exception(exception, extra = {})
        process_exception(exception, sentry: true, extra: extra)
      end

      # This should be used when you only want to log the exception,
      # but not send it to Sentry.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      def log_exception(exception, extra = {})
        process_exception(exception, extra: extra)
      end

      private

      def process_exception(exception, sentry: false, logging: true, extra:)
        exception.try(:sentry_extra_data)&.tap do |data|
          extra = extra.merge(data) if data.is_a?(Hash)
        end

        extra = sanitize_request_parameters(extra)

        if sentry && Raven.configuration.server
          Raven.capture_exception(exception, tags: default_tags, extra: extra)
        end

        if logging
          # TODO: this logic could migrate into `Gitlab::ExceptionLogFormatter`
          # and we could also flatten deep nested hashes if required for search
          # (e.g. if `extra` includes hash of hashes).
          # In the current implementation, we don't flatten multi-level folded hashes.
          log_hash = {}
          Raven.context.tags.each { |name, value| log_hash["tags.#{name}"] = value }
          Raven.context.user.each { |name, value| log_hash["user.#{name}"] = value }
          Raven.context.extra.merge(extra).each { |name, value| log_hash["extra.#{name}"] = value }

          Gitlab::ExceptionLogFormatter.format!(exception, log_hash)

          Gitlab::ErrorTracking::Logger.error(log_hash)
        end
      end

      def sanitize_request_parameters(parameters)
        filter = ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters)
        filter.filter(parameters)
      end

      def sentry_dsn
        return unless Rails.env.production? || Rails.env.development?
        return unless Gitlab.config.sentry.enabled

        Gitlab.config.sentry.dsn
      end

      def should_raise_for_dev?
        Rails.env.development? || Rails.env.test?
      end

      def default_tags
        {
          Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id,
          locale: I18n.locale
        }
      end

      def add_context_from_exception_type(event, hint)
        if ActiveModel::MissingAttributeError === hint[:exception]
          columns_hash = ActiveRecord::Base
                            .connection
                            .schema_cache
                            .instance_variable_get(:@columns_hash)
                            .map { |k, v| [k, v.map(&:first)] }
                            .to_h

          event.extra.merge!(columns_hash)
        end

        event
      end
    end
  end
end