lib/plugin_manager/state/plugin_state.ex

defmodule MishkaInstaller.PluginState do
  @moduledoc """
  This module served as the first layer in the domain of plugin state management in earlier versions;
  however, following optimization using ETS, it is now regarded as the main structure in the second layer.

  If a developer chooses to construct a custom plugin by using the `use MishkaInstaller.Hook` directive,
  then the developer actually implements this module as the first layer to register a plugin and the second layer as a supervisor.
  The scope of a plugin's state management is not restricted to the automatically specified duties performed by the `MishkaInstaller`;
  for instance, it provides developers with a temporary state and offers them the freedom to act in accordance with whatever method they choose.


  ### Each plugin should have these parameters

  ```elixir
    defstruct [:name, :event, priority: 1, status: :started, depend_type: :soft, depends: [], extra: [], parent_pid: nil]
  ```


  ### Module communication process of MishkaInstaller.PluginState

  ```
         +------------------------------------------------+
         |                                                |
         |                                                |
  +------+  MishkaInstaller.PluginStateDynamicSupervisor  |
  |      |                                                |
  |      |                                                |
  |      +--------------------------------------+----^----+
  |                                             |    |
  |                                             |    |
  |                                             |    |
  |      +------------------------+             |    |
  |      |                        |             |    |
  +------>  PluginStateRegistry   |             |    |
         |                        |             |    |
         +--------^---------------+             |    |
                  |                             |    |
                  |                             |    |
                  |                             |    |
                  |                             |    |
                  |        +--------------------v----+-----+
                  |        |                               |
                  |        |  MishkaInstaller.PluginState  |
                  +--------+                               |
                           +--------------------+--------^-+
                                                |        |
                                                |        |
                                                |        |
                                                |        |
            +----------------------------+      |        |
            |                            |      |        |
            | MishkaInstaller.PluginETS  <------+        |
            |                            |               |
            +----------------------------+               |
                                                         |
                                                         |
                                                         |
                                    +--------------------+-+
                                    |                      |
                      +-------------+ MishkaInstaller.Hook |
                      |             |                      |
                      |             +----------------------+
                      |
                      |
            +---------v----------------------+
            |                                |
            |     Behaviour References       |
            |                                |
            +--------------------------------+
  ```
  """

  use GenServer
  require Logger
  alias MishkaInstaller.PluginStateDynamicSupervisor, as: PSupervisor
  alias MishkaInstaller.Plugin
  alias __MODULE__

  defstruct [
    :name,
    :event,
    :extension,
    priority: 1,
    status: :started,
    depend_type: :soft,
    depends: [],
    extra: [],
    parent_pid: nil
  ]

  @typedoc "This type can be used when you want to introduce the parameters of a plugin"
  @type params() :: map()
  @typedoc "This type can be used when you want to introduce an ID of a plugin"
  @type id() :: String.t()
  @typedoc "This type can be used when you want to introduce an plugin name"
  @type module_name() :: String.t()
  @typedoc "This type can be used when you want to introduce an event name"
  @type event_name() :: String.t()
  @typedoc "This type can be used when you want to introduce an event owner extension"
  @type extension() :: String.t()
  @typedoc "This type can be used when you want to introduce an event"
  @type plugin() :: %PluginState{
          name: module_name(),
          event: event_name(),
          extension: module(),
          priority: integer(),
          status: :started | :stopped | :restarted,
          depend_type: :soft | :hard,
          parent_pid: any(),
          depends: list(String.t()),
          extra: list(map())
        }
  @typedoc "This type can be used when you want to introduce an event"
  @type t :: plugin()

  @doc """
  You should in no way use this function in its direct form. The supervisor coverage needs to run before using this function since
  it will establish a state for your plugin and save its PID in the registry.
  Make use of the function named `MishkaInstaller.PluginStateDynamicSupervisor.start_job/1`.
  """
  def start_link(args) do
    {id, type, parent_pid} =
      {Map.get(args, :id), Map.get(args, :type), Map.get(args, :parent_pid)}

    GenServer.start_link(__MODULE__, default(id, type, parent_pid), name: via(id, type))
  end

  @doc false
  def child_spec(process_name) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [process_name]},
      restart: :transient,
      max_restarts: 4
    }
  end

  defp default(plugin_name, event, parent_pid) do
    %PluginState{name: plugin_name, event: event, parent_pid: parent_pid}
  end

  @doc """
  This function helps you to create the state of a plugin. Please see `t:plugin/0` type documents. This function does not wait for a response.

  ## Examples
  ```elixir
  plugin =
    %MishkaInstaller.PluginState{
      name: "unnested_plugin_five",
      event: "nested_event_one",
      depend_type: :hard,
      depends: ["unnested_plugin_four"]
    }
  MishkaInstaller.PluginState.push(plugin)
  ```
  """
  @spec push(MishkaInstaller.PluginState.t()) :: :ok | {:error, :push, any}
  def push(%PluginState{} = element) do
    case PSupervisor.start_job(%{
           id: element.name,
           type: element.event,
           parent_pid: element.parent_pid
         }) do
      {:ok, status, pid} -> GenServer.cast(pid, {:push, status, element})
      {:error, result} -> {:error, :push, result}
    end
  end

  @doc """
  This function does the same thing as the `push/1` function, except that it waits for a response.
  """
  @spec push_call(MishkaInstaller.PluginState.t()) :: :ok | {:error, :push, any}
  def push_call(%PluginState{} = element) do
    case PSupervisor.start_job(%{
           id: element.name,
           type: element.event,
           parent_pid: element.parent_pid
         }) do
      {:ok, status, pid} ->
        if Mix.env() == :test, do: Logger.warn("Plugin State of #{element.name} is being pushed")
        GenServer.call(pid, {:push, status, element})

      {:error, result} ->
        {:error, :push, result}
    end
  end

  @doc """
  It gets a plugin information from the state.

  ## Examples
  ```elixir
  MishkaInstaller.PluginState.get(module: "PluginTest")
  ```
  """
  @spec get([{:module, module_name()}]) :: plugin() | {:error, :get, :not_found}
  def get(module: module_name) do
    case PSupervisor.get_plugin_pid(module_name) do
      {:ok, :get_plugin_pid, pid} -> GenServer.call(pid, {:pop, :module})
      {:error, :get_plugin_pid} -> {:error, :get, :not_found}
    end
  end

  @doc """
  This function gets all information of plugins which are under a specific event.

  ## Examples
  ```elixir
  MishkaInstaller.PluginState.get_all(event: "event_test")
  ```
  """
  def get_all(event: event_name) do
    PSupervisor.running_imports(event_name) |> Enum.map(&get(module: &1.id))
  end

  @doc """
  This function gets all information of plugins which are pushed on the state.

  ## Examples
  ```elixir
  MishkaInstaller.PluginState.get_all()
  ```
  """
  def get_all() do
    PSupervisor.running_imports() |> Enum.map(&get(module: &1.id))
  end

  @doc """
  Delete a plugin or plugins based on a specific event from the state.

  ## Examples
  ```elixir
  MishkaInstaller.PluginState.delete(module: "PluginTest")
  # or
  MishkaInstaller.PluginState.delete(event: "EventTest")
  ```
  """
  def delete(module: module_name) do
    case PSupervisor.get_plugin_pid(module_name) do
      {:ok, :get_plugin_pid, pid} ->
        GenServer.cast(pid, {:delete, :module})
        {:ok, :delete}

      {:error, :get_plugin_pid} ->
        {:error, :delete, :not_found}
    end
  end

  def delete(event: event_name) do
    PSupervisor.running_imports(event_name) |> Enum.map(&delete(module: &1.id))
  end

  @doc """
  Terminate a PID from the supervisor directly.

  ## Examples
  ```elixir
  MishkaInstaller.PluginState.delete_child(module: "PluginTest")
  ```
  """
  def delete_child(module: module_name) do
    case PSupervisor.get_plugin_pid(module_name) do
      {:ok, :get_plugin_pid, pid} -> DynamicSupervisor.terminate_child(PluginStateOtpRunner, pid)
      {:error, :get_plugin_pid} -> {:error, :delete, :not_found}
    end
  end

  @doc """
  Terminate all PIDs of the plugin state from the supervisor directly.

  ## Examples
  ```elixir
  MishkaInstaller.PluginState.terminate_all_pids()
  ```
  """
  def terminate_all_pids() do
    Enum.map(PSupervisor.running_imports(), fn item ->
      GenServer.cast(item.pid, {:delete, :module})
    end)
  end

  @doc """
  Stop a plugin state.

  ## Examples
  ```elixir
  MishkaInstaller.PluginState.stop(module: PluginTest)
  # or
  MishkaInstaller.PluginState.stop(event: "event_test")
  ```
  """
  def stop(module: module_name) do
    case PSupervisor.get_plugin_pid(module_name) do
      {:ok, :get_plugin_pid, pid} ->
        GenServer.cast(pid, {:stop, :module})
        {:ok, :stop}

      {:error, :get_plugin_pid} ->
        {:error, :stop, :not_found}
    end
  end

  def stop(event: event_name) do
    PSupervisor.running_imports(event_name) |> Enum.map(&stop(module: &1.id))
  end

  # Callbacks
  @impl true
  def init(%PluginState{} = state) do
    if Mix.env() == :test, do: MishkaInstaller.Database.Helper.get_parent_pid(state)

    Logger.info(
      "#{Map.get(state, :name)} from #{Map.get(state, :event)} event of Plugins manager system was started"
    )

    {:ok, state, {:continue, {:sync_with_database, :take}}}
  end

  @impl true
  def handle_call({:pop, :module}, _from, %PluginState{} = state) do
    {:reply, state, state}
  end

  @impl true
  def handle_call({:push, _status, %PluginState{} = element}, _from, %PluginState{} = _state) do
    element
    |> Map.from_struct()
    |> Plugin.add_or_edit_by_name()

    {:reply, element, element}
  end

  @impl true
  def handle_cast({:push, status, %PluginState{} = element}, _state) do
    {:noreply, element, {:continue, {:sync_with_database, status}}}
  end

  @impl true
  def handle_cast({:stop, :module}, %PluginState{} = state) do
    new_state = Map.merge(state, %{status: :stopped})
    {:noreply, new_state, {:continue, {:sync_with_database, :edit}}}
  end

  @impl true
  def handle_cast({:delete, :module}, %PluginState{} = state) do
    MishkaInstaller.plugin_activity("destroy", state, "high", "report")
    {:stop, :normal, state}
  end

  @impl true
  def handle_continue({:sync_with_database, _status}, %PluginState{} = state) do
    state
    |> Map.from_struct()
    |> Plugin.add_or_edit_by_name()
    |> check_output(state)

    {:noreply, state}
  end

  @impl true
  def handle_continue({:sync_with_database, :take}, %PluginState{} = state) do
    state =
      case Plugin.show_by_name("#{state.name}") do
        {:ok, :get_record_by_field, _error_atom, record_info} ->
          struct(__MODULE__, Map.from_struct(record_info))

        {:error, _result, _error_atom} ->
          state
      end

    {:noreply, state}
  end

  if Mix.env() in [:test, :dev] do
    @impl true
    def handle_info({:DOWN, _ref, :process, _pid, _reason}, state) do
      # log that this happened, etc. Don't use Repo!
      {:stop, :normal, state}
    end
  end

  @impl true
  def terminate(reason, %PluginState{} = state) do
    MishkaInstaller.plugin_activity("read", state, "high", "throw")

    if reason != :normal do
      Task.Supervisor.start_child(PluginEtsTask, fn ->
        MishkaInstaller.PluginETS.sync_with_database()
      end)

      Logger.warn(
        "#{Map.get(state, :name)} from #{Map.get(state, :event)} event of Plugins manager was Terminated,
        Reason of Terminate #{inspect(reason)}"
      )
    end
  end

  defp via(id, value) do
    {:via, Registry, {PluginStateRegistry, id, value}}
  end

  defp check_output({:error, status, _, _} = _output, %PluginState{} = state) do
    MishkaInstaller.plugin_activity("#{status}", state, "high", "error")
  end

  defp check_output({:ok, status, _, _} = _output, %PluginState{} = state) do
    action =
      cond do
        status == :add -> "add"
        state.status == :stopped -> "delete"
        true -> "edit"
      end

    MishkaInstaller.plugin_activity(action, state, "high")
  end
end