defmodule ExMCP.Native do
  @moduledoc """
  Native BEAM service dispatcher for direct process communication within Elixir clusters.

  This module provides ultra-fast, direct process communication leveraging OTP's built-in features:
  - Direct GenServer.call between processes
  - Pluggable service registry (local `Registry` by default, `Horde` for distribution)
  - Automatic distribution across BEAM nodes (with Horde adapter)
  - Built-in fault tolerance and monitoring
  - Zero serialization overhead for local calls

  ## Use Cases

  - Trusted Elixir services within the same cluster
  - High-performance internal communication (~15μs local, ~50μs cross-node)
  - Services that need to be supervised and monitored by OTP

  ## Performance Characteristics

  - Local calls: ~15μs latency
  - Cross-node calls: ~50μs latency
  - Memory overhead: Single registry entry per service
  - Throughput: Limited only by service processing speed

  ## Examples

      # Service registration (typically in init/1)
      ExMCP.Native.register_service(:my_tool_service)

      # Direct service calls
      {:ok, result} = ExMCP.Native.call(:my_service, "list_tools", %{})

      # Cross-node calls (automatic)
      {:ok, result} = ExMCP.Native.call({:my_service, :"node@host"}, "method", params)

      # Fire-and-forget notifications
      :ok = ExMCP.Native.notify(:event_service, "resource_updated", %{
        "uri" => "file:///config.json",
        "type" => "modified"
      })

  ## Service Discovery

      # List all available services across the cluster
      services = ExMCP.Native.list_services()

      # Check if a service is available
      available? = ExMCP.Native.service_available?(:my_service)
  """

  require Logger

  alias ExMCP.Internal.SecurityConfig
  alias ExMCP.Transport.SecurityGuard

  @default_timeout 5_000

  @type service_id :: atom() | {atom(), node()}
  @type method :: String.t()
  @type params :: map()
  @type result :: term()

  # Public API

  @doc """
  Registers a service with the distributed registry.

  This should typically be called in a GenServer's init/1 callback.

  ## Examples

      def init(_) do
        ExMCP.Native.register_service(:my_tools)
        {:ok, %{}}
      end
  """
  @spec register_service(atom()) :: :ok
  def register_service(name) when is_atom(name) do
    case registry_adapter().register(name, %{registered_at: DateTime.utc_now()}) do
      :ok ->
        Logger.info("Service registered: #{name}")
        :ok

      {:error, reason} ->
        Logger.warning("Failed to register service #{name}: #{inspect(reason)}")
        :ok
    end
  end

  @doc """
  Unregisters a service from the distributed registry.

  This should typically be called in a GenServer's terminate/2 callback.
  """
  @spec unregister_service(atom()) :: :ok
  def unregister_service(name) when is_atom(name) do
    registry_adapter().unregister(name)
    Logger.info("Service unregistered: #{name}")
    :ok
  end

  @doc """
  Calls a service method with the given parameters.

  Supports both local and cross-node calls transparently through the configured registry adapter.

  ## Options

  - `:timeout` - Call timeout in milliseconds (default: 5000)
  - `:progress_token` - Progress token for long-running operations
  - `:meta` - Additional metadata map

  ## Examples

      # Simple call
      {:ok, tools} = ExMCP.Native.call(:my_tools, "list_tools", %{})

      # Call with timeout and metadata
      {:ok, result} = ExMCP.Native.call(
        :my_tools,
        "process_data",
        %{"dataset" => "large_data"},
        timeout: 30_000,
        meta: %{"trace_id" => "abc", "user_id" => "user1"}
      )
  """
  @spec call(service_id(), method(), params(), keyword()) :: {:ok, result()} | {:error, term()}
  def call(service_id, method, params, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, @default_timeout)
    progress_token = Keyword.get(opts, :progress_token)
    meta = Keyword.get(opts, :meta, %{})

    case lookup_service(service_id) do
      {:ok, pid} ->
        # Check if this is an external service call that needs security validation
        case validate_beam_service_call(service_id, method, params, opts) do
          {:ok, validated_params} ->
            message = build_mcp_message(method, validated_params, progress_token, meta)
            make_service_call(pid, message, timeout)

          {:error, security_error} ->
            Logger.warning("BEAM service call blocked by security policy",
              service_id: service_id,
              method: method,
              error: security_error
            )

            {:error, {:security_violation, security_error}}
        end

      {:error, :not_found} ->
        {:error, {:service_not_found, service_id}}
    end
  end

  defp validate_beam_service_call(service_id, method, params, opts) do
    # Check if this service call involves external resources or cross-cluster communication
    case should_validate_beam_call?(service_id, method, params) do
      true ->
        # Extract URL from params if this is a resource-related call
        url = extract_url_from_params(method, params)

        security_request = %{
          url: url || "beam://#{service_id}/#{method}",
          headers: [],
          method: method,
          transport: :beam,
          user_id: extract_beam_user_id(opts)
        }

        config = SecurityConfig.get_transport_config(:beam)

        case SecurityGuard.validate_request(security_request, config) do
          {:ok, _sanitized_request} ->
            {:ok, params}

          {:error, security_error} ->
            {:error, security_error}
        end

      false ->
        # Internal BEAM service call, allow through
        {:ok, params}
    end
  end

  defp should_validate_beam_call?(service_id, method, params) do
    # Validate calls that involve external resources or cross-cluster communication
    cond do
      # Resource-related methods that might access external URLs
      String.starts_with?(method, "resources/") ->
        case extract_url_from_params(method, params) do
          nil -> false
          url -> external_url?(url)
        end

      # Cross-cluster service calls (service_id is a tuple with node)
      is_tuple(service_id) ->
        {_service, node} = service_id
        node != node()

      # Default: internal calls don't need validation
      true ->
        false
    end
  end

  defp extract_url_from_params("resources/read", %{"uri" => uri}), do: uri
  defp extract_url_from_params("resources/list", %{"uri" => uri}) when is_binary(uri), do: uri
  defp extract_url_from_params(_, _), do: nil

  defp external_url?(url) do
    case URI.parse(url) do
      %URI{scheme: scheme, host: host} when not is_nil(scheme) and not is_nil(host) ->
        true

      _ ->
        false
    end
  end

  defp extract_beam_user_id(opts) do
    # Try to extract user ID from options, fallback to node-based identification
    Keyword.get(opts, :user_id, "#{node()}_beam_user")
  end

  @doc """
  Sends a notification to a service (fire-and-forget).

  Notifications do not expect a response and are sent asynchronously.

  ## Examples

      :ok = ExMCP.Native.notify(:event_service, "resource_updated", %{
        "uri" => "file:///config.json",
        "type" => "modified"
      })
  """
  @spec notify(service_id(), method(), params()) :: :ok | {:error, term()}
  def notify(service_id, method, params) do
    case lookup_service(service_id) do
      {:ok, pid} ->
        message = %{"method" => method, "params" => params}
        GenServer.cast(pid, {:mcp_notification, message})
        :ok

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

  @doc """
  Lists all services registered in the distributed registry.

  Returns a list of tuples containing {service_name, pid, metadata}.
  """
  @spec list_services() :: [{atom(), pid(), map()}]
  def list_services do
    registry_adapter().list()
  end

  @doc """
  Checks if a service is available in the distributed registry.
  """
  @spec service_available?(atom()) :: boolean()
  def service_available?(service_name) when is_atom(service_name) do
    case registry_adapter().lookup(service_name) do
      {:ok, _pid} -> true
      {:error, :not_found} -> false
    end
  end

  # Private functions

  defp lookup_service(service_id) when is_atom(service_id) do
    registry_adapter().lookup(service_id)
  end

  defp lookup_service({service_name, target_node})
       when is_atom(service_name) and is_atom(target_node) do
    adapter = registry_adapter()

    if function_exported?(adapter, :lookup_on_node, 2) do
      adapter.lookup_on_node(service_name, target_node)
    else
      case adapter.lookup(service_name) do
        {:ok, pid} ->
          if node(pid) == target_node do
            {:ok, pid}
          else
            {:error, :not_found}
          end

        {:error, :not_found} ->
          {:error, :not_found}
      end
    end
  end

  defp registry_adapter, do: ExMCP.ServiceRegistry.adapter()

  defp build_mcp_message(method, params, progress_token, meta) do
    message = %{"method" => method, "params" => params}
    message = add_progress_token(message, progress_token)
    add_meta(message, meta)
  end

  defp add_progress_token(message, nil), do: message

  defp add_progress_token(message, progress_token) do
    message = ensure_meta_exists(message)
    put_in(message["_meta"]["progressToken"], progress_token)
  end

  defp add_meta(message, meta) when map_size(meta) == 0, do: message

  defp add_meta(message, meta) do
    message = ensure_meta_exists(message)
    Map.put(message, "_meta", Map.merge(message["_meta"], meta))
  end

  defp ensure_meta_exists(message) do
    Map.put_new(message, "_meta", %{})
  end

  defp make_service_call(pid, message, timeout) do
    case safe_genserver_call(pid, {:mcp_request, message}, timeout) do
      {:ok, {:ok, result}} -> {:ok, result}
      {:ok, {:error, reason}} -> {:error, reason}
      {:ok, result} -> {:ok, result}
      {:error, reason} -> {:error, reason}
    end
  end

  defp safe_genserver_call(pid, message, timeout) do
    {:ok, GenServer.call(pid, message, timeout)}
  catch
    :exit, {:timeout, _} -> {:error, :timeout}
    :exit, {:noproc, _} -> {:error, :service_unavailable}
    :exit, {:normal, _} -> {:error, :service_terminated}
    :exit, reason -> {:error, {:service_exit, reason}}
  end
end
