require 'aws-sdk-core'
require 'aws-xray-sdk/facets/helper'
require 'aws-xray-sdk/facets/resources/aws_params_allowlist'
require 'aws-xray-sdk/facets/resources/aws_services_allowlist'

module XRay
  class AwsSDKPlugin < Seahorse::Client::Plugin
    option :xray_recorder, default: XRay.recorder

    def add_handlers(handlers, config)
      # run before Seahorse::Client::Plugin::ParamValidator (priority 50)
      handlers.add Handler, step: :validate, priority: 49
    end

    # Handler to capture AWS API calls as subsegments
    class Handler < Seahorse::Client::Handler
      include XRay::Facets::Helper

      def call(context)
        operation = context.operation_name
        service_name = context.client.class.api.metadata['serviceAbbreviation'] ||
                       context.client.class.to_s.split('::')[1]
        if skip?(service_name, operation)
          return super
        end

        recorder = Aws.config[:xray_recorder]
        if recorder.nil? || recorder.current_entity.nil?
          return super
        end

        recorder.capture(service_name, namespace: 'aws') do |subsegment|
          # inject header string before calling downstream AWS services
          context.http_request.headers[TRACE_HEADER] = prep_header_str entity: subsegment
          response = @handler.call(context)
          http_response = context.http_response
          resp_meta = {
            status: http_response.status_code,
            content_length: http_response.headers['content-length'].to_i
          }
          aws = {
            # XRay back-end right now has strict operation name matching
            operation: sanitize_op_name(operation),
            region: context.client.config.region,
            retries: context.retries,
            request_id: http_response.headers['x-amzn-requestid']
          }
          # S3 returns special request id in response headers
          if service_name == 'S3'
            aws[:id_2] = http_response.headers['x-amz-id-2']
          end

          operation_h = AwsParams.whitelist[:services]
                                 .fetch(service_name.to_sym, {})
                                 .fetch(:operations, {})[operation]
          unless operation_h.nil?
            params_capture req_params: context.params, resp_params: response.to_h,
                           capture: operation_h, meta: aws
          end
          subsegment.aws = aws
          if err = response.error
            subsegment.add_exception exception: err, remote: true
          end
          subsegment.merge_http_response response: resp_meta
          response
        end
      end

      private

      def inject_headers(request:, entity:)
        request.headers[TRACE_HEADER] = prep_header_str entity: entity
      end

      def sanitize_op_name(opname)
        opname.to_s.split('_').collect(&:capitalize).join if opname
      end

      def params_capture(req_params:, resp_params:, capture:, meta:)
        if norm = capture[:request_parameters]
          capture_normal params: req_params, capture: norm, meta: meta
        end

        if norm = capture[:response_parameters]
          capture_normal params: resp_params, capture: norm, meta: meta
        end

        if spec = capture[:request_descriptors]
          capture_special params: req_params, capture: spec, meta: meta
        end

        if spec = capture[:response_descriptors]
          capture_special params: resp_params, capture: spec, meta: meta
        end
      end

      def capture_normal(params:, capture:, meta:)
        params.each_key do |key|
          meta[key] = params[key] if capture.include?(key)
        end
      end

      def capture_special(params:, capture:, meta:)
        params.each_key do |key|
          process_descriptor(target: params[key], descriptor: capture[key], meta: meta) if capture.include?(key)
        end
      end

      def process_descriptor(target:, descriptor:, meta:)
        # "get_count" = true
        v = target.length if descriptor[:get_count]
        # "get_keys" = true
        v = target.keys if descriptor[:get_keys]
        meta[descriptor[:rename_to]] = v
      end

      def skip?(service, op)
        return service == 'XRay' && (op == :get_sampling_rules || op == :get_sampling_targets)
      end
    end
  end

  # Add X-Ray plugin to AWS SDK clients
  module AwsSDKPatcher
    def self.patch(services: nil, recorder: XRay.recorder)
      force = services.nil?
      services ||= AwsServices.whitelist
      services.each do |s|
        begin
          Aws.const_get(%(#{s}::Client)).add_plugin XRay::AwsSDKPlugin
          Aws.config.update xray_recorder: recorder
        rescue NameError
          # swallow the error if no explicit user config
          raise unless force
        end
      end
    end
  end
end
