# frozen_string_literal: true

require_relative '../../../puppet/parser/script_compiler'

module Puppet::Pops
module Evaluator
class DeferredValue
  def initialize(proc)
    @proc = proc
  end

  def resolve
    val = @proc.call
    # Deferred sensitive values will be marked as such in resolve_futures()
    if val.is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
      val.unwrap
    else
      val
    end
  end
end

# Utility class to help resolve instances of Puppet::Pops::Types::PDeferredType::Deferred
#
class DeferredResolver
  DOLLAR = '$'
  DIG    = 'dig'

  # Resolves and replaces all Deferred values in a catalog's resource attributes
  # found as direct values or nested inside Array, Hash or Sensitive values.
  # Deferred values inside of custom Object instances are not resolved as this
  # is expected to be done by such objects.
  #
  # @param facts [Puppet::Node::Facts] the facts object for the node
  # @param catalog [Puppet::Resource::Catalog] the catalog where all deferred values should be replaced
  # @param environment [Puppet::Node::Environment] the environment whose anonymous module methods
  #  are to be mixed into the scope
  # @return [nil] does not return anything - the catalog is modified as a side effect
  #
  def self.resolve_and_replace(facts, catalog, environment = catalog.environment_instance, preprocess_deferred = true)
    compiler = Puppet::Parser::ScriptCompiler.new(environment, catalog.name, preprocess_deferred)
    resolver = new(compiler, preprocess_deferred)
    resolver.set_facts_variable(facts)
    # TODO:
    #    # When scripting the trusted data are always local, but set them anyway
    #    @scope.set_trusted(node.trusted_data)
    #
    #    # Server facts are always about the local node's version etc.
    #    @scope.set_server_facts(node.server_facts)

    resolver.resolve_futures(catalog)
    nil
  end

  # Resolves a value such that a direct Deferred, or any nested Deferred values
  # are resolved and used instead of the deferred value.
  # A direct Deferred value, or nested deferred values inside of Array, Hash or
  # Sensitive values are resolved and replaced inside of freshly created
  # containers.
  #
  # The resolution takes place in the topscope of the given compiler.
  # Variable values are supposed to already have been set.
  #
  # @param value [Object] the (possibly nested) value to resolve
  # @param compiler [Puppet::Parser::ScriptCompiler, Puppet::Parser::Compiler] the compiler in effect
  # @return [Object] the resolved value (a new Array, Hash, or Sensitive if needed), with all deferred values resolved
  #
  def self.resolve(value, compiler)
    resolver = new(compiler)
    resolver.resolve(value)
  end

  def initialize(compiler, preprocess_deferred = true)
    @compiler = compiler
    # Always resolve in top scope
    @scope = @compiler.topscope
    @deferred_class = Puppet::Pops::Types::TypeFactory.deferred.implementation_class
    @preprocess_deferred = preprocess_deferred
  end

  # @param facts [Puppet::Node::Facts] the facts to set in $facts in the compiler's topscope
  #
  def set_facts_variable(facts)
    @scope.set_facts(facts.nil? ? {} : facts.values)
  end

  def resolve_futures(catalog)
    catalog.resources.each do |r|
      overrides = {}
      r.parameters.each_pair do |k, v|
        resolved = resolve(v)
        case resolved
        when Puppet::Pops::Types::PSensitiveType::Sensitive
          # If the resolved value is instance of Sensitive - assign the unwrapped value
          # and mark it as sensitive if not already marked
          #
          resolved = resolved.unwrap
          mark_sensitive_parameters(r, k)

        when Puppet::Pops::Evaluator::DeferredValue
          # If the resolved value is a DeferredValue and it has an argument of type
          # PSensitiveType, mark it as sensitive. Since DeferredValues can nest,
          # we must walk all arguments, e.g. the DeferredValue may call the `epp`
          # function, where one of its arguments is a DeferredValue to call the
          # `vault:lookup` function.
          #
          # The DeferredValue.resolve method will unwrap the sensitive during
          # catalog application
          #
          if contains_sensitive_args?(v)
            mark_sensitive_parameters(r, k)
          end
        end
        overrides[k] = resolved
      end
      r.parameters.merge!(overrides) unless overrides.empty?
    end
  end

  # Return true if x contains an argument that is an instance of PSensitiveType:
  #
  #   Deferred('new', [Sensitive, 'password'])
  #
  # Or an instance of PSensitiveType::Sensitive:
  #
  #   Deferred('join', [['a', Sensitive('b')], ':'])
  #
  # Since deferred values can nest, descend into Arrays and Hash keys and values,
  # short-circuiting when the first occurrence is found.
  #
  def contains_sensitive_args?(x)
    case x
    when @deferred_class
      contains_sensitive_args?(x.arguments)
    when Array
      x.any? { |v| contains_sensitive_args?(v) }
    when Hash
      x.any? { |k, v| contains_sensitive_args?(k) || contains_sensitive_args?(v) }
    when Puppet::Pops::Types::PSensitiveType, Puppet::Pops::Types::PSensitiveType::Sensitive
      true
    else
      false
    end
  end
  private :contains_sensitive_args?

  def mark_sensitive_parameters(r, k)
    unless r.sensitive_parameters.include?(k.to_sym)
      r.sensitive_parameters = (r.sensitive_parameters + [k.to_sym]).freeze
    end
  end
  private :mark_sensitive_parameters

  def resolve(x)
    if x.instance_of?(@deferred_class)
      resolve_future(x)
    elsif x.is_a?(Array)
      x.map { |v| resolve(v) }
    elsif x.is_a?(Hash)
      result = {}
      x.each_pair { |k, v| result[k] = resolve(v) }
      result
    elsif x.is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
      # rewrap in a new Sensitive after resolving any nested deferred values
      Puppet::Pops::Types::PSensitiveType::Sensitive.new(resolve(x.unwrap))
    elsif x.is_a?(Puppet::Pops::Types::PBinaryType::Binary)
      # use the ASCII-8BIT string that it wraps
      x.binary_buffer
    else
      x
    end
  end

  def resolve_lazy_args(x)
    case x
    when DeferredValue
      x.resolve
    when Array
      x.map { |v| resolve_lazy_args(v) }
    when Hash
      result = {}
      x.each_pair { |k, v| result[k] = resolve_lazy_args(v) }
      result
    when Puppet::Pops::Types::PSensitiveType::Sensitive
      # rewrap in a new Sensitive after resolving any nested deferred values
      Puppet::Pops::Types::PSensitiveType::Sensitive.new(resolve_lazy_args(x.unwrap))
    else
      x
    end
  end
  private :resolve_lazy_args

  def resolve_future(f)
    # If any of the arguments to a future is a future it needs to be resolved first
    func_name = f.name
    mapped_arguments = map_arguments(f.arguments)
    # if name starts with $ then this is a call to dig
    if func_name[0] == DOLLAR
      var_name = func_name[1..]
      func_name = DIG
      mapped_arguments.insert(0, @scope[var_name])
    end

    if @preprocess_deferred
      # call the function (name in deferred, or 'dig' for a variable)
      @scope.call_function(func_name, mapped_arguments)
    else
      # call the function later
      DeferredValue.new(
        proc {
          # deferred functions can have nested deferred arguments
          resolved_arguments = mapped_arguments.map { |arg| resolve_lazy_args(arg) }
          @scope.call_function(func_name, resolved_arguments)
        }
      )
    end
  end

  def map_arguments(args)
    return [] if args.nil?

    args.map { |v| resolve(v) }
  end
  private :map_arguments
end
end
end
