# frozen_string_literal: true

# The Metric class represents a metric sample to be send by a backend.
#
# @!attribute type
#   @return [Symbol] The metric type. Must be one of {StatsD::Instrument::Metric::TYPES}
# @!attribute name
#   @return [String] The name of the metric. {StatsD#prefix} will automatically be applied
#     to the metric in the constructor, unless the <tt>:no_prefix</tt> option is set or is
#     overridden by the <tt>:prefix</tt> option. Note that <tt>:no_prefix</tt> has greater
#     precedence than <tt>:prefix</tt>.
# @!attribute value
#   @see #default_value
#   @return [Numeric, String] The value to collect for the metric. Depending on the metric
#     type, <tt>value</tt> can be a string, integer, or float.
# @!attribute sample_rate
#   The sample rate to use for the metric. How the sample rate is handled differs per backend.
#   The UDP backend will actually sample metric submissions based on the sample rate, while
#   the logger backend will just include the sample rate in its output for debugging purposes.
#   @see StatsD#default_sample_rate
#   @return [Float] The sample rate to use for this metric. This should be a value between
#     0 and 1. If not set, it will use the default sample rate set to {StatsD#default_sample_rate}.
# @!attribute tags
#   The tags to associate with the metric.
#   @note Only the Datadog implementation supports tags.
#   @see .normalize_tags
#   @return [Array<String>, Hash<String, String>, nil] the tags to associate with the metric.
#     You can either specify the tags as an array of strings, or a Hash of key/value pairs.
#
# @see StatsD The StatsD module contains methods that generate metric instances.
# @see StatsD::Instrument::Backend A StatsD::Instrument::Backend is used to collect metrics.
#
class StatsD::Instrument::Metric
  attr_accessor :type, :name, :value, :sample_rate, :tags, :metadata

  # Initializes a new metric instance.
  # Normally, you don't want to call this method directly, but use one of the metric collection
  # methods on the {StatsD} module.
  #
  # @option options [Symbol] :type The type of the metric.
  # @option options [String] :name The name of the metric without prefix.
  # @option options [String] :prefix Override the default StatsD prefix.
  # @option options [Boolean] :no_prefix Set to <tt>true</tt> if you don't want to apply a prefix.
  # @option options [Numeric, String, nil] :value The value to collect for the metric. If set to
  #   <tt>nil>/tt>, {#default_value} will be used.
  # @option options [Numeric, nil] :sample_rate The sample rate to use. If not set, it will use
  #   {StatsD#default_sample_rate}.
  # @option options [Array<String>, Hash<String, String>, nil] :tags The tags to apply to this metric.
  #   See {.normalize_tags} for more information.
  def initialize(options = {})
    if options[:type]
      @type = options[:type]
    else
      raise ArgumentError, "Metric :type is required."
    end

    if options[:name]
      @name = normalize_name(options[:name])
    else
      raise ArgumentError, "Metric :name is required."
    end

    unless options[:no_prefix]
      @name = if options[:prefix]
        "#{options[:prefix]}.#{@name}"
      else
        StatsD.prefix ? "#{StatsD.prefix}.#{@name}" : @name
      end
    end

    @value = options[:value] || default_value
    @sample_rate = options[:sample_rate] || StatsD.default_sample_rate
    @tags = StatsD::Instrument::Metric.normalize_tags(options[:tags])
    if StatsD.default_tags
      @tags = Array(@tags) + StatsD.default_tags
    end
    @metadata = options.reject { |k, _| [:type, :name, :value, :sample_rate, :tags].include?(k) }
  end

  # The default value for this metric, which will be used if it is not set.
  #
  # A default value is only defined for counter metrics (<tt>1</tt>). For all other
  # metric types, this emthod will raise an <tt>ArgumentError</tt>.
  #
  # @return [Numeric, String] The default value for this metric.
  # @raise ArgumentError if the metric type doesn't have a default value
  def default_value
    case type
    when :c then 1
    else raise ArgumentError, "A value is required for metric type #{type.inspect}."
    end
  end

  # @private
  # @return [String]
  def to_s
    str = +"#{TYPES[type]} #{name}:#{value}"
    str << " @#{sample_rate}" if sample_rate != 1.0
    tags&.each { |tag| str << " ##{tag}" }
    str
  end

  # @private
  # @return [String]
  def inspect
    "#<StatsD::Instrument::Metric #{self}>"
  end

  # The metric types that are supported by this library. Note that every StatsD server
  # implementation only supports a subset of them.
  TYPES = {
    c: 'increment',
    ms: 'measure',
    g: 'gauge',
    h: 'histogram',
    d: 'distribution',
    kv: 'key/value',
    s: 'set',
  }

  # Strip metric names of special characters used by StatsD line protocol, replace with underscore
  #
  # @param name [String]
  # @return [String]
  def normalize_name(name)
    name.tr(':|@', '_')
  end

  # Utility function to convert tags to the canonical form.
  #
  # - Tags specified as key value pairs will be converted into an array
  # - Tags are normalized to only use word characters and underscores.
  #
  # @param tags [Array<String>, Hash<String, String>, nil] Tags specified in any form.
  # @return [Array<String>, nil] the list of tags in canonical form.
  def self.normalize_tags(tags)
    return unless tags
    tags = tags.map { |k, v| k.to_s + ":" + v.to_s } if tags.is_a?(Hash)
    tags.map { |tag| tag.tr('|,', '') }
  end
end