# frozen_string_literal: true

module Motor
  module ApiQuery
    module Filter
      LIKE_FILTER_VALUE_REGEXP = /\A%?(.*?)%?\z/.freeze
      DISTINCT_RESTRICTED_COLUMN_TYPES = %i[json point].freeze

      module_function

      def call(rel, params)
        return rel if params.blank?

        normalized_params = normalize_params(Array.wrap(params))

        rel = apply_filters(rel, normalized_params)
        rel = rel.distinct if can_apply_distinct?(rel)

        rel
      end

      def clean_filters(value)
        if value.class.name == 'ActionController::Parameters'
          value.to_unsafe_h
        elsif value.is_a?(Array)
          value.map { |v| clean_filters(v) }
        else
          value
        end
      end

      def apply_predicates(rel, filters)
        joins = ActiveRecord::PredicateBuilder.filter_joins(rel.klass, filters)

        joins.flatten.reduce(rel) do |acc, j|
          if j.is_a?(String) || j.is_a?(Arel::Nodes::Join)
            acc.joins(j)
          elsif j.present?
            acc.left_outer_joins(j)
          else
            acc
          end
        end
      end

      def apply_filters(rel, filters)
        filters = clean_filters(filters)

        rel = apply_predicates(rel, filters)

        alias_tracker =
          if Rails.version.to_f >= 7.2
            rel.alias_tracker
          else
            ActiveRecord::Associations::AliasTracker.create(rel.connection, rel.table.name, [])
          end

        filter_clause_factory = ActiveRecord::Relation::FilterClauseFactory.new(rel.klass, rel.predicate_builder)

        where_clause = filter_clause_factory.build(filters, alias_tracker)

        rel_values = rel.instance_variable_get(:@values)

        if rel_values[:where]
          rel_values[:where] += where_clause
        else
          rel_values[:where] = where_clause
        end

        rel
      end

      def normalize_params(params)
        params.map do |item|
          next item if item.is_a?(String)
          next normalize_params(item) if item.is_a?(Array)

          item = item.to_unsafe_h if item.respond_to?(:to_unsafe_h)

          item.transform_values do |filter|
            if filter.is_a?(Hash)
              normalize_filter_hash(filter)
            else
              filter
            end
          end
        end.split('OR').product(['OR']).flatten(1)[0...-1]
      end

      def normalize_filter_hash(hash)
        hash.each_with_object({}) do |(action, value), acc|
          new_action, new_value =
            if value.is_a?(Hash)
              [action, normalize_filter_hash(value)]
            else
              normalize_action(action, value)
            end

          acc[new_action] = new_value

          acc
        end
      end

      def can_apply_distinct?(rel)
        rel.columns.none? do |column|
          DISTINCT_RESTRICTED_COLUMN_TYPES.include?(column.type)
        end
      end

      def normalize_action(action, value)
        case action
        when 'includes'
          ['contains', value]
        when 'contains'
          ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '%\1%')]
        when 'starts_with'
          ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '\1%')]
        when 'ends_with'
          ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '%\1')]
        when 'eqnull'
          ['eq', nil]
        when 'neqnull'
          ['neq', nil]
        else
          [action, value]
        end
      end
    end
  end
end
