# frozen_string_literal: true

require 'rack'

module OpenTracing
  module Instrumentation
    module Rack
      # TraceMiddleware observer rack requests.
      class TraceMiddleware
        DEFAULT_COMMAND_NAME_BUILDER = StaticCommandNameBuilder.new

        def initialize(
          app,
          logger: nil,
          command_name_builder: DEFAULT_COMMAND_NAME_BUILDER,
          http_tagger: HttpTagger.new,
          tracer: OpenTracing.global_tracer
        )
          @app = app
          @logger = logger
          @command_name_builder = command_name_builder
          @http_tagger = http_tagger
          @tracer = tracer
        end

        def call(env)
          trace_request(env) do |span|
            catch_error(span) do
              status, headers, body = app.call(env)

              set_response_tags(span, status, headers)
              [status, headers, body]
            end
          end
        end

        private

        attr_reader :app,
                    :http_tagger,
                    :tracer,
                    :logger

        def trace_request(env)
          extracted_ctx = tracer.extract(OpenTracing::FORMAT_RACK, env)
          logger&.info('Tracing context extracted') if extracted_ctx
          command_name = @command_name_builder.build_command_name(env)
          tracer.start_active_span(
            command_name,
            child_of: extracted_ctx,
            tags: request_tags(env),
          ) do |scope|
            yield(scope.span)
          end
        end

        def catch_error(span)
          yield
        rescue StandardError => e
          logger&.error(e)
          error_tag(span, e)
          raise
        end

        REQUEST_URI = 'REQUEST_URI'

        def request_tags(env)
          {
            'http.method' => env[::Rack::REQUEST_METHOD],
            'http.url' => env[REQUEST_URI],
            'span.kind' => 'server',
          }.merge(http_tagger.request_tags(env))
        end

        def set_response_tags(span, status, headers)
          set_status_tag(span, status)
          set_header_tags(span, headers)
        end

        def set_status_tag(span, status)
          span.set_tag('http.status', status)
        end

        def set_header_tags(span, headers)
          http_tagger
            .response_tags(headers)
            .each { |(key, value)| span.set_tag(key, value) }
        end

        def error_tag(span, error)
          span.set_tag('error', true)
          span.log_kv('error.kind': error.class.to_s)
        end
      end
    end
  end
end