# frozen_string_literal: true

require_relative 'function_provider'

module Puppet::Pops
module Lookup
# @api private
class DataDigFunctionProvider < FunctionProvider
  TAG = 'data_dig'

  # Performs a lookup with the assumption that a recursive check has been made.
  #
  # @param key [LookupKey] The key to lookup
  # @param lookup_invocation [Invocation] The current lookup invocation
  # @param merge [MergeStrategy,String,Hash{String => Object},nil] Merge strategy, merge strategy name, strategy and options hash, or nil (implies "first found")
  # @return [Object] the found object
  # @throw :no_such_key when the object is not found
  def unchecked_key_lookup(key, lookup_invocation, merge)
    lookup_invocation.with(:data_provider, self) do
      MergeStrategy.strategy(merge).lookup(locations, lookup_invocation) do |location|
        invoke_with_location(lookup_invocation, location, key, merge)
      end
    end
  end

  def invoke_with_location(lookup_invocation, location, key, merge)
    if location.nil?
      key.undig(lookup_invocation.report_found(key, validated_data_dig(key, lookup_invocation, nil, merge)))
    else
      lookup_invocation.with(:location, location) do
        key.undig(lookup_invocation.report_found(key, validated_data_dig(key, lookup_invocation, location, merge)))
      end
    end
  end

  def label
    'Data Dig'
  end

  def validated_data_dig(key, lookup_invocation, location, merge)
    validate_data_value(data_dig(key, lookup_invocation, location, merge)) do
      msg = "Value for key '#{key}', returned from #{full_name}"
      location.nil? ? msg : "#{msg}, when using location '#{location}',"
    end
  end

  private

  def data_dig(key, lookup_invocation, location, merge)
    unless location.nil? || location.exist?
      lookup_invocation.report_location_not_found
      throw :no_such_key
    end
    ctx = function_context(lookup_invocation, location)
    ctx.data_hash ||= {}
    catch(:no_such_key) do
      hash = ctx.data_hash
      hash[key] = ctx.function.call(lookup_invocation.scope, key.to_a, options(location), Context.new(ctx, lookup_invocation)) unless hash.include?(key)
      return hash[key]
    end
    lookup_invocation.report_not_found(key)
    throw :no_such_key
  end
end

# @api private
class V3BackendFunctionProvider < DataDigFunctionProvider
  TAG = 'hiera3_backend'

  def data_dig(key, lookup_invocation, location, merge)
    @backend ||= instantiate_backend(lookup_invocation)

    # A merge_behavior retrieved from hiera.yaml must not be converted here. Instead, passing the symbol :hash
    # tells the V3 backend to pick it up from the config.
    resolution_type = lookup_invocation.hiera_v3_merge_behavior? ? :hash : convert_merge(merge)
    @backend.lookup(key.to_s, lookup_invocation.scope, lookup_invocation.hiera_v3_location_overrides, resolution_type, { :recurse_guard => nil })
  end

  def full_name
    "hiera version 3 backend '#{options[HieraConfig::KEY_BACKEND]}'"
  end

  def value_is_validated?
    false
  end

  private

  def instantiate_backend(lookup_invocation)
    backend_name = options[HieraConfig::KEY_BACKEND]
    begin
      require 'hiera/backend'
      require "hiera/backend/#{backend_name.downcase}_backend"
      backend = Hiera::Backend.const_get("#{backend_name.capitalize}_backend").new
      backend.method(:lookup).arity == 4 ? Hiera::Backend::Backend1xWrapper.new(backend) : backend
    rescue LoadError => e
      lookup_invocation.report_text { "Unable to load backend '#{backend_name}': #{e.message}" }
      throw :no_such_key
    rescue NameError => e
      lookup_invocation.report_text { "Unable to instantiate backend '#{backend_name}': #{e.message}" }
      throw :no_such_key
    end
  end

  # Converts a lookup 'merge' parameter argument into a Hiera 'resolution_type' argument.
  #
  # @param merge [String,Hash,nil] The lookup 'merge' argument
  # @return [Symbol,Hash,nil] The Hiera 'resolution_type'
  def convert_merge(merge)
    case merge
    when nil, 'first', 'default'
      # Nil is OK. Defaults to Hiera :priority
      nil
    when Puppet::Pops::MergeStrategy
      convert_merge(merge.configuration)
    when 'unique'
      # Equivalent to Hiera :array
      :array
    when 'hash'
      # Equivalent to Hiera :hash with default :native merge behavior. A Hash must be passed here
      # to override possible Hiera deep merge config settings.
      { :behavior => :native }
    when 'deep', 'unconstrained_deep'
      # Equivalent to Hiera :hash with :deeper merge behavior.
      { :behavior => :deeper }
    when 'reverse_deep'
      # Equivalent to Hiera :hash with :deep merge behavior.
      { :behavior => :deep }
    when Hash
      strategy = merge['strategy']
      case strategy
      when 'deep', 'unconstrained_deep', 'reverse_deep'
        result = { :behavior => strategy == 'reverse_deep' ? :deep : :deeper }
        # Remaining entries must have symbolic keys
        merge.each_pair { |k, v| result[k.to_sym] = v unless k == 'strategy' }
        result
      else
        convert_merge(strategy)
      end
    else
      raise Puppet::DataBinding::LookupError, "Unrecognized value for request 'merge' parameter: '#{merge}'"
    end
  end
end
end
end
