lib/comm_bus/protocol/section_roles.ex

defmodule CommBus.Protocol.SectionRoles do
  @moduledoc """
  Registry for mapping CommBus sections (e.g., :system, :pre_history) to downstream
  message roles understood by provider pipelines.

  Sections default to `:system` roles, but callers can register additional mappings
  (for example, mapping `:memory` sections to `:assistant`). Overrides from pipeline
  opts still take precedence, so projects can adjust behaviour per request.
  """

  @type section :: atom() | String.t()
  @type role :: atom() | String.t()
  @type mapping :: %{optional(atom()) => atom()}

  @persist_key {__MODULE__, :roles}

  @default_roles %{
    system: :system,
    pre_history: :system,
    post_history: :system
  }

  @doc "Returns the built-in section-role mapping."
  @spec default_roles() :: mapping()
  def default_roles, do: @default_roles

  @doc "Returns the currently configured section-role mapping."
  @spec get() :: mapping()
  def get do
    :persistent_term.get(@persist_key, @default_roles)
  end

  @doc "Registers or updates a mapping between a section and a downstream role."
  @spec put(section(), role()) :: :ok | {:error, term()}
  def put(section, role) do
    with {:ok, section_atom} <- normalize_section(section),
         {:ok, role_atom} <- normalize_role(role) do
      update(fn roles -> Map.put(roles, section_atom, role_atom) end)
    end
  end

  @doc "Registers multiple mappings at once."
  @spec put_all(map() | keyword()) :: :ok
  def put_all(mappings) when is_map(mappings) or is_list(mappings) do
    normalized = normalize_map(mappings)
    update(fn roles -> Map.merge(roles, normalized) end)
  end

  @doc "Removes a custom mapping for the provided section."
  @spec delete(section()) :: :ok | {:error, term()}
  def delete(section) do
    with {:ok, section_atom} <- normalize_section(section) do
      update(fn roles -> Map.delete(roles, section_atom) end)
    end
  end

  @doc "Resets all mappings back to factory defaults."
  @spec reset() :: :ok
  def reset do
    :persistent_term.put(@persist_key, @default_roles)
    :ok
  end

  @doc "Returns the resolved mapping merged with optional overrides (map/keyword)."
  @spec resolve(map() | keyword()) :: mapping()
  def resolve(overrides \\ %{}) do
    overrides = normalize_map(overrides)
    Map.merge(get(), overrides)
  end

  defp update(fun) when is_function(fun, 1) do
    new_roles = fun.(get())
    :persistent_term.put(@persist_key, new_roles)
    :ok
  end

  defp normalize_map(mappings) when is_list(mappings) do
    mappings |> Map.new() |> normalize_map()
  end

  defp normalize_map(mappings) when is_map(mappings) do
    Enum.reduce(mappings, %{}, fn {section, role}, acc ->
      case {normalize_section(section), normalize_role(role)} do
        {{:ok, section_atom}, {:ok, role_atom}} -> Map.put(acc, section_atom, role_atom)
        _ -> acc
      end
    end)
  end

  defp normalize_section(section) when is_atom(section), do: {:ok, section}

  defp normalize_section(section) when is_binary(section) do
    case String.trim(section) do
      "" -> {:error, :invalid_section}
      trimmed -> {:ok, String.to_atom(trimmed)}
    end
  end

  defp normalize_section(_), do: {:error, :invalid_section}

  defp normalize_role(role) when is_atom(role), do: {:ok, role}

  defp normalize_role(role) when is_binary(role) do
    case String.trim(role) do
      "" -> {:error, :invalid_role}
      trimmed -> {:ok, String.to_atom(trimmed)}
    end
  end

  defp normalize_role(_), do: {:error, :invalid_role}
end