# frozen_string_literal: true

#   Represents the relationship between a event definition and a metric definition.
#
#   Example usages:
#   EventSelectionRule.new('pull_package', time_framed: true, filter: { label: 'rubygems' })
#
#   The above rule represents a defines a metric that "counts weekly number of pull_package events with label rubygems"
#
#   EventSelectionRule.new('pull_package', time_framed: false)
#
#   The above rule represents a defines a metric that "counts the total number of pull_package events"
#
module Gitlab
  module Usage
    class EventSelectionRule
      include Gitlab::Usage::TimeSeriesStorable
      include Gitlab::Usage::TimeFrame
      extend Gitlab::Utils::StrongMemoize

      TOTAL_COUNTER_KEY_PREFIX = "event_counters"
      SUM_KEY_PREFIX = "event_sums"

      # We want to avoid reading the key_overrides every time a rule is used.
      def self.key_overrides
        strong_memoize(:key_overrides) do
          YAML.safe_load(File.read(Gitlab::UsageDataCounters::HLLRedisCounter::KEY_OVERRIDES_PATH)).freeze
        end
      end

      attr_reader :filter, :name, :unique_identifier_name, :operator

      def initialize(name:, time_framed:, filter: {}, unique_identifier_name: nil, operator: nil)
        @name = name
        @time_framed = time_framed
        @filter = filter || {}
        @unique_identifier_name = unique_identifier_name
        @operator = operator
      end

      def redis_key_for_date(date = Date.today)
        redis_key(nil, date)
      end

      def redis_keys_for_time_frame(time_frame)
        if time_frame == 'all'
          [redis_key]
        else
          keys_for_aggregation(events: [path_part_of_redis_key], **time_constraint(time_frame))
        end
      end

      def time_framed?
        @time_framed
      end

      def total_counter?
        unique_identifier_name.nil? && !sum?
      end

      def sum?
        operator == 'sum(value)'
      end

      def unique_total?
        unique_identifier_name && operator == 'total'
      end

      def matches?(additional_properties)
        filter.all? do |property_name, value|
          additional_properties[property_name] == value
        end
      end

      # Implementing `==` to make sure that `a == b` is true if and only if `a` and `b` have equal properties
      # Checks equality by comparing each attribute.
      # @param [Object] Object to be compared
      def ==(other)
        other.is_a?(self.class) &&
          name == other.name &&
          time_framed? == other.time_framed? &&
          filter == other.filter &&
          unique_identifier_name == other.unique_identifier_name &&
          operator == other.operator
      end

      alias_method :eql?, :==

      # Calculates hash code according to all attributes.
      # @return [Integer] Hash code
      # Ensures 2 objects with the same attributes can't be keys
      # in the same hash twice
      def hash
        [name, time_framed?, filter, unique_identifier_name, operator].hash
      end

      private

      def redis_key(_event_name = nil, date = Date.today, _used_in_aggregate_metric = false)
        return base_key unless time_framed?

        apply_time_aggregation(base_key, date)
      end

      def base_key
        @base_key ||= "{#{redis_slot}}_" + path_part_of_redis_key
      end

      def redis_slot
        return SUM_KEY_PREFIX if sum?

        total_counter? ? TOTAL_COUNTER_KEY_PREFIX : Gitlab::UsageDataCounters::HLLRedisCounter::REDIS_SLOT
      end

      def time_constraint(time_frame)
        case time_frame
        when '28d'
          monthly_time_range
        when '7d'
          weekly_time_range
        else
          raise "Unknown time frame: #{time_frame}"
        end
      end

      def path_part_of_redis_key
        path = name

        if operator.present?
          # operator should be serialized and used in the path here
          path = "#{path}-operator:#{operator}"
        end

        if filter.present?
          sorted_filter_keys = filter.keys.sort
          serialized_filter = sorted_filter_keys.map { |key| "#{key}:#{filter[key]}" }.join(',')
          path = "#{path}-filter:[#{serialized_filter}]"
        end

        path = "#{path}-#{unique_identifier_name}" if unique_identifier_name.present?

        EventSelectionRule.key_overrides.fetch(path, path)
      end
    end
  end
end
