module JitPreloadExtension
  attr_accessor :jit_preloader
  attr_accessor :jit_n_plus_one_tracking
  attr_accessor :jit_preload_aggregates
  attr_accessor :jit_preload_scoped_relations

  def reload(*args)
    clear_jit_preloader!
    super
  end

  def clear_jit_preloader!
    self.jit_preload_aggregates = {}
    self.jit_preload_scoped_relations = {}
    if jit_preloader
      jit_preloader.records.delete(self)
      self.jit_preloader = nil
    end
  end

  if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.0.0")
    def preload_scoped_relation(name:, base_association:, preload_scope: nil)
      return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)

      base_association = base_association.to_sym
      records = jit_preloader&.records || [self]
      previous_association_values = {}

      records.each do |record|
        association = record.association(base_association)
        if association.loaded?
          previous_association_values[record] = association.target
          association.reset
        end
      end

      preloader_association = ActiveRecord::Associations::Preloader.new(
        records: records,
        associations: base_association,
        scope: preload_scope
      ).call.first

      records.each do |record|
        record.jit_preload_scoped_relations ||= {}
        association = record.association(base_association)
        record.jit_preload_scoped_relations[name] = preloader_association.records_by_owner[record] || []
        association.reset
        if previous_association_values.key?(record)
          association.target = previous_association_values[record]
        end
      end

      jit_preload_scoped_relations[name]
    end
  else
    def preload_scoped_relation(name:, base_association:, preload_scope: nil)
      return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)

      base_association = base_association.to_sym
      records = jit_preloader&.records || [self]
      previous_association_values = {}

      records.each do |record|
        association = record.association(base_association)
        if association.loaded?
          previous_association_values[record] = association.target
          association.reset
        end
      end

      ActiveRecord::Associations::Preloader.new.preload(
        records,
        base_association,
        preload_scope
      )

      records.each do |record|
        record.jit_preload_scoped_relations ||= {}
        association = record.association(base_association)
        record.jit_preload_scoped_relations[name] = association.target
        association.reset
        if previous_association_values.key?(record)
          association.target = previous_association_values[record]
        end
      end

      jit_preload_scoped_relations[name]
    end
  end

  def self.prepended(base)
    class << base
      delegate :jit_preload, to: :all

      def has_many_aggregate(assoc, name, aggregate, field, table_alias_name: nil, default: 0, max_ids_per_query: nil)
        method_name = "#{assoc}_#{name}"

        define_method(method_name) do |conditions={}|
          self.jit_preload_aggregates ||= {}

          key = "#{method_name}|#{conditions.sort.hash}"
          return jit_preload_aggregates[key] if jit_preload_aggregates.key?(key)
          if jit_preloader
            reflection = association(assoc).reflection
            primary_ids = jit_preloader.records.collect{|r| r[reflection.active_record_primary_key] }
            max_ids_per_query = max_ids_per_query || JitPreloader.max_ids_per_query
            if max_ids_per_query
              slices = primary_ids.each_slice(max_ids_per_query)
            else
              slices = [primary_ids]
            end

            klass = reflection.klass

            aggregate_association = reflection
            while aggregate_association.through_reflection
              aggregate_association = aggregate_association.through_reflection
            end

            association_scope = klass.all.merge(association(assoc).scope).unscope(where: aggregate_association.foreign_key)
            association_scope = association_scope.instance_exec(&reflection.scope).reorder(nil) if reflection.scope

            # If the query uses an alias for the association, use that instead of the table name
            table_reference = table_alias_name
            table_reference ||= association_scope.references_values.first || aggregate_association.table_name

            # If the association is a STI child model, specify its type in the condition so that it
            # doesn't include results from other child models
            parent_is_base_class = aggregate_association.klass.superclass.abstract_class? || aggregate_association.klass.superclass == ActiveRecord::Base
            has_type_column = aggregate_association.klass.column_names.include?(aggregate_association.klass.inheritance_column)
            is_child_sti_model = !parent_is_base_class && has_type_column
            if is_child_sti_model
              conditions[table_reference] = { aggregate_association.klass.inheritance_column => aggregate_association.klass.sti_name }
            end

            if reflection.type.present?
              conditions[reflection.type] = self.class.name
            end
            group_by = "#{table_reference}.#{aggregate_association.foreign_key}"

            preloaded_data = {}
            slices.each do |slice|
              data = Hash[association_scope
                            .where(conditions.deep_merge(table_reference => { aggregate_association.foreign_key => slice }))
                            .group(group_by)
                            .send(aggregate, field)
              ]
              preloaded_data.merge!(data)
            end

            jit_preloader.records.each do |record|
              record.jit_preload_aggregates ||= {}
              record.jit_preload_aggregates[key] = preloaded_data[record.id] || default
            end
          else
            self.jit_preload_aggregates[key] = send(assoc).where(conditions).send(aggregate, field) || default
          end
          jit_preload_aggregates[key]
        end
      end
    end
  end
end

ActiveRecord::Base.send(:prepend, JitPreloadExtension)
