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

require 'contrast/utils/object_share'
require 'contrast/agent/protect/input_analyzer/input_analyzer'
require 'contrast/agent/protect/rule/input_classification/extendable'
require 'contrast/agent/protect/rule/input_classification/encoding'
require 'contrast/components/logger'

module Contrast
  module Agent
    module Protect
      module Rule
        module InputClassification
          # This module will include all the similar information for all input classifications
          # between different rules
          module Base
            UNKNOWN_KEY = 'unknown'
            include Contrast::Components::Logger::InstanceMethods
            include Contrast::Agent::Protect::Rule::InputClassification::Extendable
            include Contrast::Agent::Protect::Rule::InputClassification::Encoding

            KEYS_NEEDED = [
              COOKIE_VALUE, PARAMETER_VALUE, HEADER, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE
            ].cs__freeze

            BASE64_INPUT_TYPES = [BODY, HEADER, COOKIE_VALUE, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze

            class << self
              include Contrast::Components::Logger::InstanceMethods
              include Contrast::Agent::Reporting::InputType

              # Finds key value and type based on input type and value.
              # @param request [Contrast::Agent::Request] the current request context.
              # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
              # @param value [String, Array<String>] the value of the input.
              # @return [Array<(String, Contrast::Agent::Reporting::InputType)>] key and key type.
              def find_key request, input_type, value
                # TODO: RUBY-99999 Add handling for multipart, json and if any missing types.
                case input_type
                when COOKIE_VALUE
                  [request.cookies.key(value), Contrast::Agent::Reporting::InputType::COOKIE_NAME]
                when PARAMETER_VALUE, URL_PARAMETER
                  [request.parameters.key(value), Contrast::Agent::Reporting::InputType::PARAMETER_NAME]
                when HEADER
                  [request.headers.key(value), Contrast::Agent::Reporting::InputType::HEADER]
                when UNKNOWN
                  [UNKNOWN_KEY, Contrast::Agent::Reporting::InputType::UNKNOWN]
                else
                  [nil, nil]
                end
              rescue StandardError => e
                logger.warn('[InputAnalyzer] Could not find proper key for input traced value', message: e)
                [nil, nil]
              end

              # Some input types are not yet supported from the AgentLib.
              # This will convert the type to the closet possible if viable,
              # so that the input tracing could be done.
              #
              # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
              # @return [Integer<Contrast::AgentLib::Interface::INPUT_SET>]
              def convert_input_type input_type
                case input_type
                when URI, URL_PARAMETER
                  Contrast::AGENT_LIB.input_set[:URI_PATH]
                when BODY, DWR_VALUE, SOCKET, UNDEFINED_TYPE, UNKNOWN, REQUEST, QUERYSTRING
                  Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
                when HEADER
                  Contrast::AGENT_LIB.input_set[:HEADER_VALUE]
                when MULTIPART_VALUE, MULTIPART_FIELD_NAME
                  Contrast::AGENT_LIB.input_set[:MULTIPART_NAME]
                when JSON_ARRAYED_VALUE
                  Contrast::AGENT_LIB.input_set[:JSON_KEY]
                when PARAMETER_NAME
                  Contrast::AGENT_LIB.input_set[:PARAMETER_KEY]
                else
                  Contrast::AGENT_LIB.input_set[input_type]
                end
              rescue StandardError => e
                logger.debug('[InputAnalyzer] Protect Input classification could not determine input type,
                          falling back to default',
                             error: e)
                Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
              end
            end

            # Input Classification stage is done to determine if an user input is
            # DEFINITEATTACK or to be ignored.
            #
            # @param rule_id [String] Name of the protect rule.
            # @param input_type [Symbol, Contrast::Agent::Reporting::InputType] The type of the user input.
            # @param value [String, Array<String>] the value of the input.
            # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
            #                                                       agent analysis from the current
            #                                                       Request.
            # @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
            def classify rule_id, input_type, value, input_analysis
              return unless (rule = Contrast::PROTECT.rule(rule_id))
              return unless rule.applicable_user_inputs.include?(input_type)
              return unless input_analysis.request

              Array(value).each do |val|
                Array(val).each do |v|
                  next unless v

                  result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, v)
                  append_result(input_analysis, result)
                end
              end

              input_analysis
            rescue StandardError => e
              logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
              nil
            end

            # This methods checks if input is value that matches a key in the input.
            #
            # @param request [Contrast::Agent::Request] the current request context.
            # @param ia_result [Contrast::Agent::Reporting::InputAnalysisResult] result to be updated.
            # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
            # @param value [String, Array<String>] the value of the input.
            #
            # @return result [Array<String, Symbol>] updated with key result.
            def add_needed_key request, ia_result, input_type, value
              ia_result.key, ia_result.key_type = Contrast::Agent::Protect::Rule::InputClassification::Base.
                  find_key(request, input_type, value)
            end

            private

            # Appends result to the InputAnalysis.
            #
            # @param ia_analysis [Contrast::Agent::Reporting::InputAnalysis] the current input analysis.
            # @param result [Contrast::Agent::Reporting::InputAnalysisResult] result to be appended.
            # @return [Contrast::Agent::Reporting::InputAnalysis] the input analysis with the appended result.
            def append_result ia_analysis, result
              ia_analysis.results << result if result
              ia_analysis
            end

            # Do not override this method, it will hold base operations, instead overwrite methods called inside
            # of this method.
            # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
            # key if needed and Creates new instance of InputAnalysisResult.
            #
            # @param request [Contrast::Agent::Request] the current request context.
            # @param rule_id [String] The name of the Protect Rule.
            # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
            # @param value [String, Array<String>] the value of the input.
            #
            # @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil]
            def create_new_input_result request, rule_id, input_type, value
              return unless Contrast::AGENT_LIB

              # Cache retrieve
              cached = Contrast::Agent::Protect::InputAnalyzer.lru_cache.lookout(rule_id, value, input_type, request)
              return cached.result if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)

              # Input evaluation
              input_eval = build_input_eval(rule_id, input_type, base64_decode_input(value, input_type))
              ia_result = build_ia_result(rule_id, input_type, value, request, input_eval)
              return unless ia_result

              add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
              # Input evaluation end

              # Cache save. Cache must be saved after the input evaluation is completed.
              Contrast::Agent::Protect::InputAnalyzer.lru_cache.save(rule_id, ia_result, request)
              ia_result
            end

            # Decodes the value for the given input type.
            #
            # This applies to Values sources only:
            # BODY, COOKIE_VALUE, HEADER, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE
            #
            # @param value [String]
            # @param input_type [Symbol]
            # @return input [String]
            def base64_decode_input value, input_type
              return value unless Contrast::PROTECT.normalize_base64?
              return value unless BASE64_INPUT_TYPES.include?(input_type)

              cs__decode64(value, input_type)
            end
          end
        end
      end
    end
  end
end