# This function builds a os_transport_url string from a hash of parameters.
#
# Valid hash parameteres:
#  * transport - (string) type of transport, 'rabbit' or 'amqp'
#  * host - (string) single host
#  * hosts - (array) array of hosts to use
#  * port - (string | integer) port to connect to
#  * username - (string) connection username
#  * password - (string) connection password
#  * virtual_host - (string) virtual host to connect to
#  * ssl - (string) is the connection ssl or not ('1' or '0'). overrides the ssl
#    key in the query parameter
#  * query - (hash) hash of key,value pairs used to create a query string for
#    the transport_url.
#
# Only 'transport' and either 'host' or 'hosts' are required keys for the
# parameters hash.
#
# The url format that will be generated:
# transport://user:pass@host:port[,userN:passN@hostN:portN]/virtual_host?query
#
# NOTE: ipv6 addresses will automatically be bracketed for the URI using the
# normalize_ip_for_uri function.
#
# Single Host Example:
# os_transport_url({
#   'transport'    => 'rabbit',
#   'host'         => '1.1.1.1',
#   'port'         => '5672',
#   'username'     => 'username',
#   'password'     => 'password',
#   'virtual_host' => 'virtual_host',
#   'ssl'          => '1',
#   'query'        => { 'key' => 'value' },
# })
#
# Generates:
# rabbit://username:password@1.1.1.1:5672/virtual_host?key=value&ssl=1
#
# Multiple Hosts Example:
# os_transport_url({
#   'transport'    => 'rabbit',
#   'hosts'        => [ '1.1.1.1', '2.2.2.2' ],
#   'port'         => '5672',
#   'username'     => 'username',
#   'password'     => 'password',
#   'virtual_host' => 'virtual_host',
#   'query'        => { 'key' => 'value' },
# })
#
# Generates:
# rabbit://username:password@1.1.1.1:5672,username:password@2.2.2.2:5672/virtual_host?key=value
Puppet::Functions.create_function(:os_transport_url) do
  # TODO(tobias-urdin): Rework and remove this.
  # Taken straight from stdlib v5.1.0 module.
  def _str2bool(string)
    if !!string == string
      return string
    end
    unless string.is_a?(String)
      raise(Puppet::ParseError, 'str2bool(): Requires string to work with')
    end
    result = case string
             when %r{^$}, '' then false # Empty string will be false ...
             when %r{^(1|t|y|true|yes)$}i  then true
             when %r{^(0|f|n|false|no)$}i  then false
             when %r{^(undef|undefined)$} then false # This is not likely to happen ...
             else
               raise(Puppet::ParseError, 'os_transport_url _str2bool(): Unknown type of boolean given')
             end
    return result
  end

  # TODO(tobias-urdin): Rework and remove this.
  # Taken straight from stdlib v5.1.0 module.
  def _bool2num(val)
    value = _str2bool(val)
    result = value ? 1 : 0
    return result
  end

  def os_transport_url(*args)
    require 'erb'

    unless args.size == 1
      raise(ArgumentError, 'os_transport_url(): Wrong number of arguments')
    end

    v_raw = args[0]
    klass = v_raw.class

    unless klass == Hash
      raise(Puppet::ParseError, "os_transport_url(): Requires an hash, got #{klass}")
    end

    v = {}
    # type checking for the parameter hash
    v_raw.keys.each do |key|
      if key == 'port'
        v[key] = v_raw[key].to_s
      else
        v[key] = v_raw[key]
      end
      klass = (key == 'hosts') ? Array : String
      klass = (key == 'query') ? Hash : klass
      unless (v[key].class == klass) or (v[key] == :undef)
        raise(Puppet::ParseError, "os_transport_url(): #{key} should be a #{klass}")
      end
    end

    # defaults
    parts = {
      :transport => 'rabbit',
      :hostinfo  => 'localhost',
      :path      => '/',
    }

    unless v.include?('transport')
      raise(Puppet::ParseError, 'os_transport_url(): transport is required')
    end

    unless v.include?('host') or v.include?('hosts')
      raise(Puppet::ParseError, 'os_transport_url(): host or hosts is required')
    end

    if v.include?('host') and v.include?('hosts')
      raise(Puppet::ParseError, 'os_transport_url(): cannot use both host and hosts.')
    end

    parts[:transport] = v['transport']

    if v.include?('username') and (v['username'] != :undef) and (v['username'].to_s != '')
      parts[:userinfo] = ERB::Util.url_encode(v['username'])
      if v.include?('password') and (v['password'] != :undef) and (v['password'].to_s != '')
        parts[:userinfo] += ":#{ERB::Util.url_encode(v['password'])}"
      end
    end

    if v.include?('host')
      host = call_function('normalize_ip_for_uri', v['host'])
      host += ":#{v['port'].to_s}" if v.include?('port')
      if parts.include?(:userinfo)
        parts[:hostinfo] = "#{parts[:userinfo]}@#{host}"
      else
        parts[:hostinfo] = "#{host}"
      end
    end

    if v.include?('hosts')
      hosts = call_function('normalize_ip_for_uri', v['hosts'])
      # normalize_ip_for_uri may return a string, so check that we still have an
      # array
      hosts = [hosts] if hosts.kind_of?(String)
      hosts = hosts.map{ |h| "#{h}:#{v['port'].to_s}" } if v.include?('port')
      if parts.include?(:userinfo)
        parts[:hostinfo] = hosts.map { |h| "#{parts[:userinfo]}@#{h}" }.join(',')
      else
        parts[:hostinfo] = hosts.join(',')
      end
    end

    parts[:path] = "/#{v['virtual_host']}" if v.include?('virtual_host')

    query = {}
    if v.include?('query')
      query.merge!(v['query'])
    end

    # support previous ssl option on the function. Setting ssl will
    # override ssl if passed in via the query parameters
    if v.include?('ssl')
      # ssl can be passed in as a query paramter but should be 0/1. See
      # http://docs.celeryproject.org/projects/kombu/en/latest/userguide/connections.html#urls
      # so we rely on _str2bool and _bool2num to ensure it's in the
      # format
      # TODO(tobias-urdin): Rework this to using proper data types and not the
      # legacy puppet functions that is copied into this function statement.
      # We need to do this right now because it fails testing if we call the
      # str2bool or bool2num legacy functions using call_function.
      ssl_str = _str2bool(v['ssl'])
      ssl_val = _bool2num(v['ssl'])

      query.merge!({ 'ssl' => ssl_val })
    end

    parts[:query] = query.map{ |k,val| "#{k}=#{val}" }.join('&') if ! query.empty?

    url_parts = []
    url_parts << parts[:transport]
    url_parts << '://'
    url_parts << parts[:hostinfo]
    url_parts << parts[:path]
    url_parts << '?' << parts[:query] if parts.include?(:query)
    url_parts.join()
  end
end
