# encoding: utf-8

module Hexx

  # @abstract
  # The base class for service objects.
  #
  # @example
  #   require "hexx"
  #   class GetItem < Hexx::Service
  #     allow_params :name
  #     def run
  #       publish :found, item = Item.where(name: name).first
  #     end
  #   end
  #
  #   service = GetItem.new name: name
  #   service.subscribe listener, prefix: :on
  #   service.run
  #   # => This will call the listener's method #on_found(item).
  class Service
    include Wisper::Publisher
    include ActiveModel::Validations
    include Parameters

    # @!scope class
    # @!method new(options = {})
    # Constructs the service object.
    #
    # @example
    #   service = Hexx::Service.new name: name
    #
    # @param [Hash] options The options to be assigned to the {#params}.
    # @return [Hexx::Service] The service object.
    def initialize(options = {})
      super(options)
      @messages = []
    end

    # @!scope class
    # @!visibility private
    # @!method allow_params(*params)
    # Whitelists {#params} and declares a parameter for corresponding keys.
    #
    # @example (see Hexx::Service#params)
    #
    # @example Defines corresponding readonly attributes.
    #   class GetItem < Hexx::Service
    #     allow_params :name
    #   end
    #
    #   service = GetItem.new name: "Олег", family: "Рюрикович"
    #   service.name # => "Олег"
    #
    # @param [Array<Symbol, String>] params The list of allowed keys.

    # @!scope class
    # @!method validates(attribute, options)
    # Adds a standard validation for the attribute.
    # @note The method is defined in the {ActiveModel::Validations} module.

    # @!scope class
    # @!method validate(method, options)
    # Adds a custom validation (calls given method).
    # @note The method is defined in the {ActiveModel::Validations} module.

    # @!attribute params [r] The list of service object parameters.
    #   The attribute is assigned via the {.new} method options.
    #   The keys should be explicitly declared by the {.allow_params} helper.
    #
    #   @example Only whitelisted params are being assigned.
    #     class GetItem < Hexx::Service
    #       allow_params :name
    #     end
    #
    #     service = GetItem.new name: "Олег", family: "Рюрикович"
    #     service.params # => { "name" => "Олег" }

    # @!method subscribe(listener, options = {})
    # Subscribes the listener to service object's notifications.
    # The <tt>:prefix</tt> sets the prefix to be added to a notification name
    # to provide a corresponding listener method, that should be called by
    # the publisher.
    #
    # @example (see Hexx::Service)
    # @param [Object] listener The object that should receive notifications from
    #   the service object.
    # @param [Hash] options The list of the subscription options.
    # @option options [Symbol] :prefix The prefix for the listener's callbacks.

    # @abstract
    # Runs the service object.
    # @note The method must be reloaded by a specific service class,
    #   inherited from <tt>Hexx::Service</tt>.
    # @raise [NotImplementedError] if a child class hasn't redefined the method.
    def run
      fail NotImplementedError.new "#{ self.class.name }#run not implemented."
    end

    # Makes private methods with given prefix public.
    #
    # @example Opens private methods.
    #   def GetItem < Hexx::Service
    #     private
    #     def on_success
    #       publish :success
    #     end
    #   end
    #
    #   service = GetItem.new
    #   service.respond_to? :on_success
    #   # => false
    #
    #   service_with_callbacks = service.with_callbacks
    #   service_with_callbacks.respond_to? :on_success
    #   # => true
    #
    # @return [Hexx::Service::WithCallbacks<Hexx::Service>]
    #   The decorator that allows access to the service's private methods.
    def with_callbacks(prefix: nil)
      WithCallbacks.new(self, prefix: prefix)
    end

    # @!attribute [r] messages
    # The array of service messages (instances of {Hexx::Service::Message})
    # with +text+ and +type+ attributes.
    #
    # @example
    #   class Test < Hexx::Service
    #     def run
    #       add_message "info", :ok
    #     end
    #   end
    #
    #   service = Test.new
    #   service.run # adds message
    #   service.messages
    #   # => [#<Hexx::Service::Message @text="ok" @type="info" >]
    #
    # @return [Array<Hexx::Service::Message>] The array of messages.
    attr_reader :messages

    private

    # The helper runs another service object and subscribes +self+ for the
    # service object's notifications.
    #
    # @example
    #   class AddItem < Hexx::Service
    #     allow_params :id
    #
    #     # Runs a service for finding an item.
    #     # Service notifications to be received with a prefix :on_item
    #     def find_item
    #       run_service GetItem, :on_item, id: params["id"]
    #     end
    #
    #     private
    #
    #     attr_reader :item
    #
    #     # Receives GetItem's :found notification
    #     def on_item_found(item)
    #       @item = item
    #       publish :found, item
    #     end
    #
    #     # Receives GetItem's :not_found notification
    #     def on_item_not_found(*)
    #       # ... do some stuff here
    #     end
    #   end
    #
    # @param [Class] service_class The service class to instantiate and run
    #   a service object.
    # @param [Symbol] prefix The prefix for callbacks to receive the service
    #   object's notifications.
    # @param [Hash] options ({}) The options for the service object initializer.
    # @raise [TypeError] when the service_class is not a <tt>Hexx::Service</tt>.
    def run_service(service_class, prefix, options = {})
      fail TypeError unless service_class.ancestors.include? Hexx::Service
      service = service_class.new(options)
      service.subscribe with_callbacks, prefix: prefix
      service.run
    end

    # @!method escape
    #
    # The method:
    # * rescues +StandardError+ exceptions
    # * adds error message to the service
    # * re-raises the <tt>Service::Invalid</tt> exception
    #
    # @example
    #   class GetItem < Hexx::Service
    #     def run
    #       escape { do_something_unsafe }
    #     rescue => err
    #       publish :error, err.messages
    #     end
    #       publish :success
    #     end
    #   end
    #
    # @yield the block.
    # @raise [Hexx::Service::Invalid] if the block raised the +StandardError+.
    # @return the value returned by the block.
    def escape
      yield
    rescue Invalid => err
      raise err
    rescue => err
      errors.add :base, err.message
      raise Invalid.new(self)
    end

    # Translates given key in current service's scope.
    #
    # @note The method uses I18n.t library method.
    #
    # @example Returns a translation if the first argument is a symbol.
    #   class Test < Hexx::Service
    #   end
    #   service = Test.new
    #   service.t :name
    #   # => "translation not found: en.activemodel.messages.models.test.name"
    #
    # @example Returns the string argument.
    #   service = Hexx::Service.new
    #   service.t "name"
    #   # => "name"
    #
    # @param [Symbol, String] text The text to be translated.
    # @param [Hash] options The translation options.
    # @return [String] The translation.
    def t(text, options = {})
      return text unless text.is_a? Symbol
      scope = %w(activemodel messages models) << self.class.name.underscore
      I18n.t text, options.merge(scope: scope)
    end

    attr_writer :messages

    # Adds the translated message to the {#messages} array.
    # @example (see Service#messages)
    # @param [String] type The type of the message ("error", "info", "success")
    # @param [String, Symbol] text The text of the message. The symbol will
    #   be translated using the {#t} method.
    # @param [Hash] options The translation options.
    # @return The updated {#messages} array.
    def add_message(type, text, options = {})
      messages << Message.new(type: type, text: t(text, options))
    end

    # Runs validations and fails if the service is invalid.
    #
    # @example (see Hexx::Service::Invalid)
    #
    # @example Safe usage (recommended) with the {#escape} wrapper.
    #   service GetItem < Hexx::Service
    #     allow_params :uuid
    #     validates :uuid, presence: true
    #     def run
    #       escape { validate! }
    #     end
    #   end
    #
    #   service = GetItem.new
    #   service.run # => publishes :error notification
    #
    # @raise [Hexx::Service::Invalid] when the service object isn't valid.
    def validate!
      fail Invalid.new(self) unless valid?
    end

    def on_error(messages)
      messages.map(&:text).each { |text| errors.add :base, text }
      fail Invalid.new(self)
    end
  end
end