module Raven
  # A copy of Raven's base module class methods, minus some of the integration
  # and global hooks since it's meant to be used explicitly. Useful for
  # sending errors to multiple sentry projects in a large application.
  #
  # @example
  #   class Foo
  #     def initialize
  #       @other_raven = Raven::Instance.new
  #       @other_raven.configure do |config|
  #         config.server = 'http://...'
  #       end
  #     end
  #
  #     def foo
  #       # ...
  #     rescue => e
  #       @other_raven.capture_exception(e)
  #     end
  #   end
  class Instance
    # See Raven::Client.
    attr_writer :client

    # See Raven::Configuration.
    attr_accessor :configuration

    def initialize(context = nil, config = nil)
      @context = @explicit_context = context
      self.configuration = config || Configuration.new
    end

    def context
      if @explicit_context
        @context ||= Context.new
      else
        Context.current
      end
    end

    def logger
      configuration.logger
    end

    # The client object is responsible for delivering formatted data to the
    # Sentry server.
    def client
      @client ||= Client.new(configuration)
    end

    # Tell the log that the client is good to go
    def report_status
      return unless configuration.enabled_in_current_env?
      return if configuration.silence_ready

      if configuration.capture_allowed?
        logger.info "Raven #{VERSION} ready to catch errors"
      else
        logger.info "Raven #{VERSION} configured not to capture errors: #{configuration.error_messages}"
      end
    end

    # Call this method to modify defaults in your initializers.
    #
    # @example
    #   Raven.configure do |config|
    #     config.server = 'http://...'
    #   end
    def configure
      yield(configuration) if block_given?

      self.client = Client.new(configuration)
      report_status
      client
    end

    # Send an event to the configured Sentry server
    #
    # @example
    #   evt = Raven::Event.new(:message => "An errore)
    #   Raven.send_event(evt)
    def send_event(event, hint = nil)
      client.send_event(event, hint)
    end

    # Capture and process any exceptions from the given block.
    #
    # @example
    #   Raven.capture do
    #     MyApp.run
    #   end
    def capture(options = {})
      if block_given?
        begin
          yield
        rescue Error
          raise # Don't capture Raven errors
        rescue Exception => e
          capture_type(e, options)
          raise
        end
      else
        install_at_exit_hook(options)
      end
    end

    def capture_type(obj, options = {})
      unless configuration.capture_allowed?(obj)
        logger.debug("#{obj} excluded from capture: #{configuration.error_messages}")
        return false
      end

      message_or_exc = obj.is_a?(String) ? "message" : "exception"
      options = options.deep_dup
      options[:configuration] = configuration
      options[:context] = context
      options[:breadcrumbs] = breadcrumbs

      if evt = Event.send("from_" + message_or_exc, obj, options)
        yield evt if block_given?
        if configuration.async?
          begin
            # We have to convert to a JSON-like hash, because background job
            # processors (esp ActiveJob) may not like weird types in the event hash
            configuration.async.call(evt.to_json_compatible)
          rescue => e
            logger.error("async event sending failed: #{e.message}")
            send_event(evt, make_hint(obj))
          end
        else
          send_event(evt, make_hint(obj))
        end
        Thread.current["sentry_#{object_id}_last_event_id".to_sym] = evt.id
        evt
      end
    end

    alias capture_message capture_type
    alias capture_exception capture_type

    def last_event_id
      Thread.current["sentry_#{object_id}_last_event_id".to_sym]
    end

    # Provides extra context to the exception prior to it being handled by
    # Raven. An exception can have multiple annotations, which are merged
    # together.
    #
    # The options (annotation) is treated the same as the ``options``
    # parameter to ``capture_exception`` or ``Event.from_exception``, and
    # can contain the same ``:user``, ``:tags``, etc. options as these
    # methods.
    #
    # These will be merged with the ``options`` parameter to
    # ``Event.from_exception`` at the top of execution.
    #
    # @example
    #   begin
    #     raise "Hello"
    #   rescue => exc
    #     Raven.annotate_exception(exc, :user => { 'id' => 1,
    #                              'email' => 'foo@example.com' })
    #   end
    def annotate_exception(exc, options = {})
      notes = (exc.instance_variable_defined?(:@__raven_context) && exc.instance_variable_get(:@__raven_context)) || {}
      Raven::Utils::DeepMergeHash.deep_merge!(notes, options)
      exc.instance_variable_set(:@__raven_context, notes)
      exc
    end

    # Bind user context. Merges with existing context (if any).
    #
    # It is recommending that you send at least the ``id`` and ``email``
    # values. All other values are arbitrary.
    #
    # @example
    #   Raven.user_context('id' => 1, 'email' => 'foo@example.com')
    def user_context(options = nil)
      original_user_context = context.user

      if options
        context.user = context.user.merge(options)
      else
        context.user = {}
      end

      yield if block_given?
      context.user
    ensure
      context.user = original_user_context if block_given?
    end

    # Bind tags context. Merges with existing context (if any).
    #
    # Tags are key / value pairs which generally represent things like
    # application version, environment, role, and server names.
    #
    # @example
    #   Raven.tags_context('my_custom_tag' => 'tag_value')
    def tags_context(options = nil)
      context.tags.merge!(options || {})
      yield if block_given?
      context.tags
    ensure
      context.tags.delete_if { |k, _| options.keys.include? k } if block_given?
    end

    # Bind extra context. Merges with existing context (if any).
    #
    # Extra context shows up as Additional Data within Sentry, and is
    # completely arbitrary.
    #
    # @example
    #   Raven.extra_context('my_custom_data' => 'value')
    def extra_context(options = nil)
      context.extra.merge!(options || {})
      yield if block_given?
      context.extra
    ensure
      context.extra.delete_if { |k, _| options.keys.include? k } if block_given?
    end

    def rack_context(env)
      env = nil if env.empty?

      context.rack_env = env
    end

    def breadcrumbs
      BreadcrumbBuffer.current
    end

    private

    def install_at_exit_hook(options)
      at_exit do
        exception = $ERROR_INFO
        if exception
          logger.debug "Caught a post-mortem exception: #{exception.inspect}"
          capture_type(exception, options)
        end
      end
    end

    def make_hint(obj)
      obj.is_a?(String) ? { :exception => nil, :message => obj } : { :exception => obj, :message => nil }
    end
  end
end
