# frozen_string_literal: true

# This represents a fact resolution. A resolution is a concrete
# implementation of a fact. A single fact can have many resolutions and
# the correct resolution will be chosen at runtime. Each time
# {Facter.add} is called, a new resolution is created and added to the
# set of resolutions for the fact named in the call.  Each resolution
# has a {#has_weight weight}, which defines its priority over other
# resolutions, and a set of {#confine _confinements_}, which defines the
# conditions under which it will be chosen. All confinements must be
# satisfied for a fact to be considered _suitable_.
#
# @api public
module Facter
  module Util
    class Resolution
      # @api private
      attr_accessor :code, :fact_type

      # @api private
      attr_writer :value

      extend Facter::Core::Execution

      class << self
        # Expose command execution methods that were extracted into
        # Facter::Core::Execution from Facter::Util::Resolution in Facter 2.0.0 for
        # compatibility.
        #
        # @deprecated
        #
        # @api public
        public :which, :exec

        # @api private
        public :with_env
      end

      include LegacyFacter::Core::Resolvable
      include LegacyFacter::Core::Suitable

      # @!attribute [rw] name
      # The name of this resolution. The resolution name should be unique with
      #   respect to the given fact.
      #
      # @return [String]
      #
      # @api public
      attr_accessor :name

      # @!attribute [r] fact
      #
      # @return [Facter::Util::Fact] Associated fact with this resolution.
      #
      # @api private
      attr_reader :fact, :last_evaluated, :file

      # Create a new resolution mechanism.
      #
      # @param name [String] The name of the resolution.
      # @param fact [Facter::Fact] The fact to which this
      #             resolution will be added.
      #
      # @return [Facter::Util::Resolution] The created resolution
      #
      # @api public
      def initialize(name, fact)
        @name = name
        @fact = fact
        @confines = []
        @value = nil
        @timeout = 0
        @weight = nil
      end

      # Returns the fact's resolution type
      #
      # @return [Symbol] The fact's type
      #
      # @api private
      def resolution_type
        :simple
      end

      # Evaluate the given block in the context of this resolution. If a block has
      # already been evaluated emit a warning to that effect.
      #
      # @return [String] Result of the block's evaluation
      #
      # @api private
      def evaluate(&block)
        if @last_evaluated
          msg = +"Already evaluated #{@name}"
          msg << " at #{@last_evaluated}" if msg.is_a? String
          msg << ', reevaluating anyways'
          log.warn msg
        end

        instance_eval(&block)

        @last_evaluated = block.source_location.join(':')
      end

      # Sets options for the aggregate fact
      #
      # @return [nil]
      #
      # @api private
      def options(options)
        accepted_options = %i[name value timeout weight fact_type file is_env]

        accepted_options.each do |option_name|
          instance_variable_set("@#{option_name}", options.delete(option_name)) if options.key?(option_name)
        end

        raise ArgumentError, "Invalid resolution options #{options.keys.inspect}" unless options.keys.empty?
      end

      # Sets the code block or external program that will be evaluated to
      # get the value of the fact.
      #
      # @overload setcode(string)
      #   Sets an external program to call to get the value of the resolution
      #   @param [String] string the external program to run to get the
      #     value
      #
      # @overload setcode(&block)
      #   Sets the resolution's value by evaluating a block at runtime
      #   @param [Proc] block The block to determine the resolution's value.
      #     This block is run when the fact is evaluated. Errors raised from
      #     inside the block are rescued and printed to stderr.
      #
      # @return [Facter::Util::Resolution] Returns itself
      #
      # @api public
      def setcode(string = nil, &block)
        if string
          @code = proc do
            output = Facter::Core::Execution.execute(string, on_fail: nil)
            if output.nil? || output.empty?
              nil
            else
              output
            end
          end
        elsif block_given?
          @code = block
        else
          raise ArgumentError, 'You must pass either code or a block'
        end
        self
      end

      # Comparison is done based on weight and fact type.
      #   The greater the weight, the higher the priority.
      #   If weights are equal, we consider external facts greater than custom facts.
      #
      # @return [bool] Weight comparison result
      #
      # @api private
      def <=>(other)
        if weight == other.weight
          compare_equal_weights(other)
        else
          weight <=> other.weight
        end
      end

      private

      def log
        @log ||= Facter::Log.new(self)
      end

      # If the weights are equal, we consider external facts greater tan custom facts
      def compare_equal_weights(other)
        # Other is considered greater because self is custom fact and other is external
        return -1 if fact_type == :custom && other.fact_type == :external

        # Self is considered greater, because it is external fact and other is custom
        return 1 if fact_type == :external && other.fact_type == :custom

        # They are considered equal
        0
      end

      def resolve_value
        if !@value.nil?
          @value
        elsif @code.nil?
          nil
        elsif @code
          @code.call
        end
      end
    end
  end
end
