require 'dry/configurable'
require 'rack/utils'

module Dry
  module Monitor
    module Rack
      class Logger
        extend Dry::Configurable

        setting :filtered_params, %w[_csrf password]

        REQUEST_METHOD = 'REQUEST_METHOD'.freeze
        PATH_INFO = 'PATH_INFO'.freeze
        REMOTE_ADDR = 'REMOTE_ADDR'.freeze
        RACK_INPUT = 'rack.input'.freeze
        QUERY_PARAMS = 'QUERY_PARAMS'.freeze

        START_MSG = %(Started %s "%s" for %s at %s).freeze
        STOP_MSG = %(Finished %s "%s" for %s in %sms [Status: %s]).freeze
        PARAMS_MSG = %(  Parameters %s).freeze
        QUERY_MSG = %(  Query parameters %s).freeze
        FILTERED = '[FILTERED]'.freeze

        attr_reader :logger

        attr_reader :config

        def initialize(logger, config = self.class.config)
          @logger = logger
          @config = config
        end

        def subscribe(notifications)
          notifications.subscribe(Middleware::REQUEST_START) do |id, payload|
            log_start_request(payload[:env])
          end

          notifications.subscribe(Middleware::REQUEST_STOP) do |id, payload|
            log_stop_request(payload[:env], payload[:status], payload[:time])
          end

          notifications.subscribe(Middleware::APP_ERROR) do |id, payload|
            log_exception(payload[:exception], payload[:name])
          end
        end

        def log_exception(e, app_name)
          logger.error e.message
          logger.error filter_backtrace(e.backtrace, app_name).join("\n")
        end

        def log_start_request(request)
          info START_MSG % [
                 request[REQUEST_METHOD],
                 request[PATH_INFO],
                 request[REMOTE_ADDR],
                 Time.now
               ]
          log_request_params(request)
        end

        def log_stop_request(request, status, time)
          info STOP_MSG % [
                 request[REQUEST_METHOD],
                 request[PATH_INFO],
                 request[REMOTE_ADDR],
                 time,
                 status
               ]
        end

        def log_request_params(request)
          with_http_params(request[QUERY_PARAMS]) do |params|
            info QUERY_MSG % [params.inspect]
          end
        end

        def info(*args)
          logger.info(*args)
        end

        def with_http_params(params)
          params = ::Rack::Utils.parse_nested_query(params)
          if params.size > 0
            yield(filter_params(params))
          end
        end

        def filter_backtrace(backtrace, app_name)
          # TODO: what do we want to do with this?
          backtrace.reject { |l| l.include?('gems') }
        end

        def filter_params(params)
          params.each_with_object({}) do |(k, v), h|
            if v.is_a?(Hash)
              h.update(k => filter_params(v))
            elsif v.is_a?(Array)
              h.update(k => v.map { |m| filter_params(m) })
            elsif config.filtered_params.include?(k)
              h.update(k => FILTERED)
            else
              h[k] = v
            end
          end
        end
      end

      class Middleware
        REQUEST_START = :'request.start'
        REQUEST_STOP = :'request.stop'
        APP_ERROR = :'app.error'

        attr_reader :app
        attr_reader :notifications

        def initialize(*args)
          @notifications, @app = *args

          notifications.event(REQUEST_START)
          notifications.event(REQUEST_STOP)
          notifications.event(APP_ERROR)
        end

        def new(app)
          self.class.new(notifications, app)
        end

        def call(env)
          notifications.start(REQUEST_START, env: env)
          response, time = CLOCK.measure { app.call(env) }
          notifications.stop(REQUEST_STOP, env: env, time: time, status: response[0])
          response
        end
      end
    end
  end
end