lib/runbox/scenario/output_action.ex

defmodule Runbox.Scenario.OutputAction do
  @moduledoc group: :output_actions
  @moduledoc """
  Output action represents a side effect produced by a run.

  ## Creating output actions

  Creating output action typically happens from scenario. It is done by
  creating one of the appropriate struct:

    * `t:Runbox.Scenario.OutputAction.UpsertAssetAttributes.t/0`
    * `t:Runbox.Scenario.OutputAction.DeleteAssetAttributes.t/0`
    * `t:Runbox.Scenario.OutputAction.DeleteAllAssetAttributes.t/0`
    * `t:Runbox.Scenario.OutputAction.UpsertEdge.t/0`
    * `t:Runbox.Scenario.OutputAction.DeleteEdge.t/0`
    * `t:Runbox.Scenario.OutputAction.Notification.t/0`
    * `t:Runbox.Scenario.OutputAction.Event.t/0`
    * `t:Runbox.Scenario.OutputAction.ExecuteSQL.t/0`
    * `t:Runbox.Scenario.OutputAction.Incident.t/0`
    * `t:Runbox.Scenario.OutputAction.IncidentPatch.t/0`

  For example:

      iex> %OutputAction.UpsertAssetAttributes{
      ...>   type: "/asset/camera",
      ...>   id: "one",
      ...>   attributes: %{"foo" => "bar"}
      ...> }

  ## Asset and edge output actions

  Changes done by output actions to assets and edges are scoped for each
  scenario run. The global result is computed from the changes done by
  individual runs.

  A single run is forbidden to do multiple changes to the same object (asset
  attribute or edge) at the same time. However distinct runs can perform such
  changes, because they are scoped to each run.

  The rules to compute the global state apply in the following order:

    1. Change with the higher timestamp has precedence.

    1. Updating an asset attribute or an edge has precedence to deleting it.

    1. Change with the higher value has precedence (applies only for asset attributes).
  """

  alias __MODULE__, as: OA

  @typedoc """
  Asset type.

  Assets are grouped by their asset types. Assets of the same type should be of similar nature
  (e.g. servers or buildings) and should share the same set of attributes.

  Asset types must start with a slash and must not end with a slash. Slashes can be used
  within the asset type to denote structure. For example, these are valid asset types:
  `/assets/servers`, `/servers`, and `/assets/network/routers`.

  Although asset types like `/servers` are permitted, it is recommended to prefix asset types
  with the `/assets` prefix to enable consistent configuration of common attributes (e.g., `name`)
  via the root `/assets` asset type in the UI.
  """
  @type asset_type :: String.t()

  @typedoc """
  Asset ID.

  Asset ID together with an asset type uniquely identifies an asset.

  Asset ID must not contain any slashes. They can contain any other character though.
  For example, these are valid asset IDs: `fire_alarm`, `192.168.0.142`, `Meeting room`, and
  `Příliš žluťoučký kůň`.
  """
  @type asset_id :: String.t()

  @type edge_type :: String.t()

  @type access_tag :: String.t()

  @typedoc """
  Attribute - access tag mapping.

  Access tags are defined as map where keys are attributes and values
  are tags of these attributes.

  Attributes (keys) can be defined as:
    * `:all` - all attributes in this upsert share the same access tags
    * `String.t()` - single attribute access tags definition
    * `[String.t()]` - list of attributes that share the same access tags

  Access tags (values) can be defined by single access tag or list of access tags.

  Using this map structure we can define shared access tag for multiple attributes
  as well as multiple access tag for single attribute (or combination of both).

  Attributes can be defined with `dot` syntax (e.g. `user.contact.address.street`)
  where nested attributes are separated by `.`. We can use this syntax to define
  access tags for group of nested attributes - access tags defined for
  `user.contact.address` will be used for all nested attributes such as
  `user.contact.address.city` and `user.contact.address.street`.

  For delete operations: `DeleteAllAssetAttributes`,`DeleteAssetAttributes` and `DeleteEdge`
  it is recommended to use the same access tags as those used for the corresponding upsert
  operations within the same run.

  Overlapping access tags definitions are concatenated. For example access tags
  defined as:

  ```
  %UpsertAssetAttributes{
    ...
    access_tags: %{
      all: ["tag1"],
      "user.contact" => ["tag2"],
      ["user.contact.address", "user.contact.phone"] => ["tag3"],
      "user.contact.address.street" => "tag4"
    }
  }
  ```

  would mean that:
    * `"user.contact"` has these tags defined `["tag1", "tag2"]`
    * `"user.contact.address.phone"` has these tags defined `["tag1", "tag2", "tag3"]`
    * `"user.contact.address.street"` has these tags defined `["tag1", "tag2", "tag3", "tag4"]`
  """
  @type access_tags :: %{
          (attribute :: :all | String.t() | [String.t()]) => access_tag() | [access_tag()]
        }

  defmodule UpsertAssetAttributes do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Upsert Asset Attributes.

    The resulting output action will create or update attributes of the given
    asset.
    """

    @enforce_keys [:type, :id, :attributes]
    defstruct [:type, :id, :attributes, access_tags: %{}]

    @typedoc """
    Upsert Asset Attributes

      * `:type` - asset type (for more information, see `t:asset_type/0`)
      * `:id` - asset ID (for more information, see `t:asset_id/0`)
      * `:attributes` - a map of attributes in format `%{"name" => value}` to be
        inserted or updated
      * `:access_tags` - a map of attributes mapped to its access tags (for more information,
        see `t:Runbox.Scenario.OutputAction.access_tags/0`)
    """
    @type t :: %__MODULE__{
            type: OA.asset_type(),
            id: OA.asset_id(),
            attributes: map(),
            access_tags: OA.access_tags()
          }
  end

  defmodule DeleteAssetAttributes do
    @moduledoc group: :output_actions

    @moduledoc """
    Parameters for output action Delete Asset Attributes.

    The resulting output action will delete attributes of the given asset. This
    action succeeds even if the attributes do not exist.
    """

    @enforce_keys [:type, :id, :attributes]
    defstruct [:type, :id, :attributes, access_tags: %{}]

    @typedoc """
    Delete Asset Attributes

      * `:type` - asset type
      * `:id` - asset ID
      * `:attributes` - a map of attributes to be deleted
      * `:access_tags` - a map of attributes mapped to its access tags (for more information,
        see `t:Runbox.Scenario.OutputAction.access_tags/0`)

    The leaf attributes of the `:attributes` map are deleted.
    E.g., if asset has the following attributes
    ```elixir
    %{
      "a": 1,
      "b": %{"c" => 2}
    }
    ```
    and the following `:attributes` are provided in the output action
    ```elixir
    %{
      "b": %{"c" => true}
    }
    ```
    then `b.c` attribute will be deleted, resulting in
    ```elixir
    %{
      "a": 1
      "b": %{}
    }
    ```

    """
    @type t :: %__MODULE__{
            type: OA.asset_type(),
            id: OA.asset_id(),
            attributes: map(),
            access_tags: OA.access_tags()
          }
  end

  defmodule DeleteAllAssetAttributes do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Delete All Asset Attributes.

    The resulting output action will delete all existing attributes of the
    given asset. This is scoped to the attributes created by the same run.
    """

    @enforce_keys [:type, :id]
    defstruct [:type, :id, access_tags: %{}]

    @typedoc """
    Delete All Asset Attributes

      * `:type` - asset type
      * `:id` - asset ID
      * `:access_tags` - a map of attributes mapped to its access tags (for more information,
        see `t:Runbox.Scenario.OutputAction.access_tags/0`)

    """
    @type t :: %__MODULE__{
            type: OA.asset_type(),
            id: OA.asset_id(),
            access_tags: OA.access_tags()
          }
  end

  defmodule UpsertEdge do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Upsert Edge.

    The resulting output action will create an edge between two assets. The
    assets do not have to exist at the time of the edge creation.
    """

    @enforce_keys [:from_type, :from_id, :to_type, :to_id, :type]
    defstruct [:from_type, :from_id, :to_type, :to_id, :type, access_tags: []]

    @typedoc """
    Upsert Edge

      * `:from_type` - type of the asset which this edge goes out from
      * `:from_id` - id of the asset which this edge goes out from
      * `:to_type` - type of the asset which this edge goes to
      * `:to_id` - id of the asset which this edge goes to
      * `:type` - type of the edge
      * `:access_tags` - list of access tags for this edge (for more information,
        see `t:Runbox.Scenario.OutputAction.access_tags/0`)
    """
    @type t :: %__MODULE__{
            from_type: OA.asset_type(),
            from_id: OA.asset_id(),
            to_type: OA.asset_type(),
            to_id: OA.asset_id(),
            type: OA.edge_type(),
            access_tags: [OA.access_tag()]
          }
  end

  defmodule DeleteEdge do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Delete Edge.

    The resulting output action will delete an edge between two assets. This
    action succeeds even if the edge does not exist.
    """

    @enforce_keys [:from_type, :from_id, :to_type, :to_id, :type]
    defstruct [:from_type, :from_id, :to_type, :to_id, :type, access_tags: []]

    @typedoc """
    Delete Edge

      * `:from_type` - type of the asset which this edge goes out from
      * `:from_id` - id of the asset which this edge goes out from
      * `:to_type` - type of the asset which this edge goes to
      * `:to_id` - id of the asset which this edge goes to
      * `:type` - type of the edge
      * `:access_tags` - list of access tags for this edge (for more information,
        see `t:Runbox.Scenario.OutputAction.access_tags/0`)
    """
    @type t :: %__MODULE__{
            from_type: OA.asset_type(),
            from_id: OA.asset_id(),
            to_type: OA.asset_type(),
            to_id: OA.asset_id(),
            type: OA.edge_type(),
            access_tags: [OA.access_tag()]
          }
  end

  defmodule Notification do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Notification.

    The resulting output action sends a notification.
    """

    defmodule AssetActor do
      @moduledoc group: :output_actions
      @moduledoc """
      Struct for referencing assets in Notifications.
      """
      @enforce_keys [:type, :id]
      defstruct [:type, :id]
      @type t :: %__MODULE__{type: OA.asset_type(), id: OA.asset_id()}
    end

    defmodule IncidentActor do
      @moduledoc group: :output_actions
      @moduledoc """
      Struct for referencing incidents in Notifications.
      """
      @enforce_keys [:type, :id]
      defstruct [:type, :id]
      @type t :: %__MODULE__{type: String.t(), id: String.t()}
    end

    @enforce_keys [:type, :data]
    defstruct type: nil,
              data: nil,
              primary_actor: nil,
              actors: [],
              priority: :medium,
              metadata: %{},
              direct_subscriptions: nil,
              attachments: nil,
              email_reply_to: nil,
              aqls: %{}

    @typedoc """
    Notification

    Structure Notification defines the possible attributes of notifications.
    Different types of notifications are distinguished by the use of different
    attributes and their specific values. The specific use of the Notification
    structure in the source code defines the pattern according to which
    the final notifications of a given type will be generated.

    Final notifications are sent to recipients according to the rules defined
    in Notification Groups or in `direct_subsctriptions` attribute of the Notification
    structure desribed below.

    ## Example

        %Notification{
          type: "technician_entry",
          data: %{
            "room_id" => %{type: "/assets", id: "server_room"},
            "technician_id" => %{type: "/assets/cardholder", id: "j_adams"}
          },
          priority: :medium
          direct_subscriptions: [
            %{
              "channels" => ["smtp"],
              "templates" => ["operator"],
              "user" => %{"email" => "operator@acme.com"}
            }
          ],
          email_reply_to: %{
            "Technical support" => "technician@acme.com"
            },
          aqls: %{
            "asset" => %{
              "aql" => "FROM '/assets/server_room' SELECT [\"name\"]",
              "primary" => true
            }
          },
        }

    Notification Attributes

    * `:type` - string.
      Type of the notification.

      The scenario manifest defines notification types and some additional metadata about each type.
      See details in `t:Runbox.Scenario.Manifest.t/0` attribute `notifications`.
      The `type` identifier is used, along with other criteria, to find the template through which the notification
      will be transformed into the final notification intended for sending to a specific communication channel.

    * `:data` - map.
      A map of data that can be used within the notification template.
      Keys of the map are used to reference the data.

    ## Example

        # Definition of the data attribute
        %{
          "room_id" => %{type: "/assets", id: "server_room"},
          "technician_id" => %{type: "/assets/cardholder", id: "j_adams}"
        },

        # Usage of the data attribute in the template
        <%= inspect(data["technician_id"]) %>

    * `:primary_actor` - the primary actor of this notification, optional.
      The reference to the asset or incident to which the notification relates.
      If a primary asset is included in the notification then the notification is not sent to
      recipients who do not have permission to see the primary asset. Use
      `Runbox.Scenario.OutputAction.Notification.AssetActor` and
      `Runbox.Scenario.OutputAction.Notification.IncidentActor` to reference the entities.

    * `:actors` - list of related entities/actors, optional.
      The notification will be sent only to those recipients who have the right to see
      the specified entities. These can be Assets
      (`Runbox.Scenario.OutputAction.Notification.AssetActor`) or Incidents
      (`Runbox.Scenario.OutputAction.Notification.IncidentActor`).

    * `priority` - optional, atom.
      Possible value are `:low`, `:medium`, `:high`.
      Defaults to `:medium`.
      Only applicable to some channels.
      Attribute is not used for prioritization of email notifications.

    * `:metadata` - map, optional.
      Additional metadata which can be used to filter recipients.
      Metadata filter can be set in the User UI
      (menu `NOTIFICATIONS / Notification groups / Metadata Filter`).
      Metadata should adhere to the format specified in `t:Runbox.Scenario.Manifest.t/0`
      attribute `notifications`.

    ## Example

        # metadata attribute
        %{"report_for" => "C-Level"}

    * `:direct_subscriptions` - list of maps, optional.
      Direct subscriptions provide a way to bypass routing system defined
      through notification groups and send notifications to the specific
      recipients directly.
      It is even possible to send notifications to recipients who do not have
      an Altworx account.

      Each member of the list is a map with the following structure:

        * `"user"` - is a map that with keys:
          * `"email"` - the value is a string containing email address.
          * `"notification"` - optional. The value is a map with key:
            * `"language"` - optional. String containing language of the notification template.

          Type of the channel determines which keys are used.

        * `"templates"` - list of template types that will be used to generate the final notification.
          Templates itselves are located either in the `priv/notifications/[scenario_id]/templates/[notification_type]`
          directory or in `deployment/scenario_notification_overrides/[scenario_id]/templates/[notification_type]`
          directory.

        * `"channels"` - list of channel ids through which notifications will be sent.
          Channels are defined in the Altworx Admin UI (menu `NOTIFICATIONS / CHANNELS`).

    ## Example

        # Direct_subscriptions attribute
        [
          %{
            "channels" => ["smtp"],
            "templates" => ["operator"],
            "user" => %{
              "email" => "operator@acme.com",
              "notification" => %{"language" => "en"}
            }
          }
        ]

    * `:attachments` - list of maps, optional.
      Attachments that will be attached to the notification.
      Each map has the following keys:
        * `:data` - binary. Content of the attachment.
        * `:filename` - string. Name of the file.
        * `:content_type` - string, optional. MIME type of the data.
        * `:type` - whether the attachment is to be inlined in the template (`:inline`)
          or not (`:attachment`, this is the default).

    ## Example

        # Definition of the attachments attribute
        %{
          data: "greeting",
          content_type: "text/plain",
          filename: "greeting.txt",
          type: :attachment
        }

    * `:email_reply_to` - map, optional.
      If the `email_reply_to` attribute is defined, the appropriate `Reply-To` headers
      will be inserted into the email protocol.
      The recipient of the notification will see the information as the email addresses
      to which his possible reply will be sent.

      If the attribute `email_reply_to` is not specified the value defined
      in the `reply_to` parameter of the `config.ini` configuration file
      stored in the deployment directory is used.

      Keys are the names of the recipients of the reply.
      Values are their email addreses.

    ## Example

        # Definition of the email_reply_to attribute
        %{
          "Call Centrum" => "info@acme.com"
        }

    * `:aqls` - map of AQL defintions, optional.
     `AQL` enables to add more context to the final notification.
     `AQL` also allows to simplify the scenario code. For example, if you need to insert the name
      of an asset that the notification references into the text of the notification,
      you can either manage that name yourself in scenario state or you can fetch the name using AQL.

      Without `AQL` the scenario has to  maintain the asset name in the scenario state,
      handle messages that change the asset name coming from the input topics and pass the name
      to the template via the `data` attribute. All of this is usually unrelated to the scenario
      logic and only serves to create a better text of the notification.

      Using `AQL`, developer passes an `AQL` query to the notification. This query uses an asset ID
      to retrieve the current asset name from the reality network at the time the final notification
      is created. The scenario usually needs the asset ID for its own purposes, so it is probably
      stored in the scenario state anyway.

      The keys of the `aqls` are the string identifiers under which the `AQL` query results
      are available inside the notification template.
      The values are maps providing more information about the query. Each map has the following keys:
        * `aql` - string. Contains the `AQL` query.
        * `primary` - boolean atom:
          * `true` - notifications will be sent only to those recipients who have read permission
            to all the assets returned by the query.
          * `false` - permissions are not evaluated.

    ## Example

        # Definition of the `aqls` attribute
        %{
          "asset" => %{
            "aql" => "FROM '/assets/server_room/*' SELECT [\"name\"]",
            "primary" => false
          }
        }

        # Value passed to the notification template after above AQL queries are executed:
        %{
          "asset" => [
            %AssetMap.Ecto.Model.Asset{
              type: "/assets/server_room",
              id: "Z10",
              attributes: %{"name" => "022-000033/AGPP"}
            }
          ]
        }

        # The name of the technician can be obtained inside the notification template
        # with the following command
        <%= List.first(aqls["asset"]).attributes["name"] %>
    """
    @type t :: %__MODULE__{
            type: String.t() | atom(),
            data: map(),
            primary_actor: AssetActor.t() | IncidentActor.t() | nil,
            actors: [AssetActor.t() | IncidentActor.t()],
            aqls: map(),
            priority: :low | :medium | :high | String.t(),
            metadata: map(),
            direct_subscriptions: [map()] | nil,
            attachments: [map()] | nil,
            email_reply_to: map() | nil
          }
  end

  defmodule Event do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Event.

    The resulting output action creates an event.

    See `t:t/0` for more information.

    > #### Note {: .warning}
    >
    > Beware that creating incident history manually via event's `incident_actors` property, instead
    > of relying on Incident output actions, is low-level and prone to errors. You must always ensure
    > that you create the event **and** you accordingly update the incident itself. Failing to do
    > either will result in inconsistent state.
    """

    @enforce_keys [:type, :template, :actors]
    defstruct type: nil,
              template: nil,
              actors: nil,
              incident_actors: nil,
              params: %{},
              origin_messages: []

    @typedoc """
    Event

      * `:type` - type of the event. It's declared in Scenario's Manifest.
      * `:template` - event's template that allows to interpolate actors and parameters
        (see the *Interpolation* section below).
      * `:actors` - map of asset actors that are to be linked with the event. They may also be
        interpolated in the template.
      * `:incident_actors` - map of incident actors that are to be linked with the event. This means
        the event is either part of the incident's history, or is just related. They may also be
        interpolated in the template. See `t:incident_actor/0` for more information.
      * `:params` - map of template parameters that are interpolated in the template
        (optional, defaults to `%{}`).
      * `:origin_messages` - list of references to raw messages linked with the event itself
        (optional, defaults to `[]`).

    ## Interpolation

    The template may reference actors using placeholders like `${actors.actor_key}`, where
    `actor_key` corresponds to a key in the `actors` map within this struct. In the UI,
    these placeholders are transformed into links. Each link points to the corresponding
    asset and displays the asset's name as the link text.

    Incident actors can be interpolated in a very similar manner using `incidents` keyword -
    `${incidents.fire}`.

    The template may also contain placeholders like `${params.param_key}`, where `param_key`
    is a key under the `params` map. These placeholders are simply replaced with the
    corresponding values from the `params` map. These parameters are useful for dynamic
    content other than actors.

    ## Example

        %Event{
          type: "server_login",
          template: "${actors.person} logged into ${actors.server} using OpenSSH ${params.openssh_version}",
          actors: %{
            "person" => %{
              asset_type: "/assets/person",
              asset_id: "joe"
            },
            "server" => %{
              asset_type: "/assets/server",
              asset_id: "192.168.142.18"
            }
          },
          params: %{"openssh_version" => "9.8"},
          origin_messages: [normalized_message.origin]
        }
    """
    @type t :: %__MODULE__{
            type: String.t() | atom(),
            template: String.t(),
            actors: actors(),
            incident_actors: incident_actors(),
            params: %{String.t() => String.t()},
            origin_messages: [Runbox.Message.origin()]
          }

    @type actors :: %{(actor_key :: String.t()) => actor()}
    @type actor :: %{
            asset_type: String.t(),
            asset_id: String.t()
          }
    @type incident_actors :: %{(actor_key :: String.t()) => incident_actor()}
    @typedoc """
    Incident actor of the event.

    Can mean two things depending on if `status` and `severity` is set. If either is empty, then the
    event is considered related to the incident, similar to `actors`. If both are set, then the
    event is considered a part of the incident's history. `status` and `severity` then represent the
    change of these incident properties at the time of the event. `attributes` is fully optional
    and contains further context about the change of the incident.
    """
    @type incident_actor :: %{
            :type => String.t(),
            :id => String.t(),
            optional(:status) => String.t() | nil,
            optional(:severity) => 1 | 2 | 3 | 4 | nil,
            optional(:attributes) => map() | nil
          }
  end

  defmodule ExecuteSQL do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Execute SQL.

    The resulting output action executes an SQL statement.
    """

    @enforce_keys [:db_connection, :sql_query, :data, :type]
    defstruct db_connection: nil,
              sql_query: nil,
              data: nil,
              type: nil

    @typedoc """
    Execute SQL

      * `:db_connection` - map with database connection info (`hostname`, `username`, `password`, `database`)
      * `:sql_query` - SQL query.
      * `:data` - list of parameterized data.
      * `:type` - database type, right now only PostgreSQL is supported. It's checked in `OutputProcessor` and
        the value must be `:postgresql`.

    If `:run_id` is present in `data`, it's replaced with actual run_id in `AssetMap.Api.SqlExecutor`.
    """
    @type t :: %__MODULE__{
            db_connection: map(),
            sql_query: String.t(),
            data: list(),
            type: :postgresql | String.t()
          }
  end

  @typedoc """
  String with interpolation support.

  Supports interpolating asset references to a format which is useful for
  displaying assets in the Altworx UI. The string can contain any number of
  placeholders in format

      ${assets.["/assets/camera", "one"]}

  Where `"/assets/camera"` is the asset type and `"one"` is the asset ID. The resulting interpolation is

      [Camera One[/assets/camera/one]]

  Where `Camera One` is the asset's `name` attribute and `/assets/camera/one`
  is the asset's full ID. When the asset `name` is not present, the asset's ID
  is used instead

      [one[/assets/camera/one]]

  When the asset cannot be found, the resulting interpolation is

      unknown

  The interpolation is done upon "read" operations, therefore the result can
  vary based on the asset visibility for the identity of the user performing
  the operation.
  """
  @type interpolable :: String.t()

  defmodule IncidentFuture do
    @moduledoc group: :output_actions
    @moduledoc """
    Incident Future.

    The struct represents a future possible change of an Incident. It carries information about how
    the Incident might change.

    The change can be expected to happen at a specified time (then `timestamp` is a positive integer
    - epoch time in ms) or it is unknown when the change might occur (then `timestamp` is `nil`).
    """
    @enforce_keys [
      :status,
      :severity,
      :timestamp,
      :description
    ]
    defstruct [
      :status,
      :severity,
      :timestamp,
      :description
    ]

    @type t :: %IncidentFuture{
            status: String.t(),
            severity: OA.Incident.severity(),
            timestamp: integer() | nil,
            description: OA.interpolable()
          }
  end

  defmodule IncidentHistory do
    @moduledoc group: :output_actions
    @moduledoc """
    Incident history.

    The struct represents a change of an Incident in time. It carries information about how the
    Incident changed, all describe how the Incident looks after the change. The struct contains all
    necessary parameters to create an Incident history record.

    Since Incident history is now synonymous to Events, `event_*` fields of this struct can be used
    to control how the final Event looks like.

    Available fields

    - `:status` - status of the Incident
    - `:severity` - severity of the Incident
    - `:timestamp` - time of the change
    - `:description` - description of the change - what has happened, will be used as Event template
    - `:attributes` - (optional) significant additional attributes that changed with the Incident
      (usually metrics, like temperature, server load, distance). Can be empty.
    - `:event_type` - (optional) type of the underlying Event, if empty a default type
      `incident_updated` will be used
    - `:event_actors` - (optional) additional actors of the underlying Event. If empty no actors are
      defined.
    - `:event_params` - (optional) additional parameters of the Event. If empty no params are
      defined.
    - `:event_origin_messages` - (optional) list of references to raw messages linked with the
      underlying Event. If empty no raw messages are linked with the Event.
    """
    @enforce_keys [
      :status,
      :severity,
      :timestamp,
      :description
    ]
    defstruct [
      :status,
      :severity,
      :timestamp,
      :description,
      :attributes,
      :event_type,
      :event_actors,
      :event_params,
      :event_origin_messages
    ]

    @typedoc """
    Incident history.

    Note Incident history is synonymous to Events and thus carries similar parameters.

    - `:status` - status of the Incident
    - `:severity` - severity of the Incident
    - `:timestamp` - time of the change
    - `:description` - description of the change - what has happened, will be used as Event template
    - `:attributes` - (optional) significant additional attributes that changed with the Incident
      (usually metrics, like temperature, server load, distance). Can be empty.
    - `:event_type` - (optional) type of the underlying Event, if empty a default type
      `incident_updated` will be used
    - `:event_actors` - (optional) additional actors of the underlying Event. If empty no actors are
      defined.
    - `:event_params` - (optional) additional parameters of the Event. If empty no params are
      defined.
    - `:event_origin_messages` - (optional) list of references to raw messages linked with the
      underlying Event. If empty no raw messages are linked with the Event.
    """
    @type t :: %IncidentHistory{
            status: String.t(),
            severity: OA.Incident.severity(),
            timestamp: integer(),
            description: OA.interpolable(),
            attributes: map() | nil,
            event_type: String.t() | nil,
            event_actors: Event.actors() | nil,
            event_params: %{String.t() => String.t()} | nil,
            event_origin_messages: [Runbox.Message.origin()] | nil
          }
  end

  defmodule IncidentActor do
    @moduledoc group: :output_actions
    @moduledoc "Incident Actor"
    @enforce_keys [
      :type,
      :id
    ]
    defstruct [
      :type,
      :id
    ]

    @type t :: %IncidentActor{
            type: String.t(),
            id: String.t()
          }
  end

  defmodule Incident do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Incident.

    The resulting output action creates an incident.
    """
    @enforce_keys [
      :type,
      :id,
      :subject,
      :status,
      :resolved,
      :severity,
      :future,
      :history
    ]
    defstruct [
      :type,
      :id,
      :subject,
      :status,
      :resolved,
      :severity,
      :future,
      :history,
      :actors,
      :user_actions,
      :additional_attributes
    ]

    @type severity :: 1..4

    @typedoc """
    User actions that can be fired on the incident.

    The value of a user action is a JWT obtained by calling
    `Runbox.Scenario.UserAction.pack/3`.

    Each user action has a key by which it can later be referenced in
    `Runbox.Scenario.OutputAction.IncidentPatch` and either modified or deleted.

    ## Example

        {:ok, action} = UserAction.pack("cam_actions", "close_incident", %{...})
        %{"close" => action}
    """
    @type user_actions :: %{optional(key :: String) => Joken.bearer_token()}

    @typedoc """
    Incident
    """
    @type t :: %Incident{
            type: String.t(),
            id: String.t(),
            subject: OA.interpolable(),
            status: String.t(),
            resolved: boolean(),
            severity: severity(),
            future: [IncidentFuture.t()],
            history: [IncidentHistory.t()],
            actors: [IncidentActor.t()] | nil,
            user_actions: user_actions() | nil,
            additional_attributes: map() | nil
          }
  end

  defmodule IncidentPatch do
    @moduledoc group: :output_actions
    @moduledoc """
    Parameters for output action Incident Patch.

    The resulting output action updates an incident.
    """
    @enforce_keys [
      :type,
      :id
    ]
    defstruct [
      :type,
      :id,
      :subject,
      :status,
      :resolved,
      :severity,
      :future,
      :history,
      :actors,
      :user_actions,
      :additional_attributes
    ]

    @typedoc """
    Incident Patch
    """
    @type t :: %IncidentPatch{
            type: String.t(),
            id: String.t(),
            subject: OA.interpolable() | nil,
            status: String.t() | nil,
            resolved: boolean() | nil,
            severity: OA.Incident.severity() | nil,
            future: [IncidentFuture.t()] | nil,
            history: [IncidentHistory.t()] | nil,
            actors:
              %{
                optional(:add) => [IncidentActor.t()],
                optional(:remove) => [IncidentActor.t()]
              }
              | nil,
            user_actions:
              %{
                optional(:upsert) => Incident.user_actions(),
                optional(:remove) => [key :: String.t()]
              }
              | nil,
            additional_attributes:
              %{
                optional(:upsert) => map(),
                optional(:remove) => list()
              }
              | nil
          }
  end

  defmodule BadOutputAction do
    @moduledoc group: :output_actions
    @moduledoc """
    Body of invalid output action.
    """

    @enforce_keys [:params]
    defstruct [:params]

    @typedoc """
    Bad Output Action

      * `:params` - holds the original parameters
    """
    @type t :: %__MODULE__{
            params: any()
          }
  end

  @type oa_params ::
          UpsertAssetAttributes.t()
          | DeleteAssetAttributes.t()
          | DeleteAllAssetAttributes.t()
          | UpsertEdge.t()
          | DeleteEdge.t()
          | Notification.t()
          | Event.t()
          | ExecuteSQL.t()
          | Incident.t()
          | IncidentPatch.t()

  defstruct [:body, :timestamp, :scenario_id, :run_id]

  @typedoc """
  Output Action

  This struct is produced by `Runbox.Runtime.Stage.Sandbox.execute_run/3`.
  """
  @type t :: %__MODULE__{
          body: oa_params() | BadOutputAction.t(),
          timestamp: integer()
        }
end