module Oktakit
  # Custom error class for rescuing from all Okta errors
  class Error < StandardError
    # Returns the appropriate Oktakit::Error subclass based
    # on status and response message
    #
    # @param [Hash] response HTTP response
    # @return [Oktakit::Error]
    def self.from_response(response)
      status = response[:status].to_i
      if klass = error(status)
        klass.new(response)
      end
    end

    def self.error(status)
      case status
      when 400      then Oktakit::BadRequest
      when 401      then Oktakit::Unauthorized
      when 403      then Oktakit::Forbidden
      when 404      then Oktakit::NotFound
      when 405      then Oktakit::MethodNotAllowed
      when 406      then Oktakit::NotAcceptable
      when 409      then Oktakit::Conflict
      when 415      then Oktakit::UnsupportedMediaType
      when 422      then Oktakit::UnprocessableEntity
      when 400..499 then Oktakit::ClientError
      when 500      then Oktakit::InternalServerError
      when 501      then Oktakit::NotImplemented
      when 502      then Oktakit::BadGateway
      when 503      then Oktakit::ServiceUnavailable
      when 500..599 then Oktakit::ServerError
      end
    end

    def initialize(response = nil)
      @response = response
      super(build_error_message)
    end

    # Array of validation errors
    # @return [Array<Hash>] Error info
    def errors
      if data && data.is_a?(Hash)
        data[:errors] || []
      else
        []
      end
    end

    private

    def data
      @data ||= parse_data
    end

    def parse_data
      body = @response[:body]
      return if body.empty?
      return body unless body.is_a?(String)

      headers = @response[:response_headers]
      content_type = headers && headers[:content_type] || ''
      if content_type =~ /json/
        Sawyer::Agent.serializer.decode(body)
      else
        body
      end
    end

    def response_message
      case data
      when Hash
        data[:errorSummary]
      when String
        data
      end
    end

    def build_error_message
      return nil if @response.nil?

      message =  "#{@response[:method].to_s.upcase} "
      message << redact_url(@response[:url].to_s) + ': '
      message << "#{@response[:status]} - "
      message << response_message.to_s unless response_message.nil?
      message
    end

    def redact_url(url_string)
      %w[client_secret access_token].each do |token|
        url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
      end
      url_string
    end
  end

  # Raised on errors in the 400-499 range
  class ClientError < Error; end

  # Raised when Okta returns a 400 HTTP status code
  class BadRequest < ClientError; end

  # Raised when Okta returns a 401 HTTP status code
  class Unauthorized < ClientError; end

  # Raised when Okta returns a 403 HTTP status code
  class Forbidden < ClientError; end

  # Raised when Okta returns a 404 HTTP status code
  class NotFound < ClientError; end

  # Raised when Okta returns a 405 HTTP status code
  class MethodNotAllowed < ClientError; end

  # Raised when Okta returns a 406 HTTP status code
  class NotAcceptable < ClientError; end

  # Raised when Okta returns a 409 HTTP status code
  class Conflict < ClientError; end

  # Raised when Okta returns a 414 HTTP status code
  class UnsupportedMediaType < ClientError; end

  # Raised when Okta returns a 422 HTTP status code
  class UnprocessableEntity < ClientError; end

  # Raised on errors in the 500-599 range
  class ServerError < Error; end

  # Raised when Okta returns a 500 HTTP status code
  class InternalServerError < ServerError; end

  # Raised when Okta returns a 501 HTTP status code
  class NotImplemented < ServerError; end

  # Raised when Okta returns a 502 HTTP status code
  class BadGateway < ServerError; end

  # Raised when Okta returns a 503 HTTP status code
  class ServiceUnavailable < ServerError; end

  # Raised when client fails to provide valid Content-Type
  class MissingContentType < ArgumentError; end
end
