require 'time'
require 'rack'

module Raven
  # Middleware for Rack applications. Any errors raised by the upstream
  # application will be delivered to Sentry and re-raised.
  #
  # Synopsis:
  #
  #   require 'rack'
  #   require 'raven'
  #
  #   Raven.configure do |config|
  #     config.server = 'http://my_dsn'
  #   end
  #
  #   app = Rack::Builder.app do
  #     use Raven::Rack
  #     run lambda { |env| raise "Rack down" }
  #   end
  #
  # Use a standard Raven.configure call to configure your server credentials.
  class Rack
    def self.capture_type(exception, env, options = {})
      if env['raven.requested_at']
        options[:time_spent] = Time.now - env['raven.requested_at']
      end
      Raven.capture_type(exception, options) do |evt|
        evt.interface :http do |int|
          int.from_rack(env)
        end
      end
    end
    class << self
      alias_method :capture_message, :capture_type
      alias_method :capture_exception, :capture_type
    end

    def initialize(app)
      @app = app
    end

    def call(env)
      # clear context at the beginning of the request to ensure a clean slate
      Context.clear!

      # store the current environment in our local context for arbitrary
      # callers
      env['raven.requested_at'] = Time.now
      Raven.rack_context(env)

      begin
        response = @app.call(env)
      rescue Error
        raise # Don't capture Raven errors
      rescue Exception => e
        Raven.logger.debug "Collecting %p: %s" % [ e.class, e.message ]
        Raven::Rack.capture_exception(e, env)
        raise
      end

      error = env['rack.exception'] || env['sinatra.error']
      Raven::Rack.capture_exception(error, env) if error

      response
    end
  end

  module RackInterface
    def from_rack(env_hash)
      req = ::Rack::Request.new(env_hash)

      self.url = req.scheme && req.url.split('?').first
      self.method = req.request_method
      self.query_string = req.query_string
      self.data = read_data_from(req)

      self.headers = format_headers_for_sentry(env_hash)
      self.env = format_env_for_sentry(env_hash)
    end

    private

    def read_data_from(request)
      if request.form_data?
        request.POST
      elsif request.body
        data = request.body.read
        request.body.rewind
        data
      end
    end

    def format_headers_for_sentry(env_hash)
      env_hash.each_with_object({}) do |(key, value), memo|
        key = key.to_s # rack env can contain symbols
        value = value.to_s
        next unless key.upcase == key # Non-upper case stuff isn't either
        # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
        # to think this is a Version header. Instead, this is mapped to
        # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
        # if the request has legitimately sent a Version header themselves.
        # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
        next if key == 'HTTP_VERSION' && value == ENV['SERVER_PROTOCOL']
        if key.start_with?('HTTP_')
          # Header
          http_key = key[5..key.length - 1].split('_').map(&:capitalize).join('-')
          memo[http_key] = value
        elsif %w(CONTENT_TYPE CONTENT_LENGTH).include? key
          memo[key.capitalize] = value
        end
      end
    end

    def format_env_for_sentry(env_hash)
      trimmed_hash = env_hash.select do |k, _v|
        %w(REMOTE_ADDR SERVER_NAME SERVER_PORT).include? k.to_s
      end
      Hash[trimmed_hash] # select returns an Array in Ruby 1.8
    end
  end

  class HttpInterface
    include RackInterface
  end
end
