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

module Contrast
  module Utils
    # used in Contrast::Agent::Request
    module RequestUtils
      # Return a flattened hash of params with realized paths for keys, in
      # addition to a separate, valueless, entry for each nest key.
      # See RUBY-621 for more details.
      # { key : { nested_key : ['x','y','z' ] } }
      # becomes
      # {
      #   key[nested_key][0] : 'x'
      #   key[nested_key][1] : 'y'
      #   key[nested_key][2] : 'z'
      #   key :                ''
      #   nested_key :         ''
      # }
      # @return params_hash [Hash]
      def normalize_params val, prefix: nil
        # In non-recursive invocations, val should always be a Hash
        # (rather than breaking this out into two methods)
        case val
        when Tempfile
          # Skip if it's the auto-generated value from rails when it handles
          # file uploads. The file name will still be sent to SR for analysis.
          {}
        when Hash
          res = val.each_with_object({}) do |(k, v), hash|
            k = Contrast::Utils::StringUtils.force_utf8(k)
            nested_prefix = prefix.nil? ? k : "#{ prefix }[#{ k }]"
            hash[k] = Contrast::Utils::ObjectShare::EMPTY_STRING
            hash.merge!(normalize_params(v, prefix: nested_prefix))
          end
          res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
          res
        when Enumerable
          idx = 0
          res = {}
          while idx < val.length
            res.merge!(normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]"))
            idx += 1
          end
          res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
          res
        else
          { prefix => Contrast::Utils::StringUtils.force_utf8(val) }
        end
      end

      # Read the response body and rewind.
      # A well behaved middleware would read the IO object and then rewind.
      #
      # @return body [String]
      def read_body body
        return body if body.is_a?(String)

        begin
          can_rewind = Contrast::Utils::DuckUtils.quacks_to?(body, :rewind)
          # if we are after a middleware that failed to rewind
          body.rewind if can_rewind
          body.read
        rescue StandardError => e
          logger.error('Error in attempt to read body', message: e.message)
          logger.trace('With Stack', e)
          body.to_s
        ensure
          # be a good citizen and rewind
          body.rewind if can_rewind
        end
      end

      # @param multipart_data [Object<Hash>]
      # @param current_names [Hash]
      # @return current_names [Hash<Name =>file_name]
      def traverse_parsed_multipart multipart_data, current_names
        return current_names unless multipart_data

        multipart_data.each_value do |data_value|
          next unless data_value.is_a?(Hash)

          tempfile = data_value[:tempfile]
          if tempfile.nil?
            traverse_parsed_multipart(data_value, current_names)
          else
            name = data_value[:name].to_s
            file_name = data_value[:filename].to_s
            current_names[name] = file_name
          end
        end
        current_names
      end
    end
  end
end