require 'open-uri'
require 'retryable'
require 'tempfile'
require 'zlib'

require 'ridley/helpers'

module Ridley
  class Connection < Faraday::Connection
    include Celluloid
    task_class TaskThread

    VALID_OPTIONS = [
      :retries,
      :retry_interval,
      :ssl,
      :proxy
    ]

    # @return [String]
    attr_reader :organization
    # @return [String]
    attr_reader :client_key
    # @return [String]
    attr_reader :client_name
    # @return [Integer]
    #   how many retries to attempt on HTTP requests
    attr_reader :retries
    # @return [Float]
    #   time to wait between retries
    attr_reader :retry_interval

    # @param [String] server_url
    # @param [String] client_name
    # @param [String] client_key
    #
    # @option options [Integer] :retries (5)
    #   retry requests on 5XX failures
    # @option options [Float] :retry_interval (0.5)
    #   how often we should pause between retries
    # @option options [Hash] :ssl
    #   * :verify (Boolean) [true] set to false to disable SSL verification
    # @option options [URI, String, Hash] :proxy
    #   URI, String, or Hash of HTTP proxy options
    def initialize(server_url, client_name, client_key, options = {})
      options         = options.reverse_merge(retries: 5, retry_interval: 0.5)
      @client_name    = client_name
      @client_key     = client_key
      @retries        = options.delete(:retries)
      @retry_interval = options.delete(:retry_interval)

      options[:builder] = Faraday::RackBuilder.new do |b|
        b.request :retry,
          max: @retries,
          interval: @retry_interval,
          exceptions: [
            Ridley::Errors::HTTP5XXError,
            Errno::ETIMEDOUT,
            Faraday::Error::TimeoutError
          ]
        b.request :chef_auth, client_name, client_key

        b.response :parse_json
        b.response :chef_response

        b.adapter :httpclient
      end

      uri_hash = Ridley::Helpers.options_slice(Addressable::URI.parse(server_url).to_hash, :scheme, :host, :port)

      unless uri_hash[:port]
        uri_hash[:port] = (uri_hash[:scheme] == "https" ? 443 : 80)
      end

      if org_match = server_url.match(/.*\/organizations\/(.*)/)
        @organization = org_match[1]
      end

      unless @organization.nil?
        uri_hash[:path] = "/organizations/#{@organization}"
      end

      super(Addressable::URI.new(uri_hash), options)
      @headers[:user_agent] = "Ridley v#{Ridley::VERSION}"
    end

    # @return [Symbol]
    def api_type
      organization.nil? ? :foss : :hosted
    end

    # @return [Boolean]
    def hosted?
      api_type == :hosted
    end

    # @return [Boolean]
    def foss?
      api_type == :foss
    end

    # Override Faraday::Connection#run_request to catch exceptions from {Ridley::Middleware} that
    # we expect. Caught exceptions are re-raised with Celluloid#abort so we don't crash the connection.
    def run_request(*args)
      super
    rescue Errors::HTTPError => ex
      abort ex
    rescue Faraday::Error::ConnectionFailed => ex
      abort Errors::ConnectionFailed.new(ex)
    rescue Faraday::Error::TimeoutError => ex
      abort Errors::TimeoutError.new(ex)
    rescue Faraday::Error::ClientError => ex
      abort Errors::ClientError.new(ex)
    end

    def server_url
      self.url_prefix.to_s
    end

    # Stream the response body of a remote URL to a file on the local file system
    #
    # @param [String] target
    #   a URL to stream the response body from
    # @param [String] destination
    #   a location on disk to stream the content of the response body to
    #
    # @return [Boolean] true when the destination file exists
    def stream(target, destination)
      FileUtils.mkdir_p(File.dirname(destination))

      target  = Addressable::URI.parse(target)
      headers = Middleware::ChefAuth.authentication_headers(
        client_name,
        client_key,
        http_method: "GET",
        host: target.host,
        path: target.path
      )

      unless ssl[:verify]
        headers.merge!(ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE)
      end

      local = Tempfile.new('ridley-stream')
      local.binmode

      Retryable.retryable(tries: retries, on: OpenURI::HTTPError, sleep: retry_interval) do
        open(target, 'rb', headers) do |remote|
          body = remote.read
          case remote.content_encoding
          when ['gzip']
            body = Zlib::GzipReader.new(StringIO.new(body), encoding: 'ASCII-8BIT').read
          when ['deflate']
            body = Zlib::Inflate.inflate(body)
          end
          local.write(body)
        end
      end

      local.flush

      FileUtils.cp(local.path, destination)
      File.exists?(destination)
    rescue OpenURI::HTTPError => ex
      abort(ex)
    ensure
      local.close(true) unless local.nil?
    end
  end
end
