# These modules define the structure of our MQTT messages.

defmodule Cog.Messages.ProviderRequest do
  @moduledoc """
  Input to the executor; all providers must generate one.
  """
  use Conduit
  require Cog.Chat.MessageMetadata

  # Unique request ID
  field :id, :string, required: true

  # Command or pipeline text
  field :text, :string, required: true

  # Initial input to the pipeline. Meaningful for trigger-initiated
  # pipelines.
  field :initial_context, :map, required: true

  # Provider-specific information about the user initiating the
  # request (e.g., Slack chat handle, internal Slack user ID, etc.)
  field :sender, Cog.Chat.User, required: true

  # Provider-specific information about the "room" the request was
  # initiated from (e.g., Slack channel, HTTP request, etc.)
  field :room, Cog.Chat.Room, required: true

  # Message queue topic to send the reply to
  field :reply, :string, required: true

  # Short name of provider, e.g. "slack"
  field :provider, :string, required: true

  # Extra data related to the incoming message, like the thread_id
  field :metadata, [object: Cog.Chat.MessageMetadata], required: false
end

defmodule Cog.Messages.Command do
  @moduledoc """
  Input for a single command execution. This is what is sent to a Relay
  for processing.
  """

  alias Cog.ServiceEndpoint
  use Conduit

  # Unique identifier for the entire command pipeline
  field :pipeline_id, :string, required: true

  # Unique identifier for the individual command invocation
  field :invocation_id, :string, required: true

  # Name of the command being executed
  field :command, :string, required: true

  # Arguments to the command, if given. May be a combination of
  # strings, integers, booleans, etc.
  field :args, :array, required: true

  # Options to the command, if given.
  field :options, :map, required: true

  # Output from previous pipeline stage
  field :cog_env, :map, required: true

  # Indicates which in a sequence of iterations of a single command
  # this is.
  # TODO: apparently sometimes this is nil??? Under what circumstances?
  # TODO: is this a string if not "first" or "last"?
  field :invocation_step, :string, required: false

  # Message queue topic to send the reply to
  field :reply_to, :string, required: true

  # Provider-specific information about the user initiating the
  # request (e.g., Slack chat handle, internal Slack user ID, etc.)
  #
  # (see Cog.Messages.ProviderRequest.sender)
  field :requestor, Cog.Chat.User, required: true

  # Provider-specific information about the "room" the request was
  # initiated from (e.g., Slack channel, HTTP request, etc.)
  #
  # (See Cog.Messages.ProviderRequest.room)
  field :room, Cog.Chat.Room, required: true

  # Token to access services (e.g. memory)
  field :service_token, :string, required: true

  # URL root at which to access services
  field :services_root, :string, required: true

  # Information about the Cog user invoking the command
  field :user, :map, required: true

  @doc "Creates a new Command from an invocation, options, and args"
  defmacro create(pipeline_id, invocation, options, args) do
    quote bind_quoted: [pipeline_id: pipeline_id, invocation: invocation, options: options, args: args] do
      %Cog.Messages.Command{command: invocation.meta.full_command_name,
                            options: options,
                            args: args,
                            invocation_id: invocation.id,
                            pipeline_id: pipeline_id,
                            services_root: ServiceEndpoint.url()}
    end
  end

end

defmodule Cog.Messages.CommandResponse do
  @moduledoc """
  Response sent from Relay back to Cog following the execution of a
  command.
  """
  use Conduit

  # Output generated by the command execution.
  # TODO: Not all responses currently have bodies, either... see GenCommand#send_error_reply
  field :body, :map_or_array_of_maps, required: false

  # ok    -> successful execution; pipeline continues
  # error -> an anticipated error condition occurred; pipeline ends here
  # abort -> an unanticipated error condition occurred (e.g. uncaught
  #          exception); pipeline ends here
  field :status, :string, enum: ["ok", "error", "abort"], required: true

  # Optional message describing the status. Generally used for errors.
  field :status_message, :string, required: false # TODO: actually seeing some with NULL

  # Name of the optional template to format the body with
  field :template, :string, required: false
end

defmodule Cog.Messages.SendMessage do
  @moduledoc """
  Final response sent back to the provider after all pipeline execution has finished
  """
  use Conduit
  # Final templated and formatted response
  field :response, :string, required: true

  # Provider-specific information about the "room" the response is
  # targeted to. May or may not be the same as the initial source of
  # the request (e.g., multiple redirect destinations)
  field :room, Cog.Chat.Room, required: true
end

########################################################################
# Relay Messages

