defmodule Skogsra.Core do
  @moduledoc """
  This module defines the core API for Skogsra.
  """
  alias Skogsra.Binding
  alias Skogsra.Cache
  alias Skogsra.Env

  ############
  # Public API

  @doc """
  Gets the value of a given `env`.
  """
  @spec get_env(Env.t()) :: {:ok, term()} | {:error, binary()}
  def get_env(env)

  def get_env(%Env{} = env) do
    if Env.cached?(env) do
      get_cached(env)
    else
      fsm_entry(env)
    end
  end

  @doc """
  Gets the value of a given `env`. Fails on error.
  """
  @spec get_env!(Env.t()) :: term() | no_return()
  def get_env!(env)

  def get_env!(%Env{} = env) do
    case get_env(env) do
      {:ok, value} ->
        value

      {:error, reason} ->
        raise reason
    end
  end

  @doc """
  Puts a new value for an `env`.
  """
  @spec put_env(Env.t(), term()) :: :ok | {:error, binary()}
  def put_env(env, value)

  def put_env(%Env{} = env, value) do
    if Env.cached?(env) do
      Cache.put_env(env, value)
    else
      {:error, "Cache disable for this variable"}
    end
  end

  @doc """
  Reloads an `env` variable.
  """
  @spec reload_env(Env.t()) :: {:ok, term()} | {:error, binary()}
  def reload_env(env)

  def reload_env(%Env{} = env) do
    case fsm_entry(env) do
      {:ok, value} ->
        if Env.cached?(env), do: Cache.put_env(env, value)
        {:ok, value}

      _ ->
        {:error, "Cannot reload the variable. Keeping last value."}
    end
  end

  #########
  # Helpers

  @doc false
  @spec fsm_entry(Env.t()) :: {:ok, term()} | {:error, binary()}
  def fsm_entry(env)

  def fsm_entry(%Env{} = env) do
    order = Env.binding_order(env)
    default = get_default(env)

    Enum.reduce_while(order, default, fn binding, default ->
      case Binding.get_env(binding, env) do
        nil ->
          {:cont, default}

        value ->
          {:halt, {:ok, value}}
      end
    end)
  end

  @doc false
  @spec get_cached(Env.t()) :: {:ok, term()} | {:error, binary()}
  def get_cached(env)

  def get_cached(%Env{} = env) do
    with :error <- Cache.get_env(env),
         {:ok, value} <- fsm_entry(env),
         :ok <- Cache.put_env(env, value) do
      {:ok, value}
    end
  end

  @doc false
  @spec get_default(Env.t()) :: {:ok, term()} | {:error, binary()}
  def get_default(env)

  def get_default(%Env{namespace: nil} = env) do
    case {Env.default(env), Env.required?(env)} do
      {nil, true} ->
        {:error, format_missing_var_error(env)}

      {value, _} ->
        {:ok, value}
    end
  end

  def get_default(%Env{} = env) do
    get_env(%Env{env | namespace: nil})
  end

  @spec format_missing_var_error(Env.t()) :: binary()
  defp format_missing_var_error(env) do
    keys = Enum.join(env.keys, ", ")

    if length(env.keys) > 1 do
      "Variables #{keys} in app #{env.app_name} are undefined"
    else
      "Variable #{keys} in app #{env.app_name} is undefined"
    end
  end
end
