class EagerEye::Detectors::LoopAssociation

def self.detector_name

def self.detector_name
  :loop_association
end

def build_variable_maps(ast)

def build_variable_maps(ast)
  @variable_preloads = {}
  @single_record_variables = Set.new
  traverse_ast(ast) { |node| process_variable_assignment(node) }
end

def detect(ast, file_path, association_preloads = {})

def detect(ast, file_path, association_preloads = {})
  return [] unless ast
  issues = []
  @association_preloads = association_preloads
  build_variable_maps(ast)
  traverse_ast(ast) do |node|
    next unless iteration_block?(node)
    block_var = extract_block_variable(node)
    next unless block_var
    block_body = node.children[2]
    next unless block_body
    collection_node = node.children[0]
    next if single_record_iteration?(collection_node)
    included = extract_included_associations(collection_node)
    included.merge(extract_variable_preloads(collection_node))
    included.merge(get_association_preloads(infer_model_name_from_collection(collection_node)))
    find_association_calls(block_body, block_var, file_path, issues, included)
  end
  issues
end

def excluded_method?(method, included)

def excluded_method?(method, included)
  EXCLUDED_METHODS.include?(method) ||
    !ASSOCIATION_NAMES.include?(method.to_s) ||
    included.include?(method)
end

def extract_included_associations(collection_node)

def extract_included_associations(collection_node)
  included = Set.new
  return included unless collection_node&.type == :send
  current = collection_node
  while current&.type == :send
    extract_includes_from_method(current, included) if PRELOAD_METHODS.include?(current.children[1])
    current = current.children[0]
  end
  included
end

def extract_includes_from_method(method_node, included_set)

def extract_includes_from_method(method_node, included_set)
  included_set.merge(extract_symbols_from_args(extract_method_args(method_node)))
end

def extract_variable_preloads(node)

def extract_variable_preloads(node)
  key = variable_key_for_node(node)
  (key && @variable_preloads&.[](key)) || Set.new
end

def find_association_calls(node, block_var, file_path, issues, included_associations = Set.new)

def find_association_calls(node, block_var, file_path, issues, included_associations = Set.new)
  reported = Set.new
  traverse_ast(node) do |child|
    next unless reportable_association_call?(child, block_var, reported, included_associations)
    method = child.children[1]
    issues << create_issue(
      file_path: file_path,
      line_number: child.loc.line,
      message: "Potential N+1 query: `#{block_var}.#{method}` called inside loop",
      suggestion: "Use `includes(:#{method})` before iterating"
    )
  end
end

def find_last_send_method(node)

def find_last_send_method(node)
  current = node
  while current&.type == :send
    return current.children[1] if SINGLE_RECORD_METHODS.include?(current.children[1])
    current = current.children[0]
  end
  nil
end

def get_association_preloads(model_name)

def get_association_preloads(model_name)
  preloaded = Set.new
  @association_preloads&.each do |key, assocs|
    preloaded.merge(assocs) if key.start_with?("#{model_name}#")
  end
  preloaded
end

def infer_model_name_from_collection(node)

def infer_model_name_from_collection(node)
  return nil unless node&.type == :send
  receiver = node.children[0]
  receiver.children[1].to_s if receiver&.type == :const
end

def iteration_block?(node)

def iteration_block?(node)
  node.type == :block && node.children[0]&.type == :send &&
    ITERATION_METHODS.include?(node.children[0].children[1])
end

def process_variable_assignment(node)

def process_variable_assignment(node)
  return unless %i[lvasgn ivasgn].include?(node.type)
  var_type = node.type == :lvasgn ? :lvar : :ivar
  var_name = node.children[0]
  value_node = node.children[1]
  return unless value_node
  key = [var_type, var_name]
  preloaded = extract_included_associations(value_node)
  @variable_preloads[key] = preloaded unless preloaded.empty?
  @single_record_variables.add(key) if single_record_query?(value_node)
end

def reportable_association_call?(node, block_var, reported, included)

def reportable_association_call?(node, block_var, reported, included)
  return false unless node.type == :send
  receiver = node.children[0]
  method = node.children[1]
  return false unless receiver&.type == :lvar && receiver.children[0] == block_var
  return false if excluded_method?(method, included)
  reported.add?("#{node.loc.line}:#{method}")
end

def single_record_iteration?(node)

def single_record_iteration?(node)
  return false unless node&.type == :send && (receiver = node.children[0])
  key = variable_key_for_node(receiver)
  (key && @single_record_variables&.include?(key)) || single_record_query?(receiver)
end

def single_record_query?(node)

def single_record_query?(node)
  last_send = find_last_send_method(node)
  last_send && SINGLE_RECORD_METHODS.include?(last_send)
end

def variable_key_for_node(node)

def variable_key_for_node(node)
  case node&.type
  when :lvar then [:lvar, node.children[0]]
  when :ivar then [:ivar, node.children[0]]
  when :send then variable_key_for_node(node.children[0])
  end
end