require 'date'
require 'json'
require 'rbconfig'

module Datadog
  module Diagnostics
    # A holistic collection of the environment in which ddtrace is running.
    # This logger should allow for easy reporting by users to Datadog support.
    #
    # rubocop:disable Style/DoubleNegation
    module EnvironmentLogger
      class << self
        # Outputs environment information to {Datadog.logger}.
        # Executes only for the lifetime of the program.
        def log!(transport_responses)
          return if @executed || !log?
          @executed = true

          data = EnvironmentCollector.new.collect!(transport_responses)
          data.reject! { |_, v| v.nil? } # Remove empty values from hash output

          log_environment!(data.to_json)
          log_error!('Agent Error'.freeze, data[:agent_error]) if data[:agent_error]
        rescue => e
          Datadog.logger.warn("Failed to collect environment information: #{e} location: #{e.backtrace.first}")
        end

        private

        def log_environment!(line)
          Datadog.logger.info("DATADOG TRACER CONFIGURATION - #{line}")
        end

        def log_error!(type, error)
          Datadog.logger.warn("DATADOG TRACER DIAGNOSTIC - #{type}: #{error}")
        end

        # Are we logging the environment data?
        def log?
          startup_logs_enabled = Datadog.configuration.diagnostics.startup_logs.enabled
          if startup_logs_enabled.nil?
            !repl? # Suppress logs if we running in a REPL
          else
            startup_logs_enabled
          end
        end

        REPL_PROGRAM_NAMES = %w[irb pry].freeze

        def repl?
          REPL_PROGRAM_NAMES.include?($PROGRAM_NAME)
        end
      end
    end

    # Collects environment information for diagnostic logging
    class EnvironmentCollector
      # @return [String] current time in ISO8601 format
      def date
        DateTime.now.iso8601
      end

      # Best portable guess of OS information.
      # @return [String] platform string
      def os_name
        RbConfig::CONFIG['host'.freeze]
      end

      # @return [String] ddtrace version
      def version
        VERSION::STRING
      end

      # @return [String] "ruby"
      def lang
        Ext::Runtime::LANG
      end

      # Supported Ruby language version.
      # Will be distinct from VM version for non-MRI environments.
      # @return [String]
      def lang_version
        Ext::Runtime::LANG_VERSION
      end

      # @return [String] configured application environment
      def env
        Datadog.configuration.env
      end

      # @return [Boolean, nil]
      def enabled
        Datadog.configuration.tracer.enabled
      end

      # @return [String] configured application service name
      def service
        Datadog.configuration.service
      end

      # @return [String] configured application version
      def dd_version
        Datadog.configuration.version
      end

      # @return [String] target agent URL for trace flushing
      def agent_url
        # Retrieve the effect agent URL, regardless of how it was configured
        transport = Datadog.tracer.writer.transport
        adapter = transport.client.api.adapter
        adapter.url
      end

      # Error returned by Datadog agent during a tracer flush attempt
      # @return [String] concatenated list of transport errors
      def agent_error(transport_responses)
        error_responses = transport_responses.reject(&:ok?)

        return nil if error_responses.empty?

        error_responses.map(&:inspect).join(','.freeze)
      end

      # @return [Boolean, nil] debug mode enabled in configuration
      def debug
        !!Datadog.configuration.diagnostics.debug
      end

      # @return [Boolean, nil] analytics enabled in configuration
      def analytics_enabled
        !!Datadog.configuration.analytics.enabled
      end

      # @return [Numeric, nil] tracer sample rate configured
      def sample_rate
        sampler = Datadog.configuration.tracer.sampler
        return nil unless sampler

        sampler.sample_rate(nil) rescue nil
      end

      # DEV: We currently only support SimpleRule instances.
      # DEV: These are the most commonly used rules.
      # DEV: We should expand support for other rules in the future,
      # DEV: although it is tricky to serialize arbitrary rules.
      #
      # @return [Hash, nil] sample rules configured
      def sampling_rules
        sampler = Datadog.configuration.tracer.sampler
        return nil unless sampler.is_a?(Datadog::PrioritySampler) &&
                          sampler.priority_sampler.is_a?(Datadog::Sampling::RuleSampler)

        sampler.priority_sampler.rules.map do |rule|
          next unless rule.is_a?(Datadog::Sampling::SimpleRule)

          {
            name: rule.matcher.name,
            service: rule.matcher.service,
            sample_rate: rule.sampler.sample_rate(nil)
          }
        end.compact
      end

      # @return [Hash, nil] concatenated list of global tracer tags configured
      def tags
        tags = Datadog.configuration.tags
        return nil if tags.empty?
        hash_serializer(tags)
      end

      # @return [Boolean, nil] runtime metrics enabled in configuration
      def runtime_metrics_enabled
        Datadog.configuration.runtime_metrics.enabled
      end

      # Concatenated list of integrations activated, with their gem version.
      # Example: "rails@6.0.3,rack@2.2.3"
      #
      # @return [String, nil]
      def integrations_loaded
        integrations = instrumented_integrations
        return if integrations.empty?

        integrations.map { |name, integration| "#{name}@#{integration.class.version}" }.join(','.freeze)
      end

      # Ruby VM name and version.
      # Examples: "ruby-2.7.1", "jruby-9.2.11.1", "truffleruby-20.1.0"
      # @return [String, nil]
      def vm
        # RUBY_ENGINE_VERSION returns the VM version, which
        # will differ from RUBY_VERSION for non-mri VMs.
        if defined?(RUBY_ENGINE_VERSION)
          "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}"
        else
          # Ruby < 2.3 doesn't support RUBY_ENGINE_VERSION
          "#{RUBY_ENGINE}-#{RUBY_VERSION}"
        end
      end

      # @return [Boolean, nil] partial flushing enabled in configuration
      def partial_flushing_enabled
        !!Datadog.configuration.tracer.partial_flush.enabled
      end

      # @return [Boolean, nil] priority sampling enabled in configuration
      def priority_sampling_enabled
        !!Datadog.configuration.tracer.priority_sampling
      end

      # @return [Boolean, nil] health metrics enabled in configuration
      def health_metrics_enabled
        !!Datadog.configuration.diagnostics.health_metrics.enabled
      end

      # TODO: Populate when profiling is implemented
      # def profiling_enabled
      # end

      # TODO: Populate when automatic log correlation is implemented
      # def logs_correlation_enabled
      # end

      # @return [Hash] environment information available at call time
      def collect!(transport_responses)
        {
          date: date,
          os_name: os_name,
          version: version,
          lang: lang,
          lang_version: lang_version,
          env: env,
          enabled: enabled,
          service: service,
          dd_version: dd_version,
          agent_url: agent_url,
          agent_error: agent_error(transport_responses),
          debug: debug,
          analytics_enabled: analytics_enabled,
          sample_rate: sample_rate,
          sampling_rules: sampling_rules,
          tags: tags,
          runtime_metrics_enabled: runtime_metrics_enabled,
          integrations_loaded: integrations_loaded,
          vm: vm,
          partial_flushing_enabled: partial_flushing_enabled,
          priority_sampling_enabled: priority_sampling_enabled,
          health_metrics_enabled: health_metrics_enabled,
          **instrumented_integrations_settings
        }
      end

      private

      def instrumented_integrations
        Datadog.configuration.instrumented_integrations
      end

      # Capture all active integration settings into "integrationName_settingName: value" entries.
      def instrumented_integrations_settings
        Hash[instrumented_integrations.flat_map do |name, integration|
          integration.configuration.to_h.flat_map do |setting, value|
            next [] if setting == :tracer # Skip internal Ruby objects

            # Convert value to a string to avoid custom #to_json
            # handlers possibly causing errors.
            [[:"integration_#{name}_#{setting}", value.to_s]]
          end
        end]
      end

      # Outputs "k1:v1,k2:v2,..."
      def hash_serializer(h)
        h.map { |k, v| "#{k}:#{v}" }.join(','.freeze)
      end
    end
  end
end
