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

require 'contrast/utils/env_configuration_item'
require 'ougai'
require 'contrast/configuration'

module Contrast
  module Components
    module Config
      # This component encapsulates storing the source for each entry in the config,
      # so that we can report on where the value was set from.
      class Sources
        ENVIRONMENT_VARIABLE = 'ENVIRONMENT_VARIABLE'
        COMMAND_LINE = 'COMMAND_LINE'
        CONTRAST_UI = 'CONTRAST_UI'
        DEFAULT_VALUE = 'DEFAULT_VALUE'
        APP_CONFIGURATION_FILE = 'USER_CONFIGURATION_FILE'
        # Order matters for the Configurations files. This is read when Agent starts up and will always go
        # through the YAML as priority.
        # Do not change the order!
        APP_CONFIGURATION_EXTENSIONS = %w[yaml yml].cs__freeze

        # @return [Hash]
        attr_reader :data

        def initialize data = {}
          @data = data
        end

        # Retrieves the current config source for the specified config path. If no source is
        # set then returns the Default value.
        #
        # @param path [String] the canonical name for the config entry (such as api.proxy.enable)
        # @return [String] the source for the entry
        def get path
          data.dig(*parts_for(path)) || DEFAULT_VALUE
        rescue TypeError
          DEFAULT_VALUE
        end

        # Assigns the config source for a specified config path.
        #
        # @param path [String] the canonical name for the config entry (such as api.proxy.enable)
        # @param source[String] the source for the entry
        # @return [String] the source type for the entry
        def set path, source
          assign_value(data, parts_for(path), source)
        end

        # Finds entries within config source data for the specified type, and returns
        # them along with the current values for each.
        #
        # @param type [String] a source type (ENV, CLI, ContrastUI, YAML)
        # @return [Hash] the entries for the provided source, along with the associated values
        def for type
          deep_select(data.dup, type, [])
        end

        private

        # @param path [String] the canonical name for a config entry (such as api.proxy.enable)
        # @return [Array] the path split on periods, and converted to symbols
        def parts_for path
          path.split('.').map(&:to_sym)
        end

        # @param current_level [Hash] all, or some of, the config source information
        # @param parts [Array] the parts for canonical name of the config entry
        # @param source [String] the source to be set for the specified entry
        # @return [String] the path split on periods, and converted to symbols
        def assign_value current_level, parts, source
          parts[0...-1].each do |segment|
            current_level[segment] ||= {}
            current_level = current_level[segment]
          end
          return unless current_level.cs__is_a?(Hash)

          current_level[parts[-1]] = source
        end

        # @param sources [Hash] all, or some of, the config source information
        # @param type [Array] the source type to look for entries of
        # @param path [String] the entries followed to get to this part of the config
        # @return [Hash] the entries for the provided source, along with the associated values
        def deep_select sources, type, path
          sources.each_with_object({}) do |(k, v), grouping|
            if v.cs__is_a?(Hash)
              nested_data = deep_select(v, type, path.dup.append(k.to_sym))
              grouping[k] = nested_data unless nested_data.empty?
            elsif v == type
              grouping[k] = Contrast::CONFIG.config.loaded_config.dig(*path, k)
            end
          end
        end
      end
    end
  end
end