lib/runbox.ex

defmodule Runbox do
  @moduledoc group: :internal
  @moduledoc """
  Runbox allows working with scenario releases. It can load scenario releases
  and provide information about the individual scenarios within them.
  """

  alias Runbox.Master
  alias Runbox.RunContext
  alias Runbox.RunNode
  alias Runbox.RunStartContext
  alias Runbox.Scenario
  alias Runbox.Scenario.Notification
  alias Runbox.Scenario.UserAction
  alias Runbox.ScenarioRelease
  alias Runbox.Slave
  alias Runbox.StateStore.Entity
  alias Runbox.StateStore.ScheduleUtils

  @runbox_version Mix.Project.config()[:version] |> Version.parse!()

  @doc """
  Retrieves information about scenarios in a release.
  """
  @spec get_release_scenarios(String.t()) :: {:ok, [Runbox.Scenario.t()]} | {:error, term()}
  def get_release_scenarios(release_dir) do
    ScenarioRelease.release_info(release_dir)
  end

  @doc """
  Starts new environment for run.

  Every run runs on own slave node. Caller process is linked with supervisor started during
  this call. Started supervisor will supervise all run components running on slave node.
  """
  @spec start_link_run_environment(
          run_id :: String.t(),
          release_dir :: String.t(),
          user_action_signer :: {node(), module(), function :: atom()}
        ) :: {:ok, RunContext.t()} | {:error, term}
  def start_link_run_environment(run_id, release_dir, user_action_signer) do
    signer_env = [runbox: [{UserAction.get_signer_env_key(), user_action_signer}]]

    with {:ok, _pid, node} <- Slave.start(release_dir, signer_env),
         {:ok, run_sup_pid} <- Slave.call(node, RunNode, :start_run_sup, [run_id]) do
      Process.link(run_sup_pid)
      {:ok, %RunContext{master_node: Node.self(), node: node, sup_pid: run_sup_pid}}
    end
  end

  @doc """
  Generates initial state for run component on run slave node.

  Because function should be called from code implementing scenario type (for example stage based),
  returned value is also specific for scenario type.
  """
  @spec generate_state_for_run_component(RunContext.t(), {module, atom, list}) :: term
  def generate_state_for_run_component(%RunContext{node: node}, {m, f, a}) do
    Slave.call(node, m, f, a)
  end

  @doc """
  Runs `on_start` function of scenario on run slave node.
  """
  @spec run_on_start(RunContext.t(), {module, atom, list}) :: :ok | {:error, term}
  def run_on_start(%RunContext{node: node}, {m, f, a}) do
    Slave.call(node, m, f, a)
  end

  @doc """
  Starts scenario component on slave node.
  """
  @spec start_run_component(RunContext.t(), Scenario.component_def(), RunStartContext.t()) ::
          {:ok, pid} | {:error, term}
  def start_run_component(%RunContext{node: node} = runbox_ctx, comp_def, start_ctx) do
    Slave.call(node, RunNode, :start_run_component, [comp_def, runbox_ctx, start_ctx])
  end

  @doc """
  Saves entity state to state store running on run master node.

  This call is called from slave to master.
  """
  @spec save_entity(RunContext.t(), Entity.t(), ScheduleUtils.epoch_ms()) :: :ok
  def save_entity(%RunContext{master_node: node, save_entity_mf: {m, f}}, entity, timestamp) do
    Master.call(node, m, f, [entity, timestamp])
  end

  @doc """
  Starts slave node for release in `release_dir`.

  If `app_env` is set, configured applications env variables are set on slave node
  (for example `ui_url` variable is needed to be set in `runbox` application env if
  node is used for notifications evaluation, i.e. `app_env` must be set to
  `[runbox: [ui_url: "https://some_url"]]`).
  """
  @spec start_release_node(String.t(), [
          {Application.app(), [{Application.key(), Application.value()}]}
        ]) :: {:ok, %{node: node, scenarios: [scenario_id :: String.t()]}} | {:error, term}
  def start_release_node(release_dir, app_env \\ []) do
    with {:ok, _pid, node} <- Slave.start(release_dir, app_env) do
      {:ok, %{node: node, scenarios: Slave.call(node, ScenarioRelease.SlaveFunc, :scenarios, [])}}
    end
  end

  @doc """
  Lists all template files defined for a particular scenario.

  It returns both templates from the scenario and overrides from the deployment. The source can be
  discriminated using the `:defined_in` key.
  """
  @spec list_templates(node(), String.t()) :: [Notification.template()]
  def list_templates(node, scenario_id) do
    Slave.call(node, Notification, :list_templates, [scenario_id])
  end

  @doc """
  Lists all notification types for a scenario.
  """
  @spec list_scenario_notification_types(node(), String.t()) ::
          {:ok, [String.t()]} | {:error, File.posix()}
  def list_scenario_notification_types(node, scenario_id) do
    Slave.call(node, Notification, :list_notification_types, [scenario_id])
  end

  @doc """
  Lists all template types of the specified notification.
  """
  @spec list_scenario_notification_template_types(node(), String.t(), String.t()) ::
          {:ok, [String.t()]} | {:error, File.posix()}
  def list_scenario_notification_template_types(node, scenario_id, notification_type) do
    Slave.call(node, Notification, :list_template_types, [scenario_id, notification_type])
  end

  @doc """
  Lists all channels with templates defined for scenario, notification type and template type.
  """
  @spec list_scenario_notification_channels(node(), String.t(), String.t(), String.t()) ::
          {:ok, [String.t()]} | {:error, File.posix()}
  def list_scenario_notification_channels(node, scenario_id, notification_type, template_type) do
    Slave.call(node, Notification, :list_channels, [scenario_id, notification_type, template_type])
  end

  @doc """
  Lists all template languages defined for scenario, notification type, template type and channel.
  """
  @spec list_scenario_notification_languages(
          node(),
          String.t(),
          String.t(),
          String.t(),
          String.t()
        ) :: {:ok, [String.t()]} | {:error, File.posix()}
  def list_scenario_notification_languages(
        node,
        scenario_id,
        notification_type,
        template_type,
        channel
      ) do
    Slave.call(node, Notification, :list_languages, [
      scenario_id,
      notification_type,
      template_type,
      channel
    ])
  end

  @doc """
  Loads and evaluates templates specified by scenario, notification type, template type, channel
  and language.

  If language is not defined or does not exist, `fallback_language` is used.
  """
  @spec load_and_eval_scenario_notification_templates(
          node(),
          scenario_id :: String.t(),
          notification_type :: String.t(),
          template_type :: String.t(),
          channel :: String.t(),
          language :: String.t(),
          data :: keyword(),
          fallback_language :: String.t()
        ) :: {:ok, %{String.t() => String.t()}} | {:error, atom(), String.t()}
  def load_and_eval_scenario_notification_templates(
        node,
        scenario_id,
        notification_type,
        template_type,
        channel,
        language,
        data,
        fallback_language
      ) do
    Slave.call(node, Notification, :load_and_eval_templates, [
      scenario_id,
      notification_type,
      template_type,
      channel,
      language,
      data,
      fallback_language
    ])
  end

  @spec get_runbox_version :: Version.t()
  def get_runbox_version do
    @runbox_version
  end
end