module Xeroizer
  class BadResponse < XeroizerError; end

  class XmlErrorResponse
    def initialize(response, request_body, url)
      @response = response
      @request_body = request_body
      @url = url
    end

    def raise_error!
      case response.code.to_i
      when 400
        raise_bad_request!
      when 401
        raise_error
      when 403
        raise_error
      when 404
        raise_not_found!
      when 429
        raise_rate_limit_exceeded!
      when 503
        raise_error
      else
        raise_unknown_response_error!
      end
    end

    def raise_error
      description, problem = parse

      # see http://oauth.pbworks.com/ProblemReporting
      # In addition to token_expired and token_rejected, Xero also returns
      # 'rate limit exceeded' when more than 60 requests have been made in
      # a second.
      if problem
        case problem
        when "token_expired"                then raise OAuth::TokenExpired.new(description)
        when "token_rejected"               then raise OAuth::TokenInvalid.new(description)
        when "rate limit exceeded"          then raise OAuth::RateLimitExceeded.new(description)
        when "consumer_key_unknown"         then raise OAuth::ConsumerKeyUnknown.new(description)
        when "nonce_used"                   then raise OAuth::NonceUsed.new(description)
        when "organisation offline"         then raise OAuth::OrganisationOffline.new(description)
        else raise OAuth::UnknownError.new(problem + ':' + description)
        end
      else
        raise OAuth::UnknownError.new("Xero API may be down or the way OAuth errors are provided by Xero may have changed.")
      end
    end

    private

    attr_reader :request_body, :response, :url

    def parse
      error_details = CGI.parse(response.plain_body)
      description   = error_details["oauth_problem_advice"].first
      problem = error_details["oauth_problem"].first
      [description, problem]
    end

    def raise_bad_request!

      raw_response = response.plain_body

      # XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
      # So let's ignore that :)
      raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''

      # doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
      doc = Nokogiri::XML(raw_response)

      if doc && doc.root && (doc.root.name == "ApiException" || doc.root.name == 'Response')

        raise ApiException.new(doc.root.xpath("Type").text,
          doc.root.xpath("Message").text,
          raw_response,
          doc,
          request_body)

      else
        raise Xeroizer::BadResponse.new("Unparseable 400 Response: #{raw_response}")
      end
    end

    def raise_not_found!
      case url
      when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
      when /CreditNotes/ then raise CreditNoteNotFoundError.new("Credit Note not found in Xero.")
      else raise ObjectNotFound.new(url)
      end
    end

    def raise_rate_limit_exceeded!
      retry_after = response.response.headers["retry-after"].to_i
      daily_limit_remaining = response.response.headers["x-daylimit-remaining"].to_i

      description = "Rate limit exceeded: #{daily_limit_remaining} requests left for the day, #{retry_after} seconds until you can make another request"
      raise OAuth::RateLimitExceeded.new(description, retry_after: retry_after, daily_limit_remaining: daily_limit_remaining)
    end

    def raise_unknown_response_error!
      raise Xeroizer::BadResponse.new("Unknown response code: #{response.code.to_i}")
    end
  end

  class HttpResponse
    def self.from_response(response, request_body, url)
      new(response, request_body, url)
    end

    def initialize(response, request_body, url)
      @response = response
      @request_body = request_body
      @url = url
    end

    def body
      response_code = response.code.to_i
      return nil if response_code == 204
      raise_error! unless response.code.to_i == 200
      response.plain_body
    end

    private

    def raise_error!
      begin
        error_details = JSON.parse(response.plain_body)
        description  = error_details["Detail"]
        case response.code.to_i
        when 400
          raise Xeroizer::BadResponse.new(description)
        when 401
          if description.include?("TokenExpired")
            raise OAuth::TokenExpired.new(description)
          else
            raise OAuth::TokenInvalid.new(description)
          end
        when 403
          message = "Possible xero-tenant-id header issue. Xero Error: #{description}"
          raise OAuth::Forbidden.new(message)
        when 404
          raise Xeroizer::ObjectNotFound.new(url)
        else
          raise Xeroizer::OAuth::UnknownError.new(description)
        end
      rescue JSON::ParserError
        XmlErrorResponse.new(response, request_body, url).raise_error!
      end
    end

    attr_reader :request_body, :response, :url
  end
end