defmodule Berrypod.Settings.SettingsCache do
  @moduledoc """
  GenServer that maintains an ETS table for caching settings and computed results.

  Settings are loaded once from the DB on startup and served from ETS on every
  request. The cache is invalidated (and immediately rewarmed) whenever a setting
  is written via `Settings.put_setting/3` or `Settings.delete_setting/1`.

  Also supports caching computed results (categories, shipping countries, etc.)
  under `{:cached, key}` tuple keys. These are populated lazily and invalidated
  explicitly when the underlying data changes.
  """

  use GenServer

  @table_name :settings_cache

  ## Client API — settings

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc """
  Creates the ETS table. Called from application.ex so the supervisor
  process owns the table — it survives GenServer crashes.
  """
  def create_table do
    if :ets.whereis(@table_name) == :undefined do
      :ets.new(@table_name, [
        :set,
        :public,
        :named_table,
        read_concurrency: true,
        write_concurrency: false
      ])
    end

    @table_name
  end

  @doc """
  Gets a cached setting by key.

  Returns `{:ok, value}` if found, or `:miss` if the cache hasn't been warmed
  or the key doesn't exist.
  """
  def get(key) do
    case :ets.lookup(@table_name, :all_settings) do
      [{:all_settings, map}] ->
        case Map.fetch(map, key) do
          {:ok, value} -> {:ok, value}
          :error -> :miss
        end

      [] ->
        :miss
    end
  rescue
    ArgumentError -> :miss
  end

  @doc """
  Gets a cached setting by key, returning a default on miss.
  """
  def get(key, default) do
    case get(key) do
      {:ok, value} -> value
      :miss -> default
    end
  end

  @doc """
  Invalidates the settings map, forcing a reload on next warm.
  """
  def invalidate do
    :ets.delete(@table_name, :all_settings)
    :ok
  rescue
    ArgumentError -> :ok
  end

  @doc """
  Warms the cache by loading all non-encrypted settings from the DB.
  """
  def warm do
    alias Berrypod.Repo
    alias Berrypod.Settings.Setting
    import Ecto.Query

    settings = Repo.all(from s in Setting, where: s.value_type != "encrypted")

    map =
      Map.new(settings, fn setting ->
        {setting.key, decode_value(setting)}
      end)

    :ets.insert(@table_name, {:all_settings, map})
    :ok
  end

  ## Client API — computed results (categories, countries, etc.)

  @doc """
  Gets a cached computed result by key.

  Returns `{:ok, value}` if found, or `:miss` if not yet cached.
  """
  def get_cached(key) do
    case :ets.lookup(@table_name, {:cached, key}) do
      [{{:cached, ^key}, value}] -> {:ok, value}
      [] -> :miss
    end
  rescue
    ArgumentError -> :miss
  end

  @doc """
  Caches a computed result under the given key.
  """
  def put_cached(key, value) do
    :ets.insert(@table_name, {{:cached, key}, value})
    :ok
  rescue
    ArgumentError -> :ok
  end

  @doc """
  Invalidates a single cached computed result.
  """
  def invalidate_cached(key) do
    :ets.delete(@table_name, {:cached, key})
    :ok
  rescue
    ArgumentError -> :ok
  end

  @doc """
  Clears all cached data (settings and computed results). For tests.
  """
  def invalidate_all do
    :ets.delete_all_objects(@table_name)
    :ok
  rescue
    ArgumentError -> :ok
  end

  ## Server Callbacks

  @impl true
  def init(_opts) do
    # Table is created in application.ex so it survives GenServer restarts
    create_table()
    {:ok, %{}, {:continue, :warm}}
  end

  @impl true
  def handle_continue(:warm, state) do
    try do
      warm()
    rescue
      _ -> :ok
    end

    {:noreply, state}
  end

  ## Private — mirrors Settings.decode_value/1 for non-encrypted types

  defp decode_value(%{value: value, value_type: "json"}), do: Jason.decode!(value)
  defp decode_value(%{value: value, value_type: "integer"}), do: String.to_integer(value)
  defp decode_value(%{value: value, value_type: "boolean"}), do: value == "true"
  defp decode_value(%{value: value, value_type: "string"}), do: value
end
