# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

require_relative 'constants'

module NewRelic::Agent::Instrumentation
  module Redis
    INSTRUMENTATION_NAME = NewRelic::Agent.base_name(name)

    def connect_with_tracing
      with_tracing(Constants::CONNECT, database: _nr_db) { yield }
    end

    def call_with_tracing(command, &block)
      operation = command[0]
      statement = ::NewRelic::Agent::Datastores::Redis.format_command(command)

      with_tracing(operation, statement: statement, database: _nr_db) { yield }
    end

    # Used for Redis 4.x and 3.x
    def call_pipeline_with_tracing(pipeline)
      operation = pipeline.is_a?(::Redis::Pipeline::Multi) ? Constants::MULTI_OPERATION : Constants::PIPELINE_OPERATION
      statement = ::NewRelic::Agent::Datastores::Redis.format_pipeline_commands(pipeline.commands)

      with_tracing(operation, statement: statement, database: _nr_db) { yield }
    end

    # Used for Redis 5.x+
    def call_pipelined_with_tracing(pipeline)
      operation = pipeline.flatten.include?('MULTI') ? Constants::MULTI_OPERATION : Constants::PIPELINE_OPERATION
      statement = ::NewRelic::Agent::Datastores::Redis.format_pipeline_commands(pipeline)

      with_tracing(operation, statement: statement, database: _nr_db) { yield }
    end

    private

    def with_tracing(operation, statement: nil, database: nil)
      NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME)

      segment = NewRelic::Agent::Tracer.start_datastore_segment(
        product: Constants::PRODUCT_NAME,
        operation: operation,
        host: _nr_hostname,
        port_path_or_id: _nr_port_path_or_id,
        database_name: database
      )
      begin
        segment.notice_nosql_statement(statement) if statement
        NewRelic::Agent::Tracer.capture_segment_error(segment) { yield }
      ensure
        ::NewRelic::Agent::Transaction::Segment.finish(segment)
      end
    end

    def _nr_hostname
      _nr_redis_client_config.path ? Constants::LOCALHOST : _nr_redis_client_config.host
    rescue => e
      NewRelic::Agent.logger.debug("Failed to retrieve Redis host: #{e.class} - #{e.message}")
      Constants::UNKNOWN
    end

    def _nr_port_path_or_id
      _nr_redis_client_config.path || _nr_redis_client_config.port
    rescue => e
      NewRelic::Agent.logger.debug("Failed to retrieve Redis port_path_or_id: #{e.class} - #{e.message}")
      Constants::UNKNOWN
    end

    def _nr_redis_client_config
      @nr_config ||= begin
        # redis gem
        config = if defined?(::Redis::Client) && self.is_a?(::Redis::Client)
          self
        # redis-client gem v0.11+ (self is a RedisClient::Middlewares)
        elsif respond_to?(:client)
          # The following line needs else branch coverage
          client && client.config # rubocop:disable Style/SafeNavigation
        # redis-client gem <0.11 (self is a RedisClient::Middlewares)
        elsif defined?(::RedisClient)
          ::RedisClient.config if ::RedisClient.respond_to?(:config)
        end
        raise 'Unable to locate the underlying Redis client configuration.' unless config

        config
      end
    end

    def _nr_db
      # db is a method on the Redis client in versions < 5.x
      return db if respond_to?(:db)
      # db is accessible through the RedisClient::Config object in versions > 5.x
      return _nr_redis_client_config.db if _nr_redis_client_config.respond_to?(:db)
    rescue StandardError => e
      NewRelic::Agent.logger.debug("Failed to determine configured Redis db value: #{e.class} - #{e.message}")
      nil
    end
  end
end
