require 'net/http/status'

module BerkeleyLibrary
  module TIND
    module API
      # Wrapper for network-related exceptions.
      class APIException < StandardError
        # @return [String, nil] the request URI, if any
        attr_reader :url

        # @return [Hash, nil] the API query parameters, if any
        attr_reader :params

        # @return [Integer, nil] the numeric HTTP status code, if any
        attr_reader :status_code

        # @return [String, nil] the HTTP status message, if any
        attr_reader :status_message

        # @return [RestClient::Response, nil] the response, if any
        attr_reader :response

        # Initializes a new APIException.
        #
        # @option opts [String] :msg the exception message (if not present, a default message will be constructed)
        # @option opts [String] :url the request URL, if any
        # @option opts [Hash] :params the query or form parameters, if any
        # @option opts [Integer] :status_code the numeric HTTP status code, if any
        # @option opts [String] :status_message a human-readable string representation of the HTTP status
        #   (if not present, a default will be constructed)
        # @option opts [RestClient::Response] :response the HTTP response, if any
        def initialize(msg, **opts)
          super(msg)

          @url = opts[:url].to_s if opts.key?(:url)
          @params = opts[:params]
          @status_code, default_status_message = format_status(opts[:status_code])
          @status_message = opts[:status_message] || default_status_message
          @response = opts[:response]
        end

        def body
          return @body if instance_variable_defined?(:@body)

          @body = response && response.body
        end

        private

        def format_status(status_code)
          return unless (numeric_status = Integer(status_code, exception: false))

          status_name = Net::HTTP::STATUS_CODES[numeric_status]
          default_status_message = [numeric_status, status_name].compact.join(' ')

          [numeric_status, default_status_message]
        end

        class << self
          # @param ex [Exception] the exception to wrap
          # @option opts [String] :msg the exception message (if not present, a default message will be constructed)
          # @option opts [String] :url the request URL, if any
          # @option opts [Hash] :params the query or form parameters, if any
          # @option opts [String] :msg_context context information to prepend to the default message
          def wrap(ex, **opts)
            raise ArgumentError, "Can't wrap a nil error" unless ex

            msg = opts[:msg] || message_from(ex, opts[:url], opts[:params], opts[:detail])
            options = format_options(ex, opts[:url], opts[:params])
            APIException.new(msg, **options)
          end

          private

          def format_options(ex, url, params)
            {}.tap do |opts|
              opts[:url] = url if url
              opts[:params] = params if params
              next unless %i[http_code message response].all? { |f| ex.respond_to?(f) }

              opts[:status_code] = ex.http_code
              opts[:status_message] = ex.message
              opts[:response] = ex.response
            end
          end

          def message_from(ex, url, params, detail)
            ''.tap do |msg|
              msg << "#{detail}: " if detail
              msg << (url ? "#{API.format_request(url, params)} returned #{ex}" : ex.to_s)
            end
          end
        end
      end

      # Exception raised when the API key is nil or blank.
      #
      # NOTE: TIND incorrectly returns 403 Forbidden in this case, but we don't even bother
      # to ask, we just simulate a 401.
      class APIKeyNotSet < APIException
        # @param endpoint_uri [URI] the endpoint URI
        # @param params [Hash, nil] the query parameters
        def initialize(endpoint_uri, params)
          request_str = API.format_request(endpoint_uri, params)
          super("#{request_str} failed; API key not set", status_code: 401)
        end
      end

      # Exception raised when the TIND base URI is nil or blank.
      class BaseURINotSet < APIException
        # @param endpoint [String, Symbol] the endpoint
        # @param params [Hash, nil] the query parameters
        def initialize(endpoint, params)
          msg = BaseURINotSet.format_message(endpoint, params)
          super(msg, status_code: 404)
        end

        class << self
          def format_message(endpoint, params)
            "request to endpoint #{endpoint.inspect}".tap do |msg|
              if (query_string = API.format_query(params))
                msg << " with query #{query_string}"
              end
              msg << ' failed; base URI not set'
            end
          end
        end
      end
    end
  end
end
