defmodule PhoenixLiveState.StateSpec do
  @moduledoc """
  Behaviour to define the initial state and settings of a state server.

  You can use this behaviour directly or via the __using__ macro.
  - Using the behaviour (with `@behaviour PhoenixLiveState.StateSpec`) you'll
    need to define the `on_mount/1` callback.
  - Using the macro (with `use PhoenixLiveState.StateSpec`) we inject a default
    `on_mount` that uses an empty initial state, the default TTL and ACL.

  It is recommended to use the macro method, as it is planned to have more features
  in the future.
  """
  @moduledoc since: "0.1.0"

  defstruct [:initial_state, :ttl, :acl]

  @type t :: %__MODULE__{
          initial_state: Access.t(),
          ttl: non_neg_integer() | :default,
          acl: PhoenixLiveState.StateServer.Policy.acl()
        }

  defmacro __using__(_opts) do
    quote do
      @behaviour PhoenixLiveState.StateSpec

      @impl true
      def on_mount(_key), do: {:ok, %{initial_state: %{}, ttl: :default, acl: nil}}

      defoverridable on_mount: 1
    end
  end

  @doc """
  Defines the initial configuration of a state server.
  """
  @doc since: "0.1.0"
  @callback on_mount(key :: term) :: {:ok, Access.t() | t()} | {:error, reason :: term}

  @doc """
  Invokes the given state spec and builds a StateSpec struct.
  """
  @doc since: "0.1.0"
  @spec mount(module(), term()) :: {:ok, t()} | {:error, term()}
  def mount(state_spec, key) do
    case state_spec.on_mount(key) do
      {:ok, %__MODULE__{} = spec} ->
        {:ok, spec}

      {:ok, spec} ->
        spec_map = Map.new(spec)

        spec_map =
          %{initial_state: %{}, ttl: :default, acl: nil}
          |> Map.merge(spec_map)

        {:ok, struct(__MODULE__, spec_map)}

      error ->
        error
    end
  end
end
