# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

# OpenTelemetry is an open source observability framework, providing a
# general-purpose API, SDK, and related tools required for the instrumentation
# of cloud-native software, frameworks, and libraries.
#
# The OpenTelemetry module provides global accessors for telemetry objects.
# See the documentation for the `opentelemetry-api` gem for details.
module OpenTelemetry
  # Namespace for OpenTelemetry propagator extension libraries
  module Propagator
    # Namespace for OpenTelemetry OTTrace propagation
    module OTTrace
      # Propagates context using OTTrace header format
      class TextMapPropagator
        PADDING = '0' * 16
        VALID_TRACE_ID_REGEX = /^[0-9a-f]{32}$/i
        VALID_SPAN_ID_REGEX = /^[0-9a-f]{16}$/i
        TRACE_ID_64_BIT_WIDTH = 64 / 4
        TRACE_ID_HEADER = 'ot-tracer-traceid'
        SPAN_ID_HEADER = 'ot-tracer-spanid'
        SAMPLED_HEADER = 'ot-tracer-sampled'
        BAGGAGE_HEADER_PREFIX = 'ot-baggage-'
        FIELDS = [TRACE_ID_HEADER, SPAN_ID_HEADER, SAMPLED_HEADER].freeze

        # https://github.com/open-telemetry/opentelemetry-specification/blob/14d123c121b6caa53bffd011292c42a181c9ca26/specification/context/api-propagators.md#textmap-propagator0
        VALID_BAGGAGE_HEADER_NAME_CHARS = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
        INVALID_BAGGAGE_HEADER_VALUE_CHARS = /[^\t\u0020-\u007E\u0080-\u00FF]/

        private_constant :PADDING, :VALID_TRACE_ID_REGEX, :VALID_SPAN_ID_REGEX, :TRACE_ID_64_BIT_WIDTH, :TRACE_ID_HEADER,
                         :SPAN_ID_HEADER, :SAMPLED_HEADER, :BAGGAGE_HEADER_PREFIX, :FIELDS, :VALID_BAGGAGE_HEADER_NAME_CHARS,
                         :INVALID_BAGGAGE_HEADER_VALUE_CHARS

        # Extract OTTrace context from the supplied carrier and set the active span
        # in the given context. The original context will be returned if OTTrace
        # cannot be extracted from the carrier.
        #
        # @param [Carrier] carrier The carrier to get the header from.
        # @param [optional Context] context The context to be updated with extracted context
        # @param [optional Getter] getter If the optional getter is provided, it
        #   will be used to read the header from the carrier, otherwise the default
        #   getter will be used.
        # @return [Context] Updated context with active span derived from the header, or the original
        #   context if parsing fails.
        def extract(carrier, context: Context.current, getter: Context::Propagation.text_map_getter)
          trace_id = optionally_pad_trace_id(getter.get(carrier, TRACE_ID_HEADER))
          span_id = getter.get(carrier, SPAN_ID_HEADER)
          sampled = getter.get(carrier, SAMPLED_HEADER)

          return context unless valid?(trace_id: trace_id, span_id: span_id)

          span_context = Trace::SpanContext.new(
            trace_id: Array(trace_id).pack('H*'),
            span_id: Array(span_id).pack('H*'),
            trace_flags: as_trace_flags(sampled),
            remote: true
          )

          span = OpenTelemetry::Trace.non_recording_span(span_context)
          Trace.context_with_span(span, parent_context: set_baggage(carrier: carrier, context: context, getter: getter))
        end

        # @param [Object] carrier to update with context.
        # @param [optional Context] context The active Context.
        # @param [optional Setter] setter If the optional setter is provided, it
        #   will be used to write context into the carrier, otherwise the default
        #   setter will be used.
        def inject(carrier, context: Context.current, setter: Context::Propagation.text_map_setter)
          span_context = Trace.current_span(context).context
          return unless span_context.valid?

          inject_span_context(span_context: span_context, carrier: carrier, setter: setter)
          inject_baggage(context: context, carrier: carrier, setter: setter)

          nil
        end

        # Returns the predefined propagation fields. If your carrier is reused, you
        # should delete the fields returned by this method before calling +inject+.
        #
        # @return [Array<String>] a list of fields that will be used by this propagator.
        def fields
          FIELDS
        end

        private

        def as_trace_flags(sampled)
          case sampled
          when 'true', '1'
            Trace::TraceFlags::SAMPLED
          else
            Trace::TraceFlags::DEFAULT
          end
        end

        def valid?(trace_id:, span_id:)
          !(VALID_TRACE_ID_REGEX !~ trace_id || VALID_SPAN_ID_REGEX !~ span_id)
        end

        def optionally_pad_trace_id(trace_id)
          if trace_id&.length == 16
            "#{PADDING}#{trace_id}"
          else
            trace_id
          end
        end

        def set_baggage(carrier:, context:, getter:)
          baggage.build(context: context) do |builder|
            prefix = BAGGAGE_HEADER_PREFIX
            getter.keys(carrier).each do |carrier_key|
              baggage_key = carrier_key.start_with?(prefix) && carrier_key[prefix.length..-1]
              next unless baggage_key
              next unless VALID_BAGGAGE_HEADER_NAME_CHARS.match?(baggage_key)

              value = getter.get(carrier, carrier_key)
              next if INVALID_BAGGAGE_HEADER_VALUE_CHARS.match?(value)

              builder.set_value(baggage_key, value)
            end
          end
        end

        def baggage
          OpenTelemetry::Baggage
        end

        def inject_span_context(span_context:, carrier:, setter:)
          setter.set(carrier, TRACE_ID_HEADER, span_context.hex_trace_id[TRACE_ID_64_BIT_WIDTH, TRACE_ID_64_BIT_WIDTH])
          setter.set(carrier, SPAN_ID_HEADER, span_context.hex_span_id)
          setter.set(carrier, SAMPLED_HEADER, span_context.trace_flags.sampled?.to_s)
        end

        def inject_baggage(context:, carrier:, setter:)
          baggage.values(context: context)
                 .select { |key, value| valid_baggage_entry?(key, value) }
                 .each { |key, value| setter.set(carrier, "#{BAGGAGE_HEADER_PREFIX}#{key}", value) }
        end

        def valid_baggage_entry?(key, value)
          VALID_BAGGAGE_HEADER_NAME_CHARS =~ key && INVALID_BAGGAGE_HEADER_VALUE_CHARS !~ value
        end
      end
    end
  end
end
