defmodule ExMCP.Transport do
  @moduledoc """
  Behaviour definition for MCP transport implementations.

  A transport is responsible for sending and receiving MCP protocol messages
  over a specific communication channel. ExMCP includes implementations for
  the standard MCP transports and provides this behaviour for custom implementations.

  ## Built-in Transports

  ExMCP provides these standard transports:

  - **`:stdio`** - Standard I/O communication (MCP specification)
  - **`:http`** - HTTP with optional SSE streaming (MCP specification)
  - **`:test`** - In-memory transport for testing
  - **`:beam`** - BEAM-local transport carrying MCP-shaped messages as Elixir terms.

  ## Using Transports

  Transports are specified when starting clients or servers:

      # stdio transport
      {:ok, client} = ExMCP.Client.start_link(
        transport: :stdio,
        command: ["python", "mcp-server.py"]
      )

      # HTTP transport
      {:ok, client} = ExMCP.Client.start_link(
        transport: :http,
        url: "https://api.example.com"
      )

  ## Push vs Pull Model

  Transports support two message delivery models:

  - **Pull (legacy):** Client calls `receive_message/1` in a loop via a receiver task.
    This is the default and works for all transports.

  - **Push (event-driven):** Client calls `subscribe/2` to register a handler pid.
    The transport pushes `{:transport_event, message}` messages directly to the handler.
    This eliminates the receiver task and is more efficient.

  Transports that implement `subscribe/2` should still implement `receive_message/1`
  for backwards compatibility.

  ## Custom Transport Implementation

  To implement a custom transport, create a module that implements
  all the callbacks defined in this behaviour:

      defmodule MyTransport do
        @behaviour ExMCP.Transport

        @impl true
        def connect(opts) do
          # Establish connection
          {:ok, state}
        end

        @impl true
        def send_message(message, state) do
          # Send the message
          {:ok, state}
        end

        @impl true
        def receive_message(state) do
          # Receive a message (blocking)
          {:ok, message, state}
        end

        @impl true
        def close(state) do
          # Clean up
          :ok
        end

        # Optional: enable push model
        @impl true
        def subscribe(pid, state) do
          # Start pushing {:transport_event, msg} to pid
          {:ok, %{state | subscriber: pid}}
        end
      end
  """

  @type state :: any()
  @type message :: String.t() | map() | list()
  @type opts :: keyword()

  @doc """
  Establishes a connection for the transport.

  Options are transport-specific. Should return `{:ok, state}`
  where state contains any necessary connection information.
  """
  @callback connect(opts()) :: {:ok, state()} | {:error, any()}

  @doc """
  Sends a message through the transport.

  The message will be a JSON-encoded string for wire transports, or an
  MCP-shaped map/list for local BEAM transports. Should return
  `{:ok, new_state}` on success.
  """
  @callback send_message(message(), state()) :: {:ok, state()} | {:error, any()}

  @doc """
  Receives a message from the transport.

  This should block until a message is available. Returns
  `{:ok, message, new_state}` where message is a JSON string or MCP-shaped term.

  Note: When `subscribe/2` is used, `receive_message/1` may not be called.
  Transports should still implement it for backwards compatibility.
  """
  @callback receive_message(state()) :: {:ok, message(), state()} | {:error, any()}

  @doc """
  Closes the transport connection.

  Should clean up any resources and return `:ok`.
  """
  @callback close(state()) :: :ok

  @doc """
  Optional callback to check if the transport is still connected.

  Default implementation always returns true.
  """
  @callback connected?(state()) :: boolean()

  @doc """
  Optional: Subscribe a process to receive transport events.

  When implemented, the transport pushes messages to the subscriber pid as:
  - `{:transport_event, message}` — a received message (JSON string or map)
  - `{:transport_closed, reason}` — transport connection closed
  - `{:transport_error, reason}` — transport error occurred

  This enables the push (event-driven) model, eliminating the need for a
  receiver task that polls `receive_message/1`.

  Returns `{:ok, new_state}` on success.
  """
  @callback subscribe(pid(), state()) :: {:ok, state()} | {:error, any()}

  @doc """
  Optional callback to declare transport capabilities.

  Returns a list of capability atoms that indicate special features
  supported by this transport. Clients can use this information to
  optimize their communication strategy.

  ## Capabilities

  - `:push` - Transport supports `subscribe/2` for event-driven message delivery
  - `:compression` - Transport supports message compression (future)
  - `:encryption` - Transport supports message encryption (future)

  ## Examples

      # Transport with push delivery
      def capabilities(_state), do: [:push]

      # Transport with no special capabilities (default)
      def capabilities(_state), do: []

  Default implementation returns an empty list (no special capabilities).
  """
  @callback capabilities(state()) :: [atom()]
  @optional_callbacks connected?: 1, capabilities: 1, subscribe: 2

  @doc """
  Check if a transport module supports the push (subscribe) model.
  """
  @spec supports_push?(module()) :: boolean()
  def supports_push?(transport_mod) do
    function_exported?(transport_mod, :subscribe, 2)
  end

  @doc """
  Helper to get the appropriate transport module for an atom identifier.

  ## Transport identifiers:
  - `:stdio` - Standard I/O transport (official MCP transport)
  - `:http` - Streamable HTTP transport with SSE (official MCP transport)
  - `:test` - In-memory transport for testing (non-standard)
  - `:beam` - BEAM-local transport carrying MCP-shaped messages as Elixir terms.

  Note: For direct Elixir service communication, use ExMCP.Native for service registration.
  """
  @spec get_transport(:stdio | :http | :test | :beam | module()) :: module()
  def get_transport(:stdio), do: ExMCP.Transport.Stdio
  def get_transport(:http), do: ExMCP.Transport.HTTP
  def get_transport(:test), do: ExMCP.Transport.Test
  def get_transport(:beam), do: ExMCP.Transport.Local
  def get_transport(module) when is_atom(module), do: module
end
