require 'active_support/core_ext/array/wrap'

module Sipity
  # The logical container for all things Exceptional in Sipity! And by
  # Exceptional, I mean custom exceptions that can and will be raised by Sipity.
  #
  # By focusing on custom exceptions, the Application's exception handling can
  # become much more granular.
  module Exceptions
    # Creating a base exception to further extend.
    class RuntimeError < ::RuntimeError
    end

    class InvalidAuthorizationCredentialsError < RuntimeError
    end

    class IngestUnableToCompleteError < RuntimeError
    end

    class ScheduledJobError < RuntimeError
      def initialize(works:)
        @works = works
        message = "The following Work IDs had failures in the batch ingest:\n"
        @works.each do |work|
          message += "\n\t* #{work.id} (Current Status: #{work.processing_status})"
        end
        message += "\n\nThis could mean that the Batch Ingester received the request but in"
        message += "\nthe response something got mangled (e.g., a broken pipe error). You"
        message += "\nshould look at other exceptions for these works to see what"
        message += "\nspecifically happened."

        super(message)
      end
    end

    # When you can't instantiate a specific work converter, raise this exception.
    class FailedToInitializeWorkConverterError < RuntimeError
      attr_reader :work
      def initialize(work:)
        @work = work
        super("Failed to initialize work converter for #{work.inspect}")
      end
    end

    # When a schema doesn't validate
    class InvalidSchemaError < RuntimeError
      attr_reader :errors
      def initialize(errors:)
        @errors = errors
        super("Invalid schema: #{errors.inspect}")
      end
    end

    # When you have misconfigured the email options
    class EmailAsOptionInvalidError < RuntimeError
      def initialize(as:, valid_list:)
        super("Invalid :as option for email notifier, #{as.inspect} is not in #{valid_list.inspect}")
      end
    end

    # When passing parameters through the job layer, passing complex objects
    # can cause problems; In that we don't want to pass state across the async
    # boundary.
    class NonPrimativeParameterError < RuntimeError
    end

    # Responsible for conveying the erroring information when there is an error.
    class ResponseHandlerError < RuntimeError
      attr_reader :object, :status, :errors
      def initialize(object:, errors:, status:)
        @object = object
        @errors = errors
        @status = status
        super(%(Encountered status="#{status}" for object="#{object}"\n\terrors=#{Array.wrap(errors).to_sentence}))
      end
    end

    # Uh oh! It looks like our response handler didn't know what to do.
    class UnhandledResponseError < RuntimeError
      def initialize(handler)
        super("Expected to be able to handle #{handler.inspect}.")
      end
    end

    # This is not a defined resourceful action
    class UnprocessableResourcefulActionNameError < RuntimeError
      def initialize(container:, object:)
        super("Expected #{object} to have a #name that is within #{container}")
      end
    end

    # A paginated request was outside of the pagination window
    class RequestOutsidePaginationRange < RuntimeError
    end

    # The object did not implement the expected interface.
    class InterfaceExpectationError < RuntimeError
      def initialize(object:, expectations:)
        self.expectations = expectations
        super("Expected #{object} to implement #{expected_methods}")
      end

      private

      def expected_methods
        @expectations.map { |e| "##{e}" }.inspect
      end

      def expectations=(input)
        @expectations = Array.wrap(input)
      end
    end

    # The object did not implement the expected interface.
    class InterfaceCollaboratorExpectationError < RuntimeError
      def initialize(object:, collaborator_expectations:)
        self.collaborator_expectations = collaborator_expectations
        super("Expected #{object} to collaborate #{expected_methods}")
      end

      private

      def expected_methods
        @collaborator_expectations.map(&:inspect).inspect
      end

      def collaborator_expectations=(input)
        @collaborator_expectations = Array.wrap(input)
      end
    end

    # Indicates that the returned value from the runner was incorrectly built.
    class InvalidHandledResponseStatus < RuntimeError
      def initialize(input, expected_class: Symbol)
        super("Expected #{input} to be a #{expected_class}; It was a #{input.class}")
      end
    end

    # When you go about building an object that has method missing expectations
    # you may need to raise an exception if you are planning to catch a
    # method_name via message missing, but won't because the method is already
    # defined.
    class ExistingMethodsAlreadyDefined < RuntimeError
      def initialize(context, method_names)
        super("#{context} implemented the following methods: #{method_names.inspect}. #{context} won't work as expected")
      end
    end

    # An abstract conversion error
    class ConversionError < RuntimeError
      class_attribute :conversion_target, instance_writer: false
      self.conversion_target = '<Undefined Target>'

      def initialize(attempted_conversion_object)
        super("Unable to convert #{attempted_conversion_object.inspect} to a #{conversion_target}")
      end
    end

    # Unable to convert the given object into a permanent URI
    class PermanentUriConversionError < ConversionError
      self.conversion_target = 'PermanentURI'
    end

    # Unable to convert the given object into an entity type
    class EntityTypeConversionError < ConversionError
      self.conversion_target = 'EntityType'
    end

    # Unable to convert the given object into a Role
    class RoleConversionError < ConversionError
      self.conversion_target = 'Models::Role'
    end

    # Unable to convert the given object into a Date
    class DateConversionError < ConversionError
      self.conversion_target = 'Date'
    end

    # Unable to convert the given object into a EntityActionRegister
    class RegisteredActionConversionError < ConversionError
      self.conversion_target = 'Models::Processing::EntityActionRegister'
    end

    # Processing Conversion Errors; These may often mean a database entry is
    # missing.
    class ProcessingConversionError < ConversionError
    end

    # Unable to convert the given object into a processing entity
    class ProcessingEntityConversionError < ProcessingConversionError
      self.conversion_target = 'Models::Processing::Entity'
    end

    # Unable to convert the given object into a processing actor
    class ProcessingActorConversionError < ProcessingConversionError
      self.conversion_target = 'Models::Processing::Actor'
    end

    # Unable to convert the given object into a processing strategy id
    class ProcessingStrategyIdConversionError < ProcessingConversionError
      self.conversion_target = 'Models::Processing::Strategy#id'
    end

    # Unable to convert the given object into a processing strategy action
    class ProcessingStrategyActionConversionError < ProcessingConversionError
      self.conversion_target = 'Models::Processing::StrategyAction'
    end

    # Unable to convert the given object into a processing strategy action
    class ProcessingActionNameConversionError < ProcessingConversionError
      self.conversion_target = 'Models::Processing::StrategyAction#name'
    end

    # Unable to convert the given object into a work.
    class WorkConversionError < ProcessingConversionError
      self.conversion_target = 'Models::Work'
    end

    # As you are looking up something by name, within a given container.
    class ConceptNotFoundError < RuntimeError
      def initialize(name:, container:)
        super("Unable to find #{name} within #{container}")
      end
    end

    # When you ask for an enrichment but none can be found
    class EnrichmentNotFoundError < ConceptNotFoundError
    end

    # When you just can't find that job, throw an exception.
    class JobNotFoundError < ConceptNotFoundError
    end

    # A policy was not found. Now panic!
    class PolicyNotFoundError < ConceptNotFoundError
    end

    # A Notification was not found.
    class NotificationNotFoundError < ConceptNotFoundError
    end

    # A WorkType was not found.
    class WorkTypeNotFoundError < ConceptNotFoundError
    end

    # A EventTriggerForm was not found.
    class EventTriggerFormNotFoundError < ConceptNotFoundError
    end

    # Unable to find the correct processing strategy role
    class ValidProcessingStrategyRoleNotFoundError < RuntimeError
    end

    # Exposing a custom AuthenticationFailureError
    class AuthenticationFailureError < RuntimeError
      def initialize(context)
        super("Unable to authenticate #{context}")
      end
    end

    # The authentication layer failed to build. Probably need to explain why.
    class FailedToBuildAuthenticationLayerError < RuntimeError
    end

    # The authorization layer failed to build. Probably need to explain why.
    class FailedToBuildAuthorizationLayerError < RuntimeError
    end

    # Exposing a custom AuthorizationFailureError
    class AuthorizationFailureError < RuntimeError
      attr_reader :user, :action_to_authorize, :entity
      def initialize(user:, action_to_authorize:, entity:)
        @user = user
        @action_to_authorize = action_to_authorize
        @entity = entity
        super("#{user.inspect} not allowed to #{action_to_authorize.inspect} this #{entity.inspect}")
      end
    end

    # For some reason, you have an entity in an incorrect state. Push up what
    # information we can to be helpful to the end user.
    class InvalidStateError < RuntimeError
      attr_reader :entity, :actual, :expected
      def initialize(entity:, actual:, expected: nil)
        @entity = entity
        @actual = actual
        @expected = expected
        super(build_message)
      end

      private

      def build_message
        if expected.present?
          "#{self.class}: Expected #{entity} to have state: #{expected}, but it had state: #{actual}"
        else
          "#{self.class}: #{entity} has in valid state: #{actual}"
        end
      end
    end

    # Exposing a custom EmailDeliverFailure
    class SenderNotFoundError < ArgumentError
      def initialize(context)
        super("Unable to Send message, To address is required to send a message  #{context}")
      end
    end
  end
end
