class Datadog::Core::Remote::Client

Client communicates with the agent and sync remote configuration

def gem_spec(name)

def gem_spec(name)
  (@gem_specs ||= {})[name] ||= ::Gem.loaded_specs[name] || GemSpecificationFallback.new(nil, nil)
end

def initialize(transport, capabilities, repository: Configuration::Repository.new)

def initialize(transport, capabilities, repository: Configuration::Repository.new)
  @transport = transport
  @repository = repository
  @id = SecureRandom.uuid
  @dispatcher = Dispatcher.new
  @capabilities = capabilities
  @capabilities.receivers.each do |receiver|
    dispatcher.receivers << receiver
  end
end

def native_platform

def native_platform
  return @native_platform unless @native_platform.nil?
  os = if RUBY_ENGINE == 'jruby'
         os_name = java.lang.System.get_property('os.name')
         case os_name
         when /linux/i then 'linux'
         when /mac/i   then 'darwin'
         else os_name
         end
       else
         Gem::Platform.local.os
       end
  version = if os != 'linux'
              nil
            elsif RUBY_PLATFORM =~ /linux-(.+)$/
              # Old rubygems don't handle non-gnu linux correctly
              Regexp.last_match(1)
            else
              'gnu'
            end
  cpu = if RUBY_ENGINE == 'jruby'
          os_arch = java.lang.System.get_property('os.arch')
          case os_arch
          when 'amd64' then 'x86_64'
          when 'aarch64' then os == 'darwin' ? 'arm64' : 'aarch64'
          else os_arch
          end
        else
          Gem::Platform.local.cpu
        end
  @native_platform = [cpu, os, version].compact.join('-')
end

def payload # rubocop:disable Metrics/MethodLength

rubocop:disable Metrics/MethodLength
def payload # rubocop:disable Metrics/MethodLength
  state = repository.state
  client_tracer_tags = [
    "platform:#{native_platform}", # native platform
    # "asm.config.rules:#{}", # TODO: defined|undefined
    # "asm.config.enabled:#{}", # TODO: true|false|undefined
    "ruby.tracer.version:#{Core::Environment::Identity.tracer_version}",
    "ruby.runtime.platform:#{RUBY_PLATFORM}",
    "ruby.runtime.version:#{RUBY_VERSION}",
    "ruby.runtime.engine.name:#{RUBY_ENGINE}",
    "ruby.runtime.engine.version:#{ruby_engine_version}",
    "ruby.rubygems.platform.local:#{Gem::Platform.local}",
    "ruby.gem.libddwaf.version:#{gem_spec('libddwaf').version}",
    "ruby.gem.libddwaf.platform:#{gem_spec('libddwaf').platform}",
    "ruby.gem.libdatadog.version:#{gem_spec('libdatadog').version}",
    "ruby.gem.libdatadog.platform:#{gem_spec('libdatadog').platform}",
  ]
  client_tracer = {
    runtime_id: Core::Environment::Identity.id,
    language: Core::Environment::Identity.lang,
    tracer_version: tracer_version_semver2,
    service: service_name,
    env: Datadog.configuration.env,
    tags: client_tracer_tags,
  }
  app_version = Datadog.configuration.version
  client_tracer[:app_version] = app_version if app_version
  {
    client: {
      state: {
        root_version: state.root_version,
        targets_version: state.targets_version,
        config_states: state.config_states,
        has_error: state.has_error,
        error: state.error,
        backend_client_state: state.opaque_backend_state,
      },
      id: id,
      products: @capabilities.products,
      is_tracer: true,
      is_agent: false,
      client_tracer: client_tracer,
      # base64 is needed otherwise the Go agent fails with an unmarshal error
      capabilities: @capabilities.base64_capabilities
    },
    cached_target_files: state.cached_target_files,
  }
end

def ruby_engine_version

def ruby_engine_version
  @ruby_engine_version ||= defined?(RUBY_ENGINE_VERSION) ? RUBY_ENGINE_VERSION : RUBY_VERSION
end

def service_name

def service_name
  Datadog.configuration.remote.service || Datadog.configuration.service
end

def sync

rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/CyclomaticComplexity
def sync
  # TODO: Skip sync if no capabilities are registered
  response = transport.send_config(payload)
  if response.ok?
    # when response is completely empty, do nothing as in: leave as is
    if response.empty?
      Datadog.logger.debug { 'remote: empty response => NOOP' }
      return
    end
    begin
      paths = response.client_configs.map do |path|
        Configuration::Path.parse(path)
      end
      targets = Configuration::TargetMap.parse(response.targets)
      contents = Configuration::ContentList.parse(response.target_files)
    rescue Remote::Configuration::Path::ParseError => e
      raise SyncError, e.message
    end
    # To make sure steep does not complain
    return unless paths && targets && contents
    # TODO: sometimes it can strangely be so that paths.empty?
    # TODO: sometimes it can strangely be so that targets.empty?
    changes = repository.transaction do |current, transaction|
      # paths to be removed: previously applied paths minus ingress paths
      (current.paths - paths).each { |p| transaction.delete(p) }
      # go through each ingress path
      paths.each do |path|
        # match target with path
        target = targets[path]
        # abort entirely if matching target not found
        raise SyncError, "no target for path '#{path}'" if target.nil?
        # new paths are not in previously applied paths
        new = !current.paths.include?(path)
        # updated paths are in previously applied paths
        # but the content hash changed
        changed = current.paths.include?(path) && !current.contents.find_content(path, target)
        # skip if unchanged
        same = !new && !changed
        next if same
        # match content with path and target
        content = contents.find_content(path, target)
        # abort entirely if matching content not found
        raise SyncError, "no valid content for target at path '#{path}'" if content.nil?
        # to be added or updated << config
        # TODO: metadata (hash, version, etc...)
        transaction.insert(path, target, content) if new
        transaction.update(path, target, content) if changed
      end
      # save backend opaque backend state
      transaction.set(opaque_backend_state: targets.opaque_backend_state)
      transaction.set(targets_version: targets.version)
      # upon transaction end, new list of applied config + metadata (add, change, remove) will be saved
      # TODO: also remove stale config (matching removed) from cache (client configs is exhaustive list of paths)
    end
    if changes.empty?
      Datadog.logger.debug { 'remote: no changes' }
    else
      dispatcher.dispatch(changes, repository)
    end
  elsif response.internal_error?
    raise TransportError, response.to_s
  end
end

def tracer_version_semver2

def tracer_version_semver2
  @tracer_version_semver2 ||= Core::Environment::Identity.tracer_version_semver2
end