defmodule Tarakan.AnalyticsCache do
  @moduledoc """
  Short-lived caching for public aggregate queries.

  The posture, model and suppression aggregates are grouped scans over large
  tables whose results are identical for every caller, and they sit behind
  endpoints anyone can call without an account - an embedded badge, a public
  page, the memory response every scanning client fetches. Without caching,
  each of those is a query amplifier: one cheap request, one expensive scan.

  Reads go through `Tarakan.RepositoryCode.Cache`, which already coalesces
  concurrent misses so a cold key under load runs the query once rather than
  once per caller. That coalescing is the actual defense; the TTL just keeps
  the steady state cheap.

  A refused fetch returns the caller's `:on_unavailable` value rather than
  computing directly. Falling back to the query would reintroduce exactly the
  amplification the cache exists to prevent, and these are all surfaces where
  a degraded answer is acceptable.

  Set the TTL to zero (as the test environment does) to bypass caching
  entirely, so results never leak between cases.
  """

  alias Tarakan.RepositoryCode.Cache

  @default_ttl_ms 60_000

  @doc """
  Returns a cached value, computing it at most once per TTL across callers.

  `opts`:

    * `:ttl_ms` - override the configured TTL for this key.
    * `:on_unavailable` - returned when the cache refuses the work
      (default `nil`).
  """
  def fetch(key, compute, opts \\ []) when is_function(compute, 0) do
    ttl_ms = Keyword.get(opts, :ttl_ms, configured_ttl())

    if ttl_ms > 0 do
      case Cache.fetch(key, ttl_ms, fn -> {:ok, compute.()} end) do
        {:ok, value} -> value
        {:error, _overloaded} -> Keyword.get(opts, :on_unavailable)
      end
    else
      compute.()
    end
  end

  @doc "The configured TTL in milliseconds; zero disables caching."
  def configured_ttl do
    :tarakan
    |> Application.get_env(__MODULE__, [])
    |> Keyword.get(:ttl_ms, @default_ttl_ms)
  end
end
