module Agents
  class EventFormattingAgent < Agent
    cannot_be_scheduled!

    description <<-MD
      An Event Formatting Agent allows you to format incoming Events, adding new fields as needed.

      For example, here is a possible Event:

          {
            "high": {
              "celsius": "18",
              "fahreinheit": "64"
            },
            "date": {
              "epoch": "1357959600",
              "pretty": "10:00 PM EST on January 11, 2013"
            },
            "conditions": "Rain showers",
            "data": "This is some data"
          }

      You may want to send this event to another Agent, for example a Twilio Agent, which expects a `message` key.
      You can use an Event Formatting Agent's `instructions` setting to do this in the following way:

          "instructions": {
            "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.",
            "subject": "$.data"
          }

      JSONPaths must be between < and > . Make sure that you don't use these symbols anywhere else.

      Events generated by this possible Event Formatting Agent will look like:

          {
            "message": "Today's conditions look like Rain showers with a high temperature of 18 degrees Celsius.",
            "subject": "This is some data"
          }

      In `matchers` setting you can perform regular expression matching against contents of events and expand the match data for use in `instructions` setting.  Here is an example:

          {
            "matchers": [
              {
                "path": "$.date.pretty",
                "regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
                "to": "pretty_date",
              }
            ]
          }

      This virtually merges the following hash into the original event hash:

          "pretty_date": {
            "time": "10:00 PM EST",
            "0": "10:00 PM EST on January 11, 2013"
            "1": "10:00 PM EST",
          }

      So you can use it in `instructions` like this:

          "instructions": {
            "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius according to the forecast at <$.pretty_date.time>.",
            "subject": "$.data"
          }

      If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`.

      By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time.  You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`.

      To CGI escape output (for example when creating a link), prefix with `escape`, like so:

          {
            "message": "A peak was on Twitter in <$.group_by>.  Search: https://twitter.com/search?q=<escape $.group_by>"
          }
    MD

    event_description "User defined"

    after_save :clear_matchers

    def validate_options
      errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? and options['mode'].present? and options['skip_agent'].present? and options['skip_created_at'].present?

      validate_matchers
    end

    def default_options
      {
        'instructions' => {
          'message' =>  "You received a text <$.text> from <$.fields.from>",
          'some_other_field' => "Looks like the weather is going to be <$.fields.weather>"
        },
        'matchers' => [],
        'mode' => "clean",
        'skip_agent' => "false",
        'skip_created_at' => "false"
      }
    end

    def working?
      !recent_error_logs?
    end

    def receive(incoming_events)
      incoming_events.each do |event|
        formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {}
        payload = perform_matching(event.payload)
        options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) }
        formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true"
        formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true"
        create_event :payload => formatted_event
      end
    end

    private

    def validate_matchers
      matchers = options['matchers'] or return

      unless matchers.is_a?(Array)
        errors.add(:base, "matchers must be an array if present")
        return
      end

      matchers.each do |matcher|
        unless matcher.is_a?(Hash)
          errors.add(:base, "each matcher must be a hash")
          next
        end

        regexp, path, to = matcher.values_at(*%w[regexp path to])

        if regexp.present?
          begin
            Regexp.new(regexp)
          rescue
            errors.add(:base, "bad regexp found in matchers: #{regexp}")
          end
        else
          errors.add(:base, "regexp is mandatory for a matcher and must be a string")
        end

        errors.add(:base, "path is mandatory for a matcher and must be a string") if !path.present?

        errors.add(:base, "to must be a string if present in a matcher") if to.present? && !to.is_a?(String)
      end
    end

    def perform_matching(payload)
      matchers.inject(payload.dup) { |hash, matcher|
        matcher[hash]
      }
    end

    def matchers
      @matchers ||=
        if matchers = options['matchers']
          matchers.map { |matcher|
            regexp, path, to = matcher.values_at(*%w[regexp path to])
            re = Regexp.new(regexp)
            proc { |hash|
              mhash = {}
              value = Utils.value_at(hash, path)
              if value.is_a?(String) && (m = re.match(value))
                m.to_a.each_with_index { |s, i|
                  mhash[i.to_s] = s
                }
                m.names.each do |name|
                  mhash[name] = m[name]
                end if m.respond_to?(:names)
              end
              if to
                case value = hash[to]
                when Hash
                  value.update(mhash)
                else
                  hash[to] = mhash
                end
              else
                hash.update(mhash)
              end
              hash
            }
          }
        else
          []
        end
    end

    def clear_matchers
      @matchers = nil
    end
  end
end
