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

require 'contrast/agent/reporting/reporting_events/architecture_component'
require 'contrast/api/decorators/architecture_component'
require 'contrast/components/logger'
require 'contrast/utils/object_share'
require 'contrast/utils/timer'

module Contrast
  module Agent
    module Inventory
      # Methods used for parsing database connection configurations
      # for getting inventory information from the application
      module DatabaseConfig
        extend Contrast::Components::Logger::InstanceMethods

        # TeamServer only accepts certain values for FlowMap Services.
        # DO NOT CHANGE THIS
        ADAPTER = 'adapter'
        HOST = 'host'
        PORT = 'port'
        DATABASE = 'database'
        DEFAULT = 'default'
        LOCALHOST = 'localhost'

        class << self
          # Append the available database connection information to the message being sent to TeamServer. This message
          # may be a Contrast::Api::Dtm::Activity or Contrast::Agent::Reporting::ApplicationUpdate.
          # Both report the same
          # Contrast::Api::Dtm::ArchitectureComponent, but have different names for their fields.
          #
          # @param activity_or_update [Contrast::Api::Dtm::Activity, Contrast::Agent::Reporting::ApplicationUpdate]
          # @param hash_or_str [Hash, String] the database connection information
          def append_db_config activity_or_update, hash_or_str = active_record_config
            arr = build_from_db_config(hash_or_str)
            return unless arr&.any?

            arr.each do |a|
              next unless a

              if activity_or_update.is_a?(Contrast::Api::Dtm::Activity)
                activity_or_update.architectures << a
              else
                converted_comp = Contrast::Agent::Reporting::ArchitectureComponent.convert(a)
                activity_or_update.components << converted_comp
              end
            end
          rescue StandardError => e
            logger.warn('Unable to append db config', e)
            nil
          end

          private

          # We capture the active record configuration used by this application, as reported by
          # ActiveRecord::Base.connection_db_config, so that we can record it once and report it as needed.
          #
          # @return [Hash]
          def active_record_config
            return @_active_record_config if instance_variable_defined?(:@_active_record_config)

            @_active_record_config = if ActiveRecord::Base.cs__respond_to?(:connection_db_config)
                                       ActiveRecord::Base.connection_db_config
                                     else
                                       # TODO: RUBY-99999 - Remove when Rails 6.0 is not supported
                                       ActiveRecord::Base.connection_config
                                     end
          rescue StandardError
            nil
          end

          # The classes we instrument in order to determine which, if any, database(s) an application connects to take
          # either a Hash or a String as a parameter. We install this one patch into all of those methods, letting our
          # code here, rather than at the patch level, make the determination of which path to call. We'll make that
          # decision and then parse the given configuration to create a Contrast::Api::Dtm::ArchitectureComponent for
          # reporting.
          #
          # @param hash_or_str [Hash, String]
          # @return [Array<Contrast::Api::Dtm::ArchitectureComponent>, nil]
          def build_from_db_config hash_or_str
            return unless hash_or_str

            if hash_or_str.is_a?(Hash)
              build_from_db_hash(hash_or_str)
            else
              build_from_db_string(hash_or_str.to_s)
            end
          end

          # Convert the Hash used to create a database connection into an Contrast::Api::Dtm::ArchitectureComponent
          # understandable by TeamServer.
          #
          # @param hash [Hash] the information used to open a database connection
          # @return [Array<Contrast::Api::Dtm::ArchitectureComponent>]
          def build_from_db_hash hash
            ac = Contrast::Api::Dtm::ArchitectureComponent.build_database
            ac.vendor = hash[:adapter] || hash[ADAPTER] || Contrast::Utils::ObjectShare::EMPTY_STRING
            ac.remote_host = host_from_hash(hash)
            ac.remote_port = port_from_hash(hash)
            ac.url = hash[:database] || hash[DATABASE] || DEFAULT
            [ac]
          end

          # Retrieve the host from the given connection Hash
          #
          # @param hash [Hash]
          # @return [String]
          def host_from_hash hash
            hash[:host] || hash[HOST] || Contrast::Utils::ObjectShare::EMPTY_STRING
          end

          # Retrieve the port from the given connection Hash
          #
          # @param hash [Hash]
          # @return [integer]
          def port_from_hash hash
            p = hash[:port] || hash[PORT] || Contrast::Utils::ObjectShare::EMPTY_STRING
            p.to_i
          end

          # Examples:
          # mongodb://[user:pass@]host1[:port1][,host2[:port2],[,hostN[:portN]]][/[database][?options]]
          # postgresql://scott:tiger@localhost/mydatabase # pragma: allowlist secret
          # mysql+mysqlconnector://scott:tiger@localhost/foo # pragma: allowlist secret
          #
          # @param str [String] the DB connection string
          # @return [Array<Contrast::Api::Dtm::ArchitectureComponent>, nil]
          def build_from_db_string str
            adapter, hosts, database = split_connection_str(str)
            return unless adapter && hosts && database

            acs = []
            hosts.split(Contrast::Utils::ObjectShare::COMMA).map do |s|
              host, port = s.split(Contrast::Utils::ObjectShare::COLON)

              ac = Contrast::Api::Dtm::ArchitectureComponent.build_database
              ac.vendor = Contrast::Utils::StringUtils.force_utf8(adapter)
              ac.remote_host = Contrast::Utils::StringUtils.force_utf8(host)
              ac.remote_port = port.to_i
              ac.url = Contrast::Utils::StringUtils.force_utf8(database)
              acs << ac
            end
            acs
          end

          # Parse the given string used to connect to a database into its composite components, allowing for the
          # generation of a Contrast::Api::Dtm::ArchitectureComponent
          #
          # @param str [String] the DB connection string
          # @return [Array<String>, nil] the adapter, hosts, and database
          def split_connection_str str
            adapter, str = str.split(Contrast::Utils::ObjectShare::COLON_SLASH_SLASH)
            return unless str

            _auth, str = str.split(Contrast::Utils::ObjectShare::AT)
            return unless str

            # Not currently used
            # user, pass = auth.split(Contrast::Utils::ObjectShare::COLON)
            hosts, db_and_options = str.split(Contrast::Utils::ObjectShare::SLASH)
            return unless db_and_options

            hosts << LOCALHOST if hosts.empty?
            database, _options = db_and_options.split(Contrast::Utils::ObjectShare::QUESTION_MARK)

            [adapter, hosts, database]
          end
        end
      end
    end
  end
end