module Refine::Conditions
  module UsesAttributes

    def with_attribute(value)
      @attribute = value
      self
    end

    def apply_relationship_attribute(input:, query:)
      # Split on first .
      decompose_attribute = @attribute.split(".", 2)
      # Attribute now is the back half of the initial attribute
      @attribute = decompose_attribute[1]

      if !@attribute.include?(".")
        # No more .s, deepest level
        @on_deepest_relationship = true
      end
      # Relation to be handled
      relation = decompose_attribute[0]

      # Get the Reflection object which defines the relationship between query and relation
      # First iteration pull relationship using base query which responds to model.
      instance = if query.respond_to? :model
        query.model.reflect_on_association(relation.to_sym)
      else
        # When query is sent in as subquery (recursive) the query object is the model class pulled from the
        # previous instance value
        query.reflect_on_association(relation.to_sym)
      end

      unless instance
        raise "Relationship does not exist for #{relation}."
      end

      filter.set_pending_relationship(relation, instance)
      
      # If the current condition is a refinement (filter refinement) set collapsible to true 
      if is_refinement
        filter.allow_pending_relationship_to_collapse
      end

      if can_use_where_in_relationship_subquery?(instance)
        create_pending_wherein_subquery(input: input, relation: relation, instance: instance, query: query)
      else
        create_pending_has_many_through_subquery(input: input, relation: relation, instance: instance, query: query)
      end

      filter.release_pending_relationship
      # We want the method to return nil for relationship attributes
      # The purpose of this method is to populate pending relationship subqueries
      nil
    end

    def key_1(instance)
      # Foreign key on belongs to, primary key on HasMany
      if instance.is_a? ActiveRecord::Reflection::BelongsToReflection
        instance.foreign_key.to_sym
      else
        instance.active_record_primary_key.to_sym
      end
    end

    def key_2(instance)
      if instance.is_a? ActiveRecord::Reflection::BelongsToReflection
        instance.active_record_primary_key.to_sym
      else
        instance.foreign_key.to_sym
      end
    end

    def create_pending_wherein_subquery(input:, relation:, instance:, query:)
      # This method builds out the linking keys between the provided query model and the relation
      # and saves it to pending relationship subqueries
      # Class of the relation as held in the AR::Relation object
      relation_class = instance.klass

      # Pull what's already in the tracker at this depth if already traversed
      subquery = filter.get_pending_relationship_subquery || relation_class.select([key_2(instance)]).arel

      # Primary/secondary keys keep track of how to link tables
      # If depth has been added (i.e. filter.pending_relationship_subquery_depth = [:btt_user, :btt_notes])
      # This will add the [:children] key to the pending_relationship_subqueries tracker under the parent key [:btt_user]
      filter.add_pending_relationship_subquery(subquery: subquery, primary_key: key_1(instance), secondary_key: key_2(instance))

      # Apply the condition. If a nested relationship, this apply is adding the children key (with values) to the pending_relationship_subqueries tracker
      # due to the recursive nature of the apply method. This is critical because it get is then "rolled up" in release_pending_relationship
      node = apply(input, relation_class.arel_table, relation_class, false)

      # If node is an AREL::SELECT manager we are allowing the apply condition to return a fully formed subquery - we replace
      # the linking keys in the tracker with the fully formed select query
      # Has not been tested more than one level deep
      if node.is_a? Arel::SelectManager
        filter.add_pending_relationship_subquery(subquery: node, primary_key: key_1(instance), secondary_key: key_2(instance))
      elsif node
        # This modifies subquery *in* the pending_relationship_subqueries tracker.
        subquery.where(node)
      end
    end

    def group(nodes)
      Arel::Nodes::Grouping.new(nodes)
    end

    # Determine if the clause should be flipped. For example, "not_eq" => "eq". Must also change "in" to "not in" upstream
    # @param [Object] instance
    # @param [String] clause The join clause (example: `eq` or `not_eq`)
    # @return [Boolean]
    def should_inverse_clause?(instance, clause)
      is_through_reflection = instance.is_a?(ActiveRecord::Reflection::ThroughReflection)
      is_inverse_clause_flippable = Clauses::FLIPPABLE.include?(clause)

      is_through_reflection && is_inverse_clause_flippable
    end

    def create_pending_has_many_through_subquery(input:, relation:, instance:, query:)
      # In a has_many relationship the negative has to be flipped to positive. 
      inverse_clause = should_inverse_clause?(instance, input[:clause])
      # Ex: A country has many posts through hmtt_users.
      # Use AR to properly join the relation to the base query provided
      # Convert to AREL to use with nodes 
      subquery_path = query.model.select(key_1(instance)).joins(relation.to_sym).arel
      relation_table_being_queried = instance.klass.arel_table

      relation_class = instance.klass
      
      node_to_apply = apply(input, relation_table_being_queried, relation_class, inverse_clause)

      complete_subquery = subquery_path.where(node_to_apply)
      subquery = filter.get_pending_relationship_subquery || complete_subquery
      filter.add_pending_relationship_subquery(subquery: subquery, primary_key: key_1(instance), secondary_key: nil, inverse_clause: inverse_clause)
    end

    def can_use_where_in_relationship_subquery?(instance)
      # Where in only works for belongs to, has one, or has many
      (instance.is_a? ActiveRecord::Reflection::BelongsToReflection) || (instance.is_a? ActiveRecord::Reflection::HasManyReflection) || (instance.is_a? ActiveRecord::Reflection::HasOneReflection)
    end

    def is_relationship_attribute?
      # TODO: Allow user to decide attribute is not a relationship
      # If we are on the deepest relationship, it's no longer a relationship attribute
      return false if @on_deepest_relationship
      # If the attribute includes a ., it's a relationship attribute
      @attribute.include?(".")
    end

    def raw_attribute(attribute)
      @attribute = Arel.sql(attribute)

      self
    end

    # TODO Examine the existing relationships and suggest model names if not instance is found 
    # def get_relationships(query)
      # if query.respond_to? :model
      #   associations = query.model.reflect_on_all_associations
      # else
      #   associations = query.reflect_on_all_associations
      # end
      # associations.map{|entry| puts entry.class, entry.foreign_key, entry.klass }
      # differences=[]
      # associations.each do association
      #   differences << String::Similarity.levenshtein_distance(relation, association )
      # end
      # differences
    # end
  end
end