defmodule Glific.Caches do
  @moduledoc """
  Glific Cache management
  """
  @cache_bucket :glific_cache

  @behaviour Glific.Caches.CacheBehaviour

  # set timer limit
  @ttl_limit 24

  @doc false
  @impl Glific.Caches.CacheBehaviour
  @spec set(non_neg_integer, any(), any(), Keyword.t()) :: {:ok, any()}
  def set(organization_id, key, value, opts \\ [])

  def set(organization_id, keys, value, opts) when is_list(keys),
    do: set_to_cache(organization_id, keys, value, opts)

  @impl Glific.Caches.CacheBehaviour
  def set(organization_id, key, value, opts),
    do: set_to_cache(organization_id, [key], value, opts)

  @doc false
  @spec set_to_cache(non_neg_integer, list(), any, Keyword.t()) :: {:ok, any()}
  defp set_to_cache(organization_id, keys, value, opts) do
    keys = Enum.reduce(keys, [], fn key, acc -> [{{organization_id, key}, value} | acc] end)

    # also update the reload key for consumers to refresh caches
    keys = [{{organization_id, :cache_reload_key}, Ecto.UUID.generate()} | keys]

    default_opts = [ttl: :timer.hours(@ttl_limit)]

    {:ok, true} = Cachex.put_many(@cache_bucket, keys, Keyword.merge(default_opts, opts))
    {:ok, value}
  end

  @doc """
  Get a cached value based on a key
  """
  @impl Glific.Caches.CacheBehaviour
  @spec get(non_neg_integer, any(), Keyword.t()) :: {:ok, any()} | {:ok, false}
  def get(organization_id, key, opts \\ []) do
    case Cachex.exists?(@cache_bucket, {organization_id, key}) do
      {:ok, true} ->
        refresh_cache = Keyword.get(opts, :refresh_cache, true)
        if refresh_cache, do: Cachex.refresh(@cache_bucket, {organization_id, key})
        Cachex.get(@cache_bucket, {organization_id, key})

      _ ->
        {:ok, false}
    end
  end

  @doc """
  Get a cached value based on a key with fallback
  """
  @impl Glific.Caches.CacheBehaviour
  @spec fetch(non_neg_integer, any(), (any() -> any())) ::
          {:ok | :error | :commit | :ignore, any()}
  def fetch(organization_id, key, fallback_fn) do
    Cachex.fetch(@cache_bucket, {organization_id, key}, fallback_fn)
  end

  @doc """
  Remove a value from the cache
  """
  @impl Glific.Caches.CacheBehaviour
  @spec remove(non_neg_integer, list()) :: any()
  def remove(organization_id, keys),
    do:
      Enum.map(keys, fn key ->
        {:ok, _} = Cachex.del(@cache_bucket, {organization_id, key})
      end)

  @doc """
  Set a global value, ttl is in number of hours
  For global keys, we expect relatively short ttls
  """
  @spec put_global(any, any, non_neg_integer) :: {:ok | :error, boolean()}
  def put_global(key, value, ttl),
    do: Cachex.put(@cache_bucket, {:global, key}, value, ttl: :timer.hours(ttl))

  @doc """
  Retrieve a global value from the cache global scope
  """
  @spec get_global(any()) :: {:ok | :error, any()}
  def get_global(key),
    do: Cachex.get(@cache_bucket, {:global, key})
end
