defmodule Tarearbol do
  @moduledoc """
  `Tarearbol` module provides an interface to run tasks in easy way.

  ## Examples

      iex> result = Tarearbol.ensure(fn -> raise "¡?" end, attempts: 1, raise: false)
      iex> {:error, %{job: _job, outcome: outcome}} = result
      iex> {error, _stacktrace} = outcome
      iex> error
      %RuntimeError{message: "¡?"}
  """

  use Boundary,
    deps: [Application],
    exports: [
      Errand,
      DynamicManager,
      Job,
      Jobs,
      InternalWorker
    ]

  @doc """
  Ensures the task to be completed; restarts it when necessary.

  Possible options:
  - `attempts` [_default:_ `:infinity`] Might be any of `@Tarearbol.Utils.interval`
    type (`5` for five attempts, `:random` for the random amount etc)
  - `delay` [_default:_ `1 msec`]. Might be any of `@Tarearbol.Utils.interval`
    type (`1_000` or `1.0` for one second, `:timeout` for five seconds etc)
  - `on_success` [_default:_ `nil`], the function to be called on successful
    execution (`arity ∈ [0, 1]` or tuple `{Mod, fun}` where `fun` is of arity
    zero or one.) When the arity of given function is `1`, the result of
    task execution is passed
  - `on_retry` [_default:_ `nil`], same as above, called on retries after
    insuccessful attempts **or** one of `[:debug, :info, :warn, :error]` atoms
    to log a retry with default logger
  - `on_fail` [_default:_ `nil`], same as above, called when the task finally
    failed after `attempts` amount of insuccessful attempts
  """
  @spec ensure((-> any()) | {atom(), atom(), list()}, keyword()) ::
          {:error, any} | {:ok, any}
  def ensure(job, opts \\ []), do: Tarearbol.Job.ensure(job, opts)

  @doc """
  Same as `Tarearbol.ensure/2`, but it raises on fail and returns the result
    itself on successful execution.
  """
  @spec ensure!((-> any()) | {atom(), atom(), list()}, keyword()) ::
          {:error, any} | {:ok, any}
  def ensure!(job, opts \\ []), do: Tarearbol.Job.ensure!(job, opts)

  @doc "Spawns an ensured job asynchronously, passing all options given."
  @spec spawn_ensured((-> any()) | {atom(), atom(), list()}, keyword()) :: Task.t()
  def spawn_ensured(job, opts),
    do: Tarearbol.Errand.run_in(job, :none, Keyword.merge(opts, sidekiq: true, on_retry: :warn))

  @doc "Wrapper for [`Task.Supervisor.async_stream/4`](https://hexdocs.pm/elixir/Task.Supervisor.html#async_stream/4)."
  @spec ensure_all_streamed([(-> any()) | {atom(), atom(), list()}], keyword()) ::
          Enumerable.t()
  def ensure_all_streamed(jobs, opts \\ []),
    do: Tarearbol.Jobs.ensure_all_streamed(jobs, opts)

  @doc "Executes `Tarearbol.ensure_all_streamed/2` and collects tasks results."
  @spec ensure_all([(-> any()) | {atom(), atom(), list()}], keyword()) :: [
          {:error, any} | {:ok, any}
        ]
  def ensure_all(jobs, opts \\ []), do: Tarearbol.Jobs.ensure_all(jobs, opts)

  @doc """
  Runs a task specified by the first argument in a given interval.

  See [`Tarearbol.ensure/2`] for all possible variants of the `interval` argument.
  """
  @spec run_in(
          (-> any()) | {atom(), atom(), list()},
          atom() | integer() | float(),
          keyword()
        ) :: Task.t()
  def run_in(job, interval, opts \\ []), do: Tarearbol.Errand.run_in(job, interval, opts)

  @doc """
  Runs a task specified by the first argument at a given time.

  If the second parameter is a [`DateTime`] struct, the task will be run once.
  If the second parameter is a [`Time`] struct, the task will be run at that time
    on daily basis.
  """
  @spec run_at(
          (-> any()) | {atom(), atom(), list()},
          DateTime.t() | String.t(),
          keyword()
        ) :: Task.t()
  def run_at(job, at, opts \\ []), do: Tarearbol.Errand.run_at(job, at, opts)

  @doc "Spawns the task for the immediate async execution."
  @spec spawn((-> any()) | {atom(), atom(), list()}, keyword()) :: Task.t()
  def spawn(job, opts \\ []), do: Tarearbol.Errand.spawn(job, opts)

  @doc "Executes all the scheduled tasks immediately, cleaning up the queue."
  @spec drain() :: [{:error, any} | {:ok, any}]
  def drain(jobs \\ Tarearbol.Application.jobs())
  def drain([]), do: []

  def drain(jobs) do
    _ = Tarearbol.Application.kill()
    Enum.map(jobs, &Tarearbol.ensure/1)
  end
end
