defmodule <%= inspect @app_pascal_case %>.UserTokenCleaner do
  @moduledoc """
  Periodically deletes all expired user tokens in the repo.

  `UserTokenCleaner` relies on the application's `Repo`, so it must be started after the `Repo` in the application's supervisor. See the example below.

  ## Options

  The following options may be passed to `UserTokenCleaner` via `start_link/1`:

  - `:interval_minutes` - Determines how frequently expired tokens will be deleted. The value must be a positive integer of `1` or greater. Defaults to `10`.

  ## Token Deletion

  `UserTokenCleaner` calls to the application's `Identity.delete_all_expired_tokens/0`. `UserToken` expiration is defined in the `Identity` module, and `UserTokenCleaner`'s `:interval_minutes` sets the frequency for executing the `Repo` cleanup.

  ## Example

  Start `UserTokenCleaner` with its default options:

  ```
  def start(_type, _args) do
    children = [
      MyApp.Repo,
      MyApp.UserTokenCleaner,
      ...
    ]

    opts = [strategy: :one_for_one, name: Passkeys.Supervisor]
    Supervisor.start_link(children, opts)
  end
  ```

  Start `UserTokenCleaner` with a custom interval in minutes:

  ```
  def start(_type, _args) do
    children = [
      MyApp.Repo,
      {MyApp.UserTokenCleaner, interval_minutes: 60},
      ...
    ]

    opts = [strategy: :one_for_one, name: Passkeys.Supervisor]
    Supervisor.start_link(children, opts)
  end
  ```
  """
  use GenServer
  alias <%= inspect @app_pascal_case %>.Identity
  require Logger

  defguard is_pos_integer(int) when is_integer(int) and int > 0

  @spec start_link(args :: Keyword.t()) :: GenServer.on_start()
  def start_link(args) do
    GenServer.start_link(__MODULE__, args, name: __MODULE__)
  end

  @doc """
  Manually triggers a `UserToken` cleanup.

  All expired tokens will be deleted, and the cleanup timer will be reset. Tokens will be cleaned automatically at the next `:interval_minutes` set in the GenServer's state.
  """
  @spec delete_expired_tokens :: :ok
  def delete_expired_tokens do
    Process.send(__MODULE__, :delete_expired_tokens, [])
  end

  @doc """
  Updates the interval for the cleanup process.

  The new interval will be applied to the next timer set by the cleanup process.
  """
  @spec set_interval_minutes(int :: pos_integer()) :: :ok
  def set_interval_minutes(int) when is_pos_integer(int) do
    Process.send(__MODULE__, {:update_interval_minutes, int}, [])
  end

  @doc false
  def init(args) do
    int = args[:interval_minutes] || 10
    {:ok, timer_ref} = set_timer(int)
    {:ok, [interval_minutes: int, timer_ref: timer_ref]}
  end

  defp set_timer(int) when is_pos_integer(int) do
    int
    |> :timer.minutes()
    |> :timer.apply_interval(Process, :send, [__MODULE__, :delete_expired_tokens, []])
  end

  @doc false
  def handle_info({:update_interval_minutes, int}, state) when is_pos_integer(int) do
    if ref = state[:timer_ref], do: :timer.cancel(ref)
    {:ok, timer_ref} = set_timer(int)

    new_state =
      state
      |> Keyword.put(:interval_minutes, int)
      |> Keyword.put(:timer_ref, timer_ref)

    {:noreply, new_state}
  end

  @doc false
  def handle_info(:delete_expired_tokens, state) do
    {count, _results} = Identity.delete_all_expired_tokens()
    Logger.info(expired_tokens_deleted: count)
    {:noreply, state}
  end
end
