module RemoteResource
  class Request

    SUPPORTED_HTTP_METHODS = [:get, :put, :patch, :post, :delete].freeze

    DEFAULT_HEADERS = {
      'Accept'     => 'application/json',
      'User-Agent' => "RemoteResource #{RemoteResource::VERSION}"
    }.freeze

    DEFAULT_CONTENT_TYPE = {
      'Content-Type' => 'application/json'
    }.freeze

    DEFAULT_EXTENSION = '.json'.freeze

    attr_reader :resource, :resource_klass, :http_action, :attributes

    def initialize(resource, http_action, attributes = {}, connection_options = {})
      @resource           = resource
      @resource_klass     = resource.is_a?(Class) ? resource : resource.class
      @http_action        = http_action.to_sym
      @attributes         = attributes
      @connection_options = connection_options.dup
    end

    def connection
      resource_klass.connection
    end

    def connection_options
      @combined_connection_options ||= begin
        default = resource.connection_options.to_hash # Defined on the resource (klass).
        block   = resource.try(:threaded_connection_options) || {} # Given as arguments in the .with_connection_options block.
        local   = @connection_options # Given as arguments directly.

        default.deep_merge(block).deep_merge(local)
      end
    end

    def perform
      SUPPORTED_HTTP_METHODS.include?(http_action) || raise(RemoteResource::HTTPMethodUnsupported, "Requested HTTP method=#{http_action.to_s} is NOT supported, the HTTP action MUST be a supported HTTP action=#{SUPPORTED_HTTP_METHODS.join(', ')}")

      connection_response = connection.public_send(http_action, request_url, params: query, body: body, headers: headers)
      response            = RemoteResource::Response.new(connection_response, connection_options.merge(request: self, connection_request: connection_response.request))

      if response.success? || response.unprocessable_entity?
        response
      else
        raise_http_error(self, response)
      end
    end

    def request_url
      @request_url ||= begin
        id        = @attributes[:id].presence || connection_options[:id]
        base_url  = connection_options[:base_url].presence || RemoteResource::UrlNamingDetermination.new(resource_klass, connection_options).base_url(id, check_collection_options: true)
        extension = connection_options[:extension] || DEFAULT_EXTENSION

        "#{base_url}#{extension}"
      end
    end

    def query
      @query ||= begin
        params = connection_options[:params]

        if params.present?
          RemoteResource::Util.encode_params_to_query(params)
        else
          nil
        end
      end
    end

    def body
      @body ||= begin
        if [:put, :patch, :post].include?(http_action)
          JSON.generate(attributes)
        else
          nil
        end
      end
    end

    def attributes
      if connection_options[:json_spec] == :json_api
        if @attributes
          { data: { id: @attributes[:id], type: resource_klass.name.demodulize, attributes: @attributes.except(:id) } }
        else
          { data: {} }
        end
      else
        root_element = connection_options[:root_element]

        if root_element.present?
          { root_element => @attributes }
        else
          @attributes || {}
        end
      end
    end

    def headers
      @headers ||= begin
        default_headers = connection_options[:default_headers].presence || DEFAULT_HEADERS
        global_headers  = RemoteResource::Base.global_headers.presence || {}
        headers         = connection_options[:headers].presence || {}

        default_headers.merge(global_headers).merge(headers).merge(conditional_headers)
      end
    end

    def conditional_headers
      headers = {}
      headers = headers.merge(DEFAULT_CONTENT_TYPE) if body.present?
      headers = headers.merge({ 'X-Request-Id' => RequestStore.store[:request_id] }) if RequestStore.store[:request_id].present?
      headers
    end

    private

    def raise_http_error(request, response)
      case response.try(:response_code)
      when 301, 302, 303, 307 then
        raise RemoteResource::HTTPRedirectionError.new(request, response)
      when 400 then
        raise RemoteResource::HTTPBadRequest.new(request, response)
      when 401 then
        raise RemoteResource::HTTPUnauthorized.new(request, response)
      when 403 then
        raise RemoteResource::HTTPForbidden.new(request, response)
      when 404 then
        raise RemoteResource::HTTPNotFound.new(request, response)
      when 405 then
        raise RemoteResource::HTTPMethodNotAllowed.new(request, response)
      when 406 then
        raise RemoteResource::HTTPNotAcceptable.new(request, response)
      when 408 then
        raise RemoteResource::HTTPRequestTimeout.new(request, response)
      when 409 then
        raise RemoteResource::HTTPConflict.new(request, response)
      when 410 then
        raise RemoteResource::HTTPGone.new(request, response)
      when 418 then
        raise RemoteResource::HTTPTeapot.new(request, response)
      when 444 then
        raise RemoteResource::HTTPNoResponse.new(request, response)
      when 494 then
        raise RemoteResource::HTTPRequestHeaderTooLarge.new(request, response)
      when 495 then
        raise RemoteResource::HTTPCertError.new(request, response)
      when 496 then
        raise RemoteResource::HTTPNoCert.new(request, response)
      when 497 then
        raise RemoteResource::HTTPToHTTPS.new(request, response)
      when 499 then
        raise RemoteResource::HTTPClientClosedRequest.new(request, response)
      when 400..499 then
        raise RemoteResource::HTTPClientError.new(request, response)
      when 500..599 then
        raise RemoteResource::HTTPServerError.new(request, response)
      else
        raise RemoteResource::HTTPError.new(request, response)
      end
    end

  end
end