# typed: true
# frozen_string_literal: true

require "active_record/associations/association_scope"

class ActiveRecord::Associations::AssociationScope
  def get_chain(reflection, association, tracker)
    name = reflection.name
    chain =
      T.let(
        [
          ActiveRecord::Reflection::RuntimeReflection.new(
            reflection,
            association
          )
        ],
        T.untyped
      )

    reflection
      .chain
      .drop(1)
      .each do |refl|
        refl_klass = T.cast(refl.klass, T.class_of(ActiveRecord::Base))
        if refl_klass.is_a?(HasAuxTable::ClassMethods) &&
             (aux_config = refl_klass.aux_table_for(refl.foreign_key))
          aliased_table = aux_config.aux.klass.arel_table
          chain << ReflectionProxy.new(refl, aliased_table)
        else
          aliased_table =
            tracker.aliased_table_for(refl.klass.arel_table) do
              refl.alias_candidate(name)
            end
          chain << ReflectionProxy.new(refl, aliased_table)
        end
      end
    chain
  end

  def scope(association)
    klass = association.klass
    reflection = association.reflection
    scope = klass.unscoped
    owner = association.owner
    chain = get_chain(reflection, association, scope.alias_tracker)

    scope.extending! reflection.extensions
    scope = add_constraints(scope, owner, chain)
    scope.limit!(1) unless reflection.collection?
    chain.each do |refl|
      klass = refl.klass
      next unless klass.is_a?(HasAuxTable::ClassMethods)
      next unless aux_config = klass.aux_table_for(refl.join_primary_key)
      aux_table = aux_config.aux.klass.table_name
      main_table = aux_config.main.klass.table_name
      main_keys =
        aux_config.main.primary_keys.map { |key| "'#{main_table}'.'#{key}'" }
      aux_keys =
        aux_config.aux.primary_keys.map { |key| "'#{aux_table}'.'#{key}'" }
      join_clause =
        main_keys
          .zip(aux_keys)
          .map { |(main_key, aux_key)| "#{main_key} = #{aux_key}" }
          .join(" AND ")
      scope.joins!("INNER JOIN '#{main_table}' ON (#{join_clause})")
    end if association.is_a?(
      ActiveRecord::Associations::HasManyThroughAssociation
    )
    scope
  end
end

module HasAuxTable
  module RelationExtensions
    extend T::Sig
    Util = HasAuxTable::Util

    sig { params(aux_config: AuxTableConfig).void }
    def setup_relation_extensions!(aux_config)
      main_class = aux_config.main.klass

      Util.hook_method(main_class, :where, false) do |original, *args|
        if args.length == 1 && args.first.is_a?(Hash)
          opts_remapped = aux_config.remap_conditions(args.first)
          original.call(opts_remapped)
        else
          original.call(*args)
        end
      end

      Util.hook_method(
        main_class,
        :all,
        false
      ) do |original, *args, **kwargs, &block|
        original.call(*args, **kwargs, &block).eager_load(
          aux_config.aux_association_name
        )
      end

      Util.hook_method(
        main_class,
        :unscoped,
        false
      ) do |original, *args, **kwargs, &block|
        ret = original.call(*args, **kwargs, &block)
        if ret.is_a?(ActiveRecord::Relation)
          ret.eager_load!(aux_config.aux_association_name)
        end
        ret
      end

      relation_class =
        main_class.relation_delegate_class(ActiveRecord::Relation)

      collection_proxy_class =
        main_class.relation_delegate_class(
          ActiveRecord::Associations::CollectionProxy
        )

      [
        [relation_class, :build_where_clause],
        [collection_proxy_class, :where]
      ].each do |klass, method_name|
        Util.hook_method(klass, method_name, true) do |original, opts, *rest|
          if opts.is_a?(Hash)
            opts_remapped = aux_config.remap_conditions(opts)
            T.unsafe(original).call(opts_remapped, *rest)
          else
            T.unsafe(original).call(opts, *rest)
          end
        end
      end

      Util.hook_method(
        relation_class,
        :bind_attribute,
        true
      ) do |original, name, value, &block|
        aux_config.aux_bind_attribute(name, value, &block)
      end
    end
  end
end
