defmodule Braintree do
  @moduledoc """
  A native Braintree client library for Elixir. Only a subset of the API is
  supported and this is a work in progress. That said, it is already used in
  production, and any modules that have been implemented can be used.

  For general reference please see:
  https://developers.braintreepayments.com/reference/overview
  """

  defmodule ConfigError do
    @moduledoc """
    Raised at runtime when a config variable is missing.
    """

    defexception [:message]

    @doc """
    Build a new ConfigError exception.
    """
    @impl true
    def exception(value) do
      message = "missing config for :#{value}"

      %ConfigError{message: message}
    end
  end

  @doc """
  Convenience function for retrieving braintree specfic environment values, but
  will raise an exception if values are missing.

  ## Example

      iex> Braintree.get_env(:random_value)
      ** (Braintree.ConfigError) missing config for :random_value

      iex> Braintree.get_env(:random_value, "random")
      "random"

      iex> Application.put_env(:braintree, :random_value, "not-random")
      ...> value = Braintree.get_env(:random_value)
      ...> Application.delete_env(:braintree, :random_value)
      ...> value
      "not-random"

      iex> System.put_env("RANDOM", "not-random")
      ...> Application.put_env(:braintree, :system_value, {:system, "RANDOM"})
      ...> value = Braintree.get_env(:system_value)
      ...> System.delete_env("RANDOM")
      ...> value
      "not-random"
  """
  @spec get_env(atom, any) :: any
  def get_env(key, default \\ nil) do
    case Application.fetch_env(:braintree, key) do
      {:ok, {:system, var}} when is_binary(var) ->
        fallback_or_raise(var, System.get_env(var), default)

      {:ok, value} ->
        value

      :error ->
        fallback_or_raise(key, nil, default)
    end
  end

  @doc """
  Convenience function for setting `braintree` application environment
  variables.

  ## Example

      iex> Braintree.put_env(:thingy, "thing")
      ...> Braintree.get_env(:thingy)
      "thing"
  """
  @spec put_env(atom, any) :: :ok
  def put_env(key, value) do
    Application.put_env(:braintree, key, value)
  end

  defp fallback_or_raise(key, nil, nil), do: raise(ConfigError, key)
  defp fallback_or_raise(_, nil, default) when is_function(default, 0), do: default.()
  defp fallback_or_raise(_, nil, default), do: default
  defp fallback_or_raise(_, value, _), do: value
end
