defmodule BerrypodWeb.R do
  @moduledoc """
  Runtime URL paths with ETS caching. Short name for ergonomic use.

  This module provides functions for generating URLs that respect custom URL slugs
  and route prefixes configured by the shop owner.

  ## Usage

      R.cart()              # => "/basket" (if cart is renamed to basket)
      R.product(123)        # => "/p/123" (if products prefix is "p")
      R.url(R.product(123)) # => "https://shop.com/p/123"

  ## Page slugs vs route prefixes

  - **Page slugs** are single-segment paths like `/cart`, `/about`, `/contact`
  - **Route prefixes** are multi-segment paths like `/products/:id`, `/collections/:slug`

  Page slugs are stored on individual Page records. Route prefixes are stored in Settings.

  ## Routing

  The router uses three catch-all routes (`/`, `/:prefix/:id_or_slug`, `/:slug`) that
  delegate to `Shop.Page`, which resolves the actual page type at runtime using
  `page_type_from_slug/1` and `prefix_type_from_segment/1`. See the router's
  "Dynamic Shop Routes" comment block for the full list of handled pages.

  To add a new system page type:
  1. Add a clause to `default_page_type/1` and `default_slug/1`
  2. Add the page module to `@page_modules` in `Shop.Page`
  """

  use GenServer

  alias Berrypod.{Pages, Settings}

  @table __MODULE__

  # =============================================================================
  # Client API - Path functions
  # =============================================================================

  @doc "Returns the home page path (always /)."
  def home, do: "/"

  @doc "Returns the cart page path."
  def cart, do: "/" <> page_slug(:cart)

  @doc "Returns the about page path."
  def about, do: "/" <> page_slug(:about)

  @doc "Returns the contact page path."
  def contact, do: "/" <> page_slug(:contact)

  @doc "Returns the search page path."
  def search, do: "/" <> page_slug(:search)

  @doc "Returns the delivery page path."
  def delivery, do: "/" <> page_slug(:delivery)

  @doc "Returns the privacy page path."
  def privacy, do: "/" <> page_slug(:privacy)

  @doc "Returns the terms page path."
  def terms, do: "/" <> page_slug(:terms)

  @doc "Returns the checkout success page path."
  def checkout_success, do: "/" <> page_slug(:checkout_success)

  @doc "Returns the orders page path."
  def orders, do: "/" <> page_slug(:orders)

  @doc """
  Returns the path for any page by slug.

  This is a generic version that works with any page slug, including custom pages.
  Prefer the specific functions (cart/0, about/0, etc.) when the page is known.
  """
  def page("home"), do: "/"
  def page("cart"), do: cart()
  def page("about"), do: about()
  def page("contact"), do: contact()
  def page("search"), do: search()
  def page("delivery"), do: delivery()
  def page("privacy"), do: privacy()
  def page("terms"), do: terms()
  def page("checkout_success"), do: checkout_success()
  def page("orders"), do: orders()
  def page(slug) when is_binary(slug), do: "/" <> slug

  @doc "Returns the path for a specific product."
  def product(id), do: "/" <> prefix(:products) <> "/" <> to_string(id)

  @doc "Returns the path for a specific collection."
  def collection(slug), do: "/" <> prefix(:collections) <> "/" <> slug

  @doc "Returns the path for a specific order."
  def order(order_number), do: "/" <> prefix(:orders) <> "/" <> order_number

  @doc "Returns a full URL (with scheme and host) for the given path."
  def url(path), do: BerrypodWeb.Endpoint.url() <> path

  # =============================================================================
  # Reverse lookups (for router)
  # =============================================================================

  @doc """
  Given a URL slug, returns the page type it maps to.

  Returns:
  - `{:page, :cart}` for system pages
  - `{:custom, page}` for custom pages
  - `nil` if not found
  """
  def page_type_from_slug(slug) do
    # First check if it's a custom URL slug for a system page
    case lookup({:reverse_slug, slug}) do
      {:ok, page_type} ->
        {:page, page_type}

      :not_found ->
        # Check if it's a default slug for a system page
        case default_page_type(slug) do
          nil ->
            # Check if it's a custom page
            case Pages.get_published_page_by_effective_url(slug) do
              nil -> nil
              page -> {:custom, page}
            end

          page_type ->
            {:page, page_type}
        end
    end
  end

  @doc """
  Given a URL segment, returns the prefix type it maps to.

  Returns `:products`, `:collections`, `:orders`, or `nil`.
  """
  def prefix_type_from_segment(segment) do
    case lookup({:reverse_prefix, segment}) do
      {:ok, type} -> type
      :not_found -> default_prefix_type(segment)
    end
  end

  @doc """
  Returns the current prefix for a given type.

  Used by the DynamicRoutes plug to check if a custom prefix is set.

  ## Examples

      R.prefix(:products)     # => "p" (if customised) or "products" (default)
      R.prefix(:collections)  # => "c" (if customised) or "collections" (default)
  """
  def prefix(prefix_type) do
    key = {:prefix, prefix_type}

    case lookup(key) do
      {:ok, prefix} -> prefix
      :not_found -> default_prefix(prefix_type)
    end
  end

  # =============================================================================
  # Cache management
  # =============================================================================

  @doc "Invalidates a specific cache key."
  def invalidate(key) do
    GenServer.cast(__MODULE__, {:invalidate, key})
  end

  @doc "Invalidates all cache entries (use after prefix changes)."
  def invalidate_all do
    GenServer.cast(__MODULE__, :invalidate_all)
  end

  @doc "Invalidates all cache entries synchronously. Use when you need the cache refreshed before proceeding."
  def invalidate_all_sync do
    GenServer.call(__MODULE__, :invalidate_all)
  end

  @doc "Clears all cache entries without re-warming. Used in tests to reset to defaults."
  def clear do
    GenServer.call(__MODULE__, :clear)
  end

  @doc "Warms the cache with current values from the database."
  def warm_cache do
    GenServer.cast(__MODULE__, :warm_cache)
  end

  # =============================================================================
  # GenServer
  # =============================================================================

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @impl GenServer
  def init(_) do
    :ets.new(@table, [:named_table, :public, read_concurrency: true])
    do_warm_cache()
    {:ok, %{}}
  end

  @impl GenServer
  def handle_cast({:invalidate, key}, state) do
    :ets.delete(@table, key)
    {:noreply, state}
  end

  def handle_cast(:invalidate_all, state) do
    :ets.delete_all_objects(@table)
    do_warm_cache()
    {:noreply, state}
  end

  def handle_cast(:warm_cache, state) do
    do_warm_cache()
    {:noreply, state}
  end

  @impl GenServer
  def handle_call(:invalidate_all, _from, state) do
    :ets.delete_all_objects(@table)
    do_warm_cache()
    {:reply, :ok, state}
  end

  def handle_call(:clear, _from, state) do
    :ets.delete_all_objects(@table)
    {:reply, :ok, state}
  end

  # =============================================================================
  # Private - Cache operations
  # =============================================================================

  defp page_slug(page_type) do
    key = {:slug, page_type}

    case lookup(key) do
      {:ok, slug} -> slug
      :not_found -> default_slug(page_type)
    end
  end

  defp lookup(key) do
    case :ets.lookup(@table, key) do
      [{^key, value}] -> {:ok, value}
      [] -> :not_found
    end
  rescue
    ArgumentError -> :not_found
  end

  defp do_warm_cache do
    # Load page slugs
    Pages.list_pages_with_custom_urls()
    |> Enum.each(fn page ->
      page_type = String.to_existing_atom(page.slug)
      :ets.insert(@table, {{:slug, page_type}, page.url_slug})
      :ets.insert(@table, {{:reverse_slug, page.url_slug}, page_type})
    end)

    # Load route prefixes from settings
    prefixes = Settings.get_setting("url_prefixes") || %{}

    Enum.each(prefixes, fn {type_str, prefix} ->
      type = String.to_existing_atom(type_str)
      :ets.insert(@table, {{:prefix, type}, prefix})
      :ets.insert(@table, {{:reverse_prefix, prefix}, type})
    end)
  rescue
    # During startup, tables/atoms might not exist yet
    _ -> :ok
  end

  # =============================================================================
  # Defaults
  # =============================================================================

  defp default_slug(:cart), do: "cart"
  defp default_slug(:about), do: "about"
  defp default_slug(:contact), do: "contact"
  defp default_slug(:search), do: "search"
  defp default_slug(:delivery), do: "delivery"
  defp default_slug(:privacy), do: "privacy"
  defp default_slug(:terms), do: "terms"
  defp default_slug(:checkout_success), do: "checkout/success"
  defp default_slug(:orders), do: "orders"
  defp default_slug(_), do: nil

  defp default_prefix(:products), do: "products"
  defp default_prefix(:collections), do: "collections"
  defp default_prefix(:orders), do: "orders"
  defp default_prefix(_), do: nil

  # Map default slugs back to page types (for router)
  defp default_page_type("cart"), do: :cart
  defp default_page_type("about"), do: :about
  defp default_page_type("contact"), do: :contact
  defp default_page_type("search"), do: :search
  defp default_page_type("delivery"), do: :delivery
  defp default_page_type("privacy"), do: :privacy
  defp default_page_type("terms"), do: :terms
  defp default_page_type("orders"), do: :orders
  defp default_page_type("checkout/success"), do: :checkout_success
  defp default_page_type(_), do: nil

  # Map default prefixes back to types
  defp default_prefix_type("products"), do: :products
  defp default_prefix_type("collections"), do: :collections
  defp default_prefix_type("orders"), do: :orders
  defp default_prefix_type(_), do: nil
end
