require 'addressable/uri'
require 'signet'

require 'securerandom'

module Signet #:nodoc:
  module OAuth1
    OUT_OF_BAND = 'oob'

    ##
    # Converts a value to a percent-encoded <code>String</code> according to
    # the rules given in RFC 5849.  All non-unreserved characters are
    # percent-encoded.
    #
    # @param [Symbol, #to_str] value The value to be encoded.
    #
    # @return [String] The percent-encoded value.
    def self.encode(value)
      value = value.to_s if value.kind_of?(Symbol)
      return Addressable::URI.encode_component(
        value,
        Addressable::URI::CharacterClasses::UNRESERVED
      )
    end

    ##
    # Converts a percent-encoded String to an unencoded value.
    #
    # @param [#to_str] value
    #   The percent-encoded <code>String</code> to be unencoded.
    #
    # @return [String] The unencoded value.
    def self.unencode(value)
      return Addressable::URI.unencode_component(value)
    end

    ##
    # Returns a timestamp suitable for use as an <code>'oauth_timestamp'</code>
    # value.
    #
    # @return [String] The current timestamp.
    def self.generate_timestamp()
      return Time.now.to_i.to_s
    end

    ##
    # Returns a nonce suitable for use as an <code>'oauth_nonce'</code>
    # value.
    #
    # @return [String] A random nonce.
    def self.generate_nonce()
      return SecureRandom.random_bytes(16).unpack('H*').join('')
    end

    ##
    # Processes an options <code>Hash</code> to find a credential key value.
    # Allows for greater flexibility in configuration.
    #
    # @param [Symbol] credential_type
    #   One of <code>:client</code>, <code>:temporary</code>,
    #   <code>:token</code>, <code>:consumer</code>, <code>:request</code>,
    #   or <code>:access</code>.
    #
    # @return [String] The credential key value.
    def self.extract_credential_key_option(credential_type, options)
      # Normalize key to String to allow indifferent access.
      options = options.inject({}) { |accu, (k, v)| accu[k.to_s] = v; accu }
      credential_key = "#{credential_type}_credential_key"
      credential = "#{credential_type}_credential"
      if options[credential_key]
        credential_key = options[credential_key]
      elsif options[credential]
        require 'signet/oauth_1/credential'
        if !options[credential].respond_to?(:key)
          raise TypeError,
            "Expected Signet::OAuth1::Credential, " +
            "got #{options[credential].class}."
        end
        credential_key = options[credential].key
      elsif options["client"]
        require 'signet/oauth_1/client'
        if !options["client"].kind_of?(::Signet::OAuth1::Client)
          raise TypeError,
            "Expected Signet::OAuth1::Client, got #{options["client"].class}."
        end
        credential_key = options["client"].send(credential_key)
      else
        credential_key = nil
      end
      if credential_key != nil && !credential_key.kind_of?(String)
        raise TypeError,
          "Expected String, got #{credential_key.class}."
      end
      return credential_key
    end

    ##
    # Processes an options <code>Hash</code> to find a credential secret value.
    # Allows for greater flexibility in configuration.
    #
    # @param [Symbol] credential_type
    #   One of <code>:client</code>, <code>:temporary</code>,
    #   <code>:token</code>, <code>:consumer</code>, <code>:request</code>,
    #   or <code>:access</code>.
    #
    # @return [String] The credential secret value.
    def self.extract_credential_secret_option(credential_type, options)
      # Normalize key to String to allow indifferent access.
      options = options.inject({}) { |accu, (k, v)| accu[k.to_s] = v; accu }
      credential_secret = "#{credential_type}_credential_secret"
      credential = "#{credential_type}_credential"
      if options[credential_secret]
        credential_secret = options[credential_secret]
      elsif options[credential]
        require 'signet/oauth_1/credential'
        if !options[credential].respond_to?(:secret)
          raise TypeError,
            "Expected Signet::OAuth1::Credential, " +
            "got #{options[credential].class}."
        end
        credential_secret = options[credential].secret
      elsif options["client"]
        require 'signet/oauth_1/client'
        if !options["client"].kind_of?(::Signet::OAuth1::Client)
          raise TypeError,
            "Expected Signet::OAuth1::Client, got #{options["client"].class}."
        end
        credential_secret = options["client"].send(credential_secret)
      else
        credential_secret = nil
      end
      if credential_secret != nil && !credential_secret.kind_of?(String)
        raise TypeError,
          "Expected String, got #{credential_secret.class}."
      end
      return credential_secret
    end

    ##
    # Normalizes a set of OAuth parameters according to the algorithm given
    # in RFC 5849.  Sorts key/value pairs lexically by byte order, first by
    # key, then by value, joins key/value pairs with the '=' character, then
    # joins the entire parameter list with '&' characters.
    #
    # @param [Enumerable] parameters The OAuth parameter list.
    #
    # @return [String] The normalized parameter list.
    def self.normalize_parameters(parameters)
      if !parameters.kind_of?(Enumerable)
        raise TypeError, "Expected Enumerable, got #{parameters.class}."
      end
      parameter_list = parameters.map do |k, v|
        next if k == "oauth_signature"
        # This is probably the wrong place to try to exclude the realm
        "#{self.encode(k)}=#{self.encode(v)}"
      end
      return parameter_list.compact.sort.join("&")
    end

    ##
    # Generates a signature base string according to the algorithm given in
    # RFC 5849.  Joins the method, URI, and normalized parameter string with
    # '&' characters.
    #
    # @param [String] method The HTTP method.
    # @param [Addressable::URI, String, #to_str] uri The URI.
    # @param [Enumerable] parameters The OAuth parameter list.
    #
    # @return [String] The signature base string.
    def self.generate_base_string(method, uri, parameters)
      if !parameters.kind_of?(Enumerable)
        raise TypeError, "Expected Enumerable, got #{parameters.class}."
      end
      method = method.to_s.upcase
      parsed_uri = Addressable::URI.parse(uri)
      uri = Addressable::URI.new(
        :scheme => parsed_uri.normalized_scheme,
        :authority => parsed_uri.normalized_authority,
        :path => parsed_uri.path,
        :query => parsed_uri.query,
        :fragment => parsed_uri.fragment
      )
      uri_parameters = uri.query_values.to_a
      uri = uri.omit(:query, :fragment).to_s
      merged_parameters =
        uri_parameters.concat(parameters.map { |k, v| [k, v] })
      parameter_string = self.normalize_parameters(merged_parameters)
      return [
        self.encode(method),
        self.encode(uri),
        self.encode(parameter_string)
      ].join('&')
    end

    ##
    # Generates an <code>Authorization</code> header from a parameter list
    # according to the rules given in RFC 5849.
    #
    # @param [Enumerable] parameters The OAuth parameter list.
    # @param [String] realm
    #   The <code>Authorization</code> realm.  See RFC 2617.
    #
    # @return [String] The <code>Authorization</code> header.
    def self.generate_authorization_header(parameters, realm=nil)
      if !parameters.kind_of?(Enumerable) || parameters.kind_of?(String)
        raise TypeError, "Expected Enumerable, got #{parameters.class}."
      end
      parameter_list = parameters.map do |k, v|
        if k == 'realm'
          raise ArgumentError,
            'The "realm" parameter must be specified as a separate argument.'
        end
        "#{self.encode(k)}=\"#{self.encode(v)}\""
      end
      if realm
        realm = realm.gsub('"', '\"')
        parameter_list.unshift("realm=\"#{realm}\"")
      end
      return 'OAuth ' + parameter_list.join(", ")
    end

    ##
    # Parses an <code>Authorization</code> header into its component
    # parameters.  Parameter keys and values are decoded according to the
    # rules given in RFC 5849.
    def self.parse_authorization_header(field_value)
      if !field_value.kind_of?(String)
        raise TypeError, "Expected String, got #{field_value.class}."
      end
      auth_scheme = field_value[/^([-._0-9a-zA-Z]+)/, 1]
      case auth_scheme
      when /^OAuth$/i
        # Other token types may be supported eventually
        pairs = Signet.parse_auth_param_list(field_value[/^OAuth\s+(.*)$/i, 1])
        return (pairs.inject([]) do |accu, (k, v)|
          if k != 'realm'
            k = self.unencode(k)
            v = self.unencode(v)
          end
          accu << [k, v]
          accu
        end)
      else
        raise ParseError,
          'Parsing non-OAuth Authorization headers is out of scope.'
      end
    end

    ##
    # Parses an <code>application/x-www-form-urlencoded</code> HTTP response
    # body into an OAuth key/secret pair.
    #
    # @param [String] body The response body.
    #
    # @return [Signet::OAuth1::Credential] The OAuth credentials.
    def self.parse_form_encoded_credentials(body)
      if !body.kind_of?(String)
        raise TypeError, "Expected String, got #{body.class}."
      end
      return Signet::OAuth1::Credential.new(
        Addressable::URI.form_unencode(body)
      )
    end

    ##
    # Generates an OAuth signature using the signature method indicated in the
    # parameter list.  Unsupported signature methods will result in a
    # <code>NotImplementedError</code> exception being raised.
    #
    # @param [String] method The HTTP method.
    # @param [Addressable::URI, String, #to_str] uri The URI.
    # @param [Enumerable] parameters The OAuth parameter list.
    # @param [String] client_credential_secret The client credential secret.
    # @param [String] token_credential_secret
    #   The token credential secret.  Omitted when unavailable.
    #
    # @return [String] The signature.
    def self.sign_parameters(method, uri, parameters,
        client_credential_secret, token_credential_secret=nil)
      # Technically, the token_credential_secret parameter here may actually
      # be a temporary credential secret when obtaining a token credential
      # for the first time
      base_string = self.generate_base_string(method, uri, parameters)
      parameters = parameters.inject({}) { |h,(k,v)| h[k.to_s]=v; h }
      signature_method = parameters['oauth_signature_method']
      case signature_method
      when 'HMAC-SHA1'
        require 'signet/oauth_1/signature_methods/hmac_sha1'
        return Signet::OAuth1::HMACSHA1.generate_signature(
          base_string, client_credential_secret, token_credential_secret
        )
      when 'RSA-SHA1'
        require 'signet/oauth_1/signature_methods/rsa_sha1'
        return Signet::OAuth1::RSASHA1.generate_signature(
          base_string, client_credential_secret, token_credential_secret
        )
      when 'PLAINTEXT'
        require 'signet/oauth_1/signature_methods/plaintext'
        return Signet::OAuth1::PLAINTEXT.generate_signature(
          base_string, client_credential_secret, token_credential_secret
        )
      else
        raise NotImplementedError,
          "Unsupported signature method: #{signature_method}"
      end
    end

    ##
    # Generates an OAuth parameter list to be used when obtaining a set of
    # temporary credentials.
    #
    # @param [Hash] options
    #   The configuration parameters for the request.
    #   - <code>:client_credential_key</code> -
    #     The client credential key.
    #   - <code>:callback</code> -
    #     The OAuth callback.  Defaults to {Signet::OAuth1::OUT_OF_BAND}.
    #   - <code>:signature_method</code> -
    #     The signature method.  Defaults to <code>'HMAC-SHA1'</code>.
    #   - <code>:additional_parameters</code> -
    #     Non-standard additional parameters.
    #
    # @return [Array]
    #   The parameter list as an <code>Array</code> of key/value pairs.
    def self.unsigned_temporary_credential_parameters(options={})
      options = {
        :callback => ::Signet::OAuth1::OUT_OF_BAND,
        :signature_method => 'HMAC-SHA1',
        :additional_parameters => []
      }.merge(options)
      client_credential_key =
        self.extract_credential_key_option(:client, options)
      if client_credential_key == nil
        raise ArgumentError, "Missing :client_credential_key parameter."
      end
      parameters = [
        ["oauth_consumer_key", client_credential_key],
        ["oauth_signature_method", options[:signature_method]],
        ["oauth_timestamp", self.generate_timestamp()],
        ["oauth_nonce", self.generate_nonce()],
        ["oauth_version", "1.0"],
        ["oauth_callback", options[:callback]]
      ]
      # Works for any Enumerable
      options[:additional_parameters].each do |key, value|
        parameters << [key, value]
      end
      return parameters
    end

    ##
    # Appends the optional 'oauth_token' and 'oauth_callback' parameters to
    # the base authorization URI.
    #
    # @param [Addressable::URI, String, #to_str] authorization_uri
    #   The base authorization URI.
    #
    # @return [String] The authorization URI to redirect the user to.
    def self.generate_authorization_uri(authorization_uri, options={})
      options = {
        :callback => nil,
        :additional_parameters => {}
      }.merge(options)
      temporary_credential_key =
        self.extract_credential_key_option(:temporary, options)
      parsed_uri = Addressable::URI.parse(authorization_uri).dup
      query_values = parsed_uri.query_values || {}
      if options[:additional_parameters]
        query_values = query_values.merge(
          options[:additional_parameters].inject({}) { |h,(k,v)| h[k]=v; h }
        )
      end
      if temporary_credential_key
        query_values['oauth_token'] = temporary_credential_key
      end
      if options[:callback]
        query_values['oauth_callback'] = options[:callback]
      end
      parsed_uri.query_values = query_values
      return parsed_uri.normalize.to_s
    end

    ##
    # Generates an OAuth parameter list to be used when obtaining a set of
    # token credentials.
    #
    # @param [Hash] options
    #   The configuration parameters for the request.
    #   - <code>:client_credential_key</code> -
    #     The client credential key.
    #   - <code>:temporary_credential_key</code> -
    #     The temporary credential key.
    #   - <code>:verifier</code> -
    #     The OAuth verifier.
    #   - <code>:signature_method</code> -
    #     The signature method.  Defaults to <code>'HMAC-SHA1'</code>.
    #
    # @return [Array]
    #   The parameter list as an <code>Array</code> of key/value pairs.
    def self.unsigned_token_credential_parameters(options={})
      options = {
        :signature_method => 'HMAC-SHA1',
        :verifier => nil
      }.merge(options)
      client_credential_key =
        self.extract_credential_key_option(:client, options)
      temporary_credential_key =
        self.extract_credential_key_option(:temporary, options)
      if client_credential_key == nil
        raise ArgumentError, "Missing :client_credential_key parameter."
      end
      if temporary_credential_key == nil
        raise ArgumentError, "Missing :temporary_credential_key parameter."
      end
      if options[:verifier] == nil
        raise ArgumentError, "Missing :verifier parameter."
      end
      parameters = [
        ["oauth_consumer_key", client_credential_key],
        ["oauth_token", temporary_credential_key],
        ["oauth_signature_method", options[:signature_method]],
        ["oauth_timestamp", self.generate_timestamp()],
        ["oauth_nonce", self.generate_nonce()],
        ["oauth_verifier", options[:verifier]],
        ["oauth_version", "1.0"]
      ]
      # No additional parameters allowed here
      return parameters
    end

    ##
    # Generates an OAuth parameter list to be used when requesting a
    # protected resource.
    #
    # @param [Hash] options
    #   The configuration parameters for the request.
    #   - <code>:client_credential_key</code> -
    #     The client credential key.
    #   - <code>:token_credential_key</code> -
    #     The token credential key.
    #   - <code>:signature_method</code> -
    #     The signature method.  Defaults to <code>'HMAC-SHA1'</code>.
    #   - <code>:two_legged</code> -
    #     A switch for two-legged OAuth.  Defaults to <code>false</code>.
    #
    # @return [Array]
    #   The parameter list as an <code>Array</code> of key/value pairs.
    def self.unsigned_resource_parameters(options={})
      options = {
        :signature_method => 'HMAC-SHA1',
        :two_legged => false
      }.merge(options)
      client_credential_key =
        self.extract_credential_key_option(:client, options)
      if client_credential_key == nil
        raise ArgumentError, "Missing :client_credential_key parameter."
      end
      unless options[:two_legged]
        token_credential_key =
          self.extract_credential_key_option(:token, options)
        if token_credential_key == nil
          raise ArgumentError, "Missing :token_credential_key parameter."
        end
      end
      parameters = [
        ["oauth_consumer_key", client_credential_key],
        ["oauth_signature_method", options[:signature_method]],
        ["oauth_timestamp", self.generate_timestamp()],
        ["oauth_nonce", self.generate_nonce()],
        ["oauth_version", "1.0"]
      ]
      unless options[:two_legged]
        parameters << ["oauth_token", token_credential_key]
      end
      # No additional parameters allowed here
      return parameters
    end
  end
end
