# frozen_string_literal: true

module Facter
  class QueryParser
    @log = Log.new(self)
    class << self
      # Searches for facts that could resolve a user query.
      # There are 4 types of facts:
      #   root facts
      #     e.g. networking
      #   child facts
      #     e.g. networking.dhcp
      #   composite facts
      #     e.g. networking.interfaces.en0.bindings.address
      #   regex facts (legacy)
      #     e.g. impaddress_end160
      #
      # Because a root fact will always be resolved by a collection of child facts,
      # we can return one or more child facts for each parent.
      #
      # @param query_list [Array] The list of facts to search for
      # @param loaded_facts [Array] All of the fact definitions for the current operating system
      #
      # @return [Array<SearchedFact>] a list of searchable facts that resolve the user's query
      def parse(query_list, loaded_facts)
        matched_facts = []
        @query_list = query_list

        return no_user_query(loaded_facts) unless query_list.any?

        query_list.each do |query|
          found_facts = search_for_facts(query, loaded_facts)
          matched_facts << found_facts
        end

        matched_facts.flatten(1)
      end

      def no_user_query(loaded_facts)
        searched_facts = []
        loaded_facts.each do |loaded_fact|
          searched_facts << SearchedFact.new(loaded_fact.name, loaded_fact.klass, '', loaded_fact.type)
        end
        searched_facts
      end

      def search_for_facts(query, loaded_facts)
        resolvable_fact_list = []
        query = query.to_s
        query_tokens = query.end_with?('.*') ? [query] : query.split('.')
        size = query_tokens.size

        # Try to match the most specific query_tokens to the least, returning the first match
        size.times do |i|
          query_token_range = 0..size - i - 1
          query_fact = query_tokens[query_token_range].join('.')
          resolvable_fact_list = get_facts_matching_tokens(query_tokens, query_fact, loaded_facts)

          return resolvable_fact_list if resolvable_fact_list.any?
        end

        resolvable_fact_list << SearchedFact.new(query, nil, query, :nil) if resolvable_fact_list.empty?

        resolvable_fact_list
      end

      def get_facts_matching_tokens(query_tokens, query_fact, loaded_facts)
        resolvable_fact_list = []

        loaded_facts.each do |loaded_fact|
          next unless found_fact?(loaded_fact.name, query_fact)

          searched_fact = construct_loaded_fact(query_tokens, loaded_fact)
          resolvable_fact_list << searched_fact
        end

        @log.debug "List of resolvable facts: #{resolvable_fact_list.inspect}" if resolvable_fact_list.any?
        resolvable_fact_list
      end

      def found_fact?(fact_name, query_fact)
        # This is the case where the fact_name contains a wildcard like
        # blockdevice_.*_model and we're querying for the legacy fact
        # specifically using 'blockdevice_sba_model' and we don't want the query
        # 'blockdevice.sba.model' to match
        fact_with_wildcard = fact_name.include?('.*') && !query_fact.include?('.')

        if fact_with_wildcard
          # fact_name contains wildcard, so we're intentially not escaping.
          query_fact.match("^#{fact_name}$")
        else
          processed_equery_fact = query_fact.gsub('\\', '\\\\\\\\')
          # Must escape metacharacters (like dots) to ensure the correct fact is found
          fact_name.match("^#{Regexp.escape(processed_equery_fact)}($|\\.)")
        end
      end

      def construct_loaded_fact(query_tokens, loaded_fact)
        user_query = @query_list.any? ? query_tokens.join('.') : ''
        fact_name = loaded_fact.name.to_s
        klass_name = loaded_fact.klass
        type = loaded_fact.type
        sf = SearchedFact.new(fact_name, klass_name, user_query, type)
        sf.file = loaded_fact.file

        sf
      end
    end
  end
end
