# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require "set"

require "vagrant/config/v2/util"

module Vagrant
  module Config
    module V2
      # This is the root configuration class. An instance of this is what
      # is passed into version 2 Vagrant configuration blocks.
      class Root
        # Initializes a root object that maps the given keys to specific
        # configuration classes.
        #
        # @param [Hash] config_map Map of key to config class.
        def initialize(config_map, keys=nil)
          @keys              = keys || {}
          @config_map        = config_map
          @missing_key_calls = Set.new
          @logger            = Log4r::Logger.new("vagrant::config")
        end

        # We use method_missing as a way to get the configuration that is
        # used for Vagrant and load the proper configuration classes for
        # each.
        def method_missing(name, *args)
          return @keys[name] if @keys.key?(name)

          config_klass = @config_map[name.to_sym]
          if config_klass
            # Instantiate the class and return the instance
            @keys[name] = config_klass.new
          else
            @logger.debug("missing key request name=#{name} loc=#{caller.first}")
            # Record access to a missing key as an error
            @missing_key_calls.add(name.to_s)
            @keys[name] = DummyConfig.new
          end

          @keys[name]
        end

        # Called to finalize this object just prior to it being used by
        # the Vagrant system. The "!" signifies that this is expected to
        # mutate itself.
        def finalize!
          @config_map.each do |key, klass|
            if !@keys.key?(key)
              @keys[key] = klass.new
            end
          end

          @keys.each do |_key, instance|
            instance.finalize!
            instance._finalize!
          end
        end

        # This validates the configuration and returns a hash of error
        # messages by section. If there are no errors, an empty hash
        # is returned.
        #
        # @param [Environment] env
        # @return [Hash]
        def validate(machine, ignore_provider=nil)
          # Go through each of the configuration keys and validate
          errors = {}
          @keys.each do |_key, instance|
            if instance.respond_to?(:validate)
              # Validate this single item, and if we have errors then
              # we merge them into our total errors list.
              if _key == :vm
                result = instance.validate(machine, ignore_provider)
              else
                result = instance.validate(machine)
              end
              if result && !result.empty?
                errors = Util.merge_errors(errors, result)
              end
            end
          end

          # Go through and delete empty keys
          errors.keys.each do |key|
            errors.delete(key) if errors[key].empty?
          end

          # If we have missing keys, record those as errors
          if !@missing_key_calls.empty?
            errors["Vagrant"] = @missing_key_calls.to_a.sort.map do |key|
              I18n.t("vagrant.config.root.bad_key", key: key)
            end
          end

          errors
        end

        # Returns the internal state of the root object. This is used
        # by outside classes when merging, and shouldn't be called directly.
        # Note the strange method name is to attempt to avoid any name
        # clashes with potential configuration keys.
        def __internal_state
          {
            "config_map"        => @config_map,
            "keys"              => @keys,
            "missing_key_calls" => @missing_key_calls
          }
        end

        # This sets the internal state. This is used by the core to do some
        # merging logic and shouldn't be used by the general public.
        def __set_internal_state(state)
          @config_map        = state["config_map"] if state.key?("config_map")
          @keys              = state["keys"] if state.key?("keys")
          @missing_key_calls = state["missing_key_calls"] if state.key?("missing_key_calls")
        end
      end
    end
  end
end
