defmodule PhoenixLiveState.StateServer.Policy do
  @moduledoc """
  State management policy rules.
  """
  @moduledoc since: "0.1.0"

  alias PhoenixLiveState.StateServer.InternalState

  @type client_type :: :group | :public
  @type action :: :read | :write | :delete | :reset
  @type acl :: %{client_type => [action] | :all}

  @allowed_actions [:read, :write, :delete, :reset]

  @doc """
  Checks and builds the ACL.
  """
  @spec build_acl(Access.t()) :: {:ok, acl()} | {:error, :invalid_acl}
  def build_acl(acl_candidate) do
    acl_candidate = Map.new(acl_candidate)

    [:group, :public]
    |> Enum.reduce_while(%{}, fn client_type, acc ->
      actions = Map.get(acl_candidate, client_type, nil)

      cond do
        actions == nil ->
          {:cont, Map.put(acc, client_type, [])}

        actions == :all ->
          {:cont, Map.put(acc, client_type, :all)}

        is_list(actions) && Enum.all?(actions, &(&1 in @allowed_actions)) ->
          {:cont, Map.put(acc, client_type, actions)}

        true ->
          {:halt, {:error, :invalid_acl}}
      end
    end)
    |> case do
      {:error, _error} = error -> error
      acl when is_map(acl) -> {:ok, acl}
    end
  end

  def build_acl!(acl_candidate) do
    case build_acl(acl_candidate) do
      {:ok, acl} ->
        acl

      {:error, :invalid_acl} ->
        raise "Invalid ACL: #{inspect(acl_candidate)}"
    end
  end

  @doc """
  Returns the default ACL.
  """
  def default_acl do
    %{
      group: [:read, :write, :delete, :reset],
      public: [:read]
    }
  end

  @doc """
  Determines if the given action is allowed for the client type.
  """
  @spec allowed?(pid(), action(), InternalState.t()) :: boolean()
  def allowed?(
        request_pid,
        action,
        %InternalState{} = internal_state
      ) do
    client_type = detect_client_type(request_pid, internal_state)
    acl_check(internal_state.settings.acl, client_type, action)
  end

  defp detect_client_type(request_pid, %InternalState{clients: clients}) do
    if MapSet.member?(clients, request_pid) do
      :group
    else
      :public
    end
  end

  defp acl_check(acl, client_type, action) do
    allowed_actions = Map.get(acl, client_type, [])
    allowed_actions == :all || action in allowed_actions
  end
end
