lib/mishka_installer.ex

defmodule MishkaInstaller do
  alias MishkaInstaller.PluginState

  @spec plugin_activity(String.t(), PluginState.t(), String.t(), String.t()) ::
          :ignore | {:error, any} | {:ok, pid} | {:ok, pid, any}
  def plugin_activity(action, %PluginState{} = plugin, priority, status \\ "info") do
    MishkaInstaller.Activity.create_activity_by_start_child(
      activity_required_map("plugin", "other", action, priority, status),
      Map.drop(Map.from_struct(plugin), [:parent_pid, :extension])
    )
  end

  @spec dependency_activity(map, String.t(), String.t()) :: nil
  def dependency_activity(state, priority, status \\ "error") do
    MishkaInstaller.Activity.create_activity_by_start_child(
      activity_required_map("dependency", "compiling", "compiling", priority, status),
      Map.merge(state, %{
        state:
          Enum.map(state.state, fn item ->
            case item do
              {:error, :do_deps_compile, app, operation: operation, output: output}
              when is_struct(output) ->
                %{operation: operation, output: Map.from_struct(output), status: status, app: app}

              {:error, :do_deps_compile, app, operation: operation, output: output} ->
                %{operation: operation, output: Kernel.inspect(output), status: status, app: app}

              value ->
                value
            end
          end)
      })
    )

    nil
  end

  @spec update_activity(map, String.t(), String.t()) :: nil
  def update_activity(state, priority, status \\ "error") do
    MishkaInstaller.Activity.create_activity_by_start_child(
      activity_required_map("dependency", "updating", "updating", priority, status),
      state
    )

    nil
  end

  def ip(user_ip),
    do: (is_bitstring(user_ip) && user_ip) || Enum.join(Tuple.to_list(user_ip), ".")

  def get_config(item, section \\ :basic) do
    case Application.fetch_env(:mishka_installer, section) do
      :error -> nil
      {:ok, list} -> Keyword.fetch!(list, item)
    end
  rescue
    _e -> nil
  end

  def repo() do
    env_db = "#{System.get_env("INSTALLER_DB")}" |> String.replace("Elixir.", "")

    case MishkaInstaller.get_config(:repo) do
      nil ->
        if Mix.env() == :test,
          do: Ecto.Integration.TestRepo,
          else:
            if(env_db != "",
              do: String.to_atom("Elixir.#{env_db}"),
              else: ensure_compiled(nil)
            )

      value ->
        value
    end
  end

  def gettext() do
    case MishkaInstaller.get_config(:gettext) do
      nil -> MishkaInstaller.Gettext
      value -> value
    end
  rescue
    _ -> MishkaInstaller.Gettext
  end

  def ensure_compiled(module) do
    case Code.ensure_compiled(module) do
      {:module, _} ->
        module

      {:error, _} ->
        raise "This module does not exist or is not configured if it is related to Ecto. Please read the documentation at GitHub. \n Output: #{inspect(module)}."
    end
  end

  # Ref: https://elixirforum.com/t/how-to-start-oban-out-of-application-ex/48417/6
  # Ref: https://elixirforum.com/t/cant-start-oban-as-cron-every-minute/48459/5
  @spec start_oban_in_runtime(nil | list()) ::
          :ignore | {:error, any} | {:ok, pid} | {:ok, pid, any}
  def start_oban_in_runtime(opts \\ MishkaInstaller.get_config(:oban_config)) do
    oban_opts = [
      repo: MishkaInstaller.repo(),
      queues: [compile_events: [limit: 1], update_events: [limit: 1]],
      plugins: [
        Oban.Plugins.Pruner,
        {Oban.Plugins.Cron,
         crontab: [
           {"*/5 * * * *", MishkaInstaller.DepUpdateJob}
         ]}
      ]
    ]

    DynamicSupervisor.start_child(
      MishkaInstaller.RunTimeObanSupervisor,
      {Oban, opts || oban_opts}
    )
  end

  defp activity_required_map(type, section, action, priority, status) do
    %{
      type: type,
      section: section,
      section_id: nil,
      action: action,
      priority: priority,
      status: status,
      user_id: nil
    }
  end

  def checksum(file_path) do
    File.stream!(file_path, [], 2048)
    |> Enum.reduce(:crypto.hash_init(:sha256), fn line, acc -> :crypto.hash_update(acc, line) end)
    |> :crypto.hash_final()
    |> Base.encode16()
    |> String.downcase()
  end

  def trim_url(url) do
    String.trim(
      if(String.ends_with?(url, "/"), do: String.replace_trailing(url, "/", ""), else: url)
    )
  end
end