defmodule ExMCP.Transport.Local do
  @moduledoc """
  Local BEAM transport for ExMCP.

  This module provides a high-performance transport for BEAM-based communication.
  It carries MCP-shaped JSON-RPC messages as Elixir terms between local
  processes. The transport itself does not JSON encode or decode messages.

  ## Features

  - MCP-shaped message passing without JSON serialization
  - Direct process-to-process communication
  - Built-in fault tolerance
  - Low latency for local communication

  ## Configuration

  This transport is configured via `ExMCP.Client.start_link/1`:

      {:ok, client} = ExMCP.Client.start_link(
        transport: :beam,
        server: server_pid
      )

  Options:
  - `:server` - Required for client mode if connecting to a server process.
  - `:timeout` - Optional. Call timeout in milliseconds (default: 5000).
  """

  @behaviour ExMCP.Transport

  alias ExMCP.Transport.Error
  defstruct [:server_pid, :role, :connected, :timeout, :subscriber, :forwarder_pid]

  @type t :: %__MODULE__{
          server_pid: pid() | nil,
          role: :client | :server,
          connected: boolean(),
          timeout: pos_integer()
        }

  @default_timeout 5_000

  @impl true
  def connect(opts) do
    timeout = Keyword.get(opts, :timeout, @default_timeout)

    # Determine if this is client or server mode
    cond do
      # Client mode - connecting to a server
      Keyword.has_key?(opts, :server) ->
        server_pid = Keyword.fetch!(opts, :server)

        if is_pid(server_pid) && Process.alive?(server_pid) do
          transport = %__MODULE__{
            server_pid: server_pid,
            role: :client,
            connected: true,
            timeout: timeout
          }

          # Notify server of connection
          Kernel.send(server_pid, {:test_transport_connect, self()})

          :telemetry.execute([:ex_mcp, :transport, :connection, :opened], %{}, %{
            transport: :beam
          })

          {:ok, transport}
        else
          Error.connection_error(:server_not_available)
        end

      # Client mode - using service_name (for backward compatibility)
      Keyword.has_key?(opts, :service_name) ->
        # This is the problematic case - client trying to connect directly to service
        # Return an error to force the test to be updated
        {:error,
         {:not_supported,
          "BEAM transport requires a server process. Use :server option to specify the server PID."}}

      # Server mode - listening for connections
      true ->
        transport = %__MODULE__{
          server_pid: nil,
          role: :server,
          connected: false,
          timeout: timeout
        }

        {:ok, transport}
    end
  end

  @impl true
  def close(%__MODULE__{}) do
    :ok
  end

  @impl true
  def connected?(%__MODULE__{role: :client, server_pid: pid}) when is_pid(pid) do
    Process.alive?(pid)
  end

  def connected?(%__MODULE__{role: :server, server_pid: pid}) when is_pid(pid) do
    Process.alive?(pid)
  end

  def connected?(%__MODULE__{}) do
    false
  end

  @doc """
  Subscribe to receive transport events (push model).

  For the Local transport, the peer already sends `{:transport_message, msg}`
  to the client process. The Client GenServer handles these directly,
  so subscribe just signals that no receiver task is needed.
  """
  @impl true
  def subscribe(_pid, %__MODULE__{} = state) do
    {:ok, state}
  end

  @impl true
  def capabilities(_state), do: [:push]

  @impl true
  def send_message(message, %__MODULE__{} = transport) do
    case Error.validate_connection(transport, &connected?/1) do
      :ok ->
        case transport.role do
          :client ->
            # Client sending to server
            :telemetry.execute([:ex_mcp, :transport, :message, :sent], %{}, %{
              transport: :beam,
              role: transport.role
            })

            Kernel.send(transport.server_pid, {:transport_message, message})
            {:ok, transport}

          :server ->
            # Server sending to client
            if transport.server_pid do
              :telemetry.execute([:ex_mcp, :transport, :message, :sent], %{}, %{
                transport: :beam,
                role: transport.role
              })

              Kernel.send(transport.server_pid, {:transport_message, message})
              {:ok, transport}
            else
              # No client connected yet
              {:ok, transport}
            end
        end

      error ->
        error
    end
  end

  @impl true
  def receive_message(%__MODULE__{} = transport) do
    receive_message(transport, transport.timeout)
  end

  def receive_message(%__MODULE__{} = transport, _timeout) do
    case Error.validate_connection(transport, &connected?/1) do
      :ok ->
        receive do
          {:transport_message, message} ->
            :telemetry.execute([:ex_mcp, :transport, :message, :received], %{}, %{
              transport: :beam
            })

            {:ok, message, transport}

          {:test_transport_connect, client_pid} when transport.role == :server ->
            # Server accepting client connection
            new_transport = %{transport | server_pid: client_pid, connected: true}
            # Continue waiting for actual message
            receive_message(new_transport, nil)

          {:transport_error, reason} ->
            Error.transport_error(reason)
        end

      error ->
        error
    end
  end
end
