defmodule Services.Once do
  @moduledoc """
  This module provides a mechanism to perform actions only once, using a unique
  key provided by the caller to determine whether the action has already been
  performed this session.
  """

  use Agent

  @doc """
  Starts the agent that keeps track of seen keys.
  """
  def start_link() do
    Agent.start_link(fn -> Map.new() end, name: __MODULE__)
  end

  @doc """
  Checks if a key has been seen before. If the key has not been seen, it
  returns `{:error, :not_seen}`. If the key has been seen, it returns `{:ok,
  value}` where `value` is the value associated with the key, or `true` if no
  value was specified.
  """
  def get(key) do
    Agent.get(__MODULE__, fn seen ->
      with {:ok, value} <- Map.fetch(seen, key) do
        {:ok, value}
      else
        _ -> {:error, :not_seen}
      end
    end)
  end

  @doc """
  Marks a key as seen. If the key has not been seen before, it returns `true`
  and updates the internal state. If the key has already been seen, it returns
  `false` without updating the state.
  """
  def set(key, value \\ true) do
    Agent.get_and_update(__MODULE__, fn seen ->
      if Map.has_key?(seen, key) do
        {false, seen}
      else
        {true, Map.put(seen, key, value)}
      end
    end)
  end

  @doc """
  Emits a warning (using `UI.warn/1`) if the message has not yet been emitted
  during this session.
  """
  def warn(msg) do
    if set(msg) do
      UI.warn(msg)
    end

    :ok
  end

  @doc """
  Run the given zero-arity function only once per unique key.
  Subsequent calls with the same key will be ignored.
  """
  @spec run(term(), (-> any())) :: any() | :ignore
  def run(key, fun) when is_function(fun, 0) do
    case set(key) do
      true -> fun.()
      false -> :ignore
    end
  end
end
