require 'cgi'
require 'uri'
require 'puppet/indirector'
require 'puppet/network/resolver'
require 'puppet/util/psych_support'

# This class encapsulates all of the information you need to make an
# Indirection call, and as a result also handles REST calls.  It's somewhat
# analogous to an HTTP Request object, except tuned for our Indirector.
class Puppet::Indirector::Request
  include Puppet::Util::PsychSupport

  attr_accessor :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus

  attr_accessor :server, :port, :uri, :protocol

  attr_reader :indirection_name

  # trusted_information is specifically left out because we can't serialize it
  # and keep it "trusted"
  OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment]

  # Is this an authenticated request?
  def authenticated?
    # Double negative, so we just get true or false
    ! ! authenticated
  end

  def environment
    # If environment has not been set directly, we should use the application's
    # current environment
    @environment ||= Puppet.lookup(:current_environment)
  end

  def environment=(env)
    @environment =
    if env.is_a?(Puppet::Node::Environment)
      env
    else
      Puppet.lookup(:environments).get!(env)
    end
  end

  # LAK:NOTE This is a messy interface to the cache, and it's only
  # used by the Configurer class.  I decided it was better to implement
  # it now and refactor later, when we have a better design, than
  # to spend another month coming up with a design now that might
  # not be any better.
  def ignore_cache?
    ignore_cache
  end

  def ignore_terminus?
    ignore_terminus
  end

  def initialize(indirection_name, method, key, instance, options = {})
    @instance = instance
    options ||= {}

    self.indirection_name = indirection_name
    self.method = method

    options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash }

    set_attributes(options)

    @options = options

    if key
      # If the request key is a URI, then we need to treat it specially,
      # because it rewrites the key.  We could otherwise strip server/port/etc
      # info out in the REST class, but it seemed bad design for the REST
      # class to rewrite the key.

      if key.to_s =~ /^\w+:\// and not Puppet::Util.absolute_path?(key.to_s) # it's a URI
        set_uri_key(key)
      else
        @key = key
      end
    end

    @key = @instance.name if ! @key and @instance
  end

  # Look up the indirection based on the name provided.
  def indirection
    Puppet::Indirector::Indirection.instance(indirection_name)
  end

  def indirection_name=(name)
    @indirection_name = name.to_sym
  end

  def model
    raise ArgumentError, _("Could not find indirection '%{indirection}'") % { indirection: indirection_name } unless i = indirection
    i.model
  end

  # Are we trying to interact with multiple resources, or just one?
  def plural?
    method == :search
  end

  # Create the query string, if options are present.
  def query_string
    return "" if options.nil? || options.empty?
    encode_params(expand_into_parameters(options.to_a))
  end

  def expand_into_parameters(data)
    data.inject([]) do |params, key_value|
      key, value = key_value

      expanded_value = case value
                       when Array
                         value.collect { |val| [key, val] }
                       else
                         [key_value]
                       end

      params.concat(expand_primitive_types_into_parameters(expanded_value))
    end
  end

  def expand_primitive_types_into_parameters(data)
    data.inject([]) do |params, key_value|
      key, value = key_value
      case value
      when nil
        params
      when true, false, String, Symbol, Integer, Float
        params << [key, value]
      else
        raise ArgumentError, _("HTTP REST queries cannot handle values of type '%{klass}'") % { klass: value.class }
      end
    end
  end

  def encode_params(params)
    params.collect do |key, value|
      "#{key}=#{Puppet::Util.uri_query_encode(value.to_s)}"
    end.join("&")
  end

  def initialize_from_hash(hash)
    @indirection_name = hash['indirection_name'].to_sym
    @method = hash['method'].to_sym
    @key = hash['key']
    @instance = hash['instance']
    @options = hash['options']
  end

  def to_data_hash
    { 'indirection_name' => @indirection_name.to_s,
      'method' => @method.to_s,
      'key' => @key,
      'instance' => @instance,
      'options' => @options }
  end

  def to_hash
    result = options.dup

    OPTION_ATTRIBUTES.each do |attribute|
      if value = send(attribute)
        result[attribute] = value
      end
    end
    result
  end

  def description
    return(uri ? uri : "/#{indirection_name}/#{key}")
  end

  def do_request(srv_service=:puppet, default_server=nil, default_port=nil, &block)
    # We were given a specific server to use, so just use that one.
    # This happens if someone does something like specifying a file
    # source using a puppet:// URI with a specific server.
    return yield(self) if !self.server.nil?

    if Puppet.settings[:use_srv_records]
      Puppet::Network::Resolver.each_srv_record(Puppet.settings[:srv_domain], srv_service) do |srv_server, srv_port|
        begin
          self.server = srv_server
          self.port   = srv_port
          return yield(self)
        rescue SystemCallError => e
          Puppet.warning _("Error connecting to %{srv_server}:%{srv_port}: %{message}") % { srv_server: srv_server, srv_port: srv_port, message: e.message }
        end
      end
    end

    # ... Fall back onto the default server.
    begin
      bound_server = Puppet.lookup(:server)
    rescue
      if primary_server = Puppet.settings[:server_list][0]
        bound_server = primary_server[0]
      else
        bound_server = Puppet.settings[:server]
      end
    end

    begin
      bound_port = Puppet.lookup(:serverport)
    rescue
      if primary_server = Puppet.settings[:server_list][0]
        bound_port = primary_server[1]
      else
        bound_port = Puppet.settings[:masterport]
      end
    end
    self.server = default_server || bound_server
    self.port   = default_port || bound_port

    Puppet.debug "No more servers left, falling back to #{self.server}:#{self.port}" if Puppet.settings[:use_srv_records]

    return yield(self)
  end

  def remote?
    self.node or self.ip
  end

  private

  def set_attributes(options)
    OPTION_ATTRIBUTES.each do |attribute|
      if options.include?(attribute.to_sym)
        send(attribute.to_s + "=", options[attribute])
        options.delete(attribute)
      end
    end
  end

  # Parse the key as a URI, setting attributes appropriately.
  def set_uri_key(key)
    @uri = key
    begin
      # calling uri_encode for UTF-8 characters will % escape them and keep them UTF-8
      uri = URI.parse(Puppet::Util.uri_encode(key))
    rescue => detail
      raise ArgumentError, _("Could not understand URL %{key}: %{detail}") % { key: key, detail: detail }, detail.backtrace
    end

    # Just short-circuit these to full paths
    if uri.scheme == "file"
      @key = Puppet::Util.uri_to_path(uri)
      return
    end

    @server = uri.host if uri.host

    # If the URI class can look up the scheme, it will provide a port,
    # otherwise it will default to '0'.
    if uri.port.to_i == 0 and uri.scheme == "puppet"
      @port = Puppet.settings[:masterport].to_i
    else
      @port = uri.port.to_i
    end

    # filebucket:// is only used internally to pass request details
    # from Dipper objects to the indirector. The wire always uses HTTPS.
    if uri.scheme == 'filebucket'
      @protocol = 'https'
    else
      @protocol = uri.scheme
    end

    @key = URI.unescape(uri.path.sub(/^\//, ''))
  end
end
