# frozen_string_literal: true

module Bullet
  module ActiveRecord
    def self.enable
      require 'active_record'
      ::ActiveRecord::Base.class_eval do
        class << self
          alias_method :origin_find, :find
          def find(*args)
            result = origin_find(*args)
            if Bullet.start?
              if result.is_a? Array
                Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
                Bullet::Detector::CounterCache.add_possible_objects(result)
              elsif result.is_a? ::ActiveRecord::Base
                Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
                Bullet::Detector::CounterCache.add_impossible_object(result)
              end
            end
            result
          end

          alias_method :origin_find_by_sql, :find_by_sql
          def find_by_sql(sql, binds = [])
            result = origin_find_by_sql(sql, binds)
            if Bullet.start?
              if result.is_a? Array
                if result.size > 1
                  Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
                  Bullet::Detector::CounterCache.add_possible_objects(result)
                elsif result.size == 1
                  Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
                  Bullet::Detector::CounterCache.add_impossible_object(result.first)
                end
              elsif result.is_a? ::ActiveRecord::Base
                Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
                Bullet::Detector::CounterCache.add_impossible_object(result)
              end
            end
            result
          end
        end
      end

      ::ActiveRecord::Persistence.class_eval do
        def _create_record_with_bullet(*args)
          _create_record_without_bullet(*args).tap do
            Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
            Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
          end
        end
        alias_method_chain :_create_record, :bullet
      end

      ::ActiveRecord::Relation.class_eval do
        alias_method :origin_to_a, :to_a

        # if select a collection of objects, then these objects have possible to cause N+1 query.
        # if select only one object, then the only one object has impossible to cause N+1 query.
        def to_a
          records = origin_to_a
          if Bullet.start?
            if records.first.class.name !~ /^HABTM_/
              if records.size > 1
                Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
                Bullet::Detector::CounterCache.add_possible_objects(records)
              elsif records.size == 1
                Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
                Bullet::Detector::CounterCache.add_impossible_object(records.first)
              end
            end
          end
          records
        end
      end

      ::ActiveRecord::Associations::Preloader.class_eval do
        alias_method :origin_preloaders_on, :preloaders_on

        def preloaders_on(association, records, scope)
          if Bullet.start?
            records.compact!
            if records.first.class.name !~ /^HABTM_/
              records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) }
              Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
            end
          end
          origin_preloaders_on(association, records, scope)
        end
      end

      ::ActiveRecord::FinderMethods.class_eval do
        # add includes in scope
        alias_method :origin_find_with_associations, :find_with_associations
        def find_with_associations
          return origin_find_with_associations { |r| yield r } if block_given?

          records = origin_find_with_associations
          if Bullet.start?
            associations = (eager_load_values + includes_values).uniq
            records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) }
            Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations)
          end
          records
        end
      end

      ::ActiveRecord::Associations::JoinDependency.class_eval do
        alias_method :origin_instantiate, :instantiate
        alias_method :origin_construct, :construct
        alias_method :origin_construct_model, :construct_model

        def instantiate(result_set, aliases)
          @bullet_eager_loadings = {}
          records = origin_instantiate(result_set, aliases)

          if Bullet.start?
            @bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
              objects = eager_loadings_hash.keys
              Bullet::Detector::UnusedEagerLoading.add_eager_loadings(objects, eager_loadings_hash[objects.first].to_a)
            end
          end
          records
        end

        def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
          if Bullet.start?
            unless ar_parent.nil?
              parent.children.each do |node|
                key = aliases.column_alias(node, node.primary_key)
                id = row[key]
                next unless id.nil?

                associations = [node.reflection.name]
                if node.reflection.nested?
                  associations << node.reflection.through_reflection.name
                end
                associations.each do |association|
                  Bullet::Detector::Association.add_object_associations(ar_parent, association)
                  Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
                  @bullet_eager_loadings[ar_parent.class] ||= {}
                  @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
                  @bullet_eager_loadings[ar_parent.class][ar_parent] << association
                end
              end
            end
          end

          origin_construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
        end

        # call join associations
        def construct_model(record, node, row, model_cache, id, aliases)
          result = origin_construct_model(record, node, row, model_cache, id, aliases)

          if Bullet.start?
            associations = [node.reflection.name]
            if node.reflection.nested?
              associations << node.reflection.through_reflection.name
            end
            associations.each do |association|
              Bullet::Detector::Association.add_object_associations(record, association)
              Bullet::Detector::NPlusOneQuery.call_association(record, association)
              @bullet_eager_loadings[record.class] ||= {}
              @bullet_eager_loadings[record.class][record] ||= Set.new
              @bullet_eager_loadings[record.class][record] << association
            end
          end

          result
        end
      end

      ::ActiveRecord::Associations::CollectionAssociation.class_eval do
        # call one to many associations
        alias_method :origin_load_target, :load_target
        def load_target
          records = origin_load_target

          if Bullet.start?
            Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless @inversed
            if records.first.class.name !~ /^HABTM_/
              if records.size > 1
                Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
                Bullet::Detector::CounterCache.add_possible_objects(records)
              elsif records.size == 1
                Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
                Bullet::Detector::CounterCache.add_impossible_object(records.first)
              end
            end
          end
          records
        end

        alias_method :origin_empty?, :empty?
        def empty?
          if Bullet.start? && !has_cached_counter?(@reflection)
            Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
          end
          origin_empty?
        end

        alias_method :origin_include?, :include?
        def include?(object)
          Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start?
          origin_include?(object)
        end
      end

      ::ActiveRecord::Associations::SingularAssociation.class_eval do
        # call has_one and belongs_to associations
        alias_method :origin_reader, :reader
        def reader(force_reload = false)
          result = origin_reader(force_reload)

          if Bullet.start?
            if @owner.class.name !~ /^HABTM_/ && !@inversed
              Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)

              if Bullet::Detector::NPlusOneQuery.impossible?(@owner)
                Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
              else
                Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
              end
            end
          end
          result
        end
      end

      ::ActiveRecord::Associations::HasManyAssociation.class_eval do
        alias_method :origin_many_empty?, :empty?
        def empty?
          result = origin_many_empty?
          if Bullet.start? && !has_cached_counter?(@reflection)
            Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
          end
          result
        end

        alias_method :origin_count_records, :count_records
        def count_records
          result = has_cached_counter?
          Bullet::Detector::CounterCache.add_counter_cache(@owner, @reflection.name) if Bullet.start? && !result
          origin_count_records
        end
      end

      ::ActiveRecord::Associations::CollectionProxy.class_eval do
        def count(column_name = nil, options = {})
          if Bullet.start?
            Bullet::Detector::CounterCache.add_counter_cache(proxy_association.owner, proxy_association.reflection.name)
            Bullet::Detector::NPlusOneQuery.call_association(proxy_association.owner, proxy_association.reflection.name)
          end
          super(column_name, options)
        end
      end
    end
  end
end
