defmodule Aprsme.RegexCache do
  @moduledoc """
  In-memory cache for compiled regex patterns that avoids recompilation.

  Stored values carry a monotonic access counter so eviction can drop the
  least-recently-used entries instead of an arbitrary half of the table —
  previously a thrashing pattern under diverse input would churn the cache
  and keep paying compile cost on every miss.
  """

  use GenServer

  @table_name :regex_cache
  @max_cache_size 1000
  # When the cache is full, drop this fraction of least-recently-used entries.
  # 10% keeps hot entries warm instead of wiping half the cache every churn.
  @evict_fraction 0.1

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  @impl true
  def init(_) do
    # Table stores {pattern, regex, last_access_counter}
    _table = :ets.new(@table_name, [:set, :protected, :named_table, read_concurrency: true])
    {:ok, %{counter: 0}}
  end

  @doc """
  Get or compile a regex pattern. Returns {:ok, regex} or {:error, reason}.
  """
  def get_or_compile(pattern_string) do
    GenServer.call(__MODULE__, {:get_or_compile, pattern_string})
  end

  @impl true
  def handle_call({:get_or_compile, pattern_string}, _from, %{counter: counter} = state) do
    next_counter = counter + 1

    case :ets.lookup(@table_name, pattern_string) do
      [{^pattern_string, regex, _last}] ->
        # Refresh the access counter so subsequent evictions see this as recent.
        :ets.insert(@table_name, {pattern_string, regex, next_counter})
        {:reply, {:ok, regex}, %{state | counter: next_counter}}

      [] ->
        case compile_and_cache(pattern_string, next_counter) do
          {:ok, regex} -> {:reply, {:ok, regex}, %{state | counter: next_counter}}
          error -> {:reply, error, state}
        end
    end
  end

  defp compile_and_cache(pattern_string, counter) do
    case Regex.compile(pattern_string) do
      {:ok, regex} ->
        if :ets.info(@table_name, :size) >= @max_cache_size do
          evict_lru()
        end

        :ets.insert(@table_name, {pattern_string, regex, counter})
        {:ok, regex}

      error ->
        error
    end
  end

  # Evict the @evict_fraction oldest entries by access counter. This is O(n)
  # over the table, but only runs when the table is full, not on every insert.
  defp evict_lru do
    entries = :ets.tab2list(@table_name)
    target = max(1, trunc(length(entries) * @evict_fraction))

    entries
    |> Enum.sort_by(fn {_key, _regex, last} -> last end)
    |> Enum.take(target)
    |> Enum.each(fn {key, _regex, _last} -> :ets.delete(@table_name, key) end)
  end
end
