# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


require "English"

module Google
  module Cloud
    ##
    # Base google-cloud exception class.
    class Error < StandardError
      ##
      # Construct a new Google::Cloud::Error object, optionally passing in a
      # message.
      #
      # @param msg [String, nil] an error message
      def initialize msg = nil
        super
      end

      ##
      # Returns the value of `status_code` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over HTTP/REST.
      #
      # @return [Object, nil]
      def status_code
        return nil unless cause.respond_to? :status_code
        cause.status_code
      end

      ##
      # Returns the value of `body` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over HTTP/REST.
      #
      # @return [Object, nil]
      def body
        return nil unless cause.respond_to? :body
        cause.body
      end

      ##
      # Returns the value of `header` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over HTTP/REST.
      #
      # @return [Object, nil]
      def header
        return nil unless cause.respond_to? :header
        cause.header
      end

      ##
      # Returns the value of `code` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @return [Object, nil]
      def code
        return nil unless cause.respond_to? :code
        cause.code
      end

      ##
      # Returns the value of `details` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @return [Object, nil]
      def details
        return nil unless cause.respond_to? :details
        cause.details
      end

      ##
      # Returns the value of `metadata` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @return [Object, nil]
      def metadata
        return nil unless cause.respond_to? :metadata
        cause.metadata
      end

      ##
      # Returns the value of `status_details` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @return [Object, nil]
      def status_details
        return nil unless cause.respond_to? :status_details
        cause.status_details
      end

      ##
      # Returns the `::Google::Rpc::ErrorInfo` object present in the `status_details`
      # or `details` array, given that the following is true:
      #   * either `status_details` or `details` exists and is an array
      #   * there is exactly one `::Google::Rpc::ErrorInfo` object in that array.
      # Looks in `status_details` first, then in `details`.
      #
      # @return [::Google::Rpc::ErrorInfo, nil]
      def error_info
        @error_info ||= begin
          check_property = lambda do |prop|
            if prop.is_a? Array
              error_infos = prop.find_all { |status| status.is_a? ::Google::Rpc::ErrorInfo }
              if error_infos.length == 1
                error_infos[0]
              end
            end
          end

          check_property.call(status_details) || check_property.call(details)
        end
      end

      ##
      # Returns the value of `domain` from the `::Google::Rpc::ErrorInfo`
      # object, if it exists in the `status_details` array.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @return [Object, nil]
      def domain
        return nil unless error_info.respond_to? :domain
        error_info.domain
      end

      ##
      # Returns the value of `reason` from the `::Google::Rpc::ErrorInfo`
      # object, if it exists in the `status_details` array.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @return [Object, nil]
      def reason
        return nil unless error_info.respond_to? :reason
        error_info.reason
      end

      ##
      # Returns the value of `metadata` from the `::Google::Rpc::ErrorInfo`
      # object, if it exists in the `status_details` array.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @return [Hash, nil]
      def error_metadata
        return nil unless error_info.respond_to? :metadata
        error_info.metadata.to_h
      end

      # @private Create a new error object from a client error
      def self.from_error error
        klass = if error.respond_to? :code
                  grpc_error_class_for error.code
                elsif error.respond_to? :status_code
                  gapi_error_class_for error.status_code
                else
                  self
                end
        klass.new error.message
      end

      # @private Identify the subclass for a gRPC error
      def self.grpc_error_class_for grpc_error_code
        # The gRPC status code 0 is for a successful response.
        # So there is no error subclass for a 0 status code, use current class.
        [
          self, CanceledError, UnknownError, InvalidArgumentError,
          DeadlineExceededError, NotFoundError, AlreadyExistsError,
          PermissionDeniedError, ResourceExhaustedError,
          FailedPreconditionError, AbortedError, OutOfRangeError,
          UnimplementedError, InternalError, UnavailableError, DataLossError,
          UnauthenticatedError
        ][grpc_error_code] || self
      end

      # @private Identify the subclass for a Google API Client error
      def self.gapi_error_class_for http_status_code
        # The http status codes mapped to their error classes.
        {
          400 => InvalidArgumentError, # FailedPreconditionError/OutOfRangeError
          401 => UnauthenticatedError,
          403 => PermissionDeniedError,
          404 => NotFoundError,
          409 => AlreadyExistsError, # AbortedError
          412 => FailedPreconditionError,
          429 => ResourceExhaustedError,
          499 => CanceledError,
          500 => InternalError, # UnknownError/DataLossError
          501 => UnimplementedError,
          503 => UnavailableError,
          504 => DeadlineExceededError
        }[http_status_code] || self
      end
    end

    ##
    # Canceled indicates the operation was cancelled (typically by the caller).
    class CanceledError < Error
      ##
      # gRPC error code for CANCELLED
      #
      # @return [Integer]
      def code
        1
      end
    end

    ##
    # Unknown error.  An example of where this error may be returned is
    # if a Status value received from another address space belongs to
    # an error-space that is not known in this address space.  Also
    # errors raised by APIs that do not return enough error information
    # may be converted to this error.
    class UnknownError < Error
      ##
      # gRPC error code for UNKNOWN
      #
      # @return [Integer]
      def code
        2
      end
    end

    ##
    # InvalidArgument indicates client specified an invalid argument.
    # Note that this differs from FailedPrecondition. It indicates arguments
    # that are problematic regardless of the state of the system
    # (e.g., a malformed file name).
    class InvalidArgumentError < Error
      ##
      # gRPC error code for INVALID_ARGUMENT
      #
      # @return [Integer]
      def code
        3
      end
    end

    ##
    # DeadlineExceeded means operation expired before completion.
    # For operations that change the state of the system, this error may be
    # returned even if the operation has completed successfully. For
    # example, a successful response from a server could have been delayed
    # long enough for the deadline to expire.
    class DeadlineExceededError < Error
      ##
      # gRPC error code for DEADLINE_EXCEEDED
      #
      # @return [Integer]
      def code
        4
      end
    end

    ##
    # NotFound means some requested entity (e.g., file or directory) was
    # not found.
    class NotFoundError < Error
      ##
      # gRPC error code for NOT_FOUND
      #
      # @return [Integer]
      def code
        5
      end
    end

    ##
    # AlreadyExists means an attempt to create an entity failed because one
    # already exists.
    class AlreadyExistsError < Error
      ##
      # gRPC error code for ALREADY_EXISTS
      #
      # @return [Integer]
      def code
        6
      end
    end

    ##
    # PermissionDenied indicates the caller does not have permission to
    # execute the specified operation. It must not be used for rejections
    # caused by exhausting some resource (use ResourceExhausted
    # instead for those errors).  It must not be
    # used if the caller cannot be identified (use Unauthenticated
    # instead for those errors).
    class PermissionDeniedError < Error
      ##
      # gRPC error code for PERMISSION_DENIED
      #
      # @return [Integer]
      def code
        7
      end
    end

    ##
    # ResourceExhausted indicates some resource has been exhausted, perhaps
    # a per-user quota, or perhaps the entire file system is out of space.
    class ResourceExhaustedError < Error
      ##
      # gRPC error code for RESOURCE_EXHAUSTED
      #
      # @return [Integer]
      def code
        8
      end
    end

    ##
    # FailedPrecondition indicates operation was rejected because the
    # system is not in a state required for the operation's execution.
    # For example, directory to be deleted may be non-empty, an rmdir
    # operation is applied to a non-directory, etc.
    #
    # A litmus test that may help a service implementor in deciding
    # between FailedPrecondition, Aborted, and Unavailable:
    #  (a) Use Unavailable if the client can retry just the failing call.
    #  (b) Use Aborted if the client should retry at a higher-level
    #      (e.g., restarting a read-modify-write sequence).
    #  (c) Use FailedPrecondition if the client should not retry until
    #      the system state has been explicitly fixed.  E.g., if an "rmdir"
    #      fails because the directory is non-empty, FailedPrecondition
    #      should be returned since the client should not retry unless
    #      they have first fixed up the directory by deleting files from it.
    #  (d) Use FailedPrecondition if the client performs conditional
    #      REST Get/Update/Delete on a resource and the resource on the
    #      server does not match the condition. E.g., conflicting
    #      read-modify-write on the same resource.
    class FailedPreconditionError < Error
      ##
      # gRPC error code for FAILED_PRECONDITION
      #
      # @return [Integer]
      def code
        9
      end
    end

    ##
    # Aborted indicates the operation was aborted, typically due to a
    # concurrency issue like sequencer check failures, transaction aborts,
    # etc.
    #
    # See litmus test above for deciding between FailedPrecondition,
    # Aborted, and Unavailable.
    class AbortedError < Error
      ##
      # gRPC error code for ABORTED
      #
      # @return [Integer]
      def code
        10
      end
    end

    ##
    # OutOfRange means operation was attempted past the valid range.
    # E.g., seeking or reading past end of file.
    #
    # Unlike InvalidArgument, this error indicates a problem that may
    # be fixed if the system state changes. For example, a 32-bit file
    # system will generate InvalidArgument if asked to read at an
    # offset that is not in the range [0,2^32-1], but it will generate
    # OutOfRange if asked to read from an offset past the current
    # file size.
    #
    # There is a fair bit of overlap between FailedPrecondition and
    # OutOfRange.  We recommend using OutOfRange (the more specific
    # error) when it applies so that callers who are iterating through
    # a space can easily look for an OutOfRange error to detect when
    # they are done.
    class OutOfRangeError < Error
      ##
      # gRPC error code for OUT_OF_RANGE
      #
      # @return [Integer]
      def code
        11
      end
    end

    ##
    # Unimplemented indicates operation is not implemented or not
    # supported/enabled in this service.
    class UnimplementedError < Error
      ##
      # gRPC error code for UNIMPLEMENTED
      #
      # @return [Integer]
      def code
        12
      end
    end

    ##
    # Internal errors.  Means some invariants expected by underlying
    # system has been broken.  If you see one of these errors,
    # something is very broken.
    class InternalError < Error
      ##
      # gRPC error code for INTERNAL
      #
      # @return [Integer]
      def code
        13
      end
    end

    ##
    # Unavailable indicates the service is currently unavailable.
    # This is a most likely a transient condition and may be corrected
    # by retrying with a backoff.
    #
    # See litmus test above for deciding between FailedPrecondition,
    # Aborted, and Unavailable.
    class UnavailableError < Error
      ##
      # gRPC error code for UNAVAILABLE
      #
      # @return [Integer]
      def code
        14
      end
    end

    ##
    # DataLoss indicates unrecoverable data loss or corruption.
    class DataLossError < Error
      ##
      # gRPC error code for DATA_LOSS
      #
      # @return [Integer]
      def code
        15
      end
    end

    ##
    # Unauthenticated indicates the request does not have valid
    # authentication credentials for the operation.
    class UnauthenticatedError < Error
      ##
      # gRPC error code for UNAUTHENTICATED
      #
      # @return [Integer]
      def code
        16
      end
    end
  end
end
