require "pluck_map/association_scope"
require "pluck_map/relationships/base"
require "pluck_map/relationships/many"
require "pluck_map/relationships/one"
require "pluck_map/relationships/polymorphic_one"

module PluckMap
  module Relationships
    class << self

      def one(model, name, block, options)
        reflection = reflection_for(model, name)
        if reflection.polymorphic?
          Relationships::PolymorphicOne.new(name, reflection, block, options)
        else
          Relationships::One.new(name, scope_for_reflection(reflection), block, options)
        end
      end

      def many(model, name, block, options)
        Relationships::Many.new(name, scope(model, name), block, options)
      end

    private

      def scope(model, name)
        scope_for_reflection(reflection_for(model, name))
      end

      def scope_for_reflection(reflection)
        scope_for(association_for(reflection))
      end

      def reflection_for(model, name)
        # Use `_reflections.fetch(name)` instead of `reflect_on_association(name)`
        # because they have different behavior when it comes to HasAndBelongsToMany
        # associations.
        #
        # `reflect_on_association` will return a HasAndBelongsToManyReflection
        # while `_reflections.fetch(name)` will return a ThroughReflection that
        # wraps a HasAndBelongsToManyReflection.
        #
        # ActiveRecord::Associations::AssociationScope expects the latter.
        #
        model._reflections.fetch(name.to_s) do
          raise ArgumentError, "#{name} is not an association on #{model}"
        end
      end

      def association_for(reflection)
        owner = AbstractOwner.new(reflection)
        reflection.association_class.new(owner, reflection)
      end

      def scope_for(association)
        AssociationScope.create.scope(association)
      end

      # ActiveRecord constructs an Association from a Reflection and an
      # Owner. It expects Owner to be an instance of an ActiveRecord object
      # and uses `[]` to access specific values for fields on the record.
      #
      #    e.g. WHERE books.author_id = 7
      #
      # We want to create a subquery that will reference those fields
      # but not their specific values.
      #
      #    e.g. WHERE books.author_id = authors.id
      #
      # So we create an object that serves the purpose of Owner but returns
      # appropriate selectors.
      #
      AbstractOwner = Struct.new(:reflection) do
        def class
          reflection.active_record
        end

        def [](value)
          self.class.arel_table[value]
        end
      end

    end
  end
end