# frozen_string_literal: true

require 'hiera/scope'
require_relative 'sub_lookup'
module Puppet::Pops
module Lookup
# Adds support for interpolation expressions. The expressions may contain keys that uses dot-notation
# to further navigate into hashes and arrays
#
# @api public
module Interpolation
  include SubLookup

  # @param value [Object] The value to interpolate
  # @param context [Context] The current lookup context
  # @param allow_methods [Boolean] `true` if interpolation expression that contains lookup methods are allowed
  # @return [Object] the result of resolving all interpolations in the given value
  # @api public
  def interpolate(value, context, allow_methods)
    case value
    when String
      value.index('%{').nil? ? value : interpolate_string(value, context, allow_methods)
    when Array
      value.map { |element| interpolate(element, context, allow_methods) }
    when Hash
      result = {}
      value.each_pair { |k, v| result[interpolate(k, context, allow_methods)] = interpolate(v, context, allow_methods) }
      result
    else
      value
    end
  end

  private

  EMPTY_INTERPOLATIONS = {
    '' => true,
    '::' => true,
    '""' => true,
    "''" => true,
    '"::"' => true,
    "'::'" => true
  }.freeze

  # Matches a key that is quoted using a matching pair of either single or double quotes.
  QUOTED_KEY = /^(?:"([^"]+)"|'([^']+)')$/

  def interpolate_string(subject, context, allow_methods)
    lookup_invocation = context.is_a?(Invocation) ? context : context.invocation
    lookup_invocation.with(:interpolate, subject) do
      subject.gsub(/%\{([^}]*)\}/) do |match|
        expr = ::Regexp.last_match(1)
        # Leading and trailing spaces inside an interpolation expression are insignificant
        expr.strip!
        value = nil
        unless EMPTY_INTERPOLATIONS[expr]
          method_key, key = get_method_and_data(expr, allow_methods)
          is_alias = method_key == :alias

          # Alias is only permitted if the entire string is equal to the interpolate expression
          fail(Issues::HIERA_INTERPOLATION_ALIAS_NOT_ENTIRE_STRING) if is_alias && subject != match

          value = interpolate_method(method_key).call(key, lookup_invocation, subject)

          # break gsub and return value immediately if this was an alias substitution. The value might be something other than a String
          return value if is_alias

          value = lookup_invocation.check(method_key == :scope ? "scope:#{key}" : key) { interpolate(value, lookup_invocation, allow_methods) }
        end
        value.nil? ? '' : value
      end
    end
  end

  def interpolate_method(method_key)
    @@interpolate_methods ||= begin
      global_lookup = lambda do |key, lookup_invocation, _|
        scope = lookup_invocation.scope
        if scope.is_a?(Hiera::Scope) && !lookup_invocation.global_only?
          # "unwrap" the Hiera::Scope
          scope = scope.real
        end
        lookup_invocation.with_scope(scope) do |sub_invocation|
          sub_invocation.lookup(key) { Lookup.lookup(key, nil, '', true, nil, sub_invocation) }
        end
      end
      scope_lookup = lambda do |key, lookup_invocation, subject|
        segments = split_key(key) { |problem| Puppet::DataBinding::LookupError.new("#{problem} in string: #{subject}") }
        root_key = segments.shift
        value = lookup_invocation.with(:scope, 'Global Scope') do
          ovr = lookup_invocation.override_values
          if ovr.include?(root_key)
            lookup_invocation.report_found_in_overrides(root_key, ovr[root_key])
          else
            scope = lookup_invocation.scope
            val = nil
            if (default_key_exists = lookup_invocation.default_values.include?(root_key))
              catch(:undefined_variable) { val = scope[root_key] }
            else
              val = scope[root_key]
            end
            if val.nil? && !nil_in_scope?(scope, root_key)
              if default_key_exists
                lookup_invocation.report_found_in_defaults(root_key,
                                                           lookup_invocation.default_values[root_key])
              else
                nil
              end
            else
              lookup_invocation.report_found(root_key, val)
            end
          end
        end
        unless value.nil? || segments.empty?
          found = nil;
          catch(:no_such_key) { found = sub_lookup(key, lookup_invocation, segments, value) }
          value = found;
        end
        lookup_invocation.remember_scope_lookup(key, root_key, segments, value)
        value
      end

      {
        :lookup => global_lookup,
        :hiera => global_lookup, # this is just an alias for 'lookup'
        :alias => global_lookup, # same as 'lookup' but expression must be entire string and result is not subject to string substitution
        :scope => scope_lookup,
        :literal => ->(key, _, _) { key }
      }.freeze
    end
    interpolate_method = @@interpolate_methods[method_key]
    fail(Issues::HIERA_INTERPOLATION_UNKNOWN_INTERPOLATION_METHOD, :name => method_key) unless interpolate_method

    interpolate_method
  end

  # Because the semantics of Puppet::Parser::Scope#include? differs from Hash#include?
  def nil_in_scope?(scope, key)
    if scope.is_a?(Hash)
      scope.include?(key)
    else
      scope.exist?(key)
    end
  end

  def get_method_and_data(data, allow_methods)
    match = data.match(/^(\w+)\((?:"([^"]+)"|'([^']+)')\)$/)
    if match
      fail(Issues::HIERA_INTERPOLATION_METHOD_SYNTAX_NOT_ALLOWED) unless allow_methods

      key = match[1].to_sym
      data = match[2] || match[3] # double or single qouted
    else
      key = :scope
    end
    [key, data]
  end

  def fail(issue, args = EMPTY_HASH)
    raise Puppet::DataBinding::LookupError.new(
      issue.format(args), nil, nil, nil, nil, issue.issue_code
    )
  end
end
end
end
