# frozen_string_literal: true

require_relative '../../configuration/resolver'
require_relative 'makara_resolver'
require_relative '../../../../core/telemetry/logger'

module Datadog
  module Tracing
    module Contrib
      module ActiveRecord
        module Configuration
          # Converts Symbols, Strings, and Hashes to a normalized connection settings Hash.
          #
          # When matching using a Hash, these are the valid fields:
          # ```
          # {
          #   adapter: ...,
          #   host: ...,
          #   port: ...,
          #   database: ...,
          #   username: ...,
          #   role: ...,
          # }
          # ```
          #
          # Partial matching is supported: not including certain fields or setting them to `nil`
          # will cause them to matching all values for that field. For example: `database: nil`
          # will match any database, given the remaining fields match.
          #
          # Any fields not listed above are discarded.
          #
          # When more than one configuration could be matched, the last one to match is selected,
          # based on addition order (`#add`).
          class Resolver < Contrib::Configuration::Resolver
            prepend MakaraResolver
            prepend Contrib::Configuration::CachingResolver

            def initialize(active_record_configuration = nil)
              super()

              @active_record_configuration = active_record_configuration
            end

            def active_record_configuration
              @active_record_configuration || ::ActiveRecord::Base.configurations
            end

            def add(matcher, value)
              parsed = parse_matcher(matcher)

              # In case of error parsing, don't store `nil` key
              # as it wouldn't be useful for matching configuration
              # hashes in `#resolve`.
              super(parsed, value) if parsed
            end

            def resolve(db_config)
              active_record_config = resolve_connection_key(db_config).symbolize_keys

              hash = normalize_for_resolve(active_record_config)

              # Hashes in Ruby maintain insertion order
              _, config = @configurations.reverse_each.find do |matcher, _|
                matcher.none? do |key, value|
                  value != hash[key]
                end
              end

              config
            rescue => e
              # Resolving a valid database configuration should not raise an exception,
              # but if it does, it can be due to adding a broken pattern match prior to this call.
              #
              # `db_config` input may contain sensitive information such as passwords,
              # hence provide a succinct summary for the error logging.
              #
              Datadog.logger.error(
                'Failed to resolve ActiveRecord database configuration. ' \
                "Cause: #{e.class.name} Source: #{Array(e.backtrace).first}"
              )
              Core::Telemetry::Logger.report(e, description: 'Failed to resolve ActiveRecord database configuration')

              nil
            end

            protected

            def parse_matcher(matcher)
              resolved_pattern = resolve_connection_key(matcher).symbolize_keys
              normalized = normalize_for_config(resolved_pattern)

              # Remove empty fields to allow for partial matching
              normalized.reject! { |_, v| v.nil? }

              normalized
            rescue => e
              Datadog.logger.error(
                "Failed to resolve key #{matcher.inspect}. " \
                "Cause: #{e.class.name} Source: #{Array(e.backtrace).first}"
              )
              Core::Telemetry::Logger.report(e, description: 'Failed to resolve key')

              nil
            end

            #
            # `::ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver` exists from 4+ til from 6.0.x
            #
            # `::ActiveRecord::DatabaseConfigurations` was introduced from 6+,
            # but from 6.1.x, it was refactored to encapsulates the resolving logic, hence removing the resolver
            #
            def connection_resolver
              @resolver ||=
                # From 6.1+
                if defined?(::ActiveRecord::Base.configurations.resolve)
                  ::ActiveRecord::DatabaseConfigurations.new(active_record_configuration)
                # From 4+ to 6.0.x
                else
                  ::ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new(active_record_configuration)
                end
            end

            def resolve_connection_key(key)
              result = connection_resolver.resolve(key)

              if result.respond_to?(:configuration_hash) # Rails >= 6.1
                result.configuration_hash
              else # Rails < 6.1
                result
              end
            end

            # Extract only fields we'd like to match
            # from the ActiveRecord configuration.
            def normalize_for_config(active_record_config)
              {
                adapter: active_record_config[:adapter],
                host: active_record_config[:host],
                port: active_record_config[:port],
                database: active_record_config[:database],
                username: active_record_config[:username]
              }
            end

            # Both resolvers perform the same operations for this implementation, but can be specialized
            alias_method :normalize_for_resolve, :normalize_for_config
          end
        end
      end
    end
  end
end
