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

require 'contrast/components/base'
require 'contrast/components/config'
require 'contrast/components/assess_rules'

module Contrast
  module Components
    module Assess
      # A wrapper build around the Common Agent Configuration project to allow
      # for access of the values contained in its
      # parent_configuration_spec.yaml.
      # Specifically, this allows for querying the state of the Assess product.
      class Interface # rubocop:disable Metrics/ClassLength
        include Contrast::Components::ComponentBase

        # @return [Boolean, nil]
        attr_accessor :enable
        # @return [Array, nil]
        attr_writer :enable_scan_response, :enable_dynamic_sources, :sampling, :rules, :stacktraces, :tags
        # @return [String]
        attr_reader :canon_name
        # @return [Array]
        attr_reader :config_values
        # @return [Boolean]
        attr_writer :enable_original_object
        # @return [Integer]
        attr_writer :max_context_source_events
        # @return [Integer]
        attr_writer :max_propagation_events
        # @return [Integer]
        attr_writer :max_rule_reported
        # @return [Integer]
        attr_writer :time_limit_threshold

        DEFAULT_STACKTRACES = 'ALL'
        DEFAULT_MAX_SOURCE_EVENTS = 50_000
        DEFAULT_MAX_PROPAGATION_EVENTS = 50_000
        DEFAULT_MAX_RULE_REPORTED = 100
        DEFAULT_MAX_RULE_TIME_THRESHOLD = 300_000
        CANON_NAME = 'assess'
        CONFIG_VALUES = %w[
          enabled?
          tags
          enable_scan_response
          enable_original_object
          enable_dynamic_sources
          stacktraces
          max_context_source_events
          max_propagation_events
          max_rule_reported
          time_limit_threshold
        ].cs__freeze
        # rubocop:disable Naming/MemoizedInstanceVariableName
        def initialize hsh = {}
          @config_values = CONFIG_VALUES
          @canon_name = CANON_NAME
          return unless hsh

          @enable = hsh[:enable]
          @tags = hsh[:tags]
          @enable_scan_response = hsh[:enable_scan_response]
          @enable_dynamic_sources = hsh[:enable_dynamic_sources]
          @enable_original_object = hsh[:enable_original_object]
          @sampling = Contrast::Components::Sampling::Interface.new(hsh[:sampling])
          @rules = Contrast::Components::AssessRules::Interface.new(hsh[:rules])
          @stacktraces = hsh[:stacktraces]
          assign_limits(hsh)
        end

        # @return [Boolean, true]
        def enable_scan_response
          @enable_scan_response.nil? ? true : @enable_scan_response
        end

        # @return [Boolean, true]
        def enable_dynamic_sources
          @enable_dynamic_sources.nil? ? true : @enable_dynamic_sources
        end

        # @return [Boolean, true]
        def enable_original_object
          @enable_original_object.nil? ? true : @enable_original_object
        end

        # @return [Contrast::Components::Sampling::Interface]
        def sampling
          @sampling ||= Contrast::Components::Sampling::Interface.new
        end

        # @return [Contrast::Components::AssessRules::Interface]
        def rules
          @rules ||= Contrast::Components::AssessRules::Interface.new
        end

        def stacktraces
          @stacktraces ||= DEFAULT_STACKTRACES
        end

        def max_rule_reported
          @max_rule_reported ||= DEFAULT_MAX_RULE_REPORTED
        end

        def time_limit_threshold
          @time_limit_threshold ||= DEFAULT_MAX_RULE_TIME_THRESHOLD
        end

        def max_propagation_events
          @max_propagation_events ||= DEFAULT_MAX_PROPAGATION_EVENTS
        end

        def max_context_source_events
          @max_context_source_events ||= DEFAULT_MAX_SOURCE_EVENTS
        end

        # @return [String, nil]
        def tags
          stringify_array(@tags)
        end

        def enabled?
          # config overrides if forcibly set
          return false if forcibly_disabled?
          return true  if forcibly_enabled?

          ::Contrast::SETTINGS.assess_state.enabled == true
        end

        def tainted_columns
          ::Contrast::SETTINGS.tainted_columns
        end

        def forcibly_disabled?
          @_forcibly_disabled = false?(enable) if @_forcibly_disabled.nil?

          @_forcibly_disabled
        end

        def rule_disabled? name
          disabled_rules.include?(name)
        end

        # The value of the stacktrace should be treated as an ENUM. We upcase it for
        # faster comparisons when we use it. Anything not one of the known values of
        # 'NONE',  'SOME', or 'ALL' is treated as 'ALL'
        #
        # @return [Symbol] the normalized value of ::Contrast::CONFIG.assess.stacktraces
        def capture_stacktrace_value
          @_capture_stacktrace_value ||= case stacktraces&.upcase
                                         when 'NONE'
                                           :NONE
                                         when 'SOME'
                                           :SOME
                                         else
                                           :ALL
                                         end
        end

        # Consider capture_stacktrace_value along with the node type
        # to determine whether stacktraces should be captured.
        #
        # capture_stacktrace_value -> (:ALL, :NONE, :SOME)
        # node types (SourceNode, PolicyNode, TriggerNode, PropagationNode)
        #
        # @param policy_node [Contrast::Agent::Assess::Policy::PolicyNode] The node in question.
        # @return [Boolean] to capture or not to capture, that is the question.
        def capture_stacktrace? policy_node
          return true if capture_stacktrace_value == :ALL
          return false if capture_stacktrace_value == :NONE

          # Below here capture_stacktrace_value must be :SOME.
          return true if policy_node.cs__is_a?(Contrast::Agent::Assess::Policy::SourceNode)
          return true if policy_node.cs__is_a?(Contrast::Agent::Assess::Policy::TriggerNode)

          false
        end

        def scan_response?
          @_scan_response = !false?(enable_scan_response) if @_scan_response.nil?

          @_scan_response
        end

        def require_scan?
          @_require_scan = !false?(::Contrast::CONFIG.agent.ruby.require_scan) if @_require_scan.nil?
          @_require_scan
        end

        def require_dynamic_sources?
          return @_require_dynamic_sources unless @_require_dynamic_sources.nil?

          @_require_dynamic_sources = !false?(enable_dynamic_sources)
        end

        def non_request_tracking?
          @_non_request_tracking = true?(::Contrast::CONFIG.agent.ruby.non_request_tracking) if
            @_non_request_tracking.nil?
          @_non_request_tracking
        end

        def disabled_rules
          rules&.disabled_rules || ::Contrast::SETTINGS.assess_state.disabled_assess_rules || []
        end

        def track_original_object?
          @_track_original_object = !false?(enable_original_object) if @_track_original_object.nil?

          @_track_original_object
        end

        # The id for this process, based on the session metadata or id provided by the user, as indicated in
        # application startup.
        def session_id
          ::Contrast::SETTINGS.assess_state.session_id
        end

        # Converts current configuration to effective config values class and appends them to
        # EffectiveConfig class.
        #
        # @param effective_config [Contrast::Config::Diagnostics::EffectiveConfig]
        def to_effective_config effective_config
          super
          sampling&.to_effective_config(effective_config)
          rules&.to_effective_config(effective_config)
        end

        # rubocop:enable Naming/MemoizedInstanceVariableName
        private

        def forcibly_enabled?
          @_forcibly_enabled = true?(::Contrast::CONFIG.assess.enable) if @_forcibly_enabled.nil?
          @_forcibly_enabled
        end

        # Sets Event limits from configuration and converts string numbers to integers.
        def assign_limits hsh
          return unless hsh

          source_limit = hsh[:max_context_source_events]&.to_i
          propagation_limit = hsh[:max_propagation_events]&.to_i
          max_rule_reporter = hsh[:max_rule_reported]&.to_i
          time_limit = hsh[:time_limit_threshold]&.to_i

          @max_context_source_events = source_limit if source_limit
          @max_propagation_events = propagation_limit if propagation_limit
          @max_rule_reported = max_rule_reporter if max_rule_reporter
          @time_limit_threshold = time_limit if time_limit
        end
      end
    end
  end
end