# frozen_string_literal: true

module Agents
  class KeyValueStoreAgent < Agent
    can_control_other_agents!
    cannot_be_scheduled!
    cannot_create_events!

    description <<~MD
      The Key-Value Store Agent is a data storage that keeps an associative array in its memory.  It receives events to store values and provides the data to other agents as an object via Liquid Templating.

      Liquid templates specified in the `key` and `value` options are evaluated for each received event to be stored in the memory.

      The `variable` option specifies the name by which agents that use the storage refers to the data in Liquid templating.

      The `max_keys` option specifies up to how many keys to keep in the storage.  When the number of keys goes beyond this, the oldest key-value pair gets removed. (default: 100)

      ### Storing data

      For example, say your agent receives these incoming events:

          {
            "city": "Tokyo",
            "weather": "cloudy"
          }

          {
            "city": "Osaka",
            "weather": "sunny"
          }

      Then you could configure the agent with `{ "key": "{{ city }}", "value": "{{ weather }}" }` to get the following data stored:

          {
            "Tokyo": "cloudy",
            "Osaka": "sunny"
          }

      Here are some specifications:

      - Keys are always stringified as mandated by the JSON format.
      - Values are stringified by default.  Use the `as_object` filter to store non-string values.
      - If the key is evaluated to an empty string, the event is ignored.
      - If the value is evaluated to either `null` or empty (`""`, `[]`, `{}`) the key gets deleted.
      - In the `value` template, the existing value (if any) can be accessed via the variable `_value_`.
      - In the `key` and `value` templates, the whole event payload can be accessed via the variable `_event_`.

      ### Extracting data

      To allow other agents to use the data of a Key-Value Store Agent, designate the agent as a controller.
      You can do that by adding those agents to the "Controller targets" of the agent.

      The target agents can refer to the storage via the variable specified by the `variable` option value.  So, if the store agent in the above example had an option `"variable": "weather"`, they can say something like `{{ weather[city] | default: "unknown" }}` in their templates to get the weather of a city stored in the variable `city`.
    MD

    def validate_options
      options[:key].is_a?(String) or
        errors.add(:base, "key is required and must be a string.")

      options[:value] or
        errors.add(:base, "value is required.")

      /\A(?!\d)\w+\z/ === options[:variable] or
        errors.add(:base, "variable is required and must be valid as a variable name.")

      max_keys > 0 or
        errors.add(:base, "max_keys must be a positive number.")
    end

    def default_options
      {
        'key' => '{{ id }}',
        'value' => '{{ _event_ | as_object }}',
        'variable' => 'var',
      }
    end

    def working?
      !recent_error_logs?
    end

    def control_action
      'provide'
    end

    def max_keys
      if value = options[:max_keys].presence
        value.to_i
      else
        100
      end
    end

    def receive(incoming_events)
      max_keys = max_keys()

      incoming_events.each do |event|
        interpolate_with(event) do
          interpolation_context.stack do
            interpolation_context['_event_'] = event.payload

            key = interpolate_options(options)['key'].to_s
            next if key.empty?

            storage = memory
            interpolation_context['_value_'] = storage.delete(key)

            value = interpolate_options(options)['value']

            if value.nil? || value.try(:empty?)
              storage.delete(key)
            else
              storage[key] = value
              storage.shift while storage.size > max_keys
            end

            update!(memory: storage)
          end
        end
      end
    end
  end
end
