defmodule Raxol.MCP.AgentBridge do
  @moduledoc """
  Bridges Raxol Agent Actions to MCP tools.

  Converts Action modules (which implement the `Raxol.Agent.Action` behaviour)
  into MCP Registry tool definitions, allowing external MCP clients to invoke
  agent capabilities.

  ## Usage

      actions = [MyApp.Actions.ReadFile, MyApp.Actions.WriteFile]
      tools = AgentBridge.actions_to_mcp_tools(actions)
      Raxol.MCP.Registry.register_tools(registry, tools)

  The bridge also provides meta-tools for agent management:
  `agent.list`, `agent.send`, `agent.get_model`.
  """

  @compile {:no_warn_undefined, [Raxol.Agent.Registry, Raxol.Headless]}

  @doc """
  Convert a list of Action modules to MCP tool definitions.

  Each Action module must implement `__action_meta__/0` and `call/2`.
  Tools are namespaced with `agent.` prefix.
  """
  @spec actions_to_mcp_tools([module()], map()) :: [Raxol.MCP.Registry.tool_def()]
  def actions_to_mcp_tools(action_modules, context \\ %{}) do
    Enum.map(action_modules, fn module ->
      meta = module.__action_meta__()
      input_schema = build_input_schema(meta.input_schema)

      %{
        name: "agent.#{meta.name}",
        description: meta.description,
        inputSchema: input_schema,
        callback: fn args ->
          atomized = atomize_keys(args)

          case module.call(atomized, context) do
            {:ok, result} ->
              {:ok, [%{type: "text", text: inspect(result, pretty: true)}]}

            {:ok, result, _commands} ->
              {:ok, [%{type: "text", text: inspect(result, pretty: true)}]}

            {:error, reason} ->
              {:error, reason}
          end
        end
      }
    end)
  end

  @doc """
  Returns meta-tools for agent management: list, send, get_model.
  """
  @spec meta_tools() :: [Raxol.MCP.Registry.tool_def()]
  def meta_tools do
    [
      %{
        name: "agent.list",
        description: "List all active agent sessions with their IDs and status.",
        inputSchema: %{type: "object", properties: %{}},
        callback: &list_agents/1
      },
      %{
        name: "agent.send",
        description: "Send a message to a running agent session.",
        inputSchema: %{
          type: "object",
          required: ["id", "message"],
          properties: %{
            id: %{type: "string", description: "Agent session ID"},
            message: %{type: "string", description: "Message to send to the agent"}
          }
        },
        callback: &send_to_agent/1
      },
      %{
        name: "agent.get_model",
        description: "Get the current TEA model state of an agent session.",
        inputSchema: %{
          type: "object",
          required: ["id"],
          properties: %{
            id: %{type: "string", description: "Agent session ID"}
          }
        },
        callback: &get_agent_model/1
      }
    ]
  end

  # -- Callbacks ---------------------------------------------------------------

  defp list_agents(_args) do
    sessions =
      if Code.ensure_loaded?(Raxol.Headless) and
           function_exported?(Raxol.Headless, :list, 0) do
        case Raxol.Headless.list() do
          {:ok, list} -> list
          _ -> []
        end
      else
        []
      end

    text =
      if sessions == [] do
        "No active agent sessions."
      else
        sessions
        |> Enum.map(fn s -> "- #{inspect(s)}" end)
        |> Enum.join("\n")
      end

    {:ok, [%{type: "text", text: text}]}
  end

  defp send_to_agent(%{"id" => id, "message" => message}) do
    send_to_agent(%{id: id, message: message})
  end

  defp send_to_agent(%{id: id, message: message}) do
    if Code.ensure_loaded?(Raxol.Headless) and
         function_exported?(Raxol.Headless, :send_key, 3) do
      # Send the message as individual keystrokes
      for char <- String.graphemes(message) do
        Raxol.Headless.send_key(String.to_atom(id), char, [])
      end

      {:ok, [%{type: "text", text: "Sent #{String.length(message)} characters to agent #{id}"}]}
    else
      {:error, "Headless module not available"}
    end
  end

  defp send_to_agent(_), do: {:error, "Missing required arguments: id, message"}

  defp get_agent_model(%{"id" => id}), do: get_agent_model(%{id: id})

  defp get_agent_model(%{id: id}) do
    if Code.ensure_loaded?(Raxol.Headless) and
         function_exported?(Raxol.Headless, :get_model, 1) do
      case Raxol.Headless.get_model(String.to_atom(id)) do
        {:ok, model} ->
          {:ok, [%{type: "text", text: inspect(model, pretty: true, limit: :infinity)}]}

        {:error, reason} ->
          {:error, reason}
      end
    else
      {:error, "Headless module not available"}
    end
  end

  defp get_agent_model(_), do: {:error, "Missing required argument: id"}

  # -- Private -----------------------------------------------------------------

  defp build_input_schema(schema) when is_list(schema) do
    {properties, required} =
      Enum.reduce(schema, {%{}, []}, fn {field, spec}, {props, req} ->
        type = Keyword.get(spec, :type, :string) |> to_string()
        desc = Keyword.get(spec, :description, "")

        prop = %{type: type}
        prop = if desc != "", do: Map.put(prop, :description, desc), else: prop

        props = Map.put(props, to_string(field), prop)

        req =
          if Keyword.get(spec, :required, false),
            do: [to_string(field) | req],
            else: req

        {props, req}
      end)

    schema = %{type: "object", properties: properties}
    if required != [], do: Map.put(schema, :required, Enum.reverse(required)), else: schema
  end

  defp build_input_schema(_), do: %{type: "object", properties: %{}}

  defp atomize_keys(map) when is_map(map) do
    Map.new(map, fn
      {k, v} when is_binary(k) ->
        atom =
          try do
            String.to_existing_atom(k)
          rescue
            ArgumentError -> k
          end

        {atom, v}

      {k, v} ->
        {k, v}
    end)
  end
end
