# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'forwardable'
require 'contrast/utils/object_share'

module Contrast
  module Config
    # This is the base for our configuration classes. It is intended to
    # facilitate the translation of the Common Configuration settings to usable
    # Ruby classes.
    class BaseConfiguration
      extend Forwardable

      attr_reader :configuration_map

      alias_method :to_hash, :configuration_map
      def_delegators :@configuration_map, :empty?, :key?, :delete, :fetch,
                     :[], :[]=, :each, :each_pair, :each_key, :each_value

      EMPTY_VALUE = :EMPTY_VALUE

      def initialize hsh = {}, keys = {}
        # holds configuration key value pairs
        # each configuration class can contain nested BaseConfigurations
        @configuration_map = {}
        traverse_config(hsh, keys)
      end

      def assign_value_to_path_array dot_path_array, value
        current_level = self
        dot_path_array[0...-1].each do |segment|
          current_level = current_level.send(segment) if current_level.cs__respond_to?(segment)
        end
        last_entry = dot_path_array[-1]
        if current_level.nil? == false && current_level.cs__respond_to?(last_entry)
          current_level.send("#{ last_entry }=", value)
        end
        nil
      end

      def nil?
        @configuration_map.empty?
      end

      private

      # Traverse the given entity to build out the configuration graph.
      #
      # The values will be either a hash, indicating internal nodes to
      # traverse, or a value to set or the EMPTY_VALUE symbol, indicating a
      # leaf node.
      #
      # The spec_keys are the Contrast defined keys used to access a given
      # configuration. Each child of this class maintains its own set of keys,
      # as well as Objects to which those keys map.
      def traverse_config values, spec_keys
        internal_nodes = values.cs__respond_to?(:has_key?)
        spec_keys.each_pair do |spec_key, spec_value|
          user_provided_value = internal_nodes ? value_from_key_config(spec_key, values) : EMPTY_VALUE
          define_methods(spec_key, spec_value, user_provided_value)
        end
      end

      def define_methods spec_key, spec_value, user_provided_value
        str_key = spec_key.to_s
        assign_config_value(str_key, spec_value, user_provided_value)
        define_getter(str_key)
        define_setter(str_key)
      end

      def assign_config_value str_key, spec_value, user_provided_value
        @configuration_map[str_key] = if spec_value.is_a?(Class) && spec_value <= Contrast::Config::BaseConfiguration
                                        spec_value.new(user_provided_value)
                                      elsif user_provided_value == EMPTY_VALUE
                                        spec_value
                                      else
                                        user_provided_value
                                      end
      end

      def value_from_key_config key, config_hash
        return config_hash[key] if config_hash.key?(key)
        return config_hash.fetch(key.to_sym, EMPTY_VALUE) if key.is_a?(String)

        config_hash.fetch(key.to_s, EMPTY_VALUE)
      end

      def define_getter str_key
        define_singleton_method str_key.to_sym do
          @configuration_map[str_key] == EMPTY_VALUE ? nil : @configuration_map[str_key]
        end
      end

      def define_setter str_key
        define_singleton_method "#{ str_key }=".to_sym do |new_value|
          @configuration_map[str_key] = new_value
        end
      end
    end
  end
end