# SPDX-FileCopyrightText: 2026 James Harton
#
# SPDX-License-Identifier: Apache-2.0
defmodule BB.MCP.Robots do
@moduledoc """
Resolves between agent-facing robot name strings and robot modules.
Robot names are derived from the last segment of the module name, lowercased.
MyApp.WX200 -> "wx200"
MyApp.SO101 -> "so101"
MyApp.Robot -> "robot"
MyApp.Two.Word -> "two_word" (only when the segment is itself camel-cased,
e.g. `MyApp.TwoWord` -> "two_word")
"""
@type config :: %{required(String.t()) => module()}
@doc """
Build a name → module map from a list of robot modules, raising on collisions.
"""
@spec build!([module()]) :: config()
def build!(modules) when is_list(modules) do
Enum.reduce(modules, %{}, fn module, acc ->
name = name_for(module)
case Map.fetch(acc, name) do
{:ok, other} when other != module ->
raise ArgumentError,
"bb_mcp: robot name collision on #{inspect(name)} between " <>
"#{inspect(other)} and #{inspect(module)}"
_ ->
Map.put(acc, name, module)
end
end)
end
@doc """
The agent-facing name for a robot module.
"""
@spec name_for(module()) :: String.t()
def name_for(module) when is_atom(module) do
module
|> Module.split()
|> List.last()
|> Macro.underscore()
end
@doc """
Look up a robot module by name. Returns `{:ok, module}` or `{:error, :unknown_robot}`.
"""
@spec fetch(config(), String.t()) :: {:ok, module()} | {:error, :unknown_robot}
def fetch(config, name) when is_binary(name) do
case Map.fetch(config, name) do
{:ok, module} -> {:ok, module}
:error -> {:error, :unknown_robot}
end
end
@doc """
List all configured robots as `{name, module}` tuples, sorted by name.
"""
@spec to_list(config()) :: [{String.t(), module()}]
def to_list(config), do: config |> Map.to_list() |> Enum.sort()
end