module Krikri
  ##
  # Activity wraps code execution with metadata about when it ran, which
  # agents were responsible.  It is designed to run a variety of different
  # jobs, and its #run method is passed a block that performs the actual work.
  # It records the start and end time of the job run, and provides the name of
  # the agent to whomever needs it, but it does not care what kind of activity
  # it is -- harvest, enrichment, etc.
  #
  class Activity < ActiveRecord::Base
    # @!attribute agent
    #    @return [String] a string representing the Krikri::SoftwareAgent
    #                     responsible for the activity.
    # @!attribute end_time
    #    @return [DateTime] a datestamp marking the activity's competion
    # @!attribute opts
    #    @return [JSON] the options to pass to the #agent class when running
    #                   the activity
    # @!attribute start_time
    #    @return [DateTime] a datestamp marking the activity's start
    validate :agent_must_be_a_software_agent

    def agent_must_be_a_software_agent
      errors.add(:agent, 'does not represent a SoftwareAgent') unless
        agent.constantize < Krikri::SoftwareAgent
    end

    def set_start_time
      update_attribute(:start_time, DateTime.now.utc)
    end

    def set_end_time
      now = DateTime.now.utc
      fail 'Start time must exist and be before now to set an end time' unless
        self[:start_time] && (self[:start_time] <= now)
      update_attribute(:end_time, now)
    end

    ##
    # Runs the block, setting the start and end time of the run. The given block
    # is passed an instance of the agent, and a URI representing this Activity.
    # 
    # Handles logging of activity start/stop and failure states.
    #
    # @raise [RuntimeError] re-raises logged errors on Activity failure
    def run
      if block_given?
        update_attribute(:end_time, nil) if ended?
        Krikri::Logger
          .log(:info, "Activity #{agent.constantize}-#{id} is running")
        set_start_time
        begin
          yield agent_instance, rdf_subject
        rescue => e
          Krikri::Logger.log(:error, "Error performing Activity: #{id}\n" \
                                     "#{e.message}\n#{e.backtrace}")
          raise e
        ensure
          set_end_time
          Krikri::Logger
            .log(:info, "Activity #{agent.constantize}-#{id} is done")
        end
      end
    end

    ##
    # Indicates whether the activity has ended. Does not distinguish between
    # successful and failed completion states.
    #
    # @return [Boolean] `true` if the activity has been marked as ended,
    #   else `false`
    def ended?
      !self.end_time.nil?
    end

    ##
    # Instantiates and returns an instance of the Agent class with the values in
    # opts.
    #
    # @return [Agent] an instance of the class stored in Agent
    def agent_instance
      @agent_instance ||= agent.constantize.new(parsed_opts)
    end

    def parsed_opts
      JSON.parse(opts, symbolize_names: true)
    end

    ##
    # @return [RDF::URI] the uri for this activity
    def rdf_subject
      RDF::URI(Krikri::Settings['marmotta']['ldp']) /
        Krikri::Settings['prov']['activity'] / id.to_s
    end
    alias_method :to_term, :rdf_subject
    

    ##
    # @return [String] a string reprerestation of the activity
    def to_s
      inspect.to_s
    end

    ##
    # Return an Enumerator of URI strings of entities (e.g. aggregations or
    # original records) that pertain to this activity
    #
    # @param  include_invalidated [Boolean] Whether to include entities that
    #   have been invalidated with prov:invalidatedAtTime. Default: false
    #
    # @return [Enumerator] URI strings
    #
    # @see Krikri::ProvenanceQueryClient#find_by_activity regarding
    #   invalidation.
    #
    def entity_uris(include_invalidated = false)
      activity_uri = RDF::URI(rdf_subject)  # This activity's LDP URI
      query = Krikri::ProvenanceQueryClient
        .find_by_activity(activity_uri, include_invalidated)
      query.each_solution.lazy.map do |s|
        s.record.to_s
      end
    end

    ##
    # Return an Enumerator of entities (e.g. aggregations or original records)
    # that have been affected by this activity.
    #
    # The kind of object that is returned depends on the EntityBehavior class
    # that is associated with the SoftwareAgent class that is represented by the
    # Activity's `agent' field.
    #
    # @param [Array<Object>] *args Arguments to pass along to
    #                              EntityBehavior#entities
    # @return [Enumerator] Objects
    def entities(*args)
      agent.constantize.entity_behavior.entities(self, *args)
    end
  end
end