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

require 'contrast/utils/timer'
require 'contrast/utils/object_share'
require 'contrast/components/logger'

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 ArchitectureComponents.
        # DO NOT CHANGE THIS!
        AC_TYPE_DB = 'db'
        # TeamServer only accepts certain values for FlowMap Services.
        # DO NOT CHANGE THIS
        ADAPTER = 'adapter'
        HOST = 'host'
        PORT = 'port'
        DATABASE = 'database'
        DEFAULT = 'default'
        LOCALHOST = 'localhost'

        def self.active_record_config
          return @_active_record_config if instance_variable_defined?(:@_active_record_config)

          @_active_record_config = ActiveRecord::Base.connection_config rescue nil # rubocop:disable Style/RescueModifier
        end

        def self.append_db_config(activity_or_update,
                                  hash_or_str = Contrast::Agent::Inventory::DatabaseConfig.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
              activity_or_update.components << a
            end
          end
        rescue StandardError => e
          logger.error('Unable to append db config', e)
          nil
        end

        def self.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

        def self.build_from_db_hash hash
          ac = Contrast::Api::Dtm::ArchitectureComponent.new
          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.type = AC_TYPE_DB
          ac.url = hash[:database] || hash[DATABASE] || DEFAULT
          [ac]
        end

        def self.host_from_hash hash
          hash[:host] || hash[HOST] || Contrast::Utils::ObjectShare::EMPTY_STRING
        end

        def self.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
        # mysql+mysqlconnector://scott:tiger@localhost/foo
        def self.build_from_db_string str
          adapter, hosts, database = split_connection_str(str)
          acs = []
          hosts.split(Contrast::Utils::ObjectShare::COMMA).map do |s|
            host, port = s.split(Contrast::Utils::ObjectShare::COLON)

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

        def self.split_connection_str str
          adapter, str = str.split(Contrast::Utils::ObjectShare::COLON_SLASH_SLASH)
          _auth, str = str.split(Contrast::Utils::ObjectShare::AT)
          # Not currently used
          # user, pass = auth.split(Contrast::Utils::ObjectShare::COLON)
          hosts, db_and_options = str.split(Contrast::Utils::ObjectShare::SLASH)
          hosts << LOCALHOST if hosts.empty?
          database, _options = db_and_options.split(Contrast::Utils::ObjectShare::QUESTION_MARK)

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