module Krikri
  ##
  # SoftwareAgent is a mixin for logic common to classes that carry out the 
  # work involved in a `Krikri::Activity`. This corresponds to a 
  # prov:SoftwareAgent within PROV-O & PROV-DM.
  #
  # Software Agents should handle internal errors that do not result in full
  # activity failure, and raise a `RuntimeError` when the job fails. `Activity`
  # handles logging of activity start/stop, and failure status.
  #
  # Implementers must provide a `#run` method.
  #
  # @see Krikri::Activity
  # @see https://www.w3.org/TR/prov-dm/#concept-software-agent
  module SoftwareAgent
    extend ActiveSupport::Concern

    ##
    # @!attribute [rw] entity_behavior
    #   @return [Krikri::EntityBehavior] the entity initialization behavior to  use with this 
    #     SoftwareAgent.
    #   @see Krikri::Activity#entities
    included { attr_accessor :entity_behavior }

    ##
    # Return an agent name suitable for saving in an Activity.
    # This is the name of the most-derived class upon which this is invoked.
    #
    # @return [String]
    # @see Krikri::Activity
    def agent_name
      self.class.agent_name
    end

    ##
    # @abstract Perform this agent's work. The method may accept an
    #   `activity_uri` to record as the prov:Activity in provenance metadata.
    #   If so, the implementation must be optional and handle `nil` values by
    #   declining to record provenance
    #
    # @return [Boolean] `true` if the run has succeeded; otherwise `false`
    #
    # @raise [RuntimeError] when the software agent's process fails
    #
    # @see Krirkri::Activity
    # @see Krikri::Job.run
    def run
      fail NotImplementedError
    end

    ##
    # Class methods for extension by ActiveSupport::Concern
    module ClassMethods
      ##
      # @return a string representation of this SoftwareAgent class
      def agent_name
        to_s
      end

      ##
      # @return [Krikri::EntityBehavior] the default entity initializiation 
      #   behavior for this class of SoftwareAgents
      # @see #entity_behavior
      def entity_behavior
        Krikri::AggregationEntityBehavior
      end

      ##
      # @return the name of the default queue for jobs invoking this
      #   SoftwareAgent
      def queue_name
        agent_name.downcase
      end

      ##
      # Enqueue a job.
      #
      # @example
      #   MyAgent.enqueue(:name => my_job)
      #
      # @example
      #   Krikri::Harvesters::OAIHarvester.enqueue(
      #     :harvest,
      #     opts = {
      #       uri: 'http://vcoai.lib.harvard.edu/vcoai/vc',
      #       oai: { set: 'dag', metadata_prefix: 'mods' }
      #     }
      #   )
      #
      # A worker process must be started to process jobs in the "harvest"
      # queue, either before or after they are enqueued:
      #
      #  shell$ QUEUE=harvest bundle exec rake environment resque:work
      #
      # This depends on Redis and Marmotta being available and properly
      # configured (if necessary) in the Rails app.
      #
      # @param queue_name [#to_s] the Resque queue name
      # @param opts [Hash] a hash of options that will be used to initialize
      #   the agent (an instance of this class).
      #
      # @return [Boolean]
      #
      # @see https://github.com/resque/resque/tree/1-x-stable
      # @see Krikri::Job
      # @see Krikri::SoftwareAgent#agent_name
      # @see Krikri::Harvester::expected_opts
      def enqueue(*args)
        queue = args.shift unless args.first.is_a? Hash
        queue ||= queue_name
        opts = args.shift || {}
        fail ArgumentError, "unexpected arguments #{args}" unless args.empty?
        fail ArgumentError, 'opts is not a hash' unless opts.is_a?(Hash)

        activity = Krikri::Activity.create do |a|
          a.agent = agent_name
          a.opts = JSON.generate(opts)
        end

        Krikri::Logger.log :info, "created activity #{activity.id}"
        Resque.enqueue_to(queue, Krikri::Job, activity.id)
        Krikri::Logger.log :info, "enqueued to #{queue}"
        true
      end
    end
  end
end