# TODO: This could just be a general "request for information about X"
# message; the particular "X" would be determined by the message queue
# topic the message is sent to.
defmodule Cog.Messages.Relay.ListBundles do
  @moduledoc """
  Message sent to request the bundle definitions a Relay should be serving.
  """
  use Conduit

  # Unique identifier for the Relay requesting the configuration
  field :relay_id, :string, required: true

  # Message queue topic to sent the response to
  field :reply_to, :string, required: true
end

defmodule Cog.Messages.Relay.GetDynamicConfigs do
  @moduledoc """
  Request for managed dynamic configuration
  """
  use Conduit

  # Unique identifier for the Relay requesting the configuration
  field :relay_id, :string, required: true

  # SHA256 checksum of the dynamic configuration the Relay already
  # has.  (If the Relay doesn't have any, this is currently an empty
  # string)
  field :config_hash, :string, required: true

  # Message queue topic to sent the response to
  field :reply_to, :string, required: true
end

# Defines structure of the incoming (i.e., Relay -> Cog) message
# requesting information about a) what bundles the Relay should be
# serving or b) the dynamic configuration (if any) the bundles should have
#
# TODO: Currently, both messages come in on the same message queue
# topic; we can do away with this "envelope" message (and simplify
# internal processing) by making Cog listen on different topics for
# these messages.
defmodule Cog.Messages.RelayInfo do
  use Conduit
  field :get_dynamic_configs, Cog.Messages.Relay.GetDynamicConfigs, required: false
  field :list_bundles, Cog.Messages.Relay.ListBundles, required: false
end

# Just provide a bit more structure to our messages; isn't used
# independently
defmodule Cog.Messages.Relay.Bundle do
  use Conduit

  field :name, :string, required: true
  field :version, :string, required: true
end

defmodule Cog.Messages.Relay.Announcement do
  @moduledoc """
  Sent by a Relay to Cog when it establishes contact.
  """
  use Conduit

  # Unique ID for the announcement; used as a correlation identifier
  # so Relay and Cog can ensure they're in sync with each other
  field :announcement_id, :string, required: true

  # Identifier of the relay sending the announcement
  field :relay, :string, required: true

  # Whether the relay is online or not
  field :online, :bool, required: true

  # List of bundles/versions the Relay knows about.
  # It's nil when we get the offline / last-will message
  field :bundles, [array: Cog.Messages.Relay.Bundle], required: false

  # Is the announcement is a snapshot (i.e., "Here are ALL the bundles
  # I know about") or not ("Here's a new bundle I know about")
  field :snapshot, :bool, required: true

  # Message queue topic to send the receipt (see
  # Cog.Messages.Relay.Receipt) to
  field :reply_to, :string, required: true
end

# TODO: Unwrap this; we don't need the extra "envelope" around this
# message.
defmodule Cog.Messages.Relay.Announce do
  use Conduit
  field :announce, Cog.Messages.Relay.Announcement, required: true
end

defmodule Cog.Messages.Relay.Receipt do
  @moduledoc """
  Sent by Cog back to a Relay in acknowledgment of an announcement.
  """
  use Conduit

  # Unique ID for the announcement; used as a correlation identifier
  # so Relay and Cog can ensure they're in sync with each other. This
  # must be the same as the `announcement_id` sent in the Announcement
  # this Receipt is in acknowledgment of.
  field :announcement_id, :string, required: true

  # Whether all the bundles that a Relay declared actually exist or
  # not.
  field :status, :string, required: true, enum: ["success", "failed"]

  # Currently empty when status == "success". Otherwise, if status ==
  # "failed", it contains the names of bundles that were declared in
  # the announcement that aren't actually installed.
  #
  # TODO: this should be simplified: see relays#receipt
  field :bundles, [array: :string], required: true
end

defmodule Cog.Messages.Relay.BundleResponse do
  @moduledoc """
  List of bundle definitions to send to a Relay. This is how a Relay
  knows what it should be serving up.
  """
  use Conduit
  field :bundles, [array: :map], required: true
end

# TODO: Wonder if we should have a separate message type for
# un-changed responses?
defmodule Cog.Messages.Relay.DynamicConfigResponse do
  @moduledoc """
  When Relays run in managed configuration mode, they get dynamic
  configuration from Cog.

  See Cog.Messages.Relay.GetDynamicConfigs
  """
  use Conduit

  # Indicates whether the configurations for a Relay's bundles have
  # changed (in Cog). If they have, we'll be sending the current
  # configurations to Relay
  field :changed, :bool, required: true

  # Map of bundle_name => list of configuration layers, for when they
  # have changed relative to what Relay currently knows about.
  #
  # nil if changed == true
  field :configs, :map, required: false

  # SHA256 checksum of all configuration layers being sent.
  #
  # nil if changed == true.
  field :signature, :string, required: false
end
