# frozen_string_literal: true

require "rubygems/remote_fetcher"

class Gem::CompactIndexClient
  # Fetches compact index files relative to +base_uri+ using
  # Gem::RemoteFetcher's connection infrastructure (proxy, TLS,
  # connection pooling). Implements the fetcher interface expected by
  # Gem::CompactIndexClient: #call(path, headers) returning a
  # Gem::Net::HTTP response.
  class HTTPFetcher
    REDIRECT_LIMIT = 10
    private_constant :REDIRECT_LIMIT

    def initialize(base_uri, remote_fetcher = Gem::RemoteFetcher.fetcher)
      base_uri = base_uri.to_s
      base_uri += "/" unless base_uri.end_with?("/")
      @base_uri = Gem::URI(base_uri)
      @remote_fetcher = remote_fetcher
    end

    def call(path, headers = {})
      fetch(@base_uri + path, headers, REDIRECT_LIMIT)
    end

    private

    def fetch(uri, headers, redirects_remaining)
      response = request(uri, headers)

      case response
      when Gem::Net::HTTPSuccess, Gem::Net::HTTPNotModified
        response
      when Gem::Net::HTTPMovedPermanently, Gem::Net::HTTPFound, Gem::Net::HTTPSeeOther,
           Gem::Net::HTTPTemporaryRedirect, Gem::Net::HTTPPermanentRedirect
        raise Gem::RemoteFetcher::FetchError.new("too many redirects", uri) if redirects_remaining.zero?

        location = response["Location"]
        raise Gem::RemoteFetcher::FetchError.new("redirecting but no redirect location was given", uri) unless location

        redirect = uri + location
        if https?(uri) && !https?(redirect)
          raise Gem::RemoteFetcher::FetchError.new("redirecting to non-https resource: #{Gem::Uri.redact(redirect)}", uri)
        end

        fetch(redirect, headers, redirects_remaining - 1)
      when Gem::Net::HTTPRangeNotSatisfiable
        raise Gem::RemoteFetcher::FetchError.new("bad response #{response.message} #{response.code}", uri) unless headers.key?("Range")

        # The local cache is longer than the remote file, refetch it whole.
        fetch(uri, headers.except("Range"), redirects_remaining)
      else
        raise Gem::RemoteFetcher::FetchError.new("bad response #{response.message} #{response.code}", uri)
      end
    end

    # The callers fall back to the Marshal index when a fetch fails, and they
    # only recognize a failure that arrives as a FetchError.
    def request(uri, headers)
      @remote_fetcher.request(uri, Gem::Net::HTTP::Get) do |req|
        headers.each {|name, value| req[name] = value }
      end
    rescue Gem::Timeout::Error, IOError, SocketError, SystemCallError,
           *(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e
      raise Gem::RemoteFetcher::FetchError.new("#{e.class}: #{e}", uri)
    end

    def https?(uri)
      uri.scheme == "https"
    end
  end
end
