require 'base64'
require "vagrant-skytap/version"
require 'timeout'

module VagrantPlugins
  module Skytap
    module API
      class Client
        attr_reader :config, :http

        DEFAULT_TIMEOUT = 120
        MAX_RATE_LIMIT_RETRIES = 3
        DEFAULT_RETRY_AFTER_SECONDS = 5

        def initialize(config)
          @logger = Log4r::Logger.new("vagrant_skytap::api_client")

          @config = config
          uri = URI.parse(config.base_url)
          @http = Net::HTTP.new(uri.host, uri.port)

          @http.verify_mode = OpenSSL::SSL::VERIFY_NONE

          @http.use_ssl = uri.port == 443 || uri.scheme == 'https'
        end

        def get(path, options={})
          req('GET', path, nil, options)
        end

        def post(path, body=nil, options={})
          req('POST', path, body, options)
        end

        def put(path, body=nil, options={})
          req('PUT', path, body, options)
        end

        def delete(path, options={})
          req('DELETE', path, nil, options)
        end

        # +path+ may optionally include query and fragment parts
        #
        # +options+ are:
        #   query: A string or hash of the query string
        #   extra_headers: A hash of extra headers
        def req(method, path, body, options={})
          @logger.info("REST API call: #{method} #{path} #{'body: ' + body if body.present?}")

          uri = URI.parse(path)

          if qq = options[:query]
            if qq.is_a?(Hash)
              extra_query = qq.collect{|k,v| [k,v].join('=')}.join('&')
            else
              extra_query = qq.to_s
            end
          end

          if (query = [uri.query, extra_query].compact.join('&')).present?
            path = [uri.path, query].join('?')
          end

          headers = default_headers.merge(options[:extra_headers] || {})
          retry_after = DEFAULT_RETRY_AFTER_SECONDS
          most_recent_exception = nil

          begin
            Timeout.timeout(options[:timeout] || DEFAULT_TIMEOUT) do
              begin
                http.send_request(method, URI.encode(path), body, headers).tap do |ret|
                  @logger.debug("REST API response: #{ret.body}")
                  unless ret.code =~ /^2\d\d/
                    raise Errors::DoesNotExist, object_name: "Object '#{path}'" if ret.code == '404'
                    error_class = case ret.code
                    when '403'
                      Errors::Unauthorized
                    when '422'
                      Errors::UnprocessableEntity
                    when '423'
                      Errors::ResourceBusy
                    when '429'
                      retry_after = ret['Retry-After'] || DEFAULT_RETRY_AFTER_SECONDS
                      Errors::RateLimited
                    else
                      Errors::OperationFailed
                    end
                    raise error_class, err: error_string_from_body(ret)
                  end
                end
              rescue Errors::RateLimited => ex
                most_recent_exception = ex
                @logger.info("Rate limited, wil retry in #{retry_after} seconds")
                sleep retry_after.to_f + 0.1
                retry
              rescue Errors::ResourceBusy => ex
                most_recent_exception = ex
                @logger.debug("Resource busy, retrying")
                sleep DEFAULT_RETRY_AFTER_SECONDS
                retry
              end
            end
          rescue Timeout::Error => ex
            raise most_recent_exception if most_recent_exception
            raise Errors::OperationFailed, err: "Timeout exceeded"
          end
        end

        def error_string_from_body(resp)
          resp = resp.body if resp.respond_to?(:body)
          begin
            resp = JSON.load(resp)
            errors = resp['error'] || resp['errors']
            errors = errors.join('; ') if errors.respond_to? :join
          rescue
            # treat non-JSON string as error text
            errors = resp
          end
          errors if errors.present?
        end

        private

        def user_agent_string
          "Vagrant-Skytap/#{VagrantPlugins::Skytap::VERSION} Vagrant/#{Vagrant::VERSION}"
        end

        def default_headers
          {
            'Authorization' => auth_header,
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'User-Agent' => user_agent_string,
          }
        end

        def auth_header
          "Basic #{Base64.encode64(config.username + ":" +
                                   config.api_token)}".gsub("\n", '')
        end
      end
    end
  end
end
