module Her
  # This class is where all HTTP requests are made. Before using Her, you must configure it
  # so it knows where to make those requests. In Rails, this is usually done in `config/initializers/her.rb`:
  class API

    # @private
    attr_reader :connection, :options

    # Constants
    FARADAY_OPTIONS = [:request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class, :timeout, :open_timeout].freeze

    # Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
    def self.setup(opts = {}, &block)
      @default_api = new(opts, &block)
    end

    # Create a new API object. This is useful to create multiple APIs and use them with the `uses_api` method.
    # If your application uses only one API, you should use Her::API.setup to configure the default API
    #
    # @example Setting up a new API
    #   api = Her::API.new :url => "https://api.example" do |connection|
    #     connection.use Faraday::Request::UrlEncoded
    #     connection.use Her::Middleware::DefaultParseJSON
    #   end
    #
    #   class User
    #     uses_api api
    #   end
    def initialize(*args, &blk)
      setup(*args, &blk)
    end

    # Setup the API connection.
    #
    # @param [Hash] opts the Faraday options
    # @option opts [String] :url The main HTTP API root (eg. `https://api.example.com`)
    # @option opts [String] :ssl A hash containing [SSL options](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates)
    #
    # @return Faraday::Connection
    #
    # @example Setting up the default API connection
    #   Her::API.setup :url => "https://api.example"
    #
    # @example A custom middleware added to the default list
    #   class MyAuthentication < Faraday::Middleware
    #     def call(env)
    #       env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
    #       @app.call(env)
    #     end
    #   end
    #   Her::API.setup :url => "https://api.example.com" do |connection|
    #     connection.use Faraday::Request::UrlEncoded
    #     connection.use Her::Middleware::DefaultParseJSON
    #     connection.use MyAuthentication
    #     connection.use Faraday::Adapter::NetHttp
    #   end
    #
    # @example A custom parse middleware
    #   class MyCustomParser < Faraday::Response::Middleware
    #     def on_complete(env)
    #       json = JSON.parse(env[:body], :symbolize_names => true)
    #       errors = json.delete(:errors) || {}
    #       metadata = json.delete(:metadata) || []
    #       env[:body] = { :data => json, :errors => errors, :metadata => metadata }
    #     end
    #   end
    #   Her::API.setup :url => "https://api.example.com" do |connection|
    #     connection.use Faraday::Request::UrlEncoded
    #     connection.use MyCustomParser
    #     connection.use Faraday::Adapter::NetHttp
    #   end
    def setup(opts = {}, &blk)
      opts[:url] = opts.delete(:base_uri) if opts.include?(:base_uri) # Support legacy :base_uri option
      @options = opts

      faraday_options = @options.select { |key, _| FARADAY_OPTIONS.include?(key.to_sym) }
      @connection = Faraday.new(faraday_options) do |connection|
        yield connection if block_given?
      end
      self
    end

    # Define a custom parsing procedure. The procedure is passed the response object and is
    # expected to return a hash with three keys: a main data Hash, an errors Hash
    # and a metadata Hash.
    #
    # @private
    def request(opts = {})
      method = opts.delete(:_method)
      path = opts.delete(:_path)
      headers = opts.delete(:_headers)
      options = opts.delete(:_options) || {}
      opts.delete_if { |key, _| key.to_s =~ /^_/ } # Remove all internal parameters
      if method == :options
        # Faraday doesn't support the OPTIONS verb because of a name collision with an internal options method
        # so we need to call run_request directly.
        request.headers.merge!(headers) if headers
        response = @connection.run_request method, path, opts, headers
      else
        response = @connection.send method do |request|
          request.headers.merge!(headers) if headers

          timeout = options.delete(:timeout)
          raise "options not implemented: #{options.keys.join(', ')}" unless options.empty?
          request.options[:timeout] = timeout if timeout

          if method == :get
            # For GET requests, treat additional parameters as querystring data
            request.url path, opts
          else
            # For POST, PUT and DELETE requests, treat additional parameters as request body
            request.url path
            request.body = opts
          end
        end
      end
      { :parsed_data => response.env[:body], :response => response }
    end

    private

    # @private
    def self.default_api(opts = {})
      defined?(@default_api) ? @default_api : nil
    end
  end
end
