<commit_before># frozen_string_literal: true

module GobiertoCommon
  class CustomFieldsQuery

    OPERATORS = {
      lt: "<",
      lteq: "<=",
      gt: ">",
      gteq: ">=",
      in: "IN",
      like: "ILIKE"
    }.with_indifferent_access

    CAST_FUNCTIONS = {
      numeric: "::text::numeric",
      date: "::text::date",
      default: "::text"
    }.with_indifferent_access

    attr_writer :custom_fields
    attr_reader :relation

    def self.allowed_operators
      @allowed_operators ||= OPERATORS.keys << :eq
    end

    def initialize(options = {})
      @relation = options[:relation]
      @instance = options[:instance]
      @class = options[:class] || @relation.model
    end

    def filter(filters = {})
      filtered_query(
        filters,
        instance_join_manager_for_custom_fields(filters.keys),
        model_table[Arel.star]
      )
    end

    def filter_with_fields_extraction(filters = {})
      filtered_query(
        filters,
        instance_join_manager_with_all_fields,
        model_table[Arel.star],
        *custom_fields_attributes
      )
    end

    def custom_fields
      @custom_fields ||= CustomField.where(instance: @instance).for_class(@class)
    end

    def stats(custom_field, filters)
      filters ||= {}
      data = OpenStruct.new

      data[:count] = aggregated_field(:count, custom_field, filters)
      data[:distribution] = distribution(custom_field, filters) if custom_field.vocabulary_options?

      if custom_field.date? || custom_field.numeric?
        data[:min] = aggregated_field(:min, custom_field, filters)
        data[:max] = aggregated_field(:max, custom_field, filters)
        data[:histogram] = histogram(custom_field, filters, data) if custom_field.numeric?
      end

      data.to_h
    end

    private

    def aggregated_field(aggregate_function, custom_field, filters)
      aggregate_value = Arel::Nodes::SqlLiteral.new(
        "#{aggregate_function}(#{extracted_attribute(custom_field)}) as result"
      )

      filtered_query(
        filters,
        instance_join_manager_for_custom_fields(filters.keys | [custom_field]),
        aggregate_value
      )[0]&.result
    end

    def histogram(custom_field, filters, stats)
      return unless stats.min != stats.max && stats.count > 1

      separations = Math.log(stats.count, 2).floor
      interval_width = (stats.max - stats.min).to_f / (1 + separations)
      bucket_attributes = Arel::Nodes::SqlLiteral.new(
        "width_bucket(#{extracted_attribute(custom_field)}, #{stats.min}, #{stats.max}, #{separations}) as bucket, count(*)"
      )

      histogram_query = filtered_query(
        filters,
        instance_join_manager_for_custom_fields(filters.keys | [custom_field]),
        bucket_attributes
      ).group(:bucket).order(bucket: :asc)

      histogram_query.map do |bin_data|
        { bucket: bin_data.bucket,
          start: stats.min + interval_width * (bin_data.bucket - 1),
          end: stats.min + interval_width * bin_data.bucket,
          count: bin_data.count }
      end
    end

    def distribution(custom_field, filters)
      distribution_attributes = Arel::Nodes::SqlLiteral.new(
        "#{extracted_attribute(custom_field)} as value, count(*)"
      )

      distribution_query = filtered_query(
        filters,
        instance_join_manager_for_custom_fields(filters.keys | [custom_field]),
        distribution_attributes
      ).group(:value).order(value: :asc)

      distribution_query.map do |value_data|
        { value: value_data.value,
          count: value_data.count }
      end
    end

    def extracted_attribute(custom_field)
      extraction_function = Arel::Nodes::NamedFunction.new(<commit_msg>Change meta histogram buckets count and include empty buckets<commit_after># frozen_string_literal: true

module GobiertoCommon
  class CustomFieldsQuery

    OPERATORS = {
      lt: "<",
      lteq: "<=",
      gt: ">",
      gteq: ">=",
      in: "IN",
      like: "ILIKE"
    }.with_indifferent_access

    CAST_FUNCTIONS = {
      numeric: "::text::numeric",
      date: "::text::date",
      default: "::text"
    }.with_indifferent_access

    attr_writer :custom_fields
    attr_reader :relation

    def self.allowed_operators
      @allowed_operators ||= OPERATORS.keys << :eq
    end

    def initialize(options = {})
      @relation = options[:relation]
      @instance = options[:instance]
      @class = options[:class] || @relation.model
    end

    def filter(filters = {})
      filtered_query(
        filters,
        instance_join_manager_for_custom_fields(filters.keys),
        model_table[Arel.star]
      )
    end

    def filter_with_fields_extraction(filters = {})
      filtered_query(
        filters,
        instance_join_manager_with_all_fields,
        model_table[Arel.star],
        *custom_fields_attributes
      )
    end

    def custom_fields
      @custom_fields ||= CustomField.where(instance: @instance).for_class(@class)
    end

    def stats(custom_field, filters)
      filters ||= {}
      data = OpenStruct.new

      data[:count] = aggregated_field(:count, custom_field, filters)
      data[:distribution] = distribution(custom_field, filters) if custom_field.vocabulary_options?

      if custom_field.date? || custom_field.numeric?
        data[:min] = aggregated_field(:min, custom_field, filters)
        data[:max] = aggregated_field(:max, custom_field, filters)
        data[:histogram] = histogram(custom_field, filters, data) if custom_field.numeric?
      end

      data.to_h
    end

    private

    def aggregated_field(aggregate_function, custom_field, filters)
      aggregate_value = Arel::Nodes::SqlLiteral.new(
        "#{aggregate_function}(#{extracted_attribute(custom_field)}) as result"
      )

      filtered_query(
        filters,
        instance_join_manager_for_custom_fields(filters.keys | [custom_field]),
        aggregate_value
      )[0]&.result
    end

    def histogram(custom_field, filters, stats)
      return unless stats.min != stats.max && stats.count > 1

      separations = [stats.count, 9].min
      interval_width = (stats.max - stats.min).to_f / (1 + separations)
      bucket_attributes = Arel::Nodes::SqlLiteral.new(
        "width_bucket(#{extracted_attribute(custom_field)}, #{stats.min}, #{stats.max}, #{separations}) as bucket, count(*)"
      )

      histogram_query = filtered_query(
        filters,
        instance_join_manager_for_custom_fields(filters.keys | [custom_field]),
        bucket_attributes
      ).group(:bucket).order(bucket: :asc)

      (1..separations + 1).map do |bucket|
        { bucket: bucket,
          start: stats.min + interval_width * (bucket - 1),
          end: stats.min + interval_width * bucket,
          count: histogram_query.find { |bin_data| bin_data.bucket == bucket }&.count || 0 }
      end
    end

    def distribution(custom_field, filters)
      distribution_attributes = Arel::Nodes::SqlLiteral.new(
        "#{extracted_attribute(custom_field)} as value, count(*)"
      )

      distribution_query = filtered_query(
        filters,
        instance_join_manager_for_custom_fields(filters.keys | [custom_field]),
        distribution_attributes
      ).group(:value).order(value: :asc)

      distribution_query.map do |value_data|
        { value: value_data.value,
          count: value_data.count }
      end
    end

    def extracted_attribute(custom_field)
      extraction_function = Arel::Nodes::NamedFunction.new(
{'fraction_non_alphanumeric': 0.11160058737151249, 'fraction_numerical': 0.001735415832332132, 'mean_word_length': 2.924567836563646, 'pattern_counts': {'":': 6, '<': 11, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4257859', 'n_tokens_mistral': 2282, 'n_tokens_neox': 2202, 'n_words': 415}