require 'net/http'
require 'json'
require 'aws-xray-sdk/logger'

module XRay
  module Plugins
    # A plugin that gets the EC2 instance_id, availabiity_zone, instance_type, and ami_id if running on an EC2 instance.
    # The plugin queries IMDSv2 endpoint with X-aws-ec2-metadata-token-ttl-seconds as 60 seconds, and fallback to using
    # IMDSv1 endpoint.
    # More details about EC2 instance metadata retreival: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html#instancedata-data-retrieval
    module EC2
      include Logging

      ORIGIN = 'AWS::EC2::Instance'.freeze

      METADATA_BASE_URL = 'http://169.254.169.254/latest'.freeze

      def self.aws
        @@aws = {}
        token = get_token
        ec2_metadata = get_metadata(token)
        @@aws = {
          ec2: ec2_metadata
        }
      end


      private # private methods

      def self.get_token
        token_uri = URI(METADATA_BASE_URL + '/api/token')

        req = Net::HTTP::Put.new(token_uri)
        req['X-aws-ec2-metadata-token-ttl-seconds'] = '60'
        begin
          return do_request(req)
        rescue StandardError => e
          Logging.logger.warn %(cannot get the IMDSv2 token due to: #{e.message}.)
          ''
        end
      end

      def self.get_metadata(token)
        metadata_uri = URI(METADATA_BASE_URL + '/dynamic/instance-identity/document')

        req = Net::HTTP::Get.new(metadata_uri)
        if token != ''
          req['X-aws-ec2-metadata-token'] = token
        end

        begin
          metadata_json = do_request(req)
          return parse_metadata(metadata_json)
        rescue StandardError => e
          Logging.logger.warn %(cannot get the ec2 instance metadata due to: #{e.message}.)
          {}
        end
      end

      def self.parse_metadata(json_str)
        metadata = {}
        data = JSON(json_str)
        metadata['instance_id'] = data['instanceId']
        metadata['availability_zone'] = data['availabilityZone']
        metadata['instance_type'] = data['instanceType']
        metadata['ami_id'] = data['imageId']

        metadata
      end

      def self.do_request(request)
        begin
          response = Net::HTTP.start(request.uri.hostname, read_timeout: 1) { |http|
            http.request(request)
          }

          if response.code == '200'
            return response.body
          else
            raise(StandardError.new('Unsuccessful response::' + response.code + '::' + response.message))
          end
        rescue StandardError => e
          # Two attempts in total to complete the request successfully
          @retries ||= 0
          if @retries < 1
            @retries += 1
            retry
          else
            Logging.logger.warn %(Failed to complete request due to: #{e.message}.)
            raise e
          end
        end
      end
    end
  end
end
