defmodule Berrypod.Pages.PageCache do
  @moduledoc """
  GenServer that maintains an ETS table for caching page definitions.

  Same pattern as `Berrypod.Theme.CSSCache`. Pages are cached by slug
  for O(1) lookups. Invalidated on save, warmed on startup.
  """

  use GenServer

  @table_name :page_cache

  ## Client API

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

  @doc "Gets a cached page by slug. Returns `{:ok, page_data}` or `:miss`."
  def get(slug) do
    case :ets.lookup(@table_name, slug) do
      [{^slug, page_data}] -> {:ok, page_data}
      [] -> :miss
    end
  end

  @doc "Caches a page definition by slug."
  def put(slug, page_data) do
    :ets.insert(@table_name, {slug, page_data})
    :ok
  end

  @doc "Invalidates a single cached page."
  def invalidate(slug) do
    :ets.delete(@table_name, slug)
    :ok
  rescue
    ArgumentError -> :ok
  end

  @doc "Invalidates all cached pages."
  def invalidate_all do
    :ets.delete_all_objects(@table_name)
    :ok
  rescue
    ArgumentError -> :ok
  end

  @doc "Warms the cache by loading all pages from the DB."
  def warm do
    alias Berrypod.Pages
    alias Berrypod.Pages.Page

    for slug <- Page.system_slugs() do
      page = Pages.get_page_from_db(slug)
      put(slug, page)
    end

    import Ecto.Query

    custom_slugs =
      Berrypod.Repo.all(from p in Page, where: p.type == "custom", select: p.slug)

    for slug <- custom_slugs do
      case Pages.get_page_from_db(slug) do
        nil -> :ok
        page -> put(slug, page)
      end
    end

    :ok
  end

  ## Server callbacks

  @impl true
  def init(_opts) do
    :ets.new(@table_name, [
      :set,
      :public,
      :named_table,
      read_concurrency: true,
      write_concurrency: false
    ])

    {:ok, %{}, {:continue, :warm}}
  end

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

    {:noreply, state}
  end
end
