defmodule Cloudex.Settings do
  @moduledoc """
  This is a GenServer that contains the API settings for cloudinary
  You can interact with the `get` function to retreive the settings you want.
  """

  use GenServer

  @doc """
  Required by GenServer
  """
  def init(args) do
    {:ok, args}
  end

  @doc """
  Called by the supervisor, this will use settings defined in config.exs or ENV vars
  """
  def start(:normal, []) do
    [:api_key, :secret, :cloud_name]
    |> get_app_config
    |> Cloudex.EnvOptions.merge_missing_settings()
    |> start
  end

  @doc """
    Actually starting the GenServer with given settings
  """
  def start(%{} = settings) do
    settings
    |> Map.merge(settings)
    |> validate
    |> do_start
  end

  def do_start({:error, :placeholder_settings}),
    do: {:error, placeholder_settings_error_message()}

  def do_start({:error, _} = error), do: error

  @doc """
  Helper function to start or restart the GenServer when already started with given settings
  """
  def do_start({:ok, settings}) do
    case GenServer.start(__MODULE__, settings, name: :cloudex) do
      {:ok, pid} ->
        {:ok, pid}

      {:error, {:already_started, _pid}} ->
        stop()
        start(settings)
    end
  end

  @doc """
  Helper function to stop the GenServer
  """
  def stop, do: GenServer.call(:cloudex, :stop)

  def handle_call(:stop, _caller, state), do: {:stop, :normal, :ok, state}

  def handle_call(:settings, _caller, state), do: {:reply, state, state}

  def terminate(_reason, _state), do: :ok

  @doc """
  Get the cloudinary credentials as map.

  ## Examples

      iex> Cloudex.Settings.get
      %{api_key: "my_key", secret: "my_secret", cloud_name: "my_cloud_name"}
  """
  @spec get() :: map
  def get, do: GenServer.call(:cloudex, :settings)

  @doc """
  Get a specific cloudinary credential by key.

  ## Examples
      iex> Cloudex.Settings.get(:secret)
      "my_secret"

      iex> Cloudex.Settings.get(:bogus)
      nil
  """
  def get(key) when is_atom(key) do
    env_key = String.upcase("cloudinary_" <> Atom.to_string(key))
    System.get_env(env_key) || get_from_settings(key)
  end

  defp get_from_settings(nil), do: {:error, "key not found"}
  defp get_from_settings(key), do: GenServer.call(:cloudex, :settings)[key]

  defp get_app_config(keys, map \\ %{})
  defp get_app_config([], map), do: map

  defp get_app_config([key | keys], map) do
    new_map = Map.put(map, key, Application.get_env(:cloudex, key))
    get_app_config(keys, new_map)
  end

  defp validate(%{api_key: "placeholder", secret: "placeholder", cloud_name: "placeholder"}) do
    {:error, :placeholder_settings}
  end

  defp validate(%{api_key: api_key, secret: secret, cloud_name: cloud_name} = settings)
       when is_binary(api_key) and is_binary(secret) and is_binary(cloud_name) do
    {:ok, settings}
  end

  defp validate(settings) do
    {
      :error,
      ~s<
We received the following incorrect settings : #{inspect(settings)}
You can solve this in two ways :
add the following to your config.exs or config/[dev/test/prod]: cloudex, api_key: YOUR_CLOUDINARY_API_KEY, secret: YOUR_CLOUDINARY_SECRET, cloud_name: YOUR_CLOUDINARY_CLOUD_NAME
-or-
Set the following ENV vars CLOUDEX_API_KEY CLOUDEX_SECRET CLOUDEX_CLOUD_NAME
>
    }
  end

  defp placeholder_settings_error_message do
    "Please add the following settings to your config.exs or config/[dev/test/prod].exs : config :cloudex, api_key: YOUR_CLOUDINARY_API_KEY, secret: YOUR_CLOUDINARY_SECRET, cloud_name: YOUR_CLOUDINARY_CLOUD_NAME"
  end
end
