# The session is the mechanism by which services may be connected to and accessed.
#
# @api public
class Puppet::HTTP::Session
  # capabilities for a site
  CAP_LOCALES = 'locales'.freeze
  CAP_JSON = 'json'.freeze

  # puppet version where locales mount was added
  SUPPORTED_LOCALES_MOUNT_AGENT_VERSION = Gem::Version.new("5.3.4")

  # puppet version where JSON was enabled by default
  SUPPORTED_JSON_DEFAULT = Gem::Version.new("5.0.0")

  # Create a new HTTP session. The session is the mechanism by which services
  # may be connected to and accessed. Sessions should be created using
  # `Puppet::HTTP::Client#create_session`.
  #
  # @param [Puppet::HTTP::Client] client the container for this session
  # @param [Array<Puppet::HTTP::Resolver>] resolvers array of resolver strategies
  #   to implement.
  #
  # @api private
  def initialize(client, resolvers)
    @client = client
    @resolvers = resolvers
    @resolved_services = {}
    @server_versions = {}
  end

  # If an explicit server and port are specified on the command line or
  # configuration file, this method always returns a Service with that host and
  # port. Otherwise, we walk the list of resolvers in priority order:
  #     - DNS SRV
  #     - Server List
  #     - Puppet server/port settings
  # If a given resolver fails to connect, it tries the next available resolver
  # until a successful connection is found and returned. The successful service
  # is cached and returned if `route_to` is called again.
  #
  # @param [Symbol] name the service to resolve
  # @param [URI] url optional explicit url to use, if it is already known
  # @param [Puppet::SSL::SSLContext] ssl_context ssl context to be
  #   used for connections
  #
  # @return [Puppet::HTTP::Service] the resolved service
  #
  # @api public
  def route_to(name, url: nil, ssl_context: nil)
    raise ArgumentError, "Unknown service #{name}" unless Puppet::HTTP::Service.valid_name?(name)

    # short circuit if explicit URL host & port given
    if url && url.host != nil && !url.host.empty?
      service = Puppet::HTTP::Service.create_service(@client, self, name, url.host, url.port)
      service.connect(ssl_context: ssl_context)
      return service
    end

    cached = @resolved_services[name]
    return cached if cached

    canceled = false
    canceled_handler  = lambda { |cancel| canceled = cancel }

    @resolvers.each do |resolver|
      Puppet.debug("Resolving service '#{name}' using #{resolver.class}")
      service = resolver.resolve(self, name, ssl_context: ssl_context, canceled_handler: canceled_handler)
      if service
        @resolved_services[name] = service
        Puppet.debug("Resolved service '#{name}' to #{service.url}")
        return service
      elsif canceled
        break
      end
    end

    raise Puppet::HTTP::RouteError, "No more routes to #{name}"
  end

  # Collect per-site server versions. This will allow us to modify future
  # requests based on the version of puppetserver we are talking to.
  #
  # @param [Puppet::HTTP::Response] response the request response containing headers
  #
  # @api private
  def process_response(response)
    version = response[Puppet::HTTP::HEADER_PUPPET_VERSION]
    if version
      site = Puppet::HTTP::Site.from_uri(response.url)
      @server_versions[site] = version
    end
  end

  # Determine if a session supports a capability. Depending on the server version
  # we are talking to, we know certain features are available or not. These
  # specifications are defined here so we can modify our requests appropriately.
  #
  # @param [Symbol] name name of the service to check
  # @param [String] capability the capability, ie `locales` or `json`
  #
  # @return [Boolean]
  #
  # @api public
  def supports?(name, capability)
    raise ArgumentError, "Unknown service #{name}" unless Puppet::HTTP::Service.valid_name?(name)

    service = @resolved_services[name]
    return false unless service

    site = Puppet::HTTP::Site.from_uri(service.url)
    server_version = @server_versions[site]

    case capability
    when CAP_LOCALES
      !server_version.nil? && Gem::Version.new(server_version) >= SUPPORTED_LOCALES_MOUNT_AGENT_VERSION
    when CAP_JSON
      server_version.nil? || Gem::Version.new(server_version) >= SUPPORTED_JSON_DEFAULT
    else
      false
    end
  end
end
