class Puppet::Network::HTTP::ConnectionAdapter < Puppet::Network::HTTP::Connection
  def initialize(host, port, options = {})
    super(host, port, options)

    @client = Puppet.runtime[:http]
  end

  def get(path, headers = {}, options = {})
    headers ||= {}
    options[:ssl_context] ||= resolve_ssl_context
    options[:redirect_limit] ||= @redirect_limit

    with_error_handling do
      resp = @client.get(to_url(path), headers: headers, options: options)
      resp.nethttp
    end
  end

  def post(path, data, headers = nil, options = {})
    headers ||= {}
    headers['Content-Type'] ||= "application/x-www-form-urlencoded"
    data ||= ''
    options[:ssl_context] ||= resolve_ssl_context
    options[:redirect_limit] ||= @redirect_limit

    with_error_handling do
      resp = @client.post(to_url(path), data, headers: headers, options: options)
      resp.nethttp
    end
  end

  def head(path, headers = {}, options = {})
    headers ||= {}
    options[:ssl_context] ||= resolve_ssl_context
    options[:redirect_limit] ||= @redirect_limit

    with_error_handling do
      resp = @client.head(to_url(path), headers: headers, options: options)
      resp.nethttp
    end
  end

  def delete(path, headers = {'Depth' => 'Infinity'}, options = {})
    headers ||= {}
    options[:ssl_context] ||= resolve_ssl_context
    options[:redirect_limit] ||= @redirect_limit

    with_error_handling do
      resp = @client.delete(to_url(path), headers: headers, options: options)
      resp.nethttp
    end
  end

  def put(path, data, headers = nil, options = {})
    headers ||= {}
    headers['Content-Type'] ||= "application/x-www-form-urlencoded"
    data ||= ''
    options[:ssl_context] ||= resolve_ssl_context
    options[:redirect_limit] ||= @redirect_limit

    with_error_handling do
      resp = @client.put(to_url(path), data, headers: headers, options: options)
      resp.nethttp
    end
  end

  def request_get(*args, &block)
    path, headers = *args
    headers ||= {}
    options = {
      ssl_context: resolve_ssl_context,
      redirect_limit: @redirect_limit
    }

    resp = @client.get(to_url(path), headers: headers, options: options) do |response|
      yield response.nethttp if block_given?
    end
    resp.nethttp
  end

  def request_head(*args, &block)
    path, headers = *args
    headers ||= {}
    options = {
      ssl_context: resolve_ssl_context,
      redirect_limit: @redirect_limit
    }

    response = @client.head(to_url(path), headers: headers, options: options)
    yield response.nethttp if block_given?
    response.nethttp
  end

  def request_post(*args, &block)
    path, data, headers = *args
    headers ||= {}
    headers['Content-Type'] ||= "application/x-www-form-urlencoded"
    options = {
      ssl_context: resolve_ssl_context,
      redirect_limit: @redirect_limit
    }

    resp = @client.post(to_url(path), data, headers: headers, options: options) do |response|
      yield response.nethttp if block_given?
    end
    resp.nethttp
  end

  private

  # The old Connection class ignores the ssl_context on the Puppet stack,
  # and always loads certs/keys based on what is currently in the filesystem.
  # If the files are missing, it would attempt to bootstrap the certs/keys
  # while in the process of making a network request, due to the call to
  # Puppet.lookup(:ssl_host) in Puppet::SSL::Validator::DefaultValidator#setup_connection.
  # This class doesn't preserve the boostrap behavior because that is handled
  # outside of this class, and can only be triggered by running `puppet ssl` or
  # `puppet agent`.
  def resolve_ssl_context
    # don't need an ssl context for http connections
    return nil unless @site.use_ssl?

    # if our verifier has an ssl_context, use that
    ctx = @verifier.ssl_context
    return ctx if ctx

    # load available certs
    cert = Puppet::X509::CertProvider.new
    ssl = Puppet::SSL::SSLProvider.new
    begin
      password = cert.load_private_key_password
      ssl.load_context(certname: Puppet[:certname], password: password)
    rescue Puppet::SSL::SSLError => e
      Puppet.log_exception(e)

      # if we don't have cacerts, then create a root context that doesn't
      # trust anything. The old code used to fallback to VERIFY_NONE,
      # which we don't want to emulate.
      ssl.create_root_context(cacerts: [])
    end
  end

  def to_url(path)
    if path =~ /^https?:\/\//
      # The old Connection class accepts a URL as the request path, and sends
      # it in "absolute-form" in the request line, e.g. GET https://puppet:8140/.
      # See https://httpwg.org/specs/rfc7230.html#absolute-form. It just so happens
      # to work because HTTP 1.1 servers are required to accept absolute-form even
      # though clients are only supposed to send them to proxies, so the proxy knows
      # what upstream server to CONNECT to. This method creates a URL using the
      # scheme/host/port that the connection was created with, and appends the path
      # and query portions of the absolute-form. The resulting request will use "origin-form"
      # as it should have done all along.
      abs_form = URI(path)
      url = URI("#{@site.addr}/#{normalize_path(abs_form.path)}")
      url.query = abs_form.query if abs_form.query
      url
    else
      URI("#{@site.addr}/#{normalize_path(path)}")
    end
  end

  def normalize_path(path)
    if path[0] == '/'
      path[1..-1]
    else
      path
    end
  end

  def with_error_handling(&block)
    yield
  rescue Puppet::HTTP::TooManyRedirects => e
    raise Puppet::Network::HTTP::RedirectionLimitExceededException.new(_("Too many HTTP redirections for %{host}:%{port}") % { host: @host, port: @port }, e)
  rescue Puppet::HTTP::HTTPError => e
    Puppet.log_exception(e, e.message)
    case e.cause
    when Net::OpenTimeout, Net::ReadTimeout, Net::HTTPError, EOFError
      raise e.cause
    else
      raise e
    end
  end
end
