defmodule BDS.Desktop.UILocale do
  @moduledoc """
  Per-render UI locale binding for the desktop LiveView shell.

  The shell renders the UI in the user's selected language, which can differ
  from the OS locale and from the project's `mainLanguage`. Phoenix HEEx
  templates call `translated/1,2` helpers without an explicit locale, so we
  bind the active locale once per `render/1` and the helpers read it back.

  This module encapsulates that binding so call sites do not touch the raw
  process dictionary directly. Use `with_locale/2` around any render or
  component that needs a locale binding; use `current/0` to read it.

  ## Invariant

  Every code path that evaluates HEEx templates containing `translated/1,2`
  calls **must** call `UILocale.put/1` before template evaluation:

    * `ShellLive.render/1` — sets locale at the top of every LiveView render.
    * `SidebarComponents.sidebar_content/1` — sets locale before the function
      component's HEEx (runs in the same process, may be called outside
      the parent render cycle via `send_update`).
    * `MenuBar.mount/1` and `MenuBar.handle_info({:set_ui_locale, _})` — set
      locale in the separate menu-bar process which has its own render cycle.

  Violating this invariant causes `current/0` to return a stale or `nil`
  locale, producing untranslated UI text.

  Direct use of `Process.put(:bds_ui_locale, _)` or
  `Process.get(:bds_ui_locale)` is forbidden outside this module.
  """

  @key :bds_ui_locale

  @typedoc "A normalized UI locale code such as `\"en\"` or `\"de\"`."
  @type locale :: String.t() | nil

  @doc """
  Bind `locale` for any subsequent reads of `current/0` in this process.

  Used at LiveView render boundaries. The binding persists past the call
  (mirroring per-process locale state) so that lazily evaluated child
  components see the active locale. Each render boundary overwrites it.
  """
  @spec put(locale()) :: :ok
  def put(locale) do
    Process.put(@key, locale)
    BDS.Gettext.put_locale(locale)
    :ok
  end

  @doc """
  Set the UI locale for the duration of `fun`, then restore the prior value.

  Safe under exceptions: the prior binding is restored in an `after` clause.
  Use this for short-lived non-LiveView contexts (background tasks, scripts)
  where eager evaluation guarantees the binding is consumed before `fun`
  returns. Do not use around LiveView `render/1` because the returned
  `Phoenix.LiveView.Rendered` struct evaluates its dynamic parts lazily.
  """
  @spec with_locale(locale(), (-> result)) :: result when result: var
  def with_locale(locale, fun) when is_function(fun, 0) do
    previous = Process.get(@key)
    Process.put(@key, locale)

    try do
      fun.()
    after
      restore(previous)
    end
  end

  @doc """
  Read the active UI locale binding for this process, or `nil` when unset.
  """
  @spec current() :: locale()
  def current, do: Process.get(@key)

  defp restore(nil), do: Process.delete(@key)
  defp restore(value), do: Process.put(@key, value)
end
