# encoding: utf-8
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.

require 'new_relic/agent/instrumentation/controller_instrumentation'
require 'new_relic/agent/instrumentation/sinatra/transaction_namer'
require 'new_relic/agent/instrumentation/sinatra/ignorer'
require 'new_relic/agent/parameter_filtering'

DependencyDetection.defer do
  @name = :sinatra

  depends_on do
    !NewRelic::Agent.config[:disable_sinatra] &&
      defined?(::Sinatra) && defined?(::Sinatra::Base) &&
      Sinatra::Base.private_method_defined?(:dispatch!) &&
      Sinatra::Base.private_method_defined?(:process_route) &&
      Sinatra::Base.private_method_defined?(:route_eval)
  end

  executes do
    ::NewRelic::Agent.logger.info 'Installing Sinatra instrumentation'
  end

  executes do
    ::Sinatra::Base.class_eval do
      include NewRelic::Agent::Instrumentation::Sinatra

      alias dispatch_without_newrelic dispatch!
      alias dispatch! dispatch_with_newrelic

      alias process_route_without_newrelic process_route
      alias process_route process_route_with_newrelic

      alias route_eval_without_newrelic route_eval
      alias route_eval route_eval_with_newrelic

      register NewRelic::Agent::Instrumentation::Sinatra::Ignorer
    end

    module ::Sinatra
      register NewRelic::Agent::Instrumentation::Sinatra::Ignorer
    end
  end

  executes do
    if Sinatra::Base.respond_to?(:build)
      # These requires are inside an executes block because they require rack, and
      # we can't be sure that rack is available when this file is first required.
      require 'new_relic/rack/agent_hooks'
      require 'new_relic/rack/browser_monitoring'
      require 'new_relic/rack/error_collector'

      ::Sinatra::Base.class_eval do
        class << self
          alias build_without_newrelic build
          alias build build_with_newrelic
        end
      end
    else
      ::NewRelic::Agent.logger.info("Skipping auto-injection of middleware for Sinatra - requires Sinatra 1.2.1+")
    end
  end
end

module NewRelic
  module Agent
    module Instrumentation
      # NewRelic instrumentation for Sinatra applications.  Sinatra actions will
      # appear in the UI similar to controller actions, and have breakdown charts
      # and transaction traces.
      #
      # The actions in the UI will correspond to the pattern expression used
      # to match them, not directly to full URL's.
      module Sinatra
        include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation

        # Expected method for supporting ControllerInstrumentation
        def newrelic_request_headers(_)
          request.env
        end

        def self.included(clazz)
          clazz.extend(ClassMethods)
        end

        module ClassMethods
          def newrelic_middlewares
            middlewares = [NewRelic::Rack::BrowserMonitoring]
            if NewRelic::Rack::AgentHooks.needed?
              middlewares << NewRelic::Rack::AgentHooks
            end
            middlewares
          end

          def build_with_newrelic(*args, &block)
            unless NewRelic::Agent.config[:disable_sinatra_auto_middleware]
              newrelic_middlewares.each do |middleware_class|
                try_to_use(self, middleware_class)
              end
            end
            build_without_newrelic(*args, &block)
          end

          def try_to_use(app, clazz)
            has_middleware = app.middleware.any? { |info| info[0] == clazz }
            app.use(clazz) unless has_middleware
          end
        end

        # Capture last route we've seen. Will set for transaction on route_eval
        def process_route_with_newrelic(*args, &block)
          begin
            env["newrelic.last_route"] = args[0]
          rescue => e
            ::NewRelic::Agent.logger.debug("Failed determining last route in Sinatra", e)
          end

          process_route_without_newrelic(*args, &block)
        end

        # If a transaction name is already set, this call will tromple over it.
        # This is intentional, as typically passing to a separate route is like
        # an entirely separate transaction, so we pick up the new name.
        #
        # If we're ignored, this remains safe, since set_transaction_name
        # care for the gating on the transaction's existence for us.
        def route_eval_with_newrelic(*args, &block)
          begin
            txn_name = TransactionNamer.transaction_name_for_route(env, request)
            unless txn_name.nil?
              ::NewRelic::Agent::Transaction.set_default_transaction_name(
                "#{self.class.name}/#{txn_name}", :sinatra)
            end
          rescue => e
            ::NewRelic::Agent.logger.debug("Failed during route_eval to set transaction name", e)
          end

          route_eval_without_newrelic(*args, &block)
        end

        def dispatch_with_newrelic #THREAD_LOCAL_ACCESS
          request_params = get_request_params
          filtered_params = ParameterFiltering::apply_filters(request.env, request_params || {})

          name = TransactionNamer.initial_transaction_name(request)
          perform_action_with_newrelic_trace(:category => :sinatra,
                                             :name => name,
                                             :params => filtered_params) do
            dispatch_and_notice_errors_with_newrelic
          end
        end

        def get_request_params
          begin
            @request.params
          rescue => e
            NewRelic::Agent.logger.debug("Failed to get params from Rack request.", e)
            nil
          end
        end

        def dispatch_and_notice_errors_with_newrelic
          dispatch_without_newrelic
        ensure
          # Will only see an error raised if :show_exceptions is true, but
          # will always see them in the env hash if they occur
          had_error = env.has_key?('sinatra.error')
          ::NewRelic::Agent.notice_error(env['sinatra.error']) if had_error
        end

        def do_not_trace?
          Ignorer.should_ignore?(self, :routes)
        end

        # Overrides ControllerInstrumentation implementation
        def ignore_apdex?
          Ignorer.should_ignore?(self, :apdex)
        end

        # Overrides ControllerInstrumentation implementation
        def ignore_enduser?
          Ignorer.should_ignore?(self, :enduser)
        end

      end
    end
  end
end
