# 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/attribute_processing'

module NewRelic
  module Agent
    class Transaction
      class Attributes
        KEY_LIMIT   = 255
        VALUE_LIMIT = 255
        COUNT_LIMIT = 64

        EMPTY_HASH = {}.freeze

        def initialize(filter)
          @filter = filter

          @custom_attributes = {}
          @agent_attributes = {}
          @intrinsic_attributes = {}

          @custom_destinations = {}
          @agent_destinations = {}
          @already_warned_count_limit = nil
        end

        def add_agent_attribute(key, value, default_destinations)
          destinations = @filter.apply(key, default_destinations)
          return if destinations == AttributeFilter::DST_NONE

          @agent_destinations[key] = destinations
          add(@agent_attributes, key, value)
        end

        def add_agent_attribute_with_key_check(key, value, default_destinations)
          if exceeds_bytesize_limit? key, KEY_LIMIT
            NewRelic::Agent.logger.debug("Agent attribute #{key} was dropped for exceeding key length limit #{KEY_LIMIT}")
            return
          end

          add_agent_attribute(key, value, default_destinations)
        end

        def add_intrinsic_attribute(key, value)
          add(@intrinsic_attributes, key, value)
        end

        def merge_untrusted_agent_attributes(attributes, prefix, default_destinations)
          return if @filter.high_security?
          return if !@filter.might_allow_prefix?(prefix)

          AttributeProcessing.flatten_and_coerce(attributes, prefix) do |k, v|
            add_agent_attribute_with_key_check(k, v, AttributeFilter::DST_NONE)
          end
        end

        def merge_custom_attributes(other)
          return if other.empty?
          AttributeProcessing.flatten_and_coerce(other) do |k, v|
            add_custom_attribute(k, v)
          end
        end

        def custom_attributes_for(destination)
          for_destination(@custom_attributes, @custom_destinations, destination)
        end

        def agent_attributes_for(destination)
          for_destination(@agent_attributes, @agent_destinations, destination)
        end

        def intrinsic_attributes_for(destination)
          if destination == NewRelic::Agent::AttributeFilter::DST_TRANSACTION_TRACER ||
             destination == NewRelic::Agent::AttributeFilter::DST_ERROR_COLLECTOR
            @intrinsic_attributes
          else
            EMPTY_HASH
          end
        end

        private

        def add_custom_attribute(key, value)
          if @custom_attributes.size >= COUNT_LIMIT
            unless @already_warned_count_limit
              NewRelic::Agent.logger.warn("Custom attributes count exceeded limit of #{COUNT_LIMIT}. Any additional custom attributes during this transaction will be dropped.")
              @already_warned_count_limit = true
            end
            return
          end

          if @filter.high_security?
            NewRelic::Agent.logger.debug("Unable to add custom attribute #{key} while in high security mode.")
            return
          end

          if exceeds_bytesize_limit?(key, KEY_LIMIT)
            NewRelic::Agent.logger.warn("Custom attribute key '#{key}' was longer than limit of #{KEY_LIMIT} bytes. This attribute will be dropped.")
            return
          end

          destinations = @filter.apply(key, AttributeFilter::DST_ALL)
          return if destinations == AttributeFilter::DST_NONE

          @custom_destinations[key] = destinations
          add(@custom_attributes, key, value)
        end

        def add(attributes, key, value)
          return if value.nil?

          if exceeds_bytesize_limit?(value, VALUE_LIMIT)
            value = slice(value)
          end

          attributes[key] = value
        end

        def for_destination(attributes, calculated_destinations, destination)
          # Avoid allocating anything if there are no attrs at all
          return EMPTY_HASH if attributes.empty?

          attributes.inject({}) do |memo, (key, value)|
            if @filter.allows?(calculated_destinations[key], destination)
              memo[key] = value
            end
            memo
          end
        end

        def exceeds_bytesize_limit?(value, limit)
          if value.respond_to?(:bytesize)
            value.bytesize > limit
          elsif value.is_a?(Symbol)
            value.to_s.bytesize > limit
          else
            false
          end
        end

        # Take one byte past our limit. Why? This lets us unconditionally chop!
        # the end. It'll either remove the one-character-too-many we have, or
        # peel off the partial, mangled character left by the byteslice.
        def slice(incoming)
          result = incoming.to_s.byteslice(0, VALUE_LIMIT + 1)
          result.chop!
        end
      end
    end
  end
